001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.netbeans.jellytools.properties;
042:
043: import java.awt.Component;
044: import java.awt.event.ActionEvent;
045: import java.beans.PropertyEditor;
046: import java.lang.reflect.InvocationTargetException;
047: import java.lang.reflect.Method;
048: import javax.swing.Action;
049: import javax.swing.JComponent;
050: import javax.swing.JTable;
051: import javax.swing.table.TableCellRenderer;
052: import org.netbeans.jellytools.JellyVersion;
053: import org.netbeans.jemmy.JemmyException;
054: import org.netbeans.jemmy.QueueTool;
055: import org.netbeans.jemmy.Waitable;
056: import org.netbeans.jemmy.Waiter;
057: import org.netbeans.jemmy.operators.JTableOperator;
058: import org.openide.ErrorManager;
059: import org.openide.explorer.propertysheet.editors.EnhancedPropertyEditor;
060: import org.openide.nodes.Node;
061:
062: /**
063: * Handles properties in IDE property sheets. Properties are grouped in
064: * property sheet. Their are identified by their display names. Once you
065: * have created a Property instance you can get value, set a new text value,
066: * set a new value by index of possible options or open custom editor.
067: * <p>
068: * Usage:<br>
069: * <pre>
070: PropertySheetOperator pso = new PropertySheetOperator("Properties of MyClass");
071: Property p = new Property(pso, "Name");
072: System.out.println("\nProperty name="+p.getName());
073: System.out.println("\nProperty value="+p.getValue());
074: p.setValue("ANewValue");
075: // set a new value by index where it is applicable
076: //p.setValue(2);
077: // open custom editor where it is applicable
078: //p.openEditor();
079: * </pre>
080: *
081: * @author Jiri.Skrivanek@sun.com
082: * @see PropertySheetOperator
083: */
084: public class Property {
085:
086: /** Class name of string renderer. */
087: public static final String STRING_RENDERER = "org.openide.explorer.propertysheet.RendererFactory$StringRenderer"; // NOI18N
088: /** Class name of check box renderer. */
089: public static final String CHECKBOX_RENDERER = "org.openide.explorer.propertysheet.RendererFactory$CheckboxRenderer"; // NOI18N
090: /** Class name of combo box renderer. */
091: public static final String COMBOBOX_RENDERER = "org.openide.explorer.propertysheet.RendererFactory$ComboboxRenderer"; // NOI18N
092: /** Class name of radio button renderer. */
093: public static final String RADIOBUTTON_RENDERER = "org.openide.explorer.propertysheet.RendererFactory$RadioButtonRenderer"; // NOI18N
094: /** Class name of set renderer. */
095: public static final String SET_RENDERER = "org.openide.explorer.propertysheet.RendererFactory$SetRenderer"; // NOI18N
096:
097: /** Instance of Node.Property. */
098: protected Node.Property property;
099: /** Property sheet where this property resides. */
100: protected PropertySheetOperator propertySheetOper;
101:
102: static {
103: // Checks if you run on correct jemmy version. Writes message to jemmy log if not.
104: JellyVersion.checkJemmyVersion();
105: }
106:
107: /** Waits for property with given name in specified property sheet.
108: * @param propertySheetOper PropertySheetOperator where to find property.
109: * @param name property display name
110: */
111: public Property(PropertySheetOperator propertySheetOper, String name) {
112: this .propertySheetOper = propertySheetOper;
113: this .property = waitProperty(propertySheetOper, name);
114: }
115:
116: /** Waits for index-th property in specified property sheet.
117: * @param propertySheetOper PropertySheetOperator where to find property.
118: * @param index index (row number) of property inside property sheet
119: * (starts at 0). If there categories shown in property sheet,
120: * rows occupied by their names must by added to index.
121: */
122: public Property(PropertySheetOperator propertySheetOper, int index) {
123: this .propertySheetOper = propertySheetOper;
124: this .property = waitProperty(propertySheetOper, index);
125: }
126:
127: /** Waits for property with given name in specified property sheet.
128: * @param propSheetOper PropertySheetOperator where to find property.
129: * @param name property display name
130: */
131: private Node.Property waitProperty(
132: final PropertySheetOperator propSheetOper, final String name) {
133: try {
134: Waiter waiter = new Waiter(new Waitable() {
135: public Object actionProduced(Object param) {
136: Node.Property property = null;
137: JTableOperator table = propSheetOper.tblSheet();
138: for (int row = 0; row < table.getRowCount(); row++) {
139: if (table.getValueAt(row, 1) instanceof Node.Property) {
140: property = (Node.Property) table
141: .getValueAt(row, 1);
142: if (propSheetOper.getComparator().equals(
143: property.getDisplayName(), name)) {
144: return property;
145: }
146: }
147: }
148: return null;
149: }
150:
151: public String getDescription() {
152: return ("Wait property " + name);
153: }
154: });
155: return (Node.Property) waiter.waitAction(null);
156: } catch (InterruptedException e) {
157: throw new JemmyException("Interrupted.", e);
158: }
159: }
160:
161: /** Waits for index-th property in specified property sheet.
162: * @param propSheetOper PropertySheetOperator where to find property.
163: * @param index index (row number) of property inside property sheet
164: * (starts at 0). If there are categories shown in property sheet,
165: * rows occupied by their names must by added to index.
166: */
167: private Node.Property waitProperty(
168: final PropertySheetOperator propSheetOper, final int index) {
169: try {
170: Waiter waiter = new Waiter(new Waitable() {
171: public Object actionProduced(Object param) {
172: JTableOperator table = propSheetOper.tblSheet();
173: if (table.getRowCount() <= index) {
174: // If table is empty or index out of bounds,
175: // it returns null to wait until table is populated by values
176: return null;
177: }
178: Object property = table.getValueAt(index, 1);
179: if (property instanceof Node.Property) {
180: return (Node.Property) property;
181: } else {
182: throw new JemmyException("On row " + index
183: + " in table there is no property");
184: }
185: }
186:
187: public String getDescription() {
188: return ("Wait property on row " + index + " in property sheet.");
189: }
190: });
191: //waiter.setOutput(TestOut.getNullOutput());
192: return (Node.Property) waiter.waitAction(null);
193: } catch (InterruptedException e) {
194: throw new JemmyException("Interrupted.", e);
195: }
196: }
197:
198: /** Gets display name of this property.
199: * It can differ from name given in constructor when only
200: * substring of property name is used there.
201: * @return display name of property
202: */
203: public String getName() {
204: return property.getDisplayName();
205: }
206:
207: /** Gets string representation of property value.
208: * @return value of property
209: */
210: public String getValue() {
211: return getPropertyEditor().getAsText();
212: }
213:
214: /** Sets value of this property to specified text. If a new value is
215: * not accepted, an information or error dialog is displayed by IDE.
216: * If property is not writable JemmyException is thrown.
217: * @param textValue text to be set in property (e.g. "a new value",
218: * "a new item from list", "false", "TRUE")
219: */
220: @SuppressWarnings("unchecked")
221: public void setValue(final String textValue) {
222: propertySheetOper.getOutput().printTrace(
223: "Setting value \"" + textValue + "\" of property \""
224: + getName() + "\".");
225: if (!isEnabled()) {
226: throw new JemmyException("Property \"" + getName()
227: + "\" is read only.");
228: }
229: final PropertyEditor pe = getPropertyEditor();
230: // run in dispatch thread
231: new QueueTool().invokeSmoothly(new Runnable() {
232: public void run() {
233: try {
234: pe.setAsText(textValue);
235: property.setValue(pe.getValue());
236: } catch (IllegalAccessException iae) {
237: ErrorManager.getDefault().notify(ErrorManager.USER,
238: iae);
239: } catch (IllegalArgumentException iare) {
240: ErrorManager.getDefault().notify(ErrorManager.USER,
241: iare);
242: } catch (InvocationTargetException ite) {
243: ErrorManager.getDefault().notify(ErrorManager.USER,
244: ite);
245: } catch (Exception e) {
246: throw new JemmyException(
247: "Exception while setting value of property.",
248: e);
249: }
250: }
251: });
252: }
253:
254: /** Sets value of this property by given index.
255: * It is applicable for properties which can be changed by combo box.
256: * If property doesn't support changing value by index JemmyException
257: * is thrown.
258: * @param index index of item to be selected from possible options
259: */
260: public void setValue(int index) {
261: String[] tags = getPropertyEditor().getTags();
262: if (tags != null) {
263: setValue(tags[index]);
264: } else {
265: throw new JemmyException(
266: "Property doesn't support changing value by index.");
267: }
268: }
269:
270: /** Opens custom property editor for the property by click on "..." button.
271: * It checks whether this property supports custom editor by method
272: * {@link #supportsCustomEditor}.
273: */
274: public void openEditor() {
275: if (supportsCustomEditor()) {
276: final JTableOperator table = propertySheetOper.tblSheet();
277: // Need to request focus before selection because invokeCustomEditor action works
278: // only when table is focused
279: table.makeComponentVisible();
280: table.requestFocus();
281: table.waitHasFocus();
282: // run action in a separate thread in AWT (no block)
283: new Thread(new Runnable() {
284: public void run() {
285: new QueueTool().invokeSmoothly(new Runnable() {
286: public void run() {
287: // need to select property first
288: ((javax.swing.JTable) table.getSource())
289: .changeSelection(getRow(), 0,
290: false, false);
291: // find action
292: Action customEditorAction = ((JComponent) table
293: .getSource()).getActionMap().get(
294: "invokeCustomEditor"); // NOI18N
295: customEditorAction
296: .actionPerformed(new ActionEvent(
297: table.getSource(), 0, null));
298: }
299: });
300: }
301: }, "Thread to open custom editor no block").start(); // NOI18N
302: return;
303: }
304: }
305:
306: /** Checks whether this property supports custom editor.
307: * @return true is property supports custom editor, false otherwise
308: */
309: public boolean supportsCustomEditor() {
310: return getPropertyEditor().supportsCustomEditor();
311: }
312:
313: /** Sets default value for this property. If default value is not available,
314: * it does nothing.
315: */
316: public void setDefaultValue() {
317: try {
318: property.restoreDefaultValue();
319: } catch (Exception e) {
320: throw new JemmyException(
321: "Exception while restoring default value.", e);
322: }
323: /*
324: nameButtonOperator().clickForPopup();
325: String menuItem = Bundle.getString("org.openide.explorer.propertysheet.Bundle",
326: "SetDefaultValue");
327: new JPopupMenuOperator().pushMenu(menuItem, "|");
328: // need to wait until value button is changed
329: new EventTool().waitNoEvent(100);
330: */
331: }
332:
333: /** Returns true if this property is enabled in property sheet, that means
334: * it is possible to change its value by inplace editor.
335: * @return true if this property is enabled, false otherwise
336: */
337: public boolean isEnabled() {
338: return property.canWrite();
339: }
340:
341: /** Returns true if this property can be edited as text by inplace text field.
342: * It can be both for string renderer or combo box renderer.
343: * @return true if this property can be edited, false otherwise
344: */
345: @SuppressWarnings("deprecation")
346: public boolean canEditAsText() {
347: if (property.canRead() && property.canWrite()) {
348: Boolean val = (Boolean) property.getValue("canEditAsText"); // NOI18N
349: if (val != null) {
350: return val.booleanValue();
351: }
352: PropertyEditor pe = getPropertyEditor();
353: if (pe instanceof EnhancedPropertyEditor
354: && pe.getTags() != null) {
355: return ((EnhancedPropertyEditor) pe)
356: .supportsEditingTaggedValues();
357: } else {
358: return pe.getTags() == null;
359: }
360: } else {
361: return false;
362: }
363: }
364:
365: /** Returns class name of renderer used to render this property. It can
366: * be used to determine whether correct renderer is used. Possible values
367: * are defined in constants {@link #STRING_RENDERER}, {@link #CHECKBOX_RENDERER},
368: * {@link #COMBOBOX_RENDERER}, {@link #RADIOBUTTON_RENDERER}, {@link #SET_RENDERER}.
369: * @return class name of renderer used to render this property:
370: * <UL>
371: * <LI>org.openide.explorer.propertysheet.RendererFactory$StringRenderer</LI>
372: * <LI>org.openide.explorer.propertysheet.RendererFactory$CheckboxRenderer</LI>
373: * <LI>org.openide.explorer.propertysheet.RendererFactory$ComboboxRenderer</LI>
374: * <LI>org.openide.explorer.propertysheet.RendererFactory$RadioButtonRenderer</LI>
375: * <LI>org.openide.explorer.propertysheet.RendererFactory$SetRenderer</LI>
376: * </UL>
377: * @see #STRING_RENDERER
378: * @see #CHECKBOX_RENDERER
379: * @see #COMBOBOX_RENDERER
380: * @see #RADIOBUTTON_RENDERER
381: * @see #SET_RENDERER
382: */
383: public String getRendererName() {
384: return getRenderer().getClass().getName();
385: }
386:
387: /** Returns component which represents renderer for this property. */
388: private Component getRenderer() {
389: final JTableOperator table = propertySheetOper.tblSheet();
390: int row = getRow();
391: // gets component used to render a value
392: TableCellRenderer renderer = table.getCellRenderer(row, 1);
393: Component comp = renderer.getTableCellRendererComponent(
394: (JTable) table.getSource(), table.getValueAt(row, 1),
395: false, false, row, 1);
396: // We need to find a real renderer because it can be embedded
397: // in ButtonPanel (supplies custom editor button "...")
398: // or IconPanel(supplies property marking).
399: try {
400: Class clazz = Class
401: .forName("org.openide.explorer.propertysheet.RendererPropertyDisplayer");
402: Method findInnermostRendererMethod = clazz
403: .getDeclaredMethod("findInnermostRenderer",
404: new Class[] { JComponent.class });
405: findInnermostRendererMethod.setAccessible(true);
406: comp = (Component) findInnermostRendererMethod.invoke(null,
407: new Object[] { comp });
408: } catch (Exception e) {
409: throw new JemmyException(
410: "RendererPropertyDisplayer.findInnermostRenderer() by reflection failed.",
411: e);
412: }
413: return comp;
414: }
415:
416: /** Gets short description for this property. Short description is also
417: * used in tooltip.
418: * @return short description for this property.
419: */
420: public String getShortDescription() {
421: return this .property.getShortDescription();
422: }
423:
424: /*
425: * @return row number of property inside property sheet (starts at 0).
426: * If there are categories shown in property sheet, rows occupied by their
427: * names must by taken into account.
428: */
429: public int getRow() {
430: JTableOperator table = this .propertySheetOper.tblSheet();
431: for (int row = 0; row < table.getRowCount(); row++) {
432: if (table.getValueAt(row, 1) instanceof Node.Property) {
433: if (this .property == (Node.Property) table.getValueAt(
434: row, 1)) {
435: return row;
436: }
437: }
438: }
439: throw new JemmyException(
440: "Cannot determine row number of property \""
441: + getName() + "\"");
442: }
443:
444: /** Returns property editor obtained by call PropUtils.getPropertyEditor().
445: * It should be safe in any circumstancies (e.g. when IDE starts supporting
446: * XML-based editor registration).
447: * @return PropertyEditor instance of this property.
448: */
449: private PropertyEditor getPropertyEditor() {
450: try {
451: Class clazz = Class
452: .forName("org.openide.explorer.propertysheet.PropUtils");
453: Method getPropertyEditorMethod = clazz.getDeclaredMethod(
454: "getPropertyEditor",
455: new Class[] { Node.Property.class });
456: getPropertyEditorMethod.setAccessible(true);
457: return (PropertyEditor) getPropertyEditorMethod.invoke(
458: null, new Object[] { property });
459: } catch (Exception e) {
460: throw new JemmyException(
461: "PropUtils.getPropertyEditor() by reflection failed.",
462: e);
463: }
464: }
465: }
|