/* * FurnitureCatalogTree.java 7 avr. 2006 * * Sweet Home 3D, Copyright (c) 2006 Emmanuel PUYBARET / eTeks <info@eteks.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.eteks.sweethome3d.swing; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.dnd.DnDConstants; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JEditorPane; import javax.swing.JToolTip; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; import javax.swing.event.MouseInputAdapter; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.text.AttributeSet; import javax.swing.text.Element; import javax.swing.text.html.HTML; import javax.swing.text.html.HTMLDocument; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreeCellRenderer; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import com.eteks.sweethome3d.model.CatalogPieceOfFurniture; import com.eteks.sweethome3d.model.CollectionEvent; import com.eteks.sweethome3d.model.CollectionListener; import com.eteks.sweethome3d.model.Content; import com.eteks.sweethome3d.model.FurnitureCatalog; import com.eteks.sweethome3d.model.FurnitureCategory; import com.eteks.sweethome3d.model.SelectionEvent; import com.eteks.sweethome3d.model.SelectionListener; import com.eteks.sweethome3d.model.UserPreferences; import com.eteks.sweethome3d.viewcontroller.FurnitureCatalogController; import com.eteks.sweethome3d.viewcontroller.View; /** * A tree displaying furniture catalog by category. * @author Emmanuel Puybaret */ public class FurnitureCatalogTree extends JTree implements View { private final UserPreferences preferences; private TreeSelectionListener treeSelectionListener; private FurnitureToolTip toolTip; /** * Creates a tree that displays <code>catalog</code> content. */ public FurnitureCatalogTree(FurnitureCatalog catalog) { this(catalog, null); } /** * Creates a tree controlled by <code>controller</code> that displays * <code>catalog</code> content and its selection. */ public FurnitureCatalogTree(FurnitureCatalog catalog, FurnitureCatalogController controller) { this(catalog, null, controller); } /** * Creates a tree controlled by <code>controller</code> that displays * <code>catalog</code> content and its selection. */ public FurnitureCatalogTree(FurnitureCatalog catalog, UserPreferences preferences, FurnitureCatalogController controller) { this.preferences = preferences; this.toolTip = new FurnitureToolTip(true, preferences); setModel(new CatalogTreeModel(catalog)); setRootVisible(false); setShowsRootHandles(true); setCellRenderer(new CatalogCellRenderer()); addDragListener(); if (controller != null) { updateTreeSelectedFurniture(catalog, controller); addSelectionListeners(catalog, controller); addMouseListeners(controller); } ToolTipManager.sharedInstance().registerComponent(this); // Remove Select all action getActionMap().getParent().remove("selectAll"); } /** * Adds a mouse motion listener that will initiate a drag operation * when the user drags a piece of furniture. */ private void addDragListener() { addMouseMotionListener(new MouseMotionAdapter() { public void mouseDragged(MouseEvent ev) { if (SwingUtilities.isLeftMouseButton(ev)) { TreePath clickedPath = getPathForLocation(ev.getX(), ev.getY()); if (clickedPath != null && clickedPath.getLastPathComponent() instanceof CatalogPieceOfFurniture && getTransferHandler() != null) { getTransferHandler().exportAsDrag(FurnitureCatalogTree.this, ev, DnDConstants.ACTION_COPY); } } } }); } /** * Adds the listeners that manage selection synchronization in this tree. */ private void addSelectionListeners(final FurnitureCatalog catalog, final FurnitureCatalogController controller) { final SelectionListener modelSelectionListener = new SelectionListener() { public void selectionChanged(SelectionEvent selectionEvent) { updateTreeSelectedFurniture(catalog, controller); } }; this.treeSelectionListener = new TreeSelectionListener () { public void valueChanged(TreeSelectionEvent ev) { // Updates selected furniture in catalog from selected nodes in tree. controller.removeSelectionListener(modelSelectionListener); controller.setSelectedFurniture(getSelectedFurniture()); controller.addSelectionListener(modelSelectionListener); } }; controller.addSelectionListener(modelSelectionListener); getSelectionModel().addTreeSelectionListener(this.treeSelectionListener); } /** * Updates selected nodes in tree from <code>catalog</code> selected furniture. */ private void updateTreeSelectedFurniture(FurnitureCatalog catalog, FurnitureCatalogController controller) { if (this.treeSelectionListener != null) { getSelectionModel().removeTreeSelectionListener(this.treeSelectionListener); } clearSelection(); for (CatalogPieceOfFurniture piece : controller.getSelectedFurniture()) { TreePath path = new TreePath(new Object [] {catalog, piece.getCategory(), piece}); addSelectionPath(path); scrollRowToVisible(getRowForPath(path)); } if (this.treeSelectionListener != null) { getSelectionModel().addTreeSelectionListener(this.treeSelectionListener); } } /** * Returns the selected furniture in tree. */ private List<CatalogPieceOfFurniture> getSelectedFurniture() { // Build the list of selected furniture List<CatalogPieceOfFurniture> selectedFurniture = new ArrayList<CatalogPieceOfFurniture>(); TreePath [] selectionPaths = getSelectionPaths(); if (selectionPaths != null) { for (TreePath path : selectionPaths) { // Add to selectedFurniture all the nodes that matches a piece of furniture if (path.getPathCount() == 3) { selectedFurniture.add((CatalogPieceOfFurniture)path.getLastPathComponent()); } } } return selectedFurniture; } /** * Adds mouse listeners to modify selected furniture and manage links in piece information. */ private void addMouseListeners(final FurnitureCatalogController controller) { final Cursor handCursor = new Cursor(Cursor.HAND_CURSOR); MouseInputAdapter mouseListener = new MouseInputAdapter() { @Override public void mouseClicked(MouseEvent ev) { if (SwingUtilities.isLeftMouseButton(ev)) { if (ev.getClickCount() == 2) { TreePath clickedPath = getPathForLocation(ev.getX(), ev.getY()); if (clickedPath != null && clickedPath.getLastPathComponent() instanceof CatalogPieceOfFurniture) { controller.modifySelectedFurniture(); } } else if (getCellRenderer() instanceof CatalogCellRenderer) { URL url = ((CatalogCellRenderer)getCellRenderer()).getURLAt(ev.getPoint(), (JTree)ev.getSource()); if (url != null) { SwingTools.showDocumentInBrowser(url); } } } } @Override public void mouseMoved(MouseEvent ev) { if (getCellRenderer() instanceof CatalogCellRenderer) { URL url = ((CatalogCellRenderer)getCellRenderer()).getURLAt(ev.getPoint(), (JTree)ev.getSource()); if (url != null) { EventQueue.invokeLater(new Runnable() { public void run() { setCursor(handCursor); } }); } } setCursor(Cursor.getDefaultCursor()); } }; addMouseListener(mouseListener); addMouseMotionListener(mouseListener); } /** * Returns the tool tip displayed by this tree. */ @Override public JToolTip createToolTip() { this.toolTip.setComponent(this); return this.toolTip; } /** * Returns a tooltip for furniture pieces described in this tree. */ @Override public String getToolTipText(MouseEvent ev) { TreePath path = getPathForLocation(ev.getX(), ev.getY()); if (this.preferences != null && path != null && path.getPathCount() == 3) { this.toolTip.setPieceOfFurniture((CatalogPieceOfFurniture)path.getLastPathComponent()); return this.toolTip.getToolTipText(); } else { return null; } } /** * Cell renderer for this catalog tree. */ private class CatalogCellRenderer extends JComponent implements TreeCellRenderer { private static final int DEFAULT_ICON_HEIGHT = 32; private Font defaultFont; private Font modifiablePieceFont; private DefaultTreeCellRenderer nameLabel; private JEditorPane informationPane; public CatalogCellRenderer() { setLayout(null); this.nameLabel = new DefaultTreeCellRenderer(); this.informationPane = new JEditorPane("text/html", null); this.informationPane.setOpaque(false); this.informationPane.setEditable(false); add(this.nameLabel); add(this.informationPane); } public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { // Configure name label with its icon, background and focus colors this.nameLabel.getTreeCellRendererComponent( tree, value, selected, expanded, leaf, row, hasFocus); // Initialize fonts if not done if (this.defaultFont == null) { this.defaultFont = this.nameLabel.getFont(); String bodyRule = "body { font-family: " + this.defaultFont.getFamily() + "; " + "font-size: " + this.defaultFont.getSize() + "pt; " + "top-margin: 0; }"; ((HTMLDocument)this.informationPane.getDocument()).getStyleSheet().addRule(bodyRule); this.modifiablePieceFont = new Font(this.defaultFont.getFontName(), Font.ITALIC, this.defaultFont.getSize()); } // If node is a category, change label text if (value instanceof FurnitureCategory) { this.nameLabel.setText(((FurnitureCategory)value).getName()); this.nameLabel.setFont(this.defaultFont); this.informationPane.setVisible(false); } // Else if node is a piece of furniture, change label text and icon else if (value instanceof CatalogPieceOfFurniture) { CatalogPieceOfFurniture piece = (CatalogPieceOfFurniture)value; this.nameLabel.setText(piece.getName()); this.nameLabel.setIcon(getLabelIcon(tree, piece.getIcon())); this.nameLabel.setFont(piece.isModifiable() ? this.modifiablePieceFont : this.defaultFont); String information = piece.getInformation(); if (information != null) { this.informationPane.setText(information); this.informationPane.setVisible(true); } else { this.informationPane.setVisible(false); } } return this; } @Override public void doLayout() { Dimension namePreferredSize = this.nameLabel.getPreferredSize(); this.nameLabel.setSize(namePreferredSize); if (this.informationPane.isVisible()) { Dimension informationPreferredSize = this.informationPane.getPreferredSize(); this.informationPane.setBounds(namePreferredSize.width + 2, (namePreferredSize.height - informationPreferredSize.height) / 2, informationPreferredSize.width, namePreferredSize.height); } } @Override public Dimension getPreferredSize() { Dimension preferredSize = this.nameLabel.getPreferredSize(); if (this.informationPane.isVisible()) { preferredSize.width += 2 + this.informationPane.getPreferredSize().width; } return preferredSize; } /** * The following methods are overridden for performance reasons. */ @Override public void revalidate() { } @Override public void repaint(long tm, int x, int y, int width, int height) { } @Override public void repaint(Rectangle r) { } @Override public void repaint() { } /** * Returns an Icon instance with the read image scaled at the tree row height or * an empty image if the image couldn't be read. * @param content the content of an image. */ private Icon getLabelIcon(JTree tree, Content content) { return IconManager.getInstance().getIcon(content, getRowHeight(tree), tree); } /** * Returns the height of rows in tree. */ private int getRowHeight(JTree tree) { return tree.isFixedRowHeight() ? tree.getRowHeight() : DEFAULT_ICON_HEIGHT; } @Override protected void paintChildren(Graphics g) { // Force text anti aliasing on texts ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); super.paintChildren(g); } public URL getURLAt(Point point, JTree tree) { TreePath path = tree.getPathForLocation(point.x, point.y); if (path != null && path.getLastPathComponent() instanceof CatalogPieceOfFurniture) { CatalogPieceOfFurniture piece = (CatalogPieceOfFurniture)path.getLastPathComponent(); String information = piece.getInformation(); if (information != null) { int row = tree.getRowForPath(path); getTreeCellRendererComponent(tree, piece, false, false, false, row, false).doLayout(); Rectangle rowBounds = tree.getRowBounds(row); point.x -= rowBounds.x + this.informationPane.getX(); point.y -= rowBounds.y + this.informationPane.getY(); if (point.x > 0 && point.y > 0) { // Search in information pane if point is over a HTML link int position = this.informationPane.viewToModel(point); if (position > 0) { HTMLDocument hdoc = (HTMLDocument)this.informationPane.getDocument(); Element element = hdoc.getCharacterElement(position); AttributeSet a = element.getAttributes(); AttributeSet anchor = (AttributeSet)a.getAttribute(HTML.Tag.A); if (anchor != null) { String href = (String)anchor.getAttribute(HTML.Attribute.HREF); if (href != null) { try { return new URL(href); } catch (MalformedURLException ex) { // Ignore malformed URL } } } } } } } return null; } } /** * Tree model adaptor to Catalog / Category / PieceOfFurniture classes. */ private static class CatalogTreeModel implements TreeModel { private FurnitureCatalog catalog; private List<TreeModelListener> listeners; public CatalogTreeModel(FurnitureCatalog catalog) { this.catalog = catalog; this.listeners = new ArrayList<TreeModelListener>(2); catalog.addFurnitureListener(new CatalogFurnitureListener(this)); } public Object getRoot() { return this.catalog; } public Object getChild(Object parent, int index) { if (parent instanceof FurnitureCatalog) { return ((FurnitureCatalog)parent).getCategory(index); } else { return ((FurnitureCategory)parent).getPieceOfFurniture(index); } } public int getChildCount(Object parent) { if (parent instanceof FurnitureCatalog) { return ((FurnitureCatalog)parent).getCategoriesCount(); } else { return ((FurnitureCategory)parent).getFurnitureCount(); } } public int getIndexOfChild(Object parent, Object child) { if (parent instanceof FurnitureCatalog) { return Collections.binarySearch(((FurnitureCatalog)parent).getCategories(), (FurnitureCategory)child); } else { return ((FurnitureCategory)parent).getIndexOfPieceOfFurniture((CatalogPieceOfFurniture)child); } } public boolean isLeaf(Object node) { return node instanceof CatalogPieceOfFurniture; } public void valueForPathChanged(TreePath path, Object newValue) { // Tree isn't editable } public void addTreeModelListener(TreeModelListener l) { this.listeners.add(l); } public void removeTreeModelListener(TreeModelListener l) { this.listeners.remove(l); } private void fireTreeNodesInserted(TreeModelEvent treeModelEvent) { // Work on a copy of listeners to ensure a listener // can modify safely listeners list TreeModelListener [] listeners = this.listeners. toArray(new TreeModelListener [this.listeners.size()]); for (TreeModelListener listener : listeners) { listener.treeNodesInserted(treeModelEvent); } } private void fireTreeNodesRemoved(TreeModelEvent treeModelEvent) { // Work on a copy of listeners to ensure a listener // can modify safely listeners list TreeModelListener [] listeners = this.listeners. toArray(new TreeModelListener [this.listeners.size()]); for (TreeModelListener listener : listeners) { listener.treeNodesRemoved(treeModelEvent); } } /** * Catalog furniture listener bound to this tree model with a weak reference to avoid * strong link between catalog and this tree. */ private static class CatalogFurnitureListener implements CollectionListener<CatalogPieceOfFurniture> { private WeakReference<CatalogTreeModel> catalogTreeModel; public CatalogFurnitureListener(CatalogTreeModel catalogTreeModel) { this.catalogTreeModel = new WeakReference<CatalogTreeModel>(catalogTreeModel); } public void collectionChanged(CollectionEvent<CatalogPieceOfFurniture> ev) { // If catalog tree model was garbage collected, remove this listener from catalog CatalogTreeModel catalogTreeModel = this.catalogTreeModel.get(); FurnitureCatalog catalog = (FurnitureCatalog)ev.getSource(); if (catalogTreeModel == null) { catalog.removeFurnitureListener(this); } else { CatalogPieceOfFurniture piece = ev.getItem(); switch (ev.getType()) { case ADD : if (piece.getCategory().getFurnitureCount() == 1) { // Fire nodes inserted for new category catalogTreeModel.fireTreeNodesInserted(new TreeModelEvent(catalogTreeModel, new Object [] {catalog}, new int [] {Collections.binarySearch(catalog.getCategories(), piece.getCategory())}, new Object [] {piece.getCategory()})); } else { // Fire nodes inserted for new piece catalogTreeModel.fireTreeNodesInserted(new TreeModelEvent(catalogTreeModel, new Object [] {catalog, piece.getCategory()}, new int [] {ev.getIndex()}, new Object [] {piece})); } break; case DELETE : if (piece.getCategory().getFurnitureCount() == 0) { // Fire nodes removed for deleted category catalogTreeModel.fireTreeNodesRemoved(new TreeModelEvent(catalogTreeModel, new Object [] {catalog}, new int [] {-(Collections.binarySearch(catalog.getCategories(), piece.getCategory()) + 1)}, new Object [] {piece.getCategory()})); } else { // Fire nodes removed for deleted piece catalogTreeModel.fireTreeNodesRemoved(new TreeModelEvent(catalogTreeModel, new Object [] {catalog, piece.getCategory()}, new int [] {ev.getIndex()}, new Object [] {piece})); } break; } } } } } }