PageRenderTime 59ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

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

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