/* * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI for * visualizing and manipulating spatial features with geometry and attributes. * * Copyright (C) 2003 Vivid Solutions * * 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. * * For more information, contact: * * Vivid Solutions * Suite #1A * 2328 Government Street * Victoria BC V8T 5G5 * Canada * * (250)385-6040 * www.vividsolutions.com */ package com.vividsolutions.jump.workbench.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Stack; import javax.swing.BorderFactory; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreeCellEditor; import javax.swing.tree.TreeCellRenderer; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.util.Assert; import com.vividsolutions.jump.util.Block; import com.vividsolutions.jump.util.CollectionUtil; import com.vividsolutions.jump.util.LangUtil; import com.vividsolutions.jump.workbench.model.Category; import com.vividsolutions.jump.workbench.model.CategoryEvent; import com.vividsolutions.jump.workbench.model.CategoryEventType; import com.vividsolutions.jump.workbench.model.FeatureEvent; import com.vividsolutions.jump.workbench.model.Layer; import com.vividsolutions.jump.workbench.model.LayerEvent; import com.vividsolutions.jump.workbench.model.LayerEventType; import com.vividsolutions.jump.workbench.model.LayerListener; import com.vividsolutions.jump.workbench.model.LayerManager; import com.vividsolutions.jump.workbench.model.LayerManagerProxy; import com.vividsolutions.jump.workbench.model.LayerTreeModel; import com.vividsolutions.jump.workbench.model.Layerable; import com.vividsolutions.jump.workbench.model.WMSLayer; import com.vividsolutions.jump.workbench.ui.renderer.RenderingManager; import com.vividsolutions.jump.workbench.ui.renderer.style.BasicStyle; public class TreeLayerNamePanel extends JPanel implements LayerListener, LayerNamePanel, LayerNamePanelProxy, PopupNodeProxy { private Map nodeClassToPopupMenuMap = new HashMap(); BorderLayout borderLayout1 = new BorderLayout(); JTree tree = new JTree() { public boolean isPathEditable(TreePath path) { if (!isEditable()) { return false; } return path.getLastPathComponent() instanceof Layerable || path.getLastPathComponent() instanceof Category; } // Workaround for Java Bug 4199956 "JTree shows container can be // expanded - even when empty", posted by bertrand.allo in the Java Bug // Database. [Jon Aquino] public boolean hasBeenExpanded(TreePath path) { return super.hasBeenExpanded(path) || !this.getModel().isLeaf(path.getLastPathComponent()); } // Added by Michael Michaud on 2008-08-10 // This adds the number of features in tooltip // And removed on 2008-11-01 // The ToolTipText content is better managed in LayerNameRenderer class }; private LayerTreeCellRenderer layerTreeCellRenderer; private TreeCellEditor cellEditor = new LayerTreeCellEditor(tree); private Object popupNode; private ArrayList listeners = new ArrayList(); private LayerManagerProxy layerManagerProxy; JScrollPane scrollPane = new JScrollPane(); private FirableTreeModelWrapper firableTreeModelWrapper; // used to drag Layerables among Categories private TreePath movingTreePath = null; private boolean firstTimeDragging = true; private int lastHoveringRow = -1; /** */ public TreeLayerNamePanel(LayerManagerProxy layerManagerProxy, TreeModel treeModel, RenderingManager renderingManager, Map additionalNodeClassToTreeCellRendererMap) { layerManagerProxy.getLayerManager().addLayerListener(this); this.layerManagerProxy = layerManagerProxy; try { jbInit(); } catch (Exception ex) { ex.printStackTrace(); } firableTreeModelWrapper = new FirableTreeModelWrapper(treeModel); tree.setModel(firableTreeModelWrapper); layerTreeCellRenderer = new LayerTreeCellRenderer(renderingManager); renderingManager.getPanel().getViewport().addListener( new ViewportListener() { public void zoomChanged(Envelope modelEnvelope) { // After a zoom, the scale may be outside the visible // scale range for one or more layers, in which case we // want to update the layer names to be grey. So // repaint. [Jon Aquino 2005-03-10] TreeLayerNamePanel.this.repaint(); } }); setCellRenderer(additionalNodeClassToTreeCellRendererMap); tree.getSelectionModel().setSelectionMode( TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); tree.addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { handleCheckBoxClick(e); } public void mousePressed(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { movingTreePath = tree.getPathForLocation(e.getX(), e.getY()); // move only Layerables, not Categories if (movingTreePath != null && !(movingTreePath.getLastPathComponent() instanceof Layerable)) { movingTreePath = null; } else if (movingTreePath != null && !tree.isRowSelected(tree.getClosestRowForLocation(e.getX(), e.getY()))) { movingTreePath = null; } else { lastHoveringRow = tree.getClosestRowForLocation(e.getX(), e.getY()); } } else { movingTreePath = null; } } public void mouseReleased(MouseEvent e) { if (e.getButton() != MouseEvent.BUTTON1 || movingTreePath == null) { return; } Object node = movingTreePath.getLastPathComponent(); TreePath tpDestination = tree.getClosestPathForLocation(e .getX(), e.getY()); // Fix: When dragging a Layerable onto a Category, and then // selecting a different layer, the last XOR placement of the // dragbar would appear over the Category. Need to reset // firstTimeDragging to true before returning. //movingTreePath = null; firstTimeDragging = true; lastHoveringRow = -1; if (tpDestination == null) { return; } // remove remnants of horizontal drag bar by refreshing display tree.repaint(); // Changed #update to #repaint -- less flickery for some reason // [Jon Aquino 2004-03-17] // dragging a layerable if (node instanceof Layerable) { Layerable layerable = (Layerable) node; int index = 0; Category cat = null; int oldRow = tree.getRowForPath(movingTreePath); int newRow = tree.getRowForPath(tpDestination); Category oldCat = (Category)movingTreePath.getParentPath().getLastPathComponent(); if (tpDestination.getLastPathComponent() instanceof Layerable) { // Fix: When shift-clicking to select a range of nodes, // last node would unselect because the layer would get // removed then re-added. [Jon Aquino 2004-03-11] if (layerable == tpDestination.getLastPathComponent()) { return; } cat = getLayerManager().getCategory( (Layerable) tpDestination.getLastPathComponent()); index = tree.getModel().getIndexOfChild( tpDestination.getParentPath().getLastPathComponent(), tpDestination.getLastPathComponent()); // adjust where the Layer will be drop exactly if (newRow < oldRow && cat.equals(oldCat)) index++; else if (!cat.equals(oldCat)) index++; } else if (tpDestination.getLastPathComponent() instanceof Category) { cat = (Category) tpDestination.getLastPathComponent(); // Prevent unnecessary removals and re-additions // [Jon Aquino 2004-03-11] // if (cat.contains(layerable)) { //return; // } } else { // Can get here if the node is, for example, // a LayerTreeModel.ColorThemingValue [Jon Aquino 2005-07-25] return; } getLayerManager().remove(layerable); cat.add(index, layerable); getLayerManager().fireLayerChanged(layerable, LayerEventType.METADATA_CHANGED); movingTreePath = null; } } }); tree.addMouseMotionListener(new MouseMotionAdapter() { int rowNew; Rectangle dragBar; public void mouseDragged(MouseEvent e) { // return if mouse is dragged while not originating on a tree node if (movingTreePath == null) { firstTimeDragging = true; return; } int rowOld = tree.getRowForPath(movingTreePath); rowNew = tree.getClosestRowForLocation(e.getX(), e.getY()); //rowOld = tree.getRowForPath(movingTreePath); // if the dragging of a row hasn't moved outside of the bounds // of the currently selected row, don't show the horizontal drag // bar. if (rowNew == lastHoveringRow || rowNew == rowOld-1 || rowNew == rowOld) { return; } if (!(tree.getPathForRow(rowNew).getLastPathComponent() instanceof Layer)) { tree.expandRow(rowNew); } Graphics2D g2 = (Graphics2D) tree.getGraphics(); g2.setColor(Color.RED); g2.setXORMode(Color.WHITE); // if this is the first time moving the dragbar, draw the // dragbar so XOR drawing works properly if (firstTimeDragging) { dragBar = new Rectangle(0, 0, tree.getWidth(), 3); dragBar.setLocation(0, tree.getRowBounds(lastHoveringRow).y + tree.getRowBounds(lastHoveringRow).height - 3); g2.fill(dragBar); firstTimeDragging = false; } // XOR drawing mode of horizontal drag bar g2.fill(dragBar); dragBar.setLocation(0, tree.getRowBounds(rowNew).y + tree.getRowBounds(rowNew).height - 3); g2.fill(dragBar); lastHoveringRow = rowNew; } }); tree.setCellEditor(cellEditor); tree.setInvokesStopCellEditing(true); tree.setBackground(getBackground()); tree.addTreeSelectionListener(new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent e) { fireLayerSelectionChanged(); } }); tree.getModel().addTreeModelListener(new TreeModelListener() { public void treeNodesChanged(TreeModelEvent e) { } public void treeNodesInserted(TreeModelEvent e) { for (int i = 0; i < e.getChildren().length; i++) { TreeUtil.visit(tree.getModel(), e.getTreePath() .pathByAddingChild(e.getChildren()[i]), new TreeUtil.Visitor() { public void visit(Stack path) { // When opening a task file, don't expand the ColorThemingValues. // [Jon Aquino 2005-08-01] if (path.peek() instanceof LayerTreeModel.ColorThemingValue) { return; } tree.makeVisible(new TreePath(path .toArray())); } }); } } public void treeNodesRemoved(TreeModelEvent e) { } public void treeStructureChanged(TreeModelEvent e) { } }); TreeUtil.expandAll(tree, new TreePath(tree.getModel().getRoot())); } public void addPopupMenu(Class nodeClass, JPopupMenu popupMenu) { nodeClassToPopupMenuMap.put(nodeClass, popupMenu); } private void setCellRenderer(Map additionalNodeClassToTreeCellRendererMap) { final Map map = createNodeClassToTreeCellRendererMap(); map.putAll(additionalNodeClassToTreeCellRendererMap); tree.setCellRenderer(new TreeCellRenderer() { private DefaultTreeCellRenderer defaultRenderer = new DefaultTreeCellRenderer() { { // Transparent. [Jon Aquino] setBackgroundNonSelectionColor(new Color(0, 0, 0, 0)); } }; public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { return ((TreeCellRenderer) LangUtil.ifNull(CollectionUtil.get( value.getClass(), map), defaultRenderer)) .getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); } }); } private Map createNodeClassToTreeCellRendererMap() { HashMap map = new HashMap(); map.put(Layer.class, layerTreeCellRenderer); map.put(WMSLayer.class, layerTreeCellRenderer); map.put(Category.class, layerTreeCellRenderer); map.put(LayerTreeModel.ColorThemingValue.class, createColorThemingValueRenderer()); return map; } private TreeCellRenderer createColorThemingValueRenderer() { return new TreeCellRenderer() { private JPanel panel = new JPanel(new GridBagLayout()); private ColorPanel colorPanel = new ColorPanel(); private JLabel label = new JLabel(); { panel.add(colorPanel, new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0,0,0,0), 0, 0)); panel.add(label, new GridBagConstraints(1, 0, 1, 1, 0, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0,5,0,0), 0, 0)); } public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { label.setText(((LayerTreeModel.ColorThemingValue) value) .toString()); BasicStyle style = ((LayerTreeModel.ColorThemingValue) value) .getStyle(); colorPanel.setLineColor(style.isRenderingLine() ? GUIUtil.alphaColor(style.getLineColor(), style .getAlpha()) : GUIUtil.alphaColor(Color.BLACK, 0)); colorPanel.setFillColor(style.isRenderingFill() ? GUIUtil.alphaColor(style.getFillColor(), style .getAlpha()) : GUIUtil.alphaColor(Color.BLACK, 0)); return panel; } }; } void jbInit() throws Exception { this.setLayout(borderLayout1); tree.addMouseListener(new java.awt.event.MouseAdapter() { public void mouseReleased(MouseEvent e) { tree_mouseReleased(e); } }); ToolTipManager.sharedInstance().registerComponent(tree); tree.setEditable(true); tree.setRootVisible(false); // Row height is set to -1 because otherwise, in Java 1.4, tree nodes // will be "chopped off" at the bottom [Jon Aquino] tree.setRowHeight(-1); scrollPane.getVerticalScrollBar().setUnitIncrement(20); tree.setShowsRootHandles(true); scrollPane .setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); scrollPane.setBorder(BorderFactory.createEtchedBorder()); scrollPane.getViewport().add(tree); this.add(scrollPane, BorderLayout.CENTER); } void tree_mouseReleased(MouseEvent e) { if (!SwingUtilities.isRightMouseButton(e)) { return; } TreePath popupPath = tree.getPathForLocation(e.getX(), e.getY()); if (popupPath == null) { return; } popupNode = popupPath.getLastPathComponent(); // #isAltDown returns true on a middle-click; #isMetaDown returns true // on a right-click[Jon Aquino] // Third check can't simply user JTree#isPathSelected because the node // wrappers are value objects and thus can't reliably be compared by // reference (which is what #isPathSelected seems to do). [Jon Aquino] if (!(e.isControlDown() || e.isShiftDown() || selectedNodes( Object.class).contains(popupNode))) { tree.getSelectionModel().clearSelection(); } tree.getSelectionModel().addSelectionPath(popupPath); if (getPopupMenu(popupNode.getClass()) != null) { getPopupMenu(popupNode.getClass()).show(e.getComponent(), e.getX(), e.getY()); } } private JPopupMenu getPopupMenu(Class nodeClass) { return (JPopupMenu) CollectionUtil.get(nodeClass, nodeClassToPopupMenuMap); } private void handleCheckBoxClick(MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) { return; } TreePath path = tree.getPathForLocation(e.getX(), e.getY()); if (path == null) { return; } Object node = path.getLastPathComponent(); if (!(node instanceof Layerable)) { return; } Layerable layerable = (Layerable) node; Point layerNodeLocation = tree.getUI().getPathBounds(tree, path) .getLocation(); // Initialize the LayerNameRenderer with the current node. // checkBoxBounds will be different for Layers and WMSLayers. [Jon // Aquino] layerTreeCellRenderer.getLayerNameRenderer() .getTreeCellRendererComponent(tree, path.getLastPathComponent(), false, false, false, 0, false); Rectangle checkBoxBounds = layerTreeCellRenderer.getLayerNameRenderer() .getCheckBoxBounds(); checkBoxBounds.translate((int) layerNodeLocation.getX(), (int) layerNodeLocation.getY()); if (checkBoxBounds.contains(e.getPoint())) { layerable.setVisible(!layerable.isVisible()); } } public Layer[] getSelectedLayers() { return selectedLayers(this); } public static Layer[] selectedLayers(LayerNamePanel layerNamePanel) { return (Layer[]) layerNamePanel.selectedNodes(Layer.class).toArray( new Layer[]{}); } public Collection getSelectedCategories() { return selectedNodes(Category.class); } public Collection selectedNodes(Class c) { return selectedNodes(c, tree); } public static Collection selectedNodes(Class c, JTree tree) { ArrayList selectedNodes = new ArrayList(); TreePath[] selectionPaths = tree.getSelectionPaths(); if (selectionPaths == null) { return new ArrayList(); } for (int i = 0; i < selectionPaths.length; i++) { Object node = selectionPaths[i].getLastPathComponent(); if (c.isInstance(node)) { selectedNodes.add(node); } } return selectedNodes; } public void setSelectedLayers(Layer[] layers) { tree.getSelectionModel().clearSelection(); for (int i = 0; i < layers.length; i++) { addSelectedLayer(layers[i]); } } protected void addSelectedLayer(Layer layer) { tree.addSelectionPath(TreeUtil.findTreePath(layer, tree.getModel())); } public void layerChanged(final LayerEvent e) { TreeModelEvent treeModelEvent = new TreeModelEvent(this, new Object[]{ tree.getModel().getRoot(), e.getCategory()}, new int[]{e .getLayerableIndex()}, new Object[]{e.getLayerable()}); if (e.getType() == LayerEventType.ADDED) { firableTreeModelWrapper.fireTreeNodesInserted(treeModelEvent); // firableTreeModelWrapper.fireTreeStructureChanged(treeModelEvent); if ((e.getType() == LayerEventType.ADDED) && ((selectedNodes(Layerable.class)).size() == 0) && e.getLayerable() instanceof Layer) { addSelectedLayer((Layer) e.getLayerable()); } return; } if (e.getType() == LayerEventType.REMOVED) { firableTreeModelWrapper.fireTreeNodesRemoved(treeModelEvent); return; } if (e.getType() == LayerEventType.APPEARANCE_CHANGED) { // For some reason, if we don't use #invokeLater to call #fireTreeStructureChanged, // blank lines get inserted into the JTree. For more information, see Java Bug 4498762, // "When expandPath() is called by a JTree method, extra blank lines appear", // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4498762 // [Jon Aquino 2005-07-25] SwingUtilities.invokeLater(new Runnable(){ public void run() { // Specify the path of the subtree rooted at the layer -- the ColorThemingValues // may have changed. [Jon Aquino 2005-07-25] firableTreeModelWrapper.fireTreeStructureChanged(new TreeModelEvent( this, new Object[]{tree.getModel().getRoot(), e.getCategory(), e.getLayerable()})); } }); return; } if (e.getType() == LayerEventType.METADATA_CHANGED) { firableTreeModelWrapper.fireTreeNodesChanged(treeModelEvent); return; } if (e.getType() == LayerEventType.VISIBILITY_CHANGED) { firableTreeModelWrapper.fireTreeNodesChanged(treeModelEvent); return; } Assert.shouldNeverReachHere(); } public void categoryChanged(CategoryEvent e) { TreeModelEvent treeModelEvent = new TreeModelEvent(this, new Object[]{tree.getModel().getRoot()}, new int[]{e .getCategoryIndex() + indexOfFirstCategoryInTree()}, new Object[]{e .getCategory()}); if (e.getType() == CategoryEventType.ADDED) { firableTreeModelWrapper.fireTreeNodesInserted(treeModelEvent); return; } if (e.getType() == CategoryEventType.REMOVED) { firableTreeModelWrapper.fireTreeNodesRemoved(treeModelEvent); return; } if (e.getType() == CategoryEventType.METADATA_CHANGED) { firableTreeModelWrapper.fireTreeNodesChanged(treeModelEvent); return; } Assert.shouldNeverReachHere(); } private int indexOfFirstCategoryInTree() { // Not 0 in ESE. [Jon Aquino] for (int i = 0; i < tree.getModel().getChildCount( tree.getModel().getRoot()); i++) { if (tree.getModel().getChild(tree.getModel().getRoot(), i) instanceof Category) { return i; } } Assert.shouldNeverReachHere(); return -1; } public void featuresChanged(FeatureEvent e) { } public void dispose() { // Layer events could still be fired after the TaskWindow containing // this LayerNamePanel is closed (e.g. by clones of the TaskWindow, or // by an attribute viewer). [Jon Aquino] layerManagerProxy.getLayerManager().removeLayerListener(this); } public JTree getTree() { return tree; } public void addListener(LayerNamePanelListener listener) { listeners.add(listener); } public void removeListener(LayerNamePanelListener listener) { listeners.remove(listener); } public void fireLayerSelectionChanged() { for (Iterator i = listeners.iterator(); i.hasNext();) { LayerNamePanelListener l = (LayerNamePanelListener) i.next(); l.layerSelectionChanged(); } } public LayerManager getLayerManager() { return layerManagerProxy.getLayerManager(); } public static Layer chooseEditableLayer(LayerNamePanel panel) { for (Iterator i = Arrays.asList(panel.getSelectedLayers()).iterator(); i .hasNext();) { Layer layer = (Layer) i.next(); if (layer.isEditable()) { return layer; } } if (panel.getLayerManager().getEditableLayers().isEmpty()) { return null; } return (Layer) panel.getLayerManager().getEditableLayers().iterator() .next(); } public Layer chooseEditableLayer() { return chooseEditableLayer(this); } public LayerNamePanel getLayerNamePanel() { return this; } protected FirableTreeModelWrapper getFirableTreeModelWrapper() { return firableTreeModelWrapper; } public Object getPopupNode() { return popupNode; } protected LayerTreeCellRenderer getLayerTreeCellRenderer() { return layerTreeCellRenderer; } }