/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */
/*
 * PropUtils.java
 *
 * Created on January 4, 2003, 7:31 PM
 */
package org.openide.explorer.propertysheet;

import javax.accessibility.Accessible;
import javax.accessibility.AccessibleContext;
import org.openide.*;
import org.openide.nodes.*;
import org.openide.nodes.Node.*;
import org.openide.util.*;

import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;

import java.beans.*;

import java.io.PrintWriter;
import java.io.StringWriter;

import java.lang.reflect.InvocationTargetException;

import java.text.MessageFormat;

import java.util.*;
import javax.accessibility.AccessibleRole;

import javax.swing.*;
import javax.swing.JComponent.AccessibleJComponent;
import javax.swing.border.Border;
import javax.swing.plaf.SplitPaneUI;
import javax.swing.plaf.basic.BasicSplitPaneDivider;
import javax.swing.plaf.basic.BasicSplitPaneUI;
import javax.swing.plaf.metal.*;

import org.netbeans.modules.openide.explorer.PsSettings;


/** A few utility methods useful to implementors of Inplace Editors.
 * @author  Tim Boudreau
 */
final class PropUtils {
    /**If true, hides custom editor buttons unless in editing mode.
     * Auto popup of combo boxes is suppressed in this mode */
    static boolean noCustomButtons = Boolean.getBoolean("netbeans.ps.noCustomButtons"); //NOI18N

    /**If true, radio button boolean editor will always be used */
    static boolean forceRadioButtons = 
        //          !Boolean.getBoolean ("netbeans.ps.useCheckbox");
        Boolean.getBoolean("netbeans.ps.forceRadioButtons");

    /**If true, caption on the checkbox boolean editor will be suppressed */
    static final boolean noCheckboxCaption = !Boolean.getBoolean("netbeans.ps.checkboxCaption"); //NOI18N

    /** Flag which, when true, property set expansion handles will not be
     * shown when the node only has one property set.  Leaving as an option
     * since there is still disagreement about the right way this should
     * work, and I don't want to repeatedly reimplement it */
    static final boolean hideSingleExpansion = Boolean.getBoolean("netbeans.ps.hideSingleExpansion"); //NOI18N
    static final boolean neverMargin = true;

    //    static final boolean neverMargin = Boolean.getBoolean(
    //        "netbeans.ps.neverMargin"); //NOI18N

    /** If true, user will have to press enter to write the value,
     *  otherwise (the default), if a cell loses focus, its value
     *  gets written.  */
    static final boolean psCommitOnFocusLoss = !Boolean.getBoolean("netbeans.ps.NoCommitOnFocusLoss");

    /** UIManager key for alternate color for table - if present, color will
     *  alternate between the standard background and this color    */
    private static final String KEY_ALTBG = "Tree.altbackground"; //NOI18N

    /** UIManager key for the background color for expandable sets.  If not
     *  set, the color will be derived from the default tree background
     *  color */
    private static final String KEY_SETBG = "PropSheet.setBackground"; //NOI18N

    /** UIManager key for background color of expandable sets when selected. If not
     *  set, the color will be derived from the default tree selection background
     *  color */
    private static final String KEY_SELSETBG = "PropSheet.selectedSetBackground"; //NOI18N

    /** UIManager key for foreground color of expandable sets. If not
     *  set, the color will be derived from the default tree selection background
     *  color */
    private static final String KEY_SETFG = "PropSheet.setForeground"; //NOI18N

    /** UIManager key for foreground color of expandable sets when selected. If not
     *  set, the color will be derived from the default tree selection background
     *  color */
    private static final String KEY_SELSETFG = "PropSheet.selectedSetForeground"; //NOI18N

    /** UIManager key for integer icon margin, amount of space to add beside
     *  the expandable set icon to make up the margin */
    private static final String KEY_ICONMARGIN = "netbeans.ps.iconmargin"; //NOI18N

    /** UIManager key for fixed row height for rows in property sheet.  If not
     *  explicitly set, row height will be derived from the target font */
    static final String KEY_ROWHEIGHT = "netbeans.ps.rowheight"; //NOI18N

    /** Preferences key for the show description area property */
    private static final String PREF_KEY_SHOWDESCRIPTION = "showDescriptionArea"; //NOI18N

    /** Preferences key for the storage of closed set names */
    private static final String PREF_KEY_CLOSEDSETNAMES = "closedSetNames"; //NOI18N

    /** Preferences key for the storage of sort order */
    private static final String PREF_KEY_SORTORDER = "sortOrder"; //NOI18N

    /** A scratch image for use when we need a Graphics object for calculating
     * size with no parent */
    static BufferedImage scratch = null;

    /** Disabled foreground color */
    static Color disFg = null;

    /** Factor by which default font is larger/smaller than 12 point, used for
     * calculating preferred sizes and compensating for larger font size */
    static float fsfactor = -1f;

    /** Minimum width for a property panel */
    static int minW = -1;

    /** Minimum height for a property panel */
    static int minH = -1;

    /** TextField foreground color for property panel */
    private static Color tfFg = null;

    /** TextField background color for property panel */
    private static Color tfBg = null;

    /** Flag for presence of an alternative background color (alternating
     * "zebra" style painting in property sheet) */
    static Boolean noAltBg = null;

    /** Icon used by the custom editor button */
    private static Icon bpIcon = null;

    /** Field to hold the width of the margin.  This is used for painting, so the
     *  grid is not displayed in the margin, and for figuring out if a mouse event
     *  occured in the margin (toggle expanded on a single click) or not.  */
    static int marginWidth = -1;

    /** Field to hold additional space between spinner icons and set text */
    private static int iconMargin = -1;

    /** Color for selected property set expanders - should be
     *  darker than the selection color for regular properties,
     *  to differentiate and be consistent with their unselected color */
    static Color selectedSetRendererColor = null;

    /** Color for property set expanders */
    static Color setRendererColor = null;

    /** Cached icon used for collapsed set renderers */
    static Icon collapsedIcon = null;

    /** Cached icon used for expanded set renderers */
    static Icon expandedIcon = null;

    /** Cached height of for the icon */
    static int spinnerHeight = -1;

    /** UIManager or derived control color */
    static Color controlColor = null;

    /** UIManager or derived shadow color */
    static Color shadowColor = null;

    /** Alternative background color */
    static Color altBg = null;

    /** Tab name for basic properties */
    private static String bptn = null;

    /** Comparator used by property sheet */
    private static Comparator comp = null;

    /** Left hand margin for properties in the right column of the sheet */
    private static int textMargin = -1;

    /** Foreground color for expando sets */
    private static Color setForegroundColor = null;

    /** Foreground color for expando sets when selected */
    private static Color selectedSetForegroundColor = null;

    /** Painting the custom editor button is the most expensive thing
     * we do on XP and Aqua; if this flag is set, the button panel
     * will build a bitmap and blit it (this isn't appropriate for
     * Metal, where the background color of the button changes if it
     * is selected */
    private static Boolean useOptimizedCustomButtonPainting = null;
    static boolean isAqua = "Aqua".equals(UIManager.getLookAndFeel().getID()); //NOI18N
    private static Graphics scratchGraphics = null;
    private static final String CAN_COMPARE_STRING = "Can compare only Node.Property instances.";

    //Comparators copied from original propertysheet implementation

    /** Comparator which compares types */
    private final static Comparator SORTER_TYPE = new Comparator() {
            public int compare(Object l, Object r) {
                if (!(l instanceof Node.Property)) {
                    throw new IllegalArgumentException(CAN_COMPARE_STRING); // NOI18N
                }

                if (!(r instanceof Node.Property)) {
                    throw new IllegalArgumentException(CAN_COMPARE_STRING); // NOI18N
                }

                Class t1 = ((Node.Property) l).getValueType();
                Class t2 = ((Node.Property) r).getValueType();
                String s1 = (t1 != null) ? t1.getName() : ""; //NOI18N
                String s2 = (t2 != null) ? t2.getName() : ""; //NOI18N

                int s = s1.compareToIgnoreCase(s2);

                if (s != 0) {
                    return s;
                }

                s1 = ((Node.Property) l).getDisplayName();
                s2 = ((Node.Property) r).getDisplayName();

                return s1.compareToIgnoreCase(s2);
            }

            public String toString() {
                return "Type comparator"; //NOI18N
            }
        };

    /** Comparator which compares PropertyDetils names */
    private final static Comparator SORTER_NAME = new Comparator() {
            public int compare(Object l, Object r) {
                if (!(l instanceof Node.Property)) {
                    throw new IllegalArgumentException(CAN_COMPARE_STRING);
                }

                if (!(r instanceof Node.Property)) {
                    throw new IllegalArgumentException(CAN_COMPARE_STRING);
                }

                String s1 = ((Node.Property) l).getDisplayName();
                String s2 = ((Node.Property) r).getDisplayName();

                return String.CASE_INSENSITIVE_ORDER.compare(s1, s2);
            }

            public String toString() {
                return "Name comparator"; //NOI18N
            }
        };

    private static java.util.List missing = null;

    // #52179 don't affect just edited properties or their current
    // changes will be lost due to the firing PropertyChangeEvents to
    // theirs UI counterpart
    private static Set externallyEdited = new HashSet(3);

    /** Private constructor to hide from API */
    private PropUtils() {
        //do nothing
    }

    /** If true, ButtonPanel will build a bitmap of the custom editor
     * button to use when painting - huge amounts of painting time in
     * XP and Aqua are used scaling the L&F's background button
     * bitmap (and the custom editor button is always a fixed size),
     * so this yields
     * better performance when a large number of custom editor buttons
     * are displayed. */
    static boolean useOptimizedCustomButtonPainting() {
        if (useOptimizedCustomButtonPainting == null) {
            if ("com.sun.java.swing.plaf.WindowsLookAndFeel".equals(UIManager.getLookAndFeel())) { //NOI18N
                useOptimizedCustomButtonPainting = isXPTheme() ? Boolean.TRUE : Boolean.FALSE;
            } else if ("Aqua".equals(UIManager.getLookAndFeel().getID())) { //NOI18N
                useOptimizedCustomButtonPainting = Boolean.TRUE;
            } else {
                useOptimizedCustomButtonPainting = Boolean.FALSE;
            }
        }

        return useOptimizedCustomButtonPainting.booleanValue();
    }

    static void log(Class clazz, String msg, boolean dumpstack) {
        log(clazz, msg);

        if (dumpstack) {
            dumpStack(clazz);
        }
    }

    //logging code borrowed from winsys
    static void log(Class clazz, String msg) {
        if (isLoggable(clazz)) {
            ErrorManager.getDefault().getInstance(clazz.getName()).log(msg);
        }
    }

    static void log(Class clazz, FocusEvent fe) {
        if (isLoggable(clazz)) {
            StringBuffer sb = new StringBuffer(30);
            focusEventToString(fe, sb);
            log(clazz, sb.toString());
        }
    }

    static boolean isLoggable(Class clazz) {
        if (System.getProperty(clazz.getName()) == null) {
            return false;
        }

        boolean result = ErrorManager.getDefault().getInstance(clazz.getName()).isLoggable(ErrorManager.INFORMATIONAL);

        return result;
    }

    static void logFocusOwner(Class clazz, String where) {
        if (isLoggable(clazz)) {
            StringBuffer sb = new StringBuffer(where);
            sb.append(" focus owner: "); //NOI18N

            Component owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
            compToString(owner, sb);
        }
    }

    static void focusEventToString(FocusEvent fe, final StringBuffer sb) {
        Component target = (Component) fe.getSource();
        Component opposite = (Component) fe.getOppositeComponent();
        sb.append(" focus "); //NOI18N
        sb.append((fe.getID() == FocusEvent.FOCUS_GAINED) ? " gained by " : " lost by "); //NOI18N
        compToString(target, sb);
        sb.append((fe.getID() == FocusEvent.FOCUS_GAINED) ? " from " : " to "); //NOI18N
        compToString(opposite, sb);
        sb.append(" isTemporary: "); //NOI18N
        sb.append(fe.isTemporary());
    }

    static void compToString(Component c, final StringBuffer sb) {
        if (c == null) {
            sb.append(" null "); //NOI18N

            return;
        }

        String name = c.getName();
        Class clazz = c.getClass();
        String classname = clazz.getName();
        int i = classname.lastIndexOf('.');

        if ((i != -1) && (i != (classname.length() - 1))) {
            classname = classname.substring(i + 1);
        }

        if (name != null) {
            sb.append("\""); //NOI18N
            sb.append(name);
            sb.append("\" ("); //NOI18N
            sb.append(classname);
            sb.append(") "); //NOI18N
        } else {
            sb.append(' '); //NOI18N
            sb.append(classname);
            sb.append(' '); //NOI18N
        }

        if (!c.isVisible()) {
            sb.append(" [NOT VISIBLE] "); //NOI18N
        }

        if (!c.isDisplayable()) {
            sb.append(" [HAS NO PARENT COMPONENT] "); //NOI18N
        }
    }

    public static void dumpStack(Class clazz) {
        // log(Class,String) only has an effect if INFORMATIONAL logging enabled on that prefix
        if (ErrorManager.getDefault().getInstance(clazz.getName()).isLoggable(ErrorManager.INFORMATIONAL)) {
            StringWriter sw = new StringWriter();
            new Throwable().printStackTrace(new PrintWriter(sw));
            log(clazz, sw.getBuffer().toString());
        }
    }

    /** Get the color for the custom editor button if specified by the theme or
     * look and feel, or null if the defaults should simply be used */
    static Color getButtonColor() {
        return UIManager.getColor("netbeans.ps.buttonColor"); //NOI18N
    }

    /** Get the width required by the custom editor button (this varies with
     * font size). */
    static int getCustomButtonWidth() {
        Icon ic = getCustomButtonIcon();

        return ic.getIconWidth() + (isAqua ? 5 : 3);
    }

    /** Check the myriad ways in which a property may be non-editable */
    static boolean checkEnabled(Component c, PropertyEditor editor, PropertyEnv env) {
        if (editor instanceof NoPropertyEditorEditor) {
            return false;
        }

        if (env != null) {
            Boolean canEditAsText = (Boolean) env.getFeatureDescriptor().getValue("canEditAsText"); // NOI18N

            if (!env.isEditable() || Boolean.FALSE.equals(canEditAsText)) {
                return false;
            }
        }

        return true;
    }

    /** Get a scratch graphics object which can be used to calculate string
     * widths offscreen */
    static Graphics getScratchGraphics(Component c) {
        if (scratchGraphics == null) {
            scratchGraphics = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB).getGraphics();
        }

        return scratchGraphics;
    }

    /** Get the color that should be used for text when an error or exception
     * is encountered in displaying a value.  Either the look and feel or
     * theme can supply a color via the UIDefaults key nb.errorColor or
     * a default (currently Color.RED) will be used */
    static Color getErrorColor() {
        //allow theme.xml to override error color
        Color result = UIManager.getColor("nb.errorForeground"); //NOI18N

        if (result == null) {
            result = Color.RED;
        }

        return result;
    }

    /** Get the foreground color for text on disabled components */
    static Color getDisabledForeground() {
        if (disFg == null) {
            disFg = UIManager.getColor("textInactiveText"); //NOI18N

            if (disFg == null) {
                disFg = Color.GRAY;
            }
        }

        return disFg;
    }

    /** Get a factor of the difference between the default font size NetBeans
     * uses, and the actual font size which may be different if the -fontsize
     * argument was used on startup. */
    static float getFontSizeFactor() {
        if (fsfactor == -1) {
            Font f = UIManager.getFont("controlFont"); //NOI18N

            if (f == null) {
                JLabel jl = new JLabel();
                f = jl.getFont();
            }

            int baseSize = 12; //default font size
            fsfactor = baseSize / f.getSize();
        }

        return fsfactor;
    }

    /** Minimum width for an instance of PropPanel, based on the default
     * font size */
    static int getMinimumPropPanelWidth() {
        if (minW == -1) {
            int base = 50;
            minW = Math.round(base * getFontSizeFactor());
        }

        return minW;
    }

    /** Minimum height for an instance of PropPanel based on the default
     * font size */
    static int getMinimumPropPanelHeight() {
        if (minH == -1) {
            int base = 18;
            minH = Math.round(base * getFontSizeFactor());
        }

        return minH;
    }

    /** Minimum size for an instance of PropPanel based on the default
     * font size */
    static Dimension getMinimumPanelSize() {
        return new Dimension(getMinimumPropPanelWidth(), getMinimumPropPanelHeight());
    }

    /** Update a property model with value provided by a property editor,
     * showing a dialog if the value is invalid  */
    static boolean updateProp(PropertyModel mdl, PropertyEditor ed, String title) {
        //        System.err.println("UPDATEPROP(model=" + mdl + ", editor=" + ed + ", title=" + title);
        Object newValue = ed.getValue();
        Object o = noDlgUpdateProp(mdl, ed);

        //        System.err.println(" result of noDlgUpdatePRop:" + o);
        if (o instanceof Exception) {
            if( o instanceof InvocationTargetException )
                o = ((InvocationTargetException)o).getTargetException();
            processThrowable((Exception) o, title, newValue);
        }

        boolean result = (o instanceof Boolean) ? ((Boolean) o).booleanValue() : false;

        //        System.err.println("RETURNING " + result);
        return result;
    }

    /** Try to update a property model with the value held by a property editor.
     * If any exceptions are thrown, return them - the invoker can choose to
     * process them as it wishes, but it is preferable to return them unaltered
     * rather than annotate using ErrorManager and force the client to fish
     * through the annotations to find the exception of interest */
    static Object noDlgUpdateProp(PropertyModel mdl, PropertyEditor ed) {
        //        System.err.println(" NO_DLG_UPDATE_PROP - editor value: " + ed.getValue() + " editor=" + ed + " model=" + mdl);
        Object newValue = ed.getValue();
        Object result = Boolean.FALSE;

        try {
            try {
                Object oldValue = mdl.getValue();

                //                System.err.println("Model old value is " + oldValue + " newValue is " + newValue);
                // test if newValue is not equal to oldValue
                if (((newValue != null) && !newValue.equals(oldValue)) || ((newValue == null) && (oldValue != null))) {
                    mdl.setValue(newValue);
                    result = Boolean.TRUE;
                } else {
                    //                    System.err.println(" New value equals old value - not trying to update");
                }
            } catch (ProxyNode.DifferentValuesException dve) {
                //issue 34982, failure setting value of JSP breakpoint,
                //and probably other cases as well
                mdl.setValue(newValue);
                result = Boolean.TRUE;
            }
        } catch (Exception e) {
            result = e;
        }

        //        System.err.println("NoDlgUpdateProp returning " + result);
        return result;
    }

    /** Try to update a property editor with a value object (presumably)
     * provided by an InplaceEditor.  If exceptions are thrown, returns them
     * for the caller to do with as it wishes (for example a panel that
     * commits multiple values from several editors at once will probably want to
     * aggregate them into a single error message) */
    static Exception updatePropertyEditor(PropertyEditor ed, Object value) {
        //        System.err.println("UpdatePropertyEditor " + ed + " to " + value );
        Exception result = null;

        try {
            if (value instanceof String) {
                ed.setAsText((String) value);
            } else {
                ed.setValue(value);
            }
        } catch (Exception e) {
            result = e;
        }

        //        System.err.println("returning " + result);
        return result;
    }

    /** Update a property using the model and value cached by an inplace editor */
    static boolean updateProp(InplaceEditor ine) {
        //        System.err.println("UPDATEPROP(inplaceEditor)");
        Component c = ine.getComponent();
        Cursor oldCursor = c.getCursor();

        try {
            c.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

            Object o = ine.getValue();
            Exception e = updatePropertyEditor(ine.getPropertyEditor(), o);

            //            System.err.println("UPDATE PROPERTY EDITOR RETURNED " + e);
            String newValString = (o == null) ? NbBundle.getMessage(PropUtils.class, "NULL") : o.toString(); //NOI18N

            if (e != null) {
                PropertyModel pm = ine.getPropertyModel();
                String propName;

                if (pm instanceof NodePropertyModel) {
                    Node.Property p = ((NodePropertyModel) pm).getProperty();
                    propName = p.getDisplayName();
                } else if (pm instanceof DefaultPropertyModel) {
                    propName = ((DefaultPropertyModel) pm).propertyName;
                } else {
                    //who knows what it is...
                    propName = NbBundle.getMessage(PropUtils.class, "MSG_unknown_property_name"); //NOI18N
                }

                processThrowable(e, propName, newValString);
            }

            boolean result = (e == null)
                ? PropUtils.updateProp(ine.getPropertyModel(), ine.getPropertyEditor(), newValString) : false;

            //            System.err.println("Returning " + result);
            return result;
        } finally {
            c.setCursor(oldCursor);
        }
    }

    /** Processes <code>Throwable</code> thrown from <code>setAsText</code>
     * or <code>setValue</code> call on <code>editor</code>. Helper method. */
    private static void processThrowable(Throwable throwable, String title, Object newValue) {
        //Copied from old PropertyPanel impl
        if (throwable instanceof ThreadDeath) {
            throw (ThreadDeath) throwable;
        }

        ErrorManager em = ErrorManager.getDefault();
        ErrorManager.Annotation[] anns = em.findAnnotations(throwable);

        if (((anns == null) || (anns.length == 0)) && (throwable.getLocalizedMessage() != throwable.getMessage())) { //XXX See issue 34569

            String msg = MessageFormat.format(
                    NbBundle.getMessage(PropUtils.class, "FMT_ErrorSettingProperty"), new Object[] { newValue, title }
                ); //NOI18N
            em.annotate(
                throwable, ErrorManager.USER, msg, throwable.getLocalizedMessage(), throwable, new java.util.Date()
            );
        } else if (throwable instanceof NumberFormatException) {
            //Handle NFE's from the core sun.beans property editors w/o raising stack traces
            em.annotate(
                throwable, ErrorManager.USER, throwable.getMessage(),
                MessageFormat.format(
                    NbBundle.getMessage(PropUtils.class, "FMT_BAD_NUMBER_FORMAT"), //NOI18N
                    new Object[] { newValue }
                ), null, null
            );
        }

        em.notify(throwable);
    }

    /** Fetches a localized message for an exception that may be displayed to
     * the user.  If no localized message can be found in the annotations,
     * it will provide a generic one.
     * @see org.openide.explorer.propertysheet.PropertyDisplayer.Editable.isModifiedValueLegal */
    static synchronized String findLocalizedMessage(Throwable throwable, Object newValue, String title) {
        try {
            if (throwable == null) {
                //need to catch this - for mysterious reasons, calling 
                //getLocalizedMessage on a null throwable hangs/or results in an
                //endless loop - thread will stop doing anything unrecoverably
                return null;
            }

            ErrorManager em = ErrorManager.getDefault();

            if (throwable.getLocalizedMessage() != throwable.getMessage()) {
                return throwable.getLocalizedMessage();
            }

            ErrorManager.Annotation[] anns = em.findAnnotations(throwable);

            if (throwable instanceof NumberFormatException) {
                //Handle NFE's from the core sun.beans property editors w/o raising stack traces
                return MessageFormat.format(
                    NbBundle.getMessage(PropUtils.class, "FMT_BAD_NUMBER_FORMAT"), //NOI18N
                    new Object[] { newValue }
                );
            } else if ( /* #34596 */
                anns != null
            ) {
                for (int i = 0; i < anns.length; i++) {
                    if ((anns[i].getLocalizedMessage() != null) && (anns[i].getSeverity() == ErrorManager.USER)) {
                        return anns[i].getLocalizedMessage();
                    }
                }
            }

            //No localized message could be found, log the exception
            ErrorManager.getDefault().annotate(throwable, ErrorManager.WARNING, null, null, null, null);

            //punt
            return MessageFormat.format(
                NbBundle.getMessage(PropUtils.class, "FMT_CannotUpdateProperty"), new Object[] { newValue, title }
            ); //NOI18N
        } catch (Exception e) {
            //We ABSOLUTELY cannot let this method throw exceptions or it will
            //quietly endlessly 
            ErrorManager.getDefault().notify(e);

            return null;
        }
    }

    /** Utility method to fetch a comparator for properties
     *  based on sorting mode defined in PropertySheet.  */
    static Comparator getComparator(int sortingMode) {
        switch (sortingMode) {
        case PropertySheet.UNSORTED:
            return null;

        case PropertySheet.SORTED_BY_NAMES:
            return SORTER_NAME;

        case PropertySheet.SORTED_BY_TYPES:
            return SORTER_TYPE;

        default:
            throw new IllegalArgumentException("Unknown sorting mode: " + Integer.toString(sortingMode)); //NOI18N
        }
    }

    /** Create a ComboBoxUI that does not display borders on the combo box.
     *  Since the property sheet is rendered as a table, this looks better.
     *  This UI also delegates closing of its popup to the property sheet, which
     *  has better knowledge of when this is appropriate, in the case of focus
     *  changes.  Inplace editors which employ a combo box should use the UI
     *  returned by this method, rather than the default for the look and feel.
     *  Thus the appearance will be consistent with the rest of the property
     *  sheet. */
    public static javax.swing.plaf.ComboBoxUI createComboUI(JComboBox box, boolean tableUI) {
        return new CleanComboUI(tableUI);
    }

    /** A convenience map for missing property editor classes, so users
     *  are only notified once, not every time a table cell is drawn.
     */
    private static java.util.List getMissing() {
        if (missing == null) {
            missing = new ArrayList();
        }

        return missing;
    }

    /** Gets a property editor appropriate to the class.  First
     *  checks property editors registered via XML layers, then
     *  falls back to <code>java.beans.PropertyEditorManager</code>.
     */
    static PropertyEditor getPropertyEditor(Class c) {
        PropertyEditor result = PropertyEditorManager.findEditor(c);

        if (result == null) {
            result = new NoPropertyEditorEditor();
        }

        return result;
    }

    /**
     * Call getPropertyEditor(Node.Property, boolean) method with
     * <code>updateEditor</code> as true.
     *
     * @see #getPropertyEditor(org.openide.nodes.Node.Property, boolean)
     */
    static PropertyEditor getPropertyEditor(Property p) {
        return getPropertyEditor(p, true);
    }

    /** Gets a property editor appropriate to the property.
     *  This method centralizes all code for fetching property editors
     *  used by the property sheet, such that future alternative
     *  registration systems for property editors may be easily
     *  implemented.
     *  <P><strong>Note:</strong>  This method will return a property
     *  editor with the value of the property already assigned.  Client
     *  code need not set the value in the property editor unless it
     *  should be changed, as this can result in unnecessary event
     *  firing.
     */
    static PropertyEditor getPropertyEditor(Property p, boolean updateEditor) {
        PropertyEditor result = p.getPropertyEditor();

        //XXX Next few lines replicate a hack in the original property sheet.
        //Correct solution is to move IndexedPropertyEditor & custom editor
        //to the Nodes package and just return an instance from
        //IndexedProperty.getPropertyEditor.  Appears to be here due to some
        //kind of dependancy avoidance.
        if (p instanceof Node.IndexedProperty && (result == null)) {
            result = new IndexedPropertyEditor();

            // indexed property editor does not want to fire immediately
            p.setValue(PropertyEnv.PROP_CHANGE_IMMEDIATE, Boolean.FALSE);
        }

        if (result == null) {
            result = getPropertyEditor(p.getValueType()); //XXX is this agood idea?
        }

        //handle a type with no registered property editor here
        if (result == null) {
            java.util.List missing = getMissing();
            String type = p.getValueType().getName();

            if (!(missing.contains(type))) {
                ErrorManager.getDefault().log(
                    ErrorManager.INFORMATIONAL, "No property editor registered for type " + type
                ); //NOI18N
                missing.add(type);
            }

            result = new NoPropertyEditorEditor();
        } else if (p.canRead()) {
            try {
                try {
                    try {
                        if (
                            ((p.getValueType() == Boolean.class) || (p.getValueType() == Boolean.TYPE)) &&
                                (p.getValue() == null)
                        ) {
                            // Allows Module folder nodes that use null to 
                            // indicate indeterminate state to work
                            result = new Boolean3WayEditor();
                        }

                        if (updateEditor) {
                            updateEdFromProp(p, result, p.getDisplayName());
                        }
                    } catch (ProxyNode.DifferentValuesException dve) {
                        if ((p.getValueType() == Boolean.class) || (p.getValueType() == Boolean.TYPE)) {
                            result = new Boolean3WayEditor();
                        } else {
                            result = new DifferentValuesEditor(result);
                        }
                    }
                } catch (IllegalAccessException iae) {
                    IllegalStateException ise = new IllegalStateException("Error getting property value"); //NOI18N
                    ErrorManager.getDefault().annotate(ise, iae);
                    throw ise;
                }
            } catch (InvocationTargetException ite) {
                IllegalStateException ise = new IllegalStateException("Error getting property value"); //NOI18N
                ErrorManager.getDefault().annotate(ise, ite);
                throw ise;
            }
        }

        return result;
    }

    /** Update a property editor from a Node.Property, such that the
     * value in the property editor will reflect the value of the property */
    private static void updateEdFromProp(Property p, PropertyEditor ed, String title)
    throws ProxyNode.DifferentValuesException, IllegalAccessException, InvocationTargetException {
        Object newValue = p.getValue();

        //IZ 44152, monstrous length strings from the debugger - skip the
        //equality test and blindly assign
        if (newValue instanceof String && (((String) newValue).length() > 2048)) {
            ed.setValue(newValue);

            return;
        }

        Object oldValue = ed.getValue();

        if ((newValue == null) && (oldValue == null)) {
            return;
        }

        // test if newValue is not equal to oldValue
        if (((newValue != null) && !newValue.equals(oldValue)) || ((newValue == null) && (oldValue != null))) {
            // <RAVE>
            // #5050567 Kind of mysterious livelock experienced here.
            // When updating arrays.
            // The question is whether the setter of the bean has to check if the value
            // is equal and not fire property change.. or is if the responsibility
            // is also here.
            if (
                oldValue instanceof Object[] && newValue instanceof Object[] &&
                    Arrays.equals((Object[]) oldValue, (Object[]) newValue)
            ) {
                return;
            }

            // </RAVE>
            ed.setValue(newValue);
        }
    }

    /** Get the basic color for controls in the look and feel, or a reasonable
     * default if the look and feel does not supply a value from
     * UIManager.getColor(&quot;control&quot;) */
    static Color getControlColor() {
        if (controlColor == null) {
            deriveColorsAndMargin();
        }

        return controlColor;
    }

    /** Get the shadow color specified by the look and feel, or a reasonable
     * default if none was specified */
    static Color getShadowColor() {
        if (shadowColor == null) {
            deriveColorsAndMargin();
        }

        return shadowColor;
    }

    /** Get the alternate background color, if any.  If non-null, the table will
     * show every other row using the color specified here */
    static Color getAltBg() {
        if (altBg == null) {
            deriveColorsAndMargin();
        }

        return altBg;
    }

    /** Determine if an alternate background color has been specified in the
     * look and feel or theme.  If one is supplied, then the property sheet
     * table will not show a grid, but instead, every other line will have
     * a different background color.  This method simply avoids repeated
     * attempts to look up the alternate background color if none was specified */
    static boolean noAltBg() {
        if (noAltBg == null) {
            noAltBg = (UIManager.getColor(KEY_ALTBG) == null) ? //NOI18N
                Boolean.TRUE : Boolean.FALSE;
        }

        return noAltBg.booleanValue();
    }

    /** Get the forground color for text editable property editors in edit
     * mode.  This is required because the tree selection color is typically
     * the same color as the tree selection color.  So if a string editor is
     * given the background color of a selected row in the table, it is
     * impossible to tell when some text in the field has been selected,
     * because they will be the same color.   */
    static Color getTextFieldBackground() {
        if (tfBg == null) {
            tfBg = UIManager.getColor("TextField.background"); //NOI18N

            if (tfBg == null) {
                tfBg = UIManager.getColor("text"); //NOI18N
            }

            if (tfBg == null) {
                tfBg = Color.WHITE;
            }
        }

        return tfBg;
    }

    /** Get the forground color for text editable property editors in edit
     * mode.  This is required because the tree selection color is typically
     * the same color as the tree selection color. */
    static Color getTextFieldForeground() {
        if (tfFg == null) {
            tfFg = UIManager.getColor("TextField.foreground"); //NOI18N

            if (tfFg == null) {
                tfFg = UIManager.getColor("textText"); //NOI18N
            }

            if (tfFg == null) {
                tfFg = Color.BLACK;
            }
        }

        return tfFg;
    }

    /** Initialize the various colors we will be using. */
    private static void deriveColorsAndMargin() {
        controlColor = UIManager.getColor("control"); //NOI18N

        if (controlColor == null) {
            controlColor = Color.LIGHT_GRAY;
        }

        int red;
        int green;
        int blue;

        boolean windows = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel".equals(
                UIManager.getLookAndFeel().getClass().getName()
            );

        boolean aqua = "Aqua".equals(UIManager.getLookAndFeel().getID());

        setRendererColor = UIManager.getColor(KEY_SETBG); //NOI18N
        selectedSetRendererColor = UIManager.getColor(KEY_SELSETBG); //NOI18N

        if (setRendererColor == null) {
            if (aqua) {
                setRendererColor = new Color(225, 235, 240);
            } else {
                if (setRendererColor == null) {
                    red = adjustColorComponent(controlColor.getRed(), -25, -25);
                    green = adjustColorComponent(controlColor.getGreen(), -25, -25);
                    blue = adjustColorComponent(controlColor.getBlue(), -25, -25);
                    setRendererColor = new Color(red, green, blue);
                }
            }
        }

        if (aqua) {
            selectedSetRendererColor = UIManager.getColor("Table.selectionBackground");
        }

        if (selectedSetRendererColor == null) {
            Color col = windows ? UIManager.getColor("Table.selectionBackground")
                                : UIManager.getColor("activeCaptionBorder"); //NOI18N

            if (col == null) {
                col = Color.BLUE;
            }

            red = adjustColorComponent(col.getRed(), -25, -25);
            green = adjustColorComponent(col.getGreen(), -25, -25);
            blue = adjustColorComponent(col.getBlue(), -25, -25);
            selectedSetRendererColor = new Color(red, green, blue);
        }

        shadowColor = UIManager.getColor("controlShadow"); //NOI18N

        if (shadowColor == null) {
            shadowColor = Color.GRAY;
        }

        setForegroundColor = UIManager.getColor(KEY_SETFG);

        if (setForegroundColor == null) {
            setForegroundColor = UIManager.getColor("Table.foreground"); //NOI18N

            if (setForegroundColor == null) {
                setForegroundColor = UIManager.getColor("textText");

                if (setForegroundColor == null) {
                    setForegroundColor = Color.BLACK;
                }
            }
        }

        selectedSetForegroundColor = UIManager.getColor(KEY_SELSETFG);

        if (selectedSetForegroundColor == null) {
            selectedSetForegroundColor = UIManager.getColor("Table.selectionForeground"); //NOI18N

            if (selectedSetForegroundColor == null) {
                selectedSetForegroundColor = Color.WHITE;
            }
        }

        altBg = UIManager.getColor(KEY_ALTBG); //NOI18N

        if (altBg == null) {
            altBg = UIManager.getColor("Tree.background"); //NOI18N

            if (altBg == null) {
                altBg = Color.WHITE;
            }

            noAltBg = Boolean.TRUE;
        } else {
            noAltBg = Boolean.FALSE;
        }

        collapsedIcon = UIManager.getIcon("Tree.collapsedIcon"); //NOI18N

        if (collapsedIcon == null) {
            collapsedIcon = new Icon() {
                        public int getIconHeight() {
                            return 7;
                        }

                        public int getIconWidth() {
                            return 7;
                        }

                        public void paintIcon(Component c, Graphics g, int x, int y) {
                            //do nothing
                        }
                    };
        }

        expandedIcon = UIManager.getIcon("Tree.expandedIcon"); //NOI18N

        if (expandedIcon == null) {
            //Hack for jdk 1.4.2 beta GTK L&F
            expandedIcon = collapsedIcon;
        }

        //GTK absurdly provides a 0 width icons which will figure out its size
        //the first time it paints
        if (((expandedIcon != null) && (expandedIcon.getIconHeight() <= 0)) || (expandedIcon.getIconHeight() <= 0)) {
            expandedIcon = new WrapperSizeIcon(expandedIcon);
        }

        if (((collapsedIcon != null) && (collapsedIcon.getIconHeight() <= 0)) || (collapsedIcon.getIconHeight() <= 0)) {
            collapsedIcon = new WrapperSizeIcon(collapsedIcon);
        }

        if (collapsedIcon != null) {
            marginWidth = Math.max(14, collapsedIcon.getIconWidth() - 2);
        } else {
            //on the off chance a L&F doesn't provide this
            marginWidth = 13;
        }

        //        System.err.println("Collapsed icon height " + (collapsedIcon == null ? " null " : Integer.toString (collapsedIcon.getIconHeight())));
        Integer i = (Integer) UIManager.get(KEY_ICONMARGIN); //NOI18N

        if (i != null) {
            iconMargin = i.intValue();
        } else {
            if (
                "com.sun.java.swing.plaf.windows.WindowsLookAndFeel".equals(
                        UIManager.getLookAndFeel().getClass().getName()
                    )
            ) {
                iconMargin = 4;
            } else {
                iconMargin = 0;
            }
        }

        i = (Integer) UIManager.get(KEY_ROWHEIGHT); //NOI18N

        if (i != null) {
            spinnerHeight = i.intValue();
        } else {
            spinnerHeight = expandedIcon.getIconHeight();
        }
    }

    /** Get the icon displayed by an expanded set.  Typically this is just the
     * same icon the look and feel supplies for trees */
    static Icon getExpandedIcon() {
        if (expandedIcon == null) {
            deriveColorsAndMargin();
        }

        return expandedIcon;
    }

    /** Get the icon displayed by a collapsed set. Typically this is just the
     * icon the look and feel supplies for trees */
    static Icon getCollapsedIcon() {
        if (collapsedIcon == null) {
            deriveColorsAndMargin();
        }

        return collapsedIcon;
    }

    /** Get the color for expandable sets when they are not selected */
    static Color getSetRendererColor() {
        if (setRendererColor == null) {
            deriveColorsAndMargin();
        }

        return setRendererColor;
    }

    /** Get the background color for expandable sets when they are selected */
    static Color getSelectedSetRendererColor() {
        if (selectedSetRendererColor == null) {
            deriveColorsAndMargin();
        }

        return selectedSetRendererColor;
    }

    /** Get the color for expandable sets when not selected */
    static Color getSetForegroundColor() {
        if (setForegroundColor == null) {
            deriveColorsAndMargin();
        }

        return setForegroundColor;
    }

    /** Get the text color for expandable sets when they are selected */
    static Color getSelectedSetForegroundColor() {
        if (selectedSetForegroundColor == null) {
            deriveColorsAndMargin();
        }

        return selectedSetForegroundColor;
    }

    /** Get the total width that the left side margin should have.  This is
     * calculated based on the width of the expandable set icon plus some
     * spacing */
    static int getMarginWidth() {
        if (marginWidth == -1) {
            deriveColorsAndMargin();
        }

        return marginWidth;
    }

    /** Get the height of the expansion icon */
    static int getSpinnerHeight() {
        if (spinnerHeight == -1) {
            deriveColorsAndMargin();
        }

        return spinnerHeight;
    }

    /** Get the number of pixels that should separate the expandable set
     * icon from the left edge of the property sheet.  For Metal, this can
     * be zero; for the smaller icon supplied by Windows look and feels,
     * it should be a larger number */
    static int getIconMargin() {
        if (iconMargin == -1) {
            deriveColorsAndMargin();
        }

        return iconMargin;
    }

    /** Lazily creates the custom editor button icon */
    static Icon getCustomButtonIcon() {
        if (bpIcon == null) {
            bpIcon = new BpIcon();
        }

        return bpIcon;
    }

    /** Adjust an rgb color component.
     * @param base the color, an RGB value 0-255
     * @param adjBright the amount to subtract if base > 128
     * @param adjDark the amount to add if base <=128  */
    private static int adjustColorComponent(int base, int adjBright, int adjDark) {
        if (base > 128) {
            base -= adjBright;
        } else {
            base += adjDark;
        }

        if (base < 0) {
            base = 0;
        }

        if (base > 255) {
            base = 255;
        }

        return base;
    }

    /** Get the localized name for the default category of properties - in
     * English this is &quot;Properties&quot;. Used to provide the basic category
     * and tab name */
    static String basicPropsTabName() {
        if (bptn == null) {
            bptn = NbBundle.getMessage(PropUtils.class, "LBL_BasicTab"); //NOI18N
        }

        return bptn;
    }

    static Comparator getTabListComparator() {
        if (comp == null) {
            comp = new TabListComparator();
        }

        return comp;
    }

    static SplitPaneUI createSplitPaneUI() {
        return new CleanSplitPaneUI();
    }

    /*
    private static Preferences getPreferences() {
        return Preferences.userNodeForPackage(
            PropertySheet.class);
    }
     */

    //    private static boolean shouldShowDescription=true; //XXX pending registry
    static boolean shouldShowDescription() {
        //        return shouldShowDescription; //XXX pending registry
        //        return getPreferences().getBoolean(PREF_KEY_SHOWDESCRIPTION, true);
        return PsSettings.getDefault().getBoolean(PREF_KEY_SHOWDESCRIPTION, true);
    }

    static void saveShowDescription(boolean b) {
        //        shouldShowDescription = b; //XXX pending registry
        //        getPreferences().putBoolean(PREF_KEY_SHOWDESCRIPTION, b);
        PsSettings.getDefault().putBoolean(PREF_KEY_SHOWDESCRIPTION, b);
    }

    static String[] getSavedClosedSetNames() {
        //      String s = getPreferences().get(PREF_KEY_CLOSEDSETNAMES, null);
        //        String s = savedClosedSetNames; //XXX pending registry
        String s = PsSettings.getDefault().get(PREF_KEY_CLOSEDSETNAMES, null);

        if (s != null) {
            StringTokenizer tok = new StringTokenizer(s, ","); //NOI18N
            String[] result = new String[tok.countTokens()];
            int i = 0;

            while (tok.hasMoreElements()) {
                result[i] = tok.nextToken();
                i++;
            }

            return result;
        } else {
            return new String[0];
        }
    }

    //    private static String savedClosedSetNames=""; //XXX pending registry 
    static void putSavedClosedSetNames(Set s) {
        if (s.size() > 0) {
            StringBuffer sb = new StringBuffer(s.size() * 20);
            Iterator i = s.iterator();

            while (i.hasNext()) {
                sb.append(i.next());

                if (i.hasNext()) {
                    sb.append(','); //NOI18N
                }
            }

            //            savedClosedSetNames=sb.toString(); //XXX pending registry
            //            getPreferences().put (PREF_KEY_CLOSEDSETNAMES, sb.toString());
            PsSettings.getDefault().put(PREF_KEY_CLOSEDSETNAMES, sb.toString());
        } else {
            //            getPreferences().put (PREF_KEY_CLOSEDSETNAMES, "");
            //            savedClosedSetNames=""; //XXX pending registry
            PsSettings.getDefault().put(PREF_KEY_CLOSEDSETNAMES, "");
        }
    }

    //    private static int sortOrder=PropertySheet.UNSORTED;
    static void putSortOrder(int i) {
        //        getPreferences().putInt(PREF_KEY_SORTORDER, i);
        //        sortOrder=i;
        PsSettings.getDefault().putInt(PREF_KEY_SORTORDER, i);
    }

    static int getSavedSortOrder() {
        //        return getPreferences().getInt(PREF_KEY_SORTORDER, PropertySheet.UNSORTED);
        //        return sortOrder;
        return PsSettings.getDefault().getInt(PREF_KEY_SORTORDER, PropertySheet.UNSORTED);
    }

    /** Fetch the margin that should come between the edge of a property sheet
     * cell and its text.  The default is 2, or it can be customized via the
     * UIManager integer key netbeans.ps.textMargin */
    static int getTextMargin() {
        if ("apple.laf.AquaLookAndFeel".equals(UIManager.getLookAndFeel().getClass().getName())) {
            return 0;
        }

        if (textMargin == -1) {
            Object o = UIManager.get("netbeans.ps.textMargin"); //NOI18N

            if (o instanceof Integer) {
                textMargin = ((Integer) o).intValue();
            } else {
                textMargin = 2;
            }
        }

        return textMargin;
    }

    /** HTML-ize a tooltip, splitting long lines */
    static String createHtmlTooltip(String title, String s) {
        boolean wasHtml = false;

        if (s.matches("\\<(html|HTML)\\>.*\\<\\/(html|HTML)\\>")) { // NOI18N
            s = s.replaceAll("\\<\\/{0,1}(html|HTML)\\>", ""); // NOI18N
            wasHtml = true;
        }

        // break up massive tooltips
        String token = null;

        if (s.indexOf(" ") != -1) { //NOI18N
            token = " "; //NOI18N
        } else if (s.indexOf(",") != -1) { //NOI18N
            token = ","; //NOI18N
        } else if (s.indexOf(";") != -1) { //NOI18N
            token = ";"; //NOI18N
        } else if (s.indexOf("/") != -1) { //NOI18N
            token = "/"; //NOI18N
        } else if (s.indexOf("\\") != -1) { //NOI18N
            token = "\\"; //NOI18N
        } else {
            //give up
            return s;
        }

        StringTokenizer tk = new StringTokenizer(s, token, true);

        StringBuffer sb = new StringBuffer(s.length() + 20);
        sb.append("<html>"); //NOI18N
        sb.append("<b><u>"); //NOI18N
        sb.append(title);
        sb.append("</u></b><br>"); //NOI18N

        int charCount = 0;
        int lineCount = 0;

        while (tk.hasMoreTokens()) {
            String a = tk.nextToken();

            if (!wasHtml) {
                // HTML-ize only non-html values. HTML values should already
                // contain correct HTML strings.
                a = Utilities.replaceString(a, "&", "&amp;"); //NOI18N
                a = Utilities.replaceString(a, "<", "&lt;"); //NOI18N
                a = Utilities.replaceString(a, ">", "&gt;"); //NOI18N
            }

            charCount += a.length();
            sb.append(a);

            if (tk.hasMoreTokens()) {
                charCount++;
            }

            if (charCount > 80) {
                sb.append("<br>"); //NOI18N
                charCount = 0;
                lineCount++;

                if (lineCount > 10) {
                    //Don't let things like VCS variables create
                    //a tooltip bigger than the screen. 99% of the
                    //time this is not a problem.
                    sb.append(NbBundle.getMessage(PropUtils.class, "MSG_ELLIPSIS")); //NOI18N

                    return sb.toString();
                }
            }
        }

        sb.append("</html>"); //NOI18N

        return sb.toString();
    }

    /** ButtonPanel and IconPanel implement inplace editor and proxy the
     * inner component that is the thing the user actually interacts with.
     * This method will locate the inner component of interest */
    static InplaceEditor findInnermostInplaceEditor(InplaceEditor ine) {
        while (ine instanceof IconPanel || ine instanceof ButtonPanel) {
            if (ine instanceof IconPanel) {
                ine = ((IconPanel) ine).getInplaceEditor();
            } else {
                ine = ((ButtonPanel) ine).getInplaceEditor();
            }
        }

        return ine;
    }

    /** Utility method to determine if the left edge margin should
     * be painted.  This is determined by whether the property sheet
     * items are sorted by name or natural sort, or can be overridden
     * globally with a startup flag */
    static boolean shouldDrawMargin(PropertySetModel psm) {
        if (neverMargin) {
            return false;
        }

        //hide margin and expansion handle if only one property
        //set, if flag is set (there is disagreement about the
        //right thing to do here, so it's an option for now)
        int setCount = psm.getSetCount();

        if (psm.getComparator() != null) {
            return false;
        }

        boolean includeMargin = (setCount > 1) || (!((setCount == 1) && hideSingleExpansion));

        return includeMargin;
    }

    /** Returns a fixed foreground color for the custom button icon on win xp.
     * On win xp, the button background is a white bitmap, so if we use the
     * white foreground color for the cell, the ... in the icon seems to
     * disappear when a row with a custom editor button is selected.
     * @see org.netbeans.core.NbTheme.installCustomDefaultsWinXP */
    private static final Color getIconForeground() {
        return UIManager.getColor("PropSheet.customButtonForeground"); //NOI18N
    }

    public static boolean isXPTheme() {
        Boolean isXP = (Boolean) Toolkit.getDefaultToolkit().getDesktopProperty("win.xpstyle.themeActive");

        return (isXP == null) ? false : isXP.booleanValue();
    }

    /**
     * Just a helper method which delegates to shallBeRDVEnabled(Node.Property).
     */
    static final boolean shallBeRDVEnabled(FeatureDescriptor fd) {
        if ((fd != null) && fd instanceof Node.Property) {
            return shallBeRDVEnabled((Node.Property) fd);
        }

        return false;
    }

    /**
     * Returns if the "Restore Default Value" action should be enabled for the
     * given <code>Node.Property</code>. If the properties is not null, is
     * instanceof Node.Property, supportsDefaultValue() and isDefaultValue()
     * returns true the property shall be enabled. Otherwise not.
     * <p>
     * Note that due to the backward compatibilty we return true also for
     * properties which only override Node.Property.supportsDefaultValue() to
     * return true and don't override Node.Property.isDefaultValue(). The
     * isDefaultValue() return false by default.<br>
     * For more information and detailed reason why we do so see
     * <a href="http://www.netbeans.org/issues/show_bug.cgi?id=51907">
     * Issue 51907</a>.
     */
    static final boolean shallBeRDVEnabled(Node.Property property) {
        if ((property == null) || !property.supportsDefaultValue()) {
            return false;
        }

        try {
            if (property.getClass().getMethod("isDefaultValue", null).getDeclaringClass() == Node.Property.class) {
                // if the given property didn't override isDefaultValue method
                // but override supportsDefaultValue the RDV should be always
                // enabled
                return true;
            } else {
                return !property.isDefaultValue();
            }
        } catch (NoSuchMethodException e) {
            // cannot happen since isDefaultValue is defined in Node.Property
            assert false : "No isDefaultValue in " + property.getClass() + ": " + e;

            return true; // satisfy compiler for case when assertion is disabled
        }
    }

    static void addExternallyEdited(Property p) {
        externallyEdited.add(p);
    }

    static void removeExternallyEdited(Property p) {
        externallyEdited.remove(p);
    }

    static boolean isExternallyEdited(Property p) {
        return externallyEdited.contains(p);
    }

    /** Property editor for properties which belong to more than one property, but have
     *  different values.   */
    static final class DifferentValuesEditor implements PropertyEditor {
        private PropertyEditor ed;
        private boolean notSet = true;

        public DifferentValuesEditor(PropertyEditor ed) {
            this.ed = ed;
        }

        public void addPropertyChangeListener(PropertyChangeListener listener) {
            ed.addPropertyChangeListener(listener);
        }

        public String getAsText() {
            String result;

            if (notSet) {
                result = NbBundle.getMessage(PropUtils.class, "CTL_Different_Values"); //NOI18N
            } else {
                result = ed.getAsText();
            }

            return result;
        }

        public java.awt.Component getCustomEditor() {
            return ed.getCustomEditor();
        }

        public String getJavaInitializationString() {
            return ed.getJavaInitializationString();
        }

        public String[] getTags() {
            return ed.getTags();
        }

        public Object getValue() {
            Object result;

            if (notSet) {
                result = null;
            } else {
                result = ed.getValue();
            }

            return result;
        }

        public boolean isPaintable() {
            return notSet ? false : ed.isPaintable();
        }

        public void paintValue(java.awt.Graphics gfx, java.awt.Rectangle box) {
            //issue 33341 - don't allow editors to paint if value not set
            if (isPaintable()) {
                ed.paintValue(gfx, box);
            }
        }

        public void removePropertyChangeListener(PropertyChangeListener listener) {
            ed.removePropertyChangeListener(listener);
        }

        public void setAsText(String text) throws java.lang.IllegalArgumentException {
            ed.setAsText(text);
            notSet = false;
        }

        public void setValue(Object value) {
            ed.setValue(value);
            notSet = false;
        }

        public boolean supportsCustomEditor() {
            return ed.supportsCustomEditor();
        }
    }

    /** Dummy property editor for properties which have no real editor.
     *  The property sheet does not handle null property editors;  this editor
     * stands in, and returns &quot;No property editor&quot; from getAsText() */
    static final class NoPropertyEditorEditor implements PropertyEditor {
        public void addPropertyChangeListener(PropertyChangeListener listener) {
            //no-op
        }

        public String getAsText() {
            return NbBundle.getMessage(PropertySheet.class, "CTL_NoPropertyEditor"); //NOI18N
        }

        public java.awt.Component getCustomEditor() {
            return null;
        }

        public String getJavaInitializationString() {
            return "";
        }

        public String[] getTags() {
            return null;
        }

        public Object getValue() {
            return getAsText();
        }

        public boolean isPaintable() {
            return false;
        }

        public void paintValue(java.awt.Graphics gfx, java.awt.Rectangle box) {
            //no-op
        }

        public void removePropertyChangeListener(PropertyChangeListener listener) {
            //no-op
        }

        public void setAsText(String text) throws java.lang.IllegalArgumentException {
            //no-op
        }

        public void setValue(Object value) {
            //no-op
        }

        public boolean supportsCustomEditor() {
            return false;
        }
    }

    /** Comparator for sorting the list of tabs such that basic properties
     *  is always the first tab. */
    private static class TabListComparator implements Comparator {
        public int compare(Object o1, Object o2) {
            String s1 = (String) o1;
            String s2 = (String) o2;

            if (s1 == s2) {
                return 0;
            }

            String bn = basicPropsTabName();

            if (bn.equals(s1)) {
                return -1;
            }

            if (bn.equals(s2)) {
                return 1;
            }

            return s1.compareTo(s2);
        }
    }

    private static class CleanSplitPaneUI extends BasicSplitPaneUI {
        protected void installDefaults() {
            super.installDefaults();
            divider.setBorder(new SplitBorder());
        }
        
        public BasicSplitPaneDivider createDefaultDivider() {
            return new CleanSplitPaneDivider(this);
        }
    }
    
    private static class CleanSplitPaneDivider extends BasicSplitPaneDivider implements Accessible {
        private AccessibleContext accessibleContext;
        
        public CleanSplitPaneDivider( BasicSplitPaneUI ui ) {
            super( ui );
        }
        public AccessibleContext getAccessibleContext() {
            if( null == accessibleContext ) {
                accessibleContext = new AccessibleAWTComponent() {
                            public AccessibleRole getAccessibleRole() {
                                return AccessibleRole.SPLIT_PANE;
                            }
                        };

                accessibleContext.setAccessibleName( NbBundle.getMessage(DescriptionComponent.class, "ACS_Splitter") );
                accessibleContext.setAccessibleDescription( NbBundle.getMessage(DescriptionComponent.class, "ACSD_Splitter") );
            }
            return accessibleContext;
        }
    }

    /** A split border subclass that does not draw the metal drag texture */
    private static class SplitBorder implements Border {
        public Insets getBorderInsets(Component c) {
            if (UIManager.getLookAndFeel() instanceof MetalLookAndFeel) {
                return new Insets(2, 0, 1, 0);
            } else {
                return new Insets(1, 0, 1, 0);
            }
        }

        public boolean isBorderOpaque() {
            return true;
        }

        public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
            if (UIManager.getLookAndFeel() instanceof MetalLookAndFeel) {
                g.setColor(UIManager.getColor("controlShadow"));
                g.drawLine(x, y, x + width, y);
                g.setColor(UIManager.getColor("controlHighlight"));
                g.drawLine(x, y + 1, x + width, y + 1);
                g.drawLine(x, (y + height) - 1, x + width, (y + height) - 1);
                g.setColor(UIManager.getColor("controlShadow"));
                g.drawLine(x, (y + height) - 2, x + width, (y + height) - 2);
            } else {
                //don't do flush 3d for non-metal L&F
                g.setColor(UIManager.getColor("controlHighlight"));
                g.drawLine(x, y, x + width, y);
                g.setColor(UIManager.getColor("controlShadow"));
                g.drawLine(x, (y + height) - 1, x + width, (y + height) - 1);
            }
        }
    }

    /** The ... icon for the custom editor button.  Will draw slightly
     * larger if the font size is greater */
    static class BpIcon implements Icon {
        boolean larger;

        public BpIcon() {
            Font f = UIManager.getFont("Table.font"); //NOI18N
            larger = (f != null) ? (f.getSize() > 13) : false;
        }

        public int getIconHeight() {
            return 12;
        }

        public int getIconWidth() {
            return larger ? 16 : 12;
        }

        public void paintIcon(Component c, Graphics g, int x, int y) {
            int w = c.getWidth();
            int h = c.getHeight();
            int ybase = h - 5;
            int pos2 = (w / 2);
            int pos1 = pos2 - 4;
            int pos3 = pos2 + 4;
            g.setColor((getIconForeground() == null) ? c.getForeground() : getIconForeground());
            drawDot(g, pos1 + 1, ybase, larger);
            drawDot(g, pos2, ybase, larger);
            drawDot(g, pos3 - 1, ybase, larger);
        }

        private void drawDot(Graphics g, int x, int y, boolean larger) {
            if (!larger) {
                g.drawLine(x, y, x, y);
            } else {
                g.drawLine(x - 1, y, x + 1, y);
                g.drawLine(x, y - 1, x, y + 1);
            }
        }
    }

    /**
     * Workaround for GTK L&F - they provide an icon which does not know its
     * width or height until it has been painted.  So for it to work correctly,
     * we have this silliness.
     */
    private static final class WrapperSizeIcon implements Icon {
        private Icon wrapped;

        public WrapperSizeIcon(Icon wrapped) {
            this.wrapped = wrapped;
        }

        public int getIconWidth() {
            int result = wrapped.getIconWidth();

            if (result <= 0) {
                result = 11;
            }

            return result;
        }

        public int getIconHeight() {
            int result = wrapped.getIconHeight();

            if (result <= 0) {
                result = 11;
            }

            return result;
        }

        public void paintIcon(Component c, Graphics g, int x, int y) {
            wrapped.paintIcon(c, g, x, y);
        }
    }
}
