/*
* 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.event.*;
import java.beans.*;
import java.util.*;
import javax.swing.*;
import org.openide.awt.JPopupMenuPlus;
import org.openide.awt.MouseUtils;
import org.openide.awt.SplittedPanel;
import org.openide.ErrorManager;
import org.openide.nodes.Node;
import org.openide.actions.PopupAction;
import org.openide.util.actions.ActionPerformer;
import org.openide.util.actions.CallbackSystemAction;
import org.openide.util.actions.SystemAction;
import org.openide.util.NbBundle;
import org.openide.util.WeakListener;
import org.openide.util.Mutex;
/**
* A JPanel in property sheet showing a set of properties. The set
* is represented by a Node.PropertySet instance.
* @author David Strupl
*/
class PropertySheetTab extends JPanel implements PropertyChangeListener {
/** Panel with SheetButtons with names of properties. */
private NamesPanel namesPanel;
/** Panel with PropertyPanels displaying the property values. */
private NamesPanel valuesPanel;
/** Set of properties in this tab. */
private Node.PropertySet properties;
/** */
private Node node;
/**
* Maps property name (String) --> model from property panel
* (PropertyModel)
*/
private HashMap modelCache;
/** Using this stop stop neverending loops caused
* by nodes that fire property change inside getXXX methods.
*/
private boolean changeInProgress;
/** Listens on changes in the global settings for sorter and
* displayWritableOnly.
*/
private SettingsListener settingsListener;
/** Comparator for instances of Node.Property */
private Comparator sorter;
/** Value of this can be one of the constants defined in PropertySheet
* (UNSORTED, SORTED_BY_NAMES, SORTED_BY_TYPES).
*/
private int sortingMode;
/** When it's true only writable properties are shown. */
private PropertySheet mySheet;
/** 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 only Node.Property instances."); // NOI18N
}
if (! (r instanceof Node.Property)) {
throw new IllegalArgumentException("Can compare only Node.Property instances."); // NOI18N
}
Class t1 = ((Node.Property)l).getValueType();
Class t2 = ((Node.Property)r).getValueType();
String s1 = t1 != null ? t1.getName() : "";
String s2 = t2 != null ? t2.getName() : "";
int s = s1.compareToIgnoreCase (s2);
if (s != 0) return s;
s1 = ((Node.Property)l).getDisplayName();
s2 = ((Node.Property)r).getDisplayName();
return s1.compareToIgnoreCase(s2);
}
};
/** Comparator which compares PropertyDeatils 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 only Node.Property instances."); // NOI18N
}
if (! (r instanceof Node.Property)) {
throw new IllegalArgumentException("Can compare only Node.Property instances."); // NOI18N
}
String s1 = ((Node.Property)l).getDisplayName();
String s2 = ((Node.Property)r).getDisplayName();
return String.CASE_INSENSITIVE_ORDER.compare(s1, s2);
}
};
/** Popup menu used for in this sheet. */
private JPopupMenu popupMenu;
/** Creates new PropertySheetTab */
public PropertySheetTab(Node.PropertySet properties, Node node, PropertySheet mySheet) {
this.properties = properties;
this.node = node;
modelCache = new HashMap();
this.mySheet = mySheet;
setLayout (new BorderLayout ());
add (new EmptyPanel (properties.getDisplayName()), BorderLayout.CENTER);
try {
setSortingMode(mySheet.getSortingMode());
} catch (PropertyVetoException x) {
ErrorManager.getDefault ().notify (x);
}
settingsListener = new SettingsListener();
mySheet.addPropertyChangeListener(
WeakListener.propertyChange(
settingsListener,
mySheet
)
);
}
public void addNotify () {
super.addNotify();
if (node != null) node.addPropertyChangeListener (this);
}
public void removeNotify () {
if (node != null) node.removePropertyChangeListener (this);
super.removeNotify();
}
void detachPropertyChangeListener() {
node.removePropertyChangeListener( this );
}
void setActions (final Node.Property pd) {
final CallbackSystemAction setDefault = (CallbackSystemAction)SystemAction
.get(SetDefaultValueAction.class);
// Enable / Disable DefaultValueAction
if (pd.supportsDefaultValue () && pd.canWrite ()) {
setDefault.setActionPerformer (new ActionPerformer () {
public void performAction (SystemAction a) {
try {
pd.restoreDefaultValue ();
// workaround of bug #21182: forces a node's property change
propertyChange (
new PropertyChangeEvent (this, pd.getName (), null, null));
} catch (Exception e) {
setDefault.setActionPerformer (null);
}
}
});
} else {
setDefault.setActionPerformer (null);
}
}
private boolean paneCreated;
void ensurePaneCreated() {
if (!paneCreated) {
createPane();
}
}
/**
* Displays either empty panel or namesPanel and valuesPanel.
*/
private void createPane () {
paneCreated = true;
Component c = getComponent(0);
if (properties.getProperties().length == 0) {
if ((c != null) && (c instanceof EmptyPanel)) {
// empty panel already there
return;
}
removeAll ();
add (new EmptyPanel (properties.getDisplayName()), BorderLayout.CENTER);
invalidate();
validate();
repaint();
return;
}
if (namesPanel == null) {
namesPanel = new NamesPanel ();
valuesPanel = new NamesPanel (namesPanel);
} else {
namesPanel.removeAll ();
valuesPanel.removeAll ();
}
if ((c == null) || !(c instanceof JScrollPane)) {
removeAll ();
JScrollPane scrollPane = new JScrollPane ();
scrollPane.setBorder (null);
SplittedPanel splittedPanel = new ScrollableSplittedPanel (scrollPane, namesPanel);
splittedPanel.add (namesPanel, SplittedPanel.ADD_LEFT);
splittedPanel.add (valuesPanel, SplittedPanel.ADD_RIGHT);
splittedPanel.setSplitAbsolute (true);
scrollPane.setViewportView (splittedPanel);
scrollPane.setHorizontalScrollBarPolicy (JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.getVerticalScrollBar ().setUnitIncrement (25);
add (scrollPane, BorderLayout.CENTER);
}
fillProperties();
}
/**
* Sorts the properties with a sorter. Creates a SheetButtons and
* PropertyPanels for the display and adds them to namesPanel and
* valuesPanel.
*/
private void fillProperties() {
Node.Property[]p = properties.getProperties();
ArrayList a = new ArrayList(p.length);
for (int i = 0; i < p.length; i++) {
if (mySheet.getDisplayWritableOnly() && !p[i].canWrite()) {
continue;
}
a.add(p[i]);
}
if (sorter != null) {
Collections.sort(a, sorter);
}
Object [] beans = new Object[] { node };
if (node instanceof ProxyNode) {
beans = ((ProxyNode)node).getOriginalNodes();
}
for (Iterator i = a.iterator(); i.hasNext(); ) {
final Node.Property prop = (Node.Property)i.next();
class LazyToolTipSheetButton extends SheetButton {
/** cache it, and do not compute it until requested */
private String toolTipText = null;
Node.Property pr;
public LazyToolTipSheetButton(Node.Property pr) {
super(pr.getDisplayName(), false, true);
// Cause it to be registered with manager:
this.setToolTipText("dummy"); // NOI18N
this.pr = pr;
}
public String getToolTipText(MouseEvent event) {
if (toolTipText == null) {
toolTipText = getToolTipTextForProperty(pr);
}
return toolTipText;
}
}
final SheetButton leftButton = new LazyToolTipSheetButton(prop);
leftButton.setFocusTraversable(false);
namesPanel.add(leftButton);
PropertyPanel rightPanel = new PropertyPanel(prop, beans);
modelCache.put(prop.getName(), rightPanel.getModel());
valuesPanel.add(rightPanel);
ButtonListener listener = new ButtonListener(leftButton, rightPanel);
rightPanel.addSheetButtonListener(listener);
leftButton.addSheetButtonListener(listener);
leftButton.setPlastic(rightPanel.getPlastic());
if (prop.canWrite()) {
leftButton.setActiveForeground(mySheet.getValueColor());
} else {
leftButton.setActiveForeground(mySheet.getDisabledPropertyColor());
}
leftButton.addMouseListener (
new MouseUtils.PopupMouseAdapter () {
public void showPopup (MouseEvent ev) {
setActions(prop);
createPopup();
popupMenu.show (leftButton, ev.getX(), ev.getY ());
}
}
);
rightPanel.addSheetButtonListener(
new InstallPerformerListener(rightPanel));
}
invalidate();
validate();
repaint();
}
/**
* Set whether buttons in sheet should be plastic.
* @param plastic true if so
*/
void setPlastic (boolean plastic) {
int count = namesPanel.getComponentCount();
for (int i = 0; i < count; i++) {
if (namesPanel.getComponent(i) instanceof SheetButton) {
((SheetButton)namesPanel.getComponent(i)).setPlastic(plastic);
}
}
count = valuesPanel.getComponentCount();
for (int i = 0; i < count; i++) {
if (valuesPanel.getComponent(i) instanceof PropertyPanel) {
((PropertyPanel)valuesPanel.getComponent(i)).setPlastic(plastic);
}
}
}
/**
* Set the foreground color of values.
* @param color the new color
*/
void setForegroundColor (Color color) {
int count = namesPanel.getComponentCount();
for (int i = 0; i < count; i++) {
if (namesPanel.getComponent(i) instanceof SheetButton) {
((SheetButton)namesPanel.getComponent(i)).setActiveForeground(color);
}
}
count = valuesPanel.getComponentCount();
for (int i = 0; i < count; i++) {
if (valuesPanel.getComponent(i) instanceof PropertyPanel) {
((PropertyPanel)valuesPanel.getComponent(i)).setForegroundColor(color);
}
}
}
/**
* Set the foreground color of disabled properties.
* @param color the new color
*/
void setDisabledColor (Color color) {
int count = namesPanel.getComponentCount();
for (int i = 0; i < count; i++) {
if (namesPanel.getComponent(i) instanceof SheetButton) {
((SheetButton)namesPanel.getComponent(i)).setInactiveForeground(color);
}
}
count = valuesPanel.getComponentCount();
for (int i = 0; i < count; i++) {
if (valuesPanel.getComponent(i) instanceof PropertyPanel) {
((PropertyPanel)valuesPanel.getComponent(i)).setDisabledColor(color);
}
}
}
void setPaintingStyle(int style) {
int count = valuesPanel.getComponentCount();
for (int i = 0; i < count; i++) {
if (valuesPanel.getComponent(i) instanceof PropertyPanel) {
((PropertyPanel)valuesPanel.getComponent(i)).setPaintingStyle(style);
}
}
}
/**
* 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 {
switch (sortingMode) {
case PropertySheet.UNSORTED:
sorter = null;
break;
case PropertySheet.SORTED_BY_NAMES:
sorter = SORTER_NAME;
break;
case PropertySheet.SORTED_BY_TYPES:
sorter = SORTER_TYPE;
break;
default:
throw new PropertyVetoException (
getString ("EXC_Unknown_sorting_mode"),
new PropertyChangeEvent (this, PropertySheet.PROPERTY_SORTING_MODE,
new Integer (this.sortingMode),
new Integer (sortingMode))
);
}
int oldSortingMode = this.sortingMode;
this.sortingMode = sortingMode;
firePropertyChange(PropertySheet.PROPERTY_SORTING_MODE, oldSortingMode, this.sortingMode);
}
/**
* Get the sorting mode.
*
* @return the mode
* @see #setSortingMode
*/
public int getSortingMode () {
return sortingMode;
}
/** Gets help ID for this property sheet tab.
* @see PropertySheet.HelpAwareJTabbedPane#getHelpCtx */
String getHelpID() {
return (String)properties.getValue("helpID"); // NOI18N
}
private static String getString(String key) {
return NbBundle.getBundle(PropertySheetTab.class).getString(key);
}
/** Constructs tooltip for <code>Node.Property</code>. Helper method. */
private static String getToolTipTextForProperty(Node.Property prop) {
StringBuffer buff = new StringBuffer(); // NOI18N
buff.append(prop.canRead()
? getString("CTL_Property_Read_Yes")
: getString("CTL_Property_Read_No"));
buff.append(prop.canWrite()
? getString("CTL_Property_Write_Yes")
: getString("CTL_Property_Write_No"));
buff.append(' ');
String shortDesc = prop.getShortDescription();
buff.append(shortDesc != null ? shortDesc : prop.getDisplayName());
return buff.toString();
}
/** Fires a value change in property's model.
* @param propertyName property name */
private void doPropertyChange (String propertyName) {
if (changeInProgress) {
// this is here to assure that if a node would
// refire back our property change event we
// should not end up in infinite loop
return;
}
PropertyModel m = (PropertyModel)modelCache.get (propertyName);
if (m == null) {
// the model is not in our cache, probably we are not displaying
// this property --> do nothing in such case
return;
}
if (m instanceof PropertyPanel.SimpleModel) {
PropertyPanel.SimpleModel sm = (PropertyPanel.SimpleModel)m;
try {
changeInProgress = true;
sm.fireValueChanged();
} finally {
changeInProgress = false;
}
}
}
private static final HashSet propsToIgnore = new HashSet();
static {
propsToIgnore.addAll (Arrays.asList (new String[] {
Node.PROP_COOKIE, Node.PROP_ICON, Node.PROP_OPENED_ICON,
Node.PROP_PARENT_NODE
}));
};
/**
* This is attached to the node we are taking properties from.
*/
public void propertyChange(PropertyChangeEvent evt) {
if (propsToIgnore.contains (evt.getPropertyName())) {
// ignore cookie changes and such, they're noise and
// often fired from settings nodes
return;
}
if ((evt.getPropertyName () != null) && (evt.getSource ().equals (node))) {
doPropertyChange (evt.getPropertyName ());
} else {
// bugfix #20427 if was firePropertyChange(null, null, null)
// then a property's value change is fired on all node's properties
Node.Property []prop = properties.getProperties ();
for (int i = 0; i < prop.length; i++) {
doPropertyChange (prop[i].getName ());
}
}
}
/**
* Lazy creation of the popup menu. Adds SetDeafulValuetAction
* to the menu.
*/
private void createPopup() {
if (popupMenu == null) {
popupMenu = new JPopupMenuPlus();
// popupMenu.add (new CopyAction ().getPopupPresenter ());
// popupMenu.add (new PasteAction ().getPopupPresenter ());
// popupMenu.addSeparator ();
CallbackSystemAction setDefault = (CallbackSystemAction)SystemAction.get(SetDefaultValueAction.class);
popupMenu.add(setDefault.getPopupPresenter());
}
}
// ------------------------------------------------------------------------
/**
* Shows the popup (in AWT thread).
*/
private final class PopupPerformer implements org.openide.util.actions.ActionPerformer {
private PropertyPanel panel;
public PopupPerformer(PropertyPanel p) {
panel = p;
}
public void performAction(SystemAction act) {
Mutex.EVENT.readAccess(new Runnable() {
public void run() {
PropertyModel pm = panel.getModel();
if (pm instanceof ExPropertyModel) {
ExPropertyModel epm = (ExPropertyModel)pm;
FeatureDescriptor fd = epm.getFeatureDescriptor();
if (fd instanceof Node.Property) {
Node.Property np = (Node.Property)fd;
setActions(np);
createPopup();
popupMenu.show(panel, 0, 0);
}
}
}
});
}
}
/**
* Listens on the property panel and installs and uninstalls
* appropriate PopupPerformer (for the supplied PropertyPanel).
*/
private final class InstallPerformerListener implements SheetButtonListener {
private CallbackSystemAction csa;
private PopupPerformer performer;
private PropertyPanel panel;
public InstallPerformerListener(PropertyPanel p) {
panel = p;
}
public void sheetButtonClicked(ActionEvent e) { }
public void sheetButtonEntered(ActionEvent e) {
if (csa == null) {
csa = (CallbackSystemAction) SystemAction.get (PopupAction.class);
performer = new PopupPerformer(panel);
}
csa.setActionPerformer(performer);
}
public void sheetButtonExited(ActionEvent e) {
if (csa != null && (csa.getActionPerformer() instanceof PopupPerformer)) {
csa.setActionPerformer(null);
}
}
}
/** Listener updating the two adjacent buttons */
private final class ButtonListener implements SheetButtonListener {
private SheetButton b;
private PropertyPanel p;
public ButtonListener(SheetButton b, PropertyPanel p) {
this.b = b;
this.p = p;
}
/**
* Invoked when the mouse has been clicked on a component.
*/
public void sheetButtonClicked(ActionEvent e) {
// Fix #15885. Avoid setting 'writeState' if there was
// right mouse button clicked -> popup is about to show.
if(SheetButton.RIGHT_MOUSE_COMMAND.equals(e.getActionCommand())) {
if(p.isWriteState()) {
p.setReadState();
}
return;
}
if (e.getSource() == b) {
// Is double click?
if(e.getID() == ActionEvent.ACTION_FIRST + 2) {
p.tryToSelectNextTag();
} else if(p.isWriteState()) {
p.setReadState();
p.requestDefaultFocus();
} else {
p.setWriteState();
}
}
}
/**
* Invoked when the mouse enters a component.
*/
public void sheetButtonEntered(ActionEvent e) {
if (e.getSource() == b) {
if (p.getReadComponent() != null) {
p.getReadComponent().setPressed(true);
}
} else {
b.setPressed(true);
}
}
/**
* Invoked when the mouse exits a component.
*/
public void sheetButtonExited(ActionEvent e) {
if (e.getSource() == b) {
if (p.getReadComponent() != null) {
p.getReadComponent().setPressed(false);
}
} else {
b.setPressed(false);
}
}
}
// Settings listener
final class SettingsListener implements PropertyChangeListener {
public void propertyChange (PropertyChangeEvent e) {
String name = e.getPropertyName ();
if (name == null) return;
if (name.equals (PropertySheet.PROPERTY_SORTING_MODE)) {
try {
setSortingMode (((Integer)e.getNewValue ()).intValue ());
if (paneCreated)
createPane();
} catch (PropertyVetoException ee) {
PropertyDialogManager.notify(ee);
}
} else if (name.equals (PropertySheet.PROPERTY_DISPLAY_WRITABLE_ONLY)) {
if (paneCreated)
createPane();
} else if (name.equals (PropertySheet.PROPERTY_VALUE_COLOR)) {
setForegroundColor ((Color)e.getNewValue ());
} else if (name.equals (PropertySheet.PROPERTY_DISABLED_PROPERTY_COLOR)) {
setDisabledColor ((Color)e.getNewValue ());
} else if (name.equals (PropertySheet.PROPERTY_PLASTIC)) {
setPlastic (((Boolean)e.getNewValue ()).booleanValue ());
} else if (name.equals (PropertySheet.PROPERTY_PROPERTY_PAINTING_STYLE)) {
setPaintingStyle (((Integer)e.getNewValue ()).intValue ());
}
}
}
/**
* Scrollable enhancement of SplittedPanel.
*/
private class ScrollableSplittedPanel extends SplittedPanel implements Scrollable {
private Component scroll;
private Container element;
ScrollableSplittedPanel (Component scroll, Container element) {
this.scroll = scroll;
this.element = element;
setSplitPosition (mySheet.getSavedPosition ());
JComponent c = new JPanel();
c.setPreferredSize (new Dimension(2,2));
setSplitterComponent(c);
//make borders consistent
javax.swing.border.Border b =
UIManager.getBorder ("nb.splitChildBorder"); //NOI18N
if (b != null) {
setBorder (b);
}
}
/**
* Returns the preferred size of the viewport for a view component.
*
* @return The preferredSize of a JViewport whose view is this Scrollable.
*/
public Dimension getPreferredScrollableViewportSize () {
return super.getPreferredSize ();
}
/**
* @param visibleRect The view area visible within the viewport
* @param orientation Either SwingConstants.VERTICAL or SwingConstants.HORIZONTAL.
* @param direction Less than zero to scroll up/left, greater than zero for down/right.
* @return The "unit" increment for scrolling in the specified direction
*/
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
Component[] c = element.getComponents ();
if (c.length < 1) return 1;
Dimension d = c [0].getSize ();
if (orientation == SwingConstants.VERTICAL) return d.height;
else return d.width;
}
/**
* @param visibleRect The view area visible within the viewport
* @param orientation Either SwingConstants.VERTICAL or SwingConstants.HORIZONTAL.
* @param direction Less than zero to scroll up/left, greater than zero for down/right.
* @return The "block" increment for scrolling in the specified direction.
*/
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
if (orientation == SwingConstants.VERTICAL) return scroll.getSize ().height;
else return scroll.getSize ().width;
}
/**
* Return true if a viewport should always force the width of this
* Scrollable to match the width of the viewport.
*
* @return True if a viewport should force the Scrollables width to match its own.
*/
public boolean getScrollableTracksViewportWidth () {
return true;
}
/**
* Return true if a viewport should always force the height of this
* Scrollable to match the height of the viewport.
*
* @return True if a viewport should force the Scrollables height to match its own.
*/
public boolean getScrollableTracksViewportHeight () {
return false;
}
/**
* Overriden to remember the split position.
*/
public void setSplitPosition(int value) {
super.setSplitPosition(value);
mySheet.setSavedPosition (value);
}
} // End of class ScrollableSplittedPanel.
}