PageRenderTime 102ms CodeModel.GetById 66ms app.highlight 30ms RepoModel.GetById 1ms app.codeStats 1ms

/bundles/plugins-trunk/CommonControls/common/gui/EasyOptionPane.java

#
Java | 622 lines | 350 code | 47 blank | 225 comment | 120 complexity | 49fd408ccc379b41a9fdc775a0c1186d MD5 | raw file
  1/*
  2 * EasyOptionPane.java - an easy to use AbstractOptionPane.
  3 * Copyright (c) 2007 Marcelo Vanzin
  4 *
  5 * :tabSize=4:indentSize=4:noTabs=false:
  6 * :folding=explicit:collapseFolds=1:
  7 *
  8 * This program is free software; you can redistribute it and/or
  9 * modify it under the terms of the GNU General Public License
 10 * as published by the Free Software Foundation; either version 2
 11 * of the License, or any later version.
 12 *
 13 * This program is distributed in the hope that it will be useful,
 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 16 * GNU General Public License for more details.
 17 *
 18 * You should have received a copy of the GNU General Public License
 19 * along with this program; if not, write to the Free Software
 20 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 21 */
 22package common.gui;
 23
 24import java.util.Enumeration;
 25import java.util.HashMap;
 26import java.util.Iterator;
 27import java.util.LinkedList;
 28import java.util.List;
 29import java.util.Map;
 30import java.util.Properties;
 31import java.util.StringTokenizer;
 32
 33import javax.swing.AbstractButton;
 34import javax.swing.ButtonGroup;
 35import javax.swing.JCheckBox;
 36import javax.swing.JComboBox;
 37import javax.swing.JComponent;
 38import javax.swing.JFileChooser;
 39import javax.swing.JLabel;
 40import javax.swing.JTextArea;
 41import javax.swing.text.JTextComponent;
 42import javax.swing.JTextField;
 43import javax.swing.JRadioButton;
 44
 45import org.gjt.sp.jedit.AbstractOptionPane;
 46import org.gjt.sp.jedit.jEdit;
 47import org.gjt.sp.util.Log;
 48
 49/**
 50 *	An extension of AbstractOptionPane that makes it easier for
 51 *	implementing classes to create the gui by using a list of strings
 52 *	with the description of what the components should be.
 53 *
 54 *	<p>The pane is created by passing to this class a list of strings
 55 *	that describe the components to be added. These strings follow the
 56 *	format: <code>type,label,property[,config]</code>, where:</p>
 57 *
 58 *	<ul>
 59 *		<li>type: the type of the component to be added. For supported
 60 *		types, see below.</li>
 61 *		<li>label: the name of the property containing the label for the
 62 *		component, or <codE>null</code> for no label. If a "[label].tooltip"
 63 *		property exists, then the tooltip text for the component is set
 64 *		to the value of that property.</li>
 65 *		<li>property: the name of the property where the value for the
 66 *		component is stored.</li>
 67 *		<li>config: an optional string with extra configuration; this
 68 *		string is dependent on the type, and valid values are described
 69 *		below.</li>
 70 *	</ul>
 71 *
 72 *	<p>The following are the supported types and supported configuration
 73 *	strings:</p>
 74 *
 75 *	<table border="1">
 76 *	<tr>
 77 *		<th>Type</th>
 78 *		<th>Component</th>
 79 *		<th>Config String</th>
 80 *		<th>Saved Value</th>
 81 *	</tr>
 82 *
 83 *	<tr>
 84 *		<td>text</td>
 85 *		<td>JTextField</td>
 86 *		<td>None.</td>
 87 *		<td>Contents of text field.</td>
 88 *	</tr>
 89 *
 90 *	<tr>
 91 *		<td>file, dir or filedir</td>
 92 *		<td>FileTextField</td>
 93 *		<td>"true" if it should force the file to exist.</td>
 94 *		<td>Contents of text field. Use the appropriate type depending
 95 *		on the restriction you want in the file selection.</td>
 96 *	</tr>
 97 *
 98 *	<tr>
 99 *		<td>textarea</td>
100 *		<td>JTextArea</td>
101 *		<td>A string containing the number of rows of the textarea. By
102 *		default, it's 5.</td>
103 *		<td>Contents of text area.</td>
104 *	</tr>
105 *
106 *	<tr>
107 *		<td>checkbox</td>
108 *		<td>JCheckBox</td>
109 *		<td>None.</td>
110 *		<td>"true" if check box is selected, "false" otherwise.</td>
111 *	</tr>
112 *
113 *	<tr>
114 *		<td>radio</td>
115 *		<td>JRadioButton</td>
116 *		<td>A string with the name of the radio button group where the
117 *		group will be added. The group is created automatically.</td>
118 *		<td>The index of the button in the group.</td>
119 *	</tr>
120 *
121 *	<tr>
122 *		<td>combo</td>
123 *		<td>JComboBox</td>
124 *		<td>A string with a colon-separated list of values available in
125 *		the combo box.</td>
126 *		<td>The string value of the selected item.</td>
127 *	</tr>
128 *
129 *	<tr>
130 *		<td>ecombo</td>
131 *		<td>JComboBox (editable)</td>
132 *		<td>A string with a colon-separated list of values available in
133 *		the combo box.</td>
134 *		<td>The string value of the selected item.</td>
135 *	</tr>
136 *
137 *	<tr>
138 *		<td>label</td>
139 *		<td>JLabel</td>
140 *		<td>None. The property string is also not used for this type</td>
141 *		<td>None.</td>
142 *	</tr>
143 *
144 *	<tr>
145 *		<td>sep</td>
146 *		<td>A separator line. The property string is not necessary for
147 *		this type.</td>
148 *		<td>None.</td>
149 *		<td>None.</td>
150 *	</tr>
151 *
152 *	</table>
153 *
154 *	<p>It is possible to use custom types. In this case, the subclass
155 *	should override {@link #createComponent(String,String,String,String)}
156 *	and create the desired component.<p>
157 *
158 *	<p>By default, the properties are stored in jEdit's properties. The
159 *	implementor can call {@link #setPropertyStore(Properties)} to set the
160 *	properties object from where the values will be read and to where
161 *	they will be written. Just remembed to call this method before the
162 *	{@link #_init()} method is called, i.e., before the pane is	shown.</p>
163 *
164 *	<p>By default, if the contents of a text field are empty, the class
165 *	will treat that as asking to "unset" the property, so retrieving its
166 *	value from the store will return null. To control that, call
167 *	{@link #setEmptyToNull(boolean)} appropriately.</p>
168 *
169 *  @author		Marcelo Vanzin
170 *	@version	$Id$
171 *	@since		CC 0.9.4
172 */
173public class EasyOptionPane extends AbstractOptionPane
174{
175
176	private boolean		emptyToNull = true;
177	private List 		cspec;
178	private Properties	pstore;
179
180	private Map			cinst;
181	private Map			rgroups;
182
183	/**
184	 *	Creates an empty option pane.
185	 *
186	 *	@param	name		Internal options pane name.
187	 */
188	public EasyOptionPane(String name)
189	{
190		super(name);
191	}
192
193	/**
194	 *	Creates an empty option pane with the given component list.
195	 *
196	 *	@param	name		Internal options pane name.
197	 *	@param	components	String with whitespace-delimited component specs,
198	 *						as described in the javadoc for the class.
199	 */
200	public EasyOptionPane(String name, String components)
201	{
202		super(name);
203		StringTokenizer st = new StringTokenizer(components);
204		List lst = new LinkedList();
205		while (st.hasMoreTokens()) {
206			String next = st.nextToken();
207			if (next.length() > 0) {
208				lst.add(next);
209			}
210		}
211		setComponentSpec(lst);
212	}
213
214	/**
215	 *	Creates an empty option pane with the given component list.
216	 *
217	 *	@param	name		Internal options pane name.
218	 *	@param	components	List of component specs, as described in the
219	 *						javadoc for the class.
220	 */
221	public EasyOptionPane(String name, List components)
222	{
223		super(name);
224		setComponentSpec(components);
225	}
226
227	public void _init()
228	{
229		if (cspec == null) {
230			return;
231		}
232		cinst = new HashMap();
233		for (Iterator it = cspec.iterator(); it.hasNext(); ) {
234			JComponent jcomp;
235			String comp = (String) it.next();
236			StringTokenizer st = new StringTokenizer(comp, ",");
237
238			if (st.countTokens() == 0) {
239				Log.log(Log.ERROR, this, "Invalid config string (1): " + comp);
240				continue;
241			}
242
243			String type = st.nextToken();
244
245			if (st.countTokens() < 2 && !"sep".equals(type)
246				&& !"label".equals(type))
247			{
248				Log.log(Log.ERROR, this, "Invalid config string (2): " + comp);
249				continue;
250			}
251			String label = null;
252			String tooltip = null;
253			if (st.hasMoreTokens()) {
254				label = st.nextToken();
255				if ("null".equals(label)) {
256					label = null;
257				} else if (!"sep".equals(type)) {
258					tooltip = jEdit.getProperty(label + ".tooltip", (String)null);
259					label = jEdit.getProperty(label, label);
260				}
261			}
262			String prop = null;
263			String value = null;
264			if (st.hasMoreTokens()) {
265				prop = st.nextToken();
266			}
267			String config = null;
268			if (st.hasMoreTokens()) {
269				config = st.nextToken();
270			}
271			if (st.hasMoreTokens()) {
272				Log.log(Log.WARNING, this, "component string has unused data: " + comp);
273			}
274
275			if (prop != null) {
276				value = getProperty(prop, null);
277			}
278
279			if ("checkbox".equals(type)) {
280				jcomp = createCheckBox(label, value, config);
281				label = null;
282			} else if ("combo".equals(type)) {
283				jcomp = createComboBox(value, config, false);
284			} else if ("dir".equals(type)) {
285				jcomp = createFileTextField(value, config, JFileChooser.DIRECTORIES_ONLY);
286			} else if ("ecombo".equals(type)) {
287				jcomp = createComboBox(value, config, true);
288			} else if ("file".equals(type)) {
289				jcomp = createFileTextField(value, config, JFileChooser.FILES_ONLY);
290			} else if ("filedir".equals(type)) {
291				jcomp = createFileTextField(value, config, JFileChooser.FILES_AND_DIRECTORIES);
292			} else if ("label".equals(type)) {
293				jcomp = new JLabel(label);
294				addComponent(jcomp);
295				continue;
296			} else if ("radio".equals(type)) {
297				if (config == null) {
298					Log.log(Log.WARNING, this, "Radio button with no group: " + comp);
299					continue;
300				}
301				jcomp = createRadioButton(label, value, config);
302				label = null;
303
304				// add the radio group to the map, instead of the
305				// radio button
306				addComponent(jcomp);
307				cinst.put(prop, rgroups.get(config));
308				continue;
309			} else if ("sep".equals(type)) {
310				if (label != null) {
311					addSeparator(label);
312				} else {
313					addSeparator();
314				}
315				continue;
316			} else if ("text".equals(type)) {
317				jcomp = createTextField(value, config);
318			} else if ("textarea".equals(type)) {
319				jcomp = createTextArea(value, config);
320			} else {
321				Object ocomp = createComponent(type, label, value, config);
322				if (ocomp == null) {
323					Log.log(Log.WARNING, this, "Unknown type: " + type);
324					continue;
325				} else {
326					cinst.put(prop, ocomp);
327					continue;
328				}
329			}
330
331			cinst.put(prop, jcomp);
332
333			if (tooltip != null) {
334				jcomp.setToolTipText(tooltip);
335			}
336
337			if (label != null) {
338				addComponent(label, jcomp);
339			} else {
340				addComponent(jcomp);
341			}
342		}
343
344	}
345
346	public void _save() {
347		if (cinst == null) {
348			return;
349		}
350
351		for (Iterator it = cinst.keySet().iterator(); it.hasNext(); ) {
352			String prop = (String) it.next();
353			Object comp = cinst.get(prop);
354			setProperty(prop, parseComponent(comp, prop));
355		}
356	}
357
358	/**
359	 *	Sets whether empty strings in text fields should be converted to
360	 *	"null" when saving the values. This means the property will be
361	 *	"unset" is it's empty.
362	 */
363	public void setEmptyToNull(boolean flag) {
364		this.emptyToNull = flag;
365	}
366
367	/**
368	 *	Sets the internal component spec list. This will overwrite the
369	 *	existing list, if any. The behavior of the pane is undefined if
370	 *	this is called after {@link #_init()} has been called.
371	 */
372	protected void setComponentSpec(List components)
373	{
374		this.cspec = components;
375	}
376
377	/**
378	 *	Sets the properties object where the properties will be saved to.
379	 *	If the object is null, the jEdit properties store will be used.
380	 */
381	protected void setPropertyStore(Properties p)
382	{
383		this.pstore = p;
384	}
385
386	/**
387	 *	Returns the instance of the component that has been linked to
388	 *	the given property name. This will return null if called before
389	 *	{@link #_init()}. This might not return a JComponent if the
390	 *	property is for a radio group (in which case it returns a
391	 *	ButtonGroup).
392	 */
393	protected Object getComponent(String prop)
394	{
395		return (cinst != null) ? cinst.get(prop) : null;
396	}
397
398	/**
399	 *	Returns the value of the named property in the internal property
400	 *	store, of the given default value if the value is null.
401	 */
402	protected String getProperty(String name, String dflt)
403	{
404		if (pstore == null) {
405			return jEdit.getProperty(name, dflt);
406		} else {
407			String ret = pstore.getProperty(name);
408			return (ret == null) ? dflt : ret;
409		}
410	}
411
412	/**
413	 *	Sets the value of the named property in the internal property
414	 *	store.
415	 */
416	protected void setProperty(String name, String val)
417	{
418		if (val != null && val.length() == 0 && emptyToNull) {
419			val = null;
420		}
421		if (pstore == null) {
422			if (val != null) {
423				jEdit.setProperty(name, val);
424			} else {
425				jEdit.unsetProperty(name);
426			}
427		} else {
428			if (val != null) {
429				pstore.setProperty(name, val);
430			} else {
431				pstore.remove(name);
432			}
433		}
434	}
435
436	/**
437	 *	Removes the named property from the internal property store.
438	 */
439	protected void removeProperty(String name)
440	{
441		if (pstore == null) {
442			jEdit.unsetProperty(name);
443		} else {
444			pstore.remove(name);
445		}
446	}
447
448	/**
449	 *	Removes all properties related to a component as defined in the
450	 *	component spec list. If called before {@link #_init()}, this does
451	 *	nothing.
452	 */
453	protected void cleanup()
454	{
455		if (cinst == null) {
456			return;
457		}
458		for (Iterator it = cinst.keySet().iterator(); it.hasNext(); ) {
459			String prop = (String) it.next();
460			removeProperty(prop);
461		}
462	}
463
464	/**
465	 *	If an unknown type is found in the component spec, this method
466	 *	will be called. If it returns any object, the object will be added
467	 *	to the component map, but nothing will be automatically added to
468	 *	the GUI. If null is	returned, an error will be logged.
469	 *
470	 *	@param	type	The type string from the spec.
471	 *	@param	label	The label string (not the key for lookup!).
472	 *	@param	value	The value of the property, retrieved from the store.
473	 *	@param	config	The config string from the spec.
474	 */
475	protected Object createComponent(String type, String label,
476									 String value, String config)
477	{
478		return null;
479	}
480
481	/**
482	 *	This method is called to transform the contents of a component
483	 *	into a string to be persisted. The default implementation
484	 *	understands all the available types described in this class's
485	 *	javadoc. If you need special treatment for some specific
486	 *	component, or you added a custom component that is not handled
487	 *	by the default implementation, override this method.
488	 *
489	 *	@param	comp	The component added to the component map.
490	 * 	@param	name	The name of the property attached to the component.
491	 */
492	protected String parseComponent(Object comp, String name) {
493		String ret = null;
494		if (comp instanceof ButtonGroup) {
495			Enumeration buttons = ((ButtonGroup)comp).getElements();
496			int idx = 0;
497			while (buttons.hasMoreElements()) {
498				AbstractButton abtn = (AbstractButton) buttons.nextElement();
499				if (abtn.isSelected()) {
500					break;
501				}
502				idx++;
503			}
504			ret = String.valueOf(idx);
505		} else if (comp instanceof FileTextField) {
506			ret = ((FileTextField)comp).getTextField().getText();
507		} else if (comp instanceof JCheckBox) {
508			boolean val = ((JCheckBox)comp).isSelected();
509			ret = String.valueOf(val);
510		} else if (comp instanceof JComboBox) {
511			ret = ((JComboBox)comp).getSelectedItem().toString();
512		} else if (comp instanceof JTextComponent) {
513			ret = ((JTextComponent)comp).getText();
514		} else {
515			Log.log(Log.WARNING, this, "Unhandled component type: " + comp.getClass().getName());
516		}
517		return ret;
518	}
519
520	private JCheckBox createCheckBox(String label, String prop, String config)
521	{
522		JCheckBox ret = new JCheckBox(label);
523		ret.setSelected("true".equalsIgnoreCase(prop));
524		return ret;
525	}
526
527	private JComboBox createComboBox(String prop,
528	                                 String config,
529                                     boolean editable)
530	{
531		JComboBox ret = new JComboBox();
532		ret.setEditable(editable);
533
534		boolean found = false;
535		int idx = 0;
536		if (config != null) {
537			StringTokenizer vals = new StringTokenizer(config, ":");
538			while (vals.hasMoreTokens()) {
539				String next = vals.nextToken();
540				ret.addItem(next);
541				if (next.equals(prop)) {
542					found = true;
543				} else if (!found && vals.hasMoreTokens()) {
544					idx++;
545				}
546			}
547		}
548
549		if (!found && prop != null) {
550			ret.addItem(prop);
551		}
552		if (ret.getItemCount() > 0) {
553			ret.setSelectedIndex(idx);
554		}
555
556		return ret;
557	}
558
559	private FileTextField createFileTextField(String prop, String config, int mode)
560	{
561		FileTextField ret = new FileTextField("true".equalsIgnoreCase(config));
562		if (prop != null) {
563			ret.getTextField().setText(prop);
564		}
565		ret.setFileSelectionMode(mode);
566		return ret;
567	}
568
569	private JRadioButton createRadioButton(String label, String prop, String config)
570	{
571		JRadioButton ret = new JRadioButton(label);
572		if (config != null) {
573			if (rgroups == null) {
574				rgroups = new HashMap();
575			}
576
577			ButtonGroup grp = (ButtonGroup) rgroups.get(config);
578			if (grp == null) {
579				grp = new ButtonGroup();
580				rgroups.put(config, grp);
581			}
582
583			grp.add(ret);
584
585			int idx = 0;
586			try {
587				idx = Integer.parseInt(prop);
588			} catch (NumberFormatException nfe) {
589			}
590
591			ret.setSelected(idx == grp.getButtonCount());
592		}
593
594		return ret;
595	}
596
597	private JTextField createTextField(String prop, String config)
598	{
599		JTextField ret = new JTextField();
600		if (prop != null) {
601			ret.setText(prop);
602		}
603		return ret;
604	}
605
606	private JTextArea createTextArea(String prop, String config)
607	{
608		JTextArea ret = new JTextArea();
609		if (prop != null) {
610			ret.setText(prop);
611		}
612		if (config != null) {
613			try {
614				ret.setRows(Integer.parseInt(config));
615			} catch (NumberFormatException nfe) {
616				ret.setRows(5);
617			}
618		}
619		return ret;
620	}
621
622}