/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun * Microsystems, Inc. All Rights Reserved. */ package org.openide.explorer.propertysheet; import java.awt.*; import java.awt.datatransfer.*; import java.awt.event.*; import java.beans.*; import java.lang.reflect.Method; import javax.swing.*; import javax.swing.event.*; import org.openide.ErrorManager; import org.openide.actions.*; import org.openide.awt.SplittedPanel; import org.openide.util.HelpCtx; import org.openide.util.NbBundle; import org.openide.util.Mutex; import org.openide.util.WeakListener; import org.openide.util.Utilities; import org.openide.nodes.Node; import org.openide.nodes.NodeAdapter; import org.openide.nodes.NodeListener; import org.openide.nodes.NodeOperation; import org.openide.util.Lookup; import org.openide.util.RequestProcessor; /** * Implements a "property sheet" for a set of selected beans. * * <P> * <TABLE BORDER COLS=3 WIDTH=100%> * <TR><TH WIDTH=15%>Property<TH WIDTH=15%>Property Type<TH>Description * <TR><TD> <code>paintingStyle</code> <TD> <code>int</code> <TD> style of painting properties ({@link #ALWAYS_AS_STRING}, {@link #STRING_PREFERRED}, {@link #PAINTING_PREFERRED}) * <TR><TD> <code>currentPage</code> <TD> <code>int</code> <TD> currently showed page (e.g. properties, expert, events) * <TR><TD> <code>expert</code> <TD> <code>boolean</code> <TD> expert mode as in the JavaBeans specifications * </TABLE> * * @author Jan Jancura, Jaroslav Tulach * @version 1.23, Sep 07, 1998 */ public class PropertySheet extends JPanel { /** generated Serialized Version UID */ static final long serialVersionUID = -7698351033045864945L; // public constants ........................................................ /** Property giving current sorting mode. */ public static final String PROPERTY_SORTING_MODE = "sortingMode"; // NOI18N /** Property giving current value color. */ public static final String PROPERTY_VALUE_COLOR = "valueColor"; // NOI18N /** Property giving current disabled property color. */ public static final String PROPERTY_DISABLED_PROPERTY_COLOR = "disabledPropertyColor"; // NOI18N /** Property with the current page index. */ public static final String PROPERTY_CURRENT_PAGE = "currentPage"; // NOI18N /** Property for "plastic" mode. */ // NOI18N public static final String PROPERTY_PLASTIC = "plastic"; // NOI18N /** Property for the painting style. */ public static final String PROPERTY_PROPERTY_PAINTING_STYLE = "propertyPaintingStyle"; // NOI18N /** Property for whether only writable properties should be displayed. */ public static final String PROPERTY_DISPLAY_WRITABLE_ONLY = "displayWritableOnly"; // NOI18N /** Constant for showing properties as a string always. */ public static final int ALWAYS_AS_STRING = 1; /** Constant for preferably showing properties as string. */ public static final int STRING_PREFERRED = 2; /** Constant for preferably painting property values. */ public static final int PAINTING_PREFERRED = 3; /** Constant for unsorted sorting mode. */ public static final int UNSORTED = 0; /** Constant for by-name sorting mode. */ public static final int SORTED_BY_NAMES = 1; /** Constant for by-type sorting mode. */ public static final int SORTED_BY_TYPES = 2; /** Init delay for second change of the selected nodes. */ private static final int INIT_DELAY = 70; /** Maximum delay for repeated change of the selected nodes. */ private static final int MAX_DELAY = 350; /** Icon for the toolbar. * @deprecated Presumably noone uses this variable. If you want to customize * the property sheet look you can change the image files directly (or use your * own). */ static protected Icon iNoSort; /** Icon for the toolbar. * @deprecated Presumably noone uses this variable. If you want to customize * the property sheet look you can change the image files directly (or use your * own). */ static protected Icon iAlphaSort; /** Icon for the toolbar. * @deprecated Presumably noone uses this variable. If you want to customize * the property sheet look you can change the image files directly (or use your * own). */ static protected Icon iTypeSort; /** Icon for the toolbar. * @deprecated Presumably noone uses this variable. If you want to customize * the property sheet look you can change the image files directly (or use your * own). */ static protected Icon iDisplayWritableOnly; /** Icon for the toolbar. * @deprecated Presumably noone uses this variable. If you want to customize * the property sheet look you can change the image files directly (or use your * own). */ static protected Icon iCustomize; static final String PROP_HAS_CUSTOMIZER = "hasCustomizer"; // NOI18N static final String PROP_PAGE_HELP_ID = "pageHelpID"; // NOI18N private static String getString(String key) { return NbBundle.getBundle(PropertySheet.class).getString(key); } /** Remember the position of the splitter. This is used when the property window is re-used for another node */ private int savedSplitterPosition = SplittedPanel.FIRST_PREFERRED; // private variables for visual controls ........................................... private transient JTabbedPane pages; private transient EmptyPanel emptyPanel; private transient int pageIndex = 0; private transient ChangeListener tabListener = new ChangeListener () { public void stateChanged (ChangeEvent e) { int index = pages.getSelectedIndex (); setCurrentPage (index); } }; private Node activeNode; private NodeListener activeNodeListener; private boolean displayWritableOnly; private int propertyPaintingStyle; private int sortingMode; private boolean plastic; private Color disabledPropertyColor; private Color valueColor; // init ............................................................................. public PropertySheet() { setLayout (new BorderLayout ()); boolean problem = false; try { Class c = Class.forName("org.openide.explorer.propertysheet.PropertySheet$PropertySheetSettingsInvoker"); // NOI18N Runnable r = (Runnable)c.newInstance(); current.set(this); r.run(); } catch (Exception e) { problem = true; } catch (LinkageError le) { problem = true; } if (problem) { // set defaults without P ropertySheetSettings displayWritableOnly = false; propertyPaintingStyle = PAINTING_PREFERRED; sortingMode = SORTED_BY_NAMES; plastic = false; disabledPropertyColor = UIManager.getColor("textInactiveText"); valueColor = new Color (0, 0, 128); } pages = new HelpAwareJTabbedPane (); pages.getAccessibleContext().setAccessibleName(getString("ACS_PropertySheetTabs")); pages.getAccessibleContext().setAccessibleDescription(getString("ACSD_PropertySheetTabs")); pages.addChangeListener(tabListener); emptyPanel = new EmptyPanel (getString ("CTL_NoProperties")); pages.setTabPlacement (JTabbedPane.BOTTOM); // add (emptyPanel, BorderLayout.CENTER); PropertySheetToolbar p = new PropertySheetToolbar(this); p.setBorder (UIManager.getBorder ("Toolbar.border")); addPropertyChangeListener(p); add (p, BorderLayout.NORTH); // setNodes(new Node[0]); setPreferredSize(new Dimension(280, 300)); } /** Overridden to provide a larger preferred size if the default font * is larger, for locales that require this. */ public Dimension getPreferredSize() { //issue 34157, bad sizing/split location for Chinese locales that require //a larger default font size Dimension result = super.getPreferredSize(); int fontsize = javax.swing.UIManager.getFont ("Tree.font").getSize(); //NOI18N if (fontsize > 11) { int factor = fontsize - 11; result.height += 15 * factor; result.width += 50 * factor; Dimension screen = Utilities.getScreenSize(); if (result.height > screen.height) { result.height = screen.height -30; } if (result.width > screen.width) { result.width = screen.width -30; } } else { result.width += 20; result.height +=20; } return result; } static ThreadLocal current = new ThreadLocal(); /** Reference to PropertySheetSettings are separated here.*/ private static class PropertySheetSettingsInvoker implements Runnable { // constructor avoid IllegalAccessException during creating new instance public PropertySheetSettingsInvoker() {} public void run() { PropertySheet instance = (PropertySheet)current.get(); current.set(null); if (instance == null) { throw new IllegalStateException(); } PropertySheetSettings pss = PropertySheetSettings.getDefault(); instance.displayWritableOnly = pss.getDisplayWritableOnly(); instance.propertyPaintingStyle = pss.getPropertyPaintingStyle(); instance.sortingMode = pss.getSortingMode(); instance.plastic = pss.getPlastic(); instance.disabledPropertyColor = pss.getDisabledPropertyColor(); instance.valueColor = pss.getValueColor(); } } /** * Set the nodes explored by this property sheet. * * @param nodes nodes to be explored */ public void setNodes (Node[] nodes) { setHelperNodes (nodes); } // delayed setting nodes (partly impl issue 27781) private transient Node[] helperNodes; private synchronized void setHelperNodes (Node[] nodes) { RequestProcessor.Task task = getScheduleTask (); helperNodes = nodes; if (task.equals (initTask)) { //if task is only init task then set nodes immediatelly scheduleTask.schedule (0); task.schedule (INIT_DELAY); } else { // in a task run then increase delay and reschedule task int delay = task.getDelay () * 2; if (delay > MAX_DELAY) delay = MAX_DELAY; if (delay < INIT_DELAY) delay = INIT_DELAY; task.schedule (delay); } } private synchronized Node[] getHelperNodes () { return helperNodes; } private transient RequestProcessor.Task scheduleTask; private transient RequestProcessor.Task initTask; private synchronized RequestProcessor.Task getScheduleTask () { if (scheduleTask == null) { scheduleTask = RequestProcessor.getDefault ().post (new Runnable () { public void run () { Node[] nodes = getHelperNodes (); final Node n = (nodes.length == 1) ? nodes[0] : (nodes.length==0 ? null : new ProxyNode (nodes)); SwingUtilities.invokeLater (new Runnable () { public void run () { setCurrentNode (n); } }); } }); initTask = RequestProcessor.getDefault ().post (new Runnable () { public void run () { } }); } // if none task runs then return initTask to wait for next changes if (initTask.isFinished () && scheduleTask.isFinished ()) { return initTask; } // if some task runs then return schedule task which will set nodes return scheduleTask; } // end of delayed /** * Set property paint mode. * @param style one of {@link #ALWAYS_AS_STRING}, {@link #STRING_PREFERRED}, or {@link #PAINTING_PREFERRED} */ public void setPropertyPaintingStyle (int style) { if (style == propertyPaintingStyle) return; int oldVal = propertyPaintingStyle; propertyPaintingStyle = style; firePropertyChange(PROPERTY_PROPERTY_PAINTING_STYLE, new Integer(oldVal), new Integer(style)); } /** * Get property paint mode. * * @return the mode * @see #setPropertyPaintingStyle */ public int getPropertyPaintingStyle () { return propertyPaintingStyle; } /** * Set the sorting mode. * * @param sortingMode one of {@link #UNSORTED}, {@link #SORTED_BY_NAMES}, {@link #SORTED_BY_TYPES} */ public void setSortingMode (int sortingMode) throws PropertyVetoException { if (this.sortingMode == sortingMode) return; int oldVal = this.sortingMode; this.sortingMode = sortingMode; firePropertyChange(PROPERTY_SORTING_MODE, new Integer(oldVal), new Integer(sortingMode)); } /** * Get the sorting mode. * * @return the mode * @see #setSortingMode */ public int getSortingMode () { return sortingMode; } /** * Set the currently selected page. * * @param index index of the page to select */ public void setCurrentPage (int index) { if (pageIndex == index) return; pageIndex = index; if (index < 0) return; if (index != pages.getSelectedIndex ()) { pages.setSelectedIndex (index); } int selected = pages.getSelectedIndex(); if (selected >= 0) { Component comp = pages.getComponentAt(selected); if (comp instanceof PropertySheetTab) { ((PropertySheetTab)comp).ensurePaneCreated(); } } firePropertyChange(PROP_PAGE_HELP_ID, null, null); } /** * Set the currently selected page. * * @param str name of the tab to select */ public boolean setCurrentPage (String str) { int index = pages.indexOfTab (str); if (index < 0) return false; setCurrentPage (index); return true; } /** * Get the currently selected page. * @return index of currently selected page */ public int getCurrentPage () { return pages.getSelectedIndex (); } String getPageHelpID() { if (isAncestorOf(pages)) { Component comp = pages.getSelectedComponent(); if (comp instanceof PropertySheetTab) { String helpID = ((PropertySheetTab)comp).getHelpID(); if (helpID != null) { return helpID; } } } return null; } /** * Set whether buttons in sheet should be plastic. * @param plastic true if so */ public void setPlastic (boolean plastic) { if (this.plastic == plastic) return; this.plastic = plastic; firePropertyChange(PROPERTY_PLASTIC, plastic ? Boolean.FALSE:Boolean.TRUE, plastic ? Boolean.TRUE:Boolean.FALSE); } /** * Test whether buttons in sheet are plastic. * @return <code>true</code> if so */ public boolean getPlastic () { return plastic; } /** * Set the foreground color of values. * @param color the new color */ public void setValueColor (Color color) { if (valueColor.equals(color)) return; Color oldVal = valueColor; valueColor = color; firePropertyChange(PROPERTY_VALUE_COLOR, oldVal, color); } /** * Get the foreground color of values. * @return the color */ public Color getValueColor() { return valueColor; } /** * Set the foreground color of disabled properties. * @param color the new color */ public void setDisabledPropertyColor (Color color) { if (disabledPropertyColor.equals(color)) return; Color oldVal = disabledPropertyColor; disabledPropertyColor = color; firePropertyChange(PROPERTY_DISABLED_PROPERTY_COLOR, oldVal, color); } /** * Get the foreground color of disabled properties. * @return the color */ public Color getDisabledPropertyColor () { return disabledPropertyColor; } /** * Set whether only writable properties are displayed. * @param b <code>true</code> if this is desired */ public void setDisplayWritableOnly (boolean b) { if (displayWritableOnly == b) return; displayWritableOnly = b; firePropertyChange(PROPERTY_DISPLAY_WRITABLE_ONLY, b ? Boolean.FALSE:Boolean.TRUE, b ? Boolean.TRUE:Boolean.FALSE); } /** * Test whether only writable properties are currently displayed. * @return <code>true</code> if so */ public boolean getDisplayWritableOnly () { return displayWritableOnly; } void setSavedPosition (int savedPostion) { savedSplitterPosition = savedPostion; } int getSavedPosition () { return savedSplitterPosition; } // private helper methods .................................................................... private final String detachFromNode () { String result = null; if (activeNode != null) { activeNode.removeNodeListener( activeNodeListener ); attached = false; Node.PropertySet [] oldP = activeNode.getPropertySets(); if (oldP == null) { // illegal node behavior => log warning about it ErrorManager.getDefault ().log (ErrorManager.WARNING, "Node "+activeNode+": getPropertySets() returns null!"); // NOI18N oldP = new Node.PropertySet[] {}; } if ((pageIndex >= 0) && (pageIndex < oldP.length)) { result = oldP[pageIndex].getDisplayName(); } for (int i = 0, tabCount = pages.getTabCount(); i < tabCount; i++) { ((PropertySheetTab)pages.getComponentAt(i)).detachPropertyChangeListener(); } pages.removeAll(); } return result; } public void addNotify () { super.addNotify(); if (activeNode != null) { if (!attached) { attachToNode (activeNode); createPages(); if (storedTab != null) { navToCorrectPage (storedTab); storedTab = null; } else { if (pages.getTabCount() > 0) { String first = pages.getTitleAt(0); navToCorrectPage (first); } else { add (emptyPanel, BorderLayout.CENTER); } } } } else { remove (pages); add (emptyPanel, BorderLayout.CENTER); } } private String storedTab = null; public void removeNotify() { if (attached) storedTab = detachFromNode(); super.removeNotify(); } private boolean attached=false; private final void attachToNode (Node node) { //XXX There is probably no reason to be using WeakListener here - //attach and detach occasions are well-defined unless two threads //enter setCurrentNode concurrently. Test for this added to //setCurrentNode to determine if there are really cases of this // activeNodeListener = WeakListener.node (new ActiveNodeListener(), // activeNode); if (activeNodeListener == null) { activeNodeListener = new ActiveNodeListener(); } activeNode.addNodeListener(activeNodeListener); attached = true; } private final void createPages () { Node.PropertySet [] propsets = activeNode.getPropertySets(); if (propsets == null) { // illegal node behavior => log warning about it ErrorManager.getDefault ().log (ErrorManager.WARNING, "Node "+activeNode+": getPropertySets() returns null!"); // NOI18N propsets = new Node.PropertySet[] {}; } for (int i = 0, n = propsets.length; i < n; i++) { Node.PropertySet set = propsets[i]; if (set.isHidden()) continue; pages.addTab(set.getDisplayName(), null, new PropertySheetTab(set, activeNode, this), set.getShortDescription() ); } } private final void navToCorrectPage (String selectedTabName) { if (isAncestorOf(emptyPanel)) { remove(emptyPanel); } add(pages, BorderLayout.CENTER); if (selectedTabName != null) { setCurrentPage(selectedTabName); } int selected = pages.getSelectedIndex(); if (selected >= 0) { Component comp = pages.getComponentAt(selected); if (comp instanceof PropertySheetTab) { ((PropertySheetTab)comp).ensurePaneCreated(); } } } /** This has to be called from the AWT thread. */ private void setCurrentNode(Node node) { if (activeNode == node) return; //if this should only be called from the AWT thread, enforce it if (!(SwingUtilities.isEventDispatchThread())) { throw new IllegalStateException ("Current node for propertysheet set from off the AWT thread: " //NOI18N + Thread.currentThread()); } String selectedTabName = detachFromNode(); activeNode = node; if (getParent() == null) { return; } if (activeNode != null) { attachToNode (activeNode); createPages(); if (pages.getTabCount() > 0) { navToCorrectPage(selectedTabName); } else { if (isAncestorOf(pages)) { remove(pages); add(emptyPanel, BorderLayout.CENTER); } } } else { if (isAncestorOf(pages)) { remove(pages); add(emptyPanel, BorderLayout.CENTER); } } revalidate(); repaint(); if (activeNode != null && activeNode.hasCustomizer()) { firePropertyChange(PROP_HAS_CUSTOMIZER, null, Boolean.TRUE); } else { firePropertyChange(PROP_HAS_CUSTOMIZER, null, Boolean.FALSE); } firePropertyChange(PROP_PAGE_HELP_ID, null, null); } /** * Invokes the customization on the currently selected Node (JavaBean). */ void invokeCustomization () { NodeOperation.getDefault().customize(activeNode); } /** Show help on the selected tab. */ void invokeHelp() { HelpCtx h = new HelpCtx(getPageHelpID()); // Awkward but should work. Copied from NbTopManager.showHelp. try { Class c = ((ClassLoader)Lookup.getDefault().lookup(ClassLoader.class)).loadClass("org.netbeans.api.javahelp.Help"); // NOI18N Object o = Lookup.getDefault().lookup(c); if (o != null) { Method m = c.getMethod("showHelp", new Class[] {HelpCtx.class}); // NOI18N m.invoke(o, new Object[] {h}); return; } } catch (ClassNotFoundException cnfe) { // ignore - maybe javahelp module is not installed, not so strange } catch (Exception e) { // potentially more serious ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e); } // Did not work. Toolkit.getDefaultToolkit().beep(); } private class ActiveNodeListener extends NodeAdapter { int id; ActiveNodeListener() {} public void propertyChange(PropertyChangeEvent evt) { if (evt.getSource() != activeNode) { return; } if (evt.getPropertyName() == null || Node.PROP_PROPERTY_SETS.equals(evt.getPropertyName())) { // force refresh of the whole sheet, must be done in AWT event // thread Mutex.EVENT.readAccess(new Runnable() { public void run() { String selectedTabName = null; if (activeNode != null) { Node.PropertySet [] oldP = activeNode.getPropertySets(); if (oldP == null) { // illegal node behavior => log warning about it ErrorManager.getDefault ().log (ErrorManager.WARNING, "Node "+activeNode+": getPropertySets() returns null!"); // NOI18N oldP = new Node.PropertySet[] {}; } if ((pageIndex >= 0) && (pageIndex < oldP.length)) { selectedTabName = oldP[pageIndex].getDisplayName(); } } Node old = activeNode; setCurrentNode(null); setCurrentNode(old); if (selectedTabName != null) { setCurrentPage(selectedTabName); } } }); } } } /** JTabbedPane subclass which has a getHelpCtx method that will be * understood by HelpCtx.findHelp. */ private static final class HelpAwareJTabbedPane extends JTabbedPane implements HelpCtx.Provider { public HelpAwareJTabbedPane () { // XXX(-ttran) experimental code, needs cleanup before release if (Boolean.getBoolean("netbeans.scrolling.tabs")) { boolean jdk14 = org.openide.modules.Dependency.JAVA_SPEC.compareTo(new org.openide.modules.SpecificationVersion("1.4")) >= 0; if (jdk14) { try { java.lang.reflect.Method method = getClass().getMethod("setTabLayoutPolicy", new Class[] {Integer.TYPE}); method.invoke(this, new Object[] {new Integer(1)}); } catch(NoSuchMethodException nme) { } catch(SecurityException se) { } catch(IllegalAccessException iae) { } catch(IllegalArgumentException iare) { } catch(java.lang.reflect.InvocationTargetException ite) { } } } } /** Gets HelpCtx for currently selected tab, retrieved from * <code>Node.PropertySet</code> <em>getValue("helpID")</em> method. * @see PropertySheetTab#getHelpID */ public HelpCtx getHelpCtx () { Component comp = getSelectedComponent(); if(comp instanceof PropertySheetTab) { String helpID = ((PropertySheetTab)comp).getHelpID(); if(helpID != null) { return new HelpCtx(helpID); } } return HelpCtx.findHelp(getParent()); } } }