/** * L2FProd.com Common Components 7.3 License. * * Copyright 2005-2007 L2FProd.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.l2fprod.common.propertysheet; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.LayoutManager; import java.awt.event.ActionEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.beans.BeanInfo; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyDescriptor; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.JEditorPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JToggleButton; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.UIManager; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableColumnModel; import org.rr.commons.swing.layout.EqualsLayout; import com.l2fprod.common.swing.IconPool; import com.l2fprod.common.swing.LookAndFeelTweaks; import com.l2fprod.common.util.ResourceManager; /** * An implementation of a PropertySheet which shows a table to edit/view values, a description pane which updates when the selection changes and buttons to * toggle between a flat view and a by-category view of the properties. A button in the toolbar allows to sort the properties and categories by name. * <p> * Default sorting is by name (case-insensitive). Custom sorting can be implemented through * {@link com.l2fprod.common.propertysheet.PropertySheetTableModel#setCategorySortingComparator(Comparator)} and * {@link com.l2fprod.common.propertysheet.PropertySheetTableModel#setPropertySortingComparator(Comparator)} */ public class PropertySheetPanel extends JPanel implements PropertySheet, PropertyChangeListener { private static final long serialVersionUID = 5266942995257597731L; private PropertySheetTable table; private PropertySheetTableModel model; private JScrollPane tableScroll; private ListSelectionListener selectionListener = new SelectionListener(); private KeyListener spaceKeyListener = new SpaceKeyListener(); private JPanel actionPanel; private JToggleButton sortButton; private JToggleButton asCategoryButton; private JToggleButton descriptionButton; private JSplitPane split; private int lastDescriptionHeight; private JEditorPane descriptionPanel; private JScrollPane descriptionScrollPane; private boolean showCategoryButton = true; private boolean showSortButton = true; private boolean showDescriptionButton = true; private PropertyChangeListener[] listener; boolean isShowCategoryButton() { return showCategoryButton; } public void setShowCategoryButton(boolean showCategoryButton) { this.showCategoryButton = showCategoryButton; if (showCategoryButton && asCategoryButton == null) { this.addCategoryButton(); } else { this.removeCategoryButton(); } } public boolean isShowSortButton() { return showSortButton; } public void setShowSortButton(boolean showSortButton) { this.showSortButton = showSortButton; if (showSortButton && sortButton == null) { this.addSortButton(); } else { this.removeSortButton(); } } public boolean isShowDescriptionButton() { return showDescriptionButton; } public void setShowDescriptionButton(boolean showDescriptionButton) { this.showDescriptionButton = showDescriptionButton; if (showDescriptionButton && descriptionButton == null) { this.addDescriptionButton(); } else { this.removeDescriptionButton(); } } public PropertySheetPanel() { this(new PropertySheetTable()); } public PropertySheetPanel(PropertySheetTable table) { buildUI(); setTable(table); } public PropertySheetPanel(PropertySheetTableModel model) { this.model = model; buildUI(); setTable(new PropertySheetTable(model)); } /** * Sets the table used by this panel. * * Note: listeners previously added with {@link PropertySheetPanel#addPropertySheetChangeListener(PropertyChangeListener)} must be re-added after this call * if the table model is not the same as the previous table. * * @param table */ public void setTable(final PropertySheetTable table) { if (table == null) { throw new IllegalArgumentException("table must not be null"); } // remove the property change listener from any previous model if (model != null) model.removePropertyChangeListener(this); // get the model from the table model = (PropertySheetTableModel) table.getModel(); model.addPropertyChangeListener(this); if(listener != null) { for(PropertyChangeListener l : listener) { model.removePropertyChangeListener(l); model.addPropertyChangeListener(l); } } // remove the listener from the old table if (this.table != null) { this.table.getSelectionModel().removeListSelectionListener(selectionListener); } // prepare the new table table.getSelectionModel().addListSelectionListener(selectionListener); tableScroll.getViewport().setView(table); table.addKeyListener(spaceKeyListener); // use the new table as our table this.table = table; } public ListSelectionModel getSelectionModel() { ListSelectionModel selectionModel = this.table.getSelectionModel(); return selectionModel; } /** * React to property changes by repainting. */ public void propertyChange(PropertyChangeEvent evt) { repaint(); } /** * @return the table used to edit/view Properties. */ public PropertySheetTable getTable() { return table; } /** * Toggles the visibility of the description panel. * * @param visible */ public void setDescriptionVisible(boolean visible) { if (visible) { add("Center", split); split.setTopComponent(tableScroll); split.setBottomComponent(descriptionScrollPane); // restore the divider location split.setDividerLocation(split.getHeight() - lastDescriptionHeight); } else { // save the size of the description pane to restore it later lastDescriptionHeight = split.getHeight() - split.getDividerLocation(); remove(split); add("Center", tableScroll); } descriptionButton.setSelected(visible); PropertySheetPanel.this.revalidate(); } /** * Get the splitpane divider location. * @return The location for the plsitpane divider location. */ public int getDescriptionDividerLocation() { return split.getDividerLocation(); } public void setDescriptionDividerLocation(int location) { split.setDividerLocation(location); } /** * Toggles the visibility of the toolbar panel * * @param visible */ public void setToolBarVisible(boolean visible) { actionPanel.setVisible(visible); PropertySheetPanel.this.revalidate(); } /** * Set the current mode, either {@link PropertySheet#VIEW_AS_CATEGORIES} or {@link PropertySheet#VIEW_AS_FLAT_LIST}. */ public void setMode(int mode) { model.setMode(mode); asCategoryButton.setSelected(PropertySheet.VIEW_AS_CATEGORIES == mode); } public void setProperties(Property[] properties) { model.setProperties(properties); } public List<Property> getProperties() { return model.getProperties(); } public void addProperty(Property property) { model.addProperty(property); } public void addProperty(int index, Property property) { model.addProperty(index, property); } public void removeProperty(Property property) { model.removeProperty(property); } public int getPropertyCount() { return model.getPropertyCount(); } public Iterator<Property> propertyIterator() { return model.propertyIterator(); } public void setBeanInfo(BeanInfo beanInfo) { setProperties(beanInfo.getPropertyDescriptors()); } public void setProperties(PropertyDescriptor[] descriptors) { Property[] properties = new Property[descriptors.length]; for (int i = 0, c = descriptors.length; i < c; i++) { properties[i] = new PropertyDescriptorAdapter(descriptors[i]); } setProperties(properties); } /** * Initializes the PropertySheet from the given object. If any, it cancels pending edit before proceeding with properties. * * @param data */ public void readFromObject(Object data) { // cancel pending edits getTable().cancelEditing(); List<Property> properties = model.getProperties(); for (int i = 0, c = properties.size(); i < c; i++) { properties.get(i).readFromObject(data); } repaint(); } /** * Writes the PropertySheet to the given object. If any, it commits pending edit before proceeding with properties. * * @param data */ public void writeToObject(Object data) { // ensure pending edits are committed getTable().commitEditing(); List<Property> properties = getProperties(); for (int i = 0, c = properties.size(); i < c; i++) { properties.get(i).writeToObject(data); } } public void addPropertySheetChangeListener(PropertyChangeListener listener) { model.addPropertyChangeListener(listener); this.listener = model.getPropertyChangeListener(); } public void removePropertySheetChangeListener(PropertyChangeListener listener) { model.removePropertyChangeListener(listener); this.listener = model.getPropertyChangeListener(); } public void setEditorFactory(PropertyEditorFactory factory) { table.setEditorFactory(factory); } public PropertyEditorFactory getEditorFactory() { return table.getEditorFactory(); } public void setRendererFactory(PropertyRendererFactory factory) { table.setRendererFactory(factory); } public PropertyRendererFactory getRendererFactory() { return table.getRendererFactory(); } /** * Sets sorting of categories enabled or disabled. * * @param value * true to enable sorting */ public void setSortingCategories(boolean value) { model.setSortingCategories(value); sortButton.setSelected(isSorting()); } /** * Is sorting of categories enabled. * * @return true if category sorting is enabled */ public boolean isSortingCategories() { return model.isSortingCategories(); } /** * Sets sorting of properties enabled or disabled. * * @param value * true to enable sorting */ public void setSortingProperties(boolean value) { model.setSortingProperties(value); sortButton.setSelected(isSorting()); } /** * Is sorting of properties enabled. * * @return true if property sorting is enabled */ public boolean isSortingProperties() { return model.isSortingProperties(); } /** * Sets sorting properties and categories enabled or disabled. * * @param value * true to enable sorting */ public void setSorting(boolean value) { model.setSortingCategories(value); model.setSortingProperties(value); sortButton.setSelected(value); } /** * @return true if properties or categories are sorted. */ public boolean isSorting() { return model.isSortingCategories() || model.isSortingProperties(); } /** * Sets the Comparator to be used with categories. Categories are treated as String-objects. * * @param comp * java.util.Comparator used to compare categories */ public void setCategorySortingComparator(Comparator comp) { model.setCategorySortingComparator(comp); } /** * Sets the Comparator to be used with Property-objects. * * @param comp * java.util.Comparator used to compare Property-objects */ public void setPropertySortingComparator(Comparator comp) { model.setPropertySortingComparator(comp); } /** * Set wether or not toggle states are restored when new properties are applied. * * @param value * true to enable */ public void setRestoreToggleStates(boolean value) { model.setRestoreToggleStates(value); } /** * @return true is toggle state restore is enabled */ public boolean isRestoreToggleStates() { return model.isRestoreToggleStates(); } /** * @return the category view toggle states. */ public Map getToggleStates() { return model.getToggleStates(); } /** * Sets the toggle states for the category views. Note this <b>MUST</b> be called <b>BEFORE</b> setting any properties. * * @param toggleStates * the toggle states as returned by getToggleStates */ public void setToggleStates(Map toggleStates) { model.setToggleStates(toggleStates); } private void addCategoryButton() { asCategoryButton = new JToggleButton(new ToggleModeAction()); // asCategoryButton.setUI(new BlueishButtonUI()); asCategoryButton.setText(null); asCategoryButton.setOpaque(false); actionPanel.add(asCategoryButton); } private void removeCategoryButton() { if (asCategoryButton != null) { actionPanel.remove(asCategoryButton); asCategoryButton = null; } } private void addDescriptionButton() { descriptionButton = new JToggleButton(new ToggleDescriptionAction()); // descriptionButton.setUI(new BlueishButtonUI()); descriptionButton.setText(null); descriptionButton.setOpaque(false); actionPanel.add(descriptionButton); } /** * Adds a button or something else behind the description and sort button * to the toolbar. * @param c The component which should be added. */ public void addToolbarComponent(Component c) { actionPanel.add(c); } private void removeDescriptionButton() { if (descriptionButton != null) { actionPanel.remove(descriptionButton); descriptionButton = null; } } private void addSortButton() { sortButton = new JToggleButton(new ToggleSortingAction()); // sortButton.setUI(new BlueishButtonUI()); sortButton.setText(null); sortButton.setOpaque(false); actionPanel.add(sortButton); } private void removeSortButton() { if (sortButton != null) { actionPanel.remove(sortButton); sortButton = null; } } private void buildUI() { LookAndFeelTweaks.setBorderLayout(this); LookAndFeelTweaks.setBorder(this); LayoutManager layout = new EqualsLayout(SwingConstants.LEFT, 2); // FlowLayout layout = new FlowLayout(FlowLayout.LEADING, 2, 0); actionPanel = new JPanel(layout); actionPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 2, 0)); actionPanel.setOpaque(false); add("North", actionPanel); this.addSortButton(); this.addCategoryButton(); this.addDescriptionButton(); split = new JSplitPane(JSplitPane.VERTICAL_SPLIT); split.setBorder(null); split.setResizeWeight(1.0); split.setContinuousLayout(true); add("Center", split); tableScroll = new JScrollPane(); tableScroll.setBorder(BorderFactory.createEmptyBorder()); split.setTopComponent(tableScroll); descriptionPanel = new JEditorPane("text/html", "<html>"); descriptionPanel.setBorder(BorderFactory.createEmptyBorder()); descriptionPanel.setEditable(false); descriptionPanel.setBackground(UIManager.getColor("Panel.background")); LookAndFeelTweaks.htmlize(descriptionPanel); selectionListener = new SelectionListener(); descriptionScrollPane = new JScrollPane(descriptionPanel); descriptionScrollPane.setBorder(LookAndFeelTweaks.addMargin(BorderFactory.createLineBorder(UIManager.getColor("controlDkShadow")))); descriptionScrollPane.getViewport().setBackground(descriptionPanel.getBackground()); descriptionScrollPane.setMinimumSize(new Dimension(50, 50)); split.setBottomComponent(descriptionScrollPane); // by default description is not visible, toolbar is visible. setDescriptionVisible(false); setToolBarVisible(true); } /** * listener takes care that editing is started at the second column always * the space bar is pressed. */ class SpaceKeyListener extends KeyAdapter { @Override public void keyReleased(KeyEvent e) { if(!PropertySheetPanel.this.table.isEditing() && e.getKeyCode() == KeyEvent.VK_SPACE) { int row = PropertySheetPanel.this.table.getSelectedRow(); PropertySheetPanel.this.table.editCellAt(row, 1); } } } class SelectionListener implements ListSelectionListener { public void valueChanged(ListSelectionEvent e) { int row = table.getSelectedRow(); Property prop = null; if (row >= 0 && table.getRowCount() > row) prop = model.getPropertySheetElement(row).getProperty(); if (prop != null) { descriptionPanel.setText("<html>" + "<b>" + (prop.getDisplayDescriptionName() == null ? "" : prop.getDisplayDescriptionName()) + "</b><br>" + (prop.getShortDescription() == null ? "" : prop.getShortDescription())); } else { descriptionPanel.setText("<html>"); } // position it at the top descriptionPanel.setCaretPosition(0); } } class ToggleModeAction extends AbstractAction { public ToggleModeAction() { super("toggle", IconPool.shared().get(PropertySheet.class.getResource("icons/category.gif"))); putValue(Action.SHORT_DESCRIPTION, ResourceManager.get(PropertySheet.class).getString("PropertySheetPanel.category.shortDescription")); } public void actionPerformed(ActionEvent e) { if (asCategoryButton.isSelected()) { model.setMode(PropertySheet.VIEW_AS_CATEGORIES); } else { model.setMode(PropertySheet.VIEW_AS_FLAT_LIST); } } } class ToggleDescriptionAction extends AbstractAction { public ToggleDescriptionAction() { super("toggleDescription", IconPool.shared().get(PropertySheet.class.getResource("icons/description.gif"))); putValue(Action.SHORT_DESCRIPTION, ResourceManager.get(PropertySheet.class).getString("PropertySheetPanel.description.shortDescription")); } public void actionPerformed(ActionEvent e) { setDescriptionVisible(descriptionButton.isSelected()); } } class ToggleSortingAction extends AbstractAction { public ToggleSortingAction() { super("toggleSorting", IconPool.shared().get(PropertySheet.class.getResource("icons/sort.gif"))); putValue(Action.SHORT_DESCRIPTION, ResourceManager.get(PropertySheet.class).getString("PropertySheetPanel.sort.shortDescription")); } public void actionPerformed(ActionEvent e) { setSorting(sortButton.isSelected()); } } public PropertySheetTableModel getModel() { return model; } public void setModel(PropertySheetTableModel model) { if(this.model != null) { this.model.dispose(); } this.model = model; this.table.setColumnModel(new DefaultTableColumnModel()); this.table.setModel(model); setTable(this.table); } }