/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2007 - 2008, Open Source Geospatial Foundation (OSGeo) * (C) 2008 - 2011, Johann Sorel * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library 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 * Lesser General Public License for more details. */ package org.geotoolkit.gui.swing.contexttree; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.EventObject; import java.util.List; import java.util.logging.Level; import javax.swing.DropMode; import javax.swing.ImageIcon; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.JTree; import javax.swing.ScrollPaneConstants; import javax.swing.SwingUtilities; import javax.swing.TransferHandler; import javax.swing.event.CellEditorListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.MutableTreeNode; import javax.swing.tree.TreeCellEditor; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.geotoolkit.data.FeatureStoreContentEvent; import org.geotoolkit.data.FeatureStoreListener; import org.geotoolkit.data.FeatureStoreManagementEvent; import org.geotoolkit.data.session.Session; import org.geotoolkit.display.PortrayalException; import org.geotoolkit.display2d.primitive.GraphicJ2D; import org.geotoolkit.display2d.service.DefaultGlyphService; import org.geotoolkit.gui.swing.style.JOpacitySlider; import org.geotoolkit.map.FeatureMapLayer; import org.geotoolkit.map.GraphicBuilder; import org.geotoolkit.map.ItemListener; import org.geotoolkit.map.MapContext; import org.geotoolkit.map.MapItem; import org.geotoolkit.map.MapLayer; import org.geotoolkit.util.collection.CollectionChangeEvent; import org.geotoolkit.style.MutableStyleFactory; import org.apache.sis.util.iso.SimpleInternationalString; import org.geotoolkit.style.DefaultStyleFactory; import org.geotoolkit.style.MutableFeatureTypeStyle; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.logging.Logging; import org.opengis.style.Description; import org.opengis.style.FeatureTypeStyle; import org.opengis.style.Rule; import static org.apache.sis.util.ArgumentChecks.*; import org.geotoolkit.font.FontAwesomeIcons; import org.geotoolkit.font.IconBuilder; import org.geotoolkit.gui.swing.style.JStyleTree; public class JContextTree extends JScrollPane { private static final DataFlavor ITEM_FLAVOR = new DataFlavor(org.geotoolkit.map.MapItem.class, "geo/item"); private static final MutableStyleFactory SF = new DefaultStyleFactory(); private static final ImageIcon ICON_FTS = JStyleTree.ICON_FTS; private static final ImageIcon ICON_GROUP = IconBuilder.createIcon(FontAwesomeIcons.ICON_FOLDER_O,16,FontAwesomeIcons.DEFAULT_COLOR); private final List<TreePopupItem> controls = new ArrayList<TreePopupItem>(); private final JTree tree = new JTree(new DefaultTreeModel(new DefaultMutableTreeNode())); private final TreePopup popup = new TreePopup(this); private final ContextCellRenderer editor = new ContextCellRenderer(); private final ContextCellRenderer renderer = new ContextCellRenderer(); public JContextTree() { add(tree); tree.setCellRenderer(renderer); tree.setCellEditor(editor); tree.setShowsRootHandles(false); tree.setEditable(true); tree.setDragEnabled(true); tree.setTransferHandler(new LayerHandler()); tree.setDropMode(DropMode.ON_OR_INSERT); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.setComponentPopupMenu(popup); tree.setScrollsOnExpand(false); tree.setLargeModel(true); setViewportView(tree); setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); initCellEditAcceleration(); } public void setRootVisible(final boolean visible){ tree.setRootVisible(visible); } public boolean isRootVisible(){ return tree.isRootVisible(); } public boolean isEditable(){ return tree.isEditable(); } public void setEditable(final boolean edit){ tree.setEditable(edit); } public void setContext(final MapContext context) { final DefaultMutableTreeNode node; if(context != null){ node = new MapItemTreeNode(context); }else{ node = new DefaultMutableTreeNode(); } tree.setModel(new DefaultTreeModel(node)); tree.expandPath(new TreePath(node.getPath())); } public MapContext getContext() { final DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getModel().getRoot(); return (MapContext) node.getUserObject(); } /** * @return live list of TreePopupItem */ public List<TreePopupItem> controls() { return controls; } JTree getRealTree(){ return tree; } private int getRowAt(final Point p){ int row = tree.getRowForLocation(p.x, p.y); if(row == -1){ //more intensive search, row selectable area might be small for(int i=0,n=tree.getRowCount();i<n;i++){ final Rectangle rect = tree.getRowBounds(i); if(p.y> rect.y && p.y< rect.y+rect.height){ row = i; break; } } } return row; } /** * add mouse listener to set cell in edit mode when mouseover */ private void initCellEditAcceleration() { //listener to set cell in select mode on mouse over tree.addMouseMotionListener(new MouseMotionListener() { @Override public void mouseDragged(MouseEvent e) { } @Override public void mouseMoved(MouseEvent e) { final Point p = e.getPoint(); if (p == null) { return; } int row = getRowAt(p); final TreePath overPath = tree.getPathForRow(row); final TreePath editPath = tree.getEditingPath(); if(tree.isEditing()){ tree.stopEditing(); } tree.setSelectionPath(overPath); } }); //listener to propage mouse events in edition mode tree.addMouseListener(new MouseListener() { private MouseEvent pressedEvent = null; @Override public void mouseClicked(final MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { this.pressedEvent = e; } @Override public void mouseReleased(MouseEvent releaseEvent) { if(releaseEvent.getButton() != MouseEvent.BUTTON1){ //forward event only on left click to avoid disturbing popup menu. return; } int row = getRowAt(releaseEvent.getPoint()); final TreePath under = tree.getPathForRow(row); if (under != null && tree.getRowForLocation(releaseEvent.getPoint().x, releaseEvent.getPoint().y) == -1) { if (!under.equals(tree.getEditingPath())) { //different node, change edition tree.stopEditing(); tree.startEditingAtPath(under); //propagate event to undereath component final Point componentPoint = SwingUtilities.convertPoint(tree, new Point(releaseEvent.getX(), releaseEvent.getY()), editor.panel); final Component destination = SwingUtilities.getDeepestComponentAt(editor.panel, componentPoint.x, componentPoint.y); if (destination != null && pressedEvent != null) { destination.dispatchEvent(SwingUtilities.convertMouseEvent(tree, pressedEvent, destination)); destination.dispatchEvent(SwingUtilities.convertMouseEvent(tree, releaseEvent, destination)); } releaseEvent.consume(); } } } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } }); } private static String label(final Description desc){ if(desc != null && desc.getTitle() != null){ return desc.getTitle().toString().replace("{}", ""); }else{ return ""; } } private static String label(final MapItem item) { String label = ""; final Description desc = item.getDescription(); if (desc != null && desc.getTitle() != null) { label = desc.getTitle().toString().replace("{}", ""); } if (label.isEmpty() && item.getName() != null) { label = item.getName(); } return label; } //////////////////////////////////////////////////////////////////////////// //private classes ////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// private class ContextCellRenderer extends DefaultTreeCellRenderer implements TreeCellEditor { private final JPanel panel; private final JLabel icon = new JLabel(); private final JOpacitySlider opacity = new JOpacitySlider(); private final JCheckBox visibleCheck = new VisibleCheck(); private final JCheckBox selectCheck = new SelectionCheck(); private final JLabel label = new JLabel(" "){ @Override public Dimension getPreferredSize() { final Dimension dim = super.getPreferredSize(); dim.height += 2; return dim; } }; private final JTextField field = new JTextField(); private Object value = null; public ContextCellRenderer() { final FlowLayout layout = new FlowLayout(FlowLayout.LEFT,1,0); panel = new JPanel(layout); field.setOpaque(false); field.setPreferredSize(new Dimension(140,label.getPreferredSize().height)); visibleCheck.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (value != null && value instanceof MapItem) { new Thread(){ @Override public void run() { ((MapItem) value).setVisible(visibleCheck.isSelected()); } }.start(); } } }); selectCheck.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (value != null && value instanceof MapLayer) { new Thread(){ @Override public void run() { ((MapLayer) value).setSelectable(selectCheck.isSelected()); } }.start(); } } }); opacity.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (value != null && value instanceof MapLayer) { new Thread(){ @Override public void run() { ((MapLayer) value).setOpacity(opacity.getOpacity()); } }.start(); } } }); panel.setOpaque(false); opacity.setPreferredSize(new Dimension(60, 22)); opacity.setSize(new Dimension(60, 22)); } @Override public Component getTreeCellRendererComponent(final JTree tree, final Object obj, final boolean selected, final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) { return getComponent(obj, false); } @Override public Component getTreeCellEditorComponent(final JTree tree, final Object obj, final boolean isSelected, final boolean expanded, final boolean leaf, final int row) { return getComponent(obj, true); } private Component getComponent(Object obj, final boolean edition){ final DefaultMutableTreeNode node = (DefaultMutableTreeNode) obj; if (node != null) obj = node.getUserObject(); value = obj; this.label.setIcon(null); panel.removeAll(); panel.revalidate(); panel.repaint(); if (obj instanceof MapLayer) { final MapLayer layer = (MapLayer) obj; opacity.setOpacity(layer.getOpacity()); this.visibleCheck.setSelected(layer.isVisible()); panel.add(visibleCheck); this.selectCheck.setSelected(layer.isSelectable()); panel.add(selectCheck); panel.add(opacity); if(edition){ this.field.setText(label(layer)); panel.add(field); }else{ this.label.setText(label(layer)+" "); panel.add(label); } if(layer instanceof FeatureMapLayer){ final FeatureMapLayer fml = (FeatureMapLayer) layer; final Session session = fml.getCollection().getSession(); if(session != null && session.hasPendingChanges()){ this.label.setText(label.getText()+"*"); } } } else if (obj instanceof MapItem) { final MapItem item = (MapItem) obj; this.icon.setIcon(ICON_GROUP); panel.add(icon); this.visibleCheck.setSelected(item.isVisible()); panel.add(visibleCheck); if(edition){ this.field.setText(label(item)); panel.add(field); }else{ this.label.setText(label(item)+" "); panel.add(label); } }else if(obj instanceof FeatureTypeStyle){ final FeatureTypeStyle fts = (FeatureTypeStyle) obj; this.icon.setIcon(ICON_FTS); panel.add(icon); this.label.setText(label(fts.getDescription())); panel.add(label); }else if(obj instanceof Rule){ final Rule rule = (Rule) obj; MapLayer layer = null; DefaultMutableTreeNode parent = (DefaultMutableTreeNode) node.getParent(); while(layer == null && parent != null){ Object cdt = parent.getUserObject(); if(cdt instanceof MapLayer){ layer = (MapLayer) cdt; }else{ parent = (DefaultMutableTreeNode) parent.getParent(); } } final Dimension dim = DefaultGlyphService.glyphPreferredSize(rule, null, layer); final BufferedImage img = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_ARGB); DefaultGlyphService.render(rule, new Rectangle(dim), img.createGraphics(),layer); this.icon.setIcon(new ImageIcon(img)); panel.add(icon); this.label.setText(label(rule.getDescription())); panel.add(label); } else if(obj instanceof Image){ final Image img = (Image) obj; this.label.setText(""); this.label.setIcon(new ImageIcon(img)); panel.add(label); } else { this.label.setText("-"); panel.add(label); } panel.revalidate(); panel.repaint(); return panel; } @Override public Object getCellEditorValue() { if (value instanceof MapLayer) { final MapLayer layer = (MapLayer) value; final Description old = layer.getDescription(); layer.setDescription(SF.description(new SimpleInternationalString(field.getText()), old.getAbstract())); layer.setSelectable(selectCheck.isSelected()); layer.setVisible(visibleCheck.isSelected()); } else if (value instanceof MapItem) { final MapItem item = (MapItem) value; final Description old = item.getDescription(); item.setDescription(SF.description(new SimpleInternationalString(field.getText()), old.getAbstract())); } Object temp = this.value; this.value = null; return temp; } @Override public boolean isCellEditable(final EventObject anEvent) { final TreePath path = tree.getSelectionPath(); if(path != null){ final Object obj = ((DefaultMutableTreeNode)path.getLastPathComponent()).getUserObject(); return obj instanceof MapItem; } return false; } @Override public boolean shouldSelectCell(final EventObject anEvent) { return true; } @Override public boolean stopCellEditing() { return true; } @Override public void cancelCellEditing() { } @Override public void addCellEditorListener(final CellEditorListener l) { } @Override public void removeCellEditorListener(final CellEditorListener l) { } } private class LayerHandler extends TransferHandler { @Override public int getSourceActions(final JComponent c) { return TransferHandler.MOVE; } @Override protected Transferable createTransferable(final JComponent c) { final JTree tree = (JTree) c; final TreePath path = tree.getSelectionPath(); final DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); if (node != null && (node.getUserObject() instanceof MapItem && !node.isRoot())) { return new MapItemTransferable(path); } return null; } @Override public boolean canImport(final TransferHandler.TransferSupport support) { if (!support.isDataFlavorSupported(ITEM_FLAVOR) || !support.isDrop()) { return false; } final JTree.DropLocation dropLocation = (JTree.DropLocation) support.getDropLocation(); return dropLocation.getPath() != null; } @Override public boolean importData(final TransferHandler.TransferSupport support) { if (!canImport(support)) { return false; } final JTree.DropLocation dropLocation = (JTree.DropLocation) support.getDropLocation(); final TreePath dropPath = dropLocation.getPath(); final Transferable transferable = support.getTransferable(); int dropIndex = dropLocation.getChildIndex(); final TreePath sourcePath; try { sourcePath = (TreePath) transferable.getTransferData(ITEM_FLAVOR); } catch (UnsupportedFlavorException | IOException ex) { Logging.getLogger("org.geotoolkit.gui.swing.contexttree").log(Level.INFO, null, ex); return false; } if(sourcePath == null){ return false; } final DefaultMutableTreeNode lastNode = ((DefaultMutableTreeNode)sourcePath.getLastPathComponent()); final MapItem dragged = (MapItem) lastNode.getUserObject(); final MapItem lastParent = (MapItem) ((DefaultMutableTreeNode)lastNode.getParent()).getUserObject(); if(ArraysExt.contains(dropPath.getPath(), lastNode)){ //trying to drop a node in himself, not possible return true; } DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) dropPath.getLastPathComponent(); Object parent = parentNode.getUserObject(); if(!(parent instanceof MapItem)){ return true; } //flip drop index if(dropIndex != -1){ dropIndex = ((MapItem)parent).items().size() - dropIndex; } if(parent instanceof MapLayer){ //we adjust the drop location final MapItem newParent = (MapItem) ((DefaultMutableTreeNode)(parentNode.getParent())).getUserObject(); final MapLayer layer = (MapLayer) parent; parent = newParent; dropIndex = newParent.items().indexOf(layer); } //this far we are sure it's a MapItem final MapItem newParent = (MapItem) parent; if(lastParent == newParent && dropIndex != -1){ //moving node is the same parent, readjust dropIndex if(lastParent.items().indexOf(dragged) < dropIndex){ dropIndex--; } }else{ dropIndex = 0; } //remove from previous position lastParent.items().remove(dragged); newParent.items().add(dropIndex, dragged); return true; } } private class MapItemTransferable implements Transferable { private final TreePath path; private MapItemTransferable(final TreePath path) { this.path = path; } @Override public DataFlavor[] getTransferDataFlavors() { return new DataFlavor[]{ITEM_FLAVOR}; } @Override public boolean isDataFlavorSupported(final DataFlavor flavor) { return flavor.equals(ITEM_FLAVOR); } @Override public Object getTransferData(final DataFlavor flavor) throws UnsupportedFlavorException, IOException { if(ITEM_FLAVOR.equals(flavor)){ return path; } return null; } } private class MapItemTreeNode extends DefaultMutableTreeNode implements ItemListener, FeatureStoreListener{ private MapItemTreeNode(final MapItem item){ super(item); ensureNonNull("item", item); item.addItemListener(new ItemListener.Weak(item, MapItemTreeNode.this)); if(item instanceof FeatureMapLayer){ final FeatureMapLayer fml = (FeatureMapLayer) item; fml.getCollection().getSession().addStorageListener(new FeatureStoreListener.Weak(this)); } resetStructure(); } @Override public MapItem getUserObject() { return (MapItem)super.getUserObject(); } @Override public void setUserObject(final Object userObject) { //not allowed to modify this object } private synchronized void resetStructure(){ boolean expanded = false; if(this.getChildCount()>0){ final TreePath tp = new TreePath(((DefaultMutableTreeNode)this.getFirstChild()).getPath()); expanded = tree.isExpanded(tp); } removeAllChildren(); final MapItem item = (MapItem) getUserObject(); final List<MapItem> childs = new ArrayList<MapItem>(item.items()); Collections.reverse(childs); for(final MapItem child : childs){ final MapItemTreeNode childNode = new MapItemTreeNode(child); add(childNode); } if(item instanceof MapLayer){ fillStyleNodes((MapLayer)item); } ((DefaultTreeModel)tree.getModel()).nodeStructureChanged(this); if(expanded && this.getChildCount()>0){ final TreePath tp = new TreePath(((DefaultMutableTreeNode)this.getFirstChild()).getPath()); tree.expandPath(tp); } } @Override public synchronized void itemChange(final CollectionChangeEvent<MapItem> event) { if(CollectionChangeEvent.ITEM_ADDED == event.getType()){ final List<MapItem> childs = new ArrayList<MapItem>(getUserObject().items()); Collections.reverse(childs); int i=0; for(MapItem child : childs){ final MapItemTreeNode pair = (i<getChildCount()) ? ((MapItemTreeNode)getChildAt(i)) : null; if(pair == null || !child.equals(pair.getUserObject())){ //this child was added MapItemTreeNode childNode = new MapItemTreeNode(child); ((DefaultTreeModel)tree.getModel()).insertNodeInto(childNode, this, i); tree.expandPath(new TreePath(this.getPath())); } i++; } }else if(CollectionChangeEvent.ITEM_REMOVED == event.getType()){ final List<MutableTreeNode> toRemove = new ArrayList<MutableTreeNode>(); for(final MapItem item : event.getItems()){ for(int i=0,n=getChildCount(); i<n;i++){ final DefaultMutableTreeNode child = ((DefaultMutableTreeNode)getChildAt(i)); if(item.equals( child.getUserObject())){ toRemove.add(child); } } } for(final MutableTreeNode n : toRemove){ ((DefaultTreeModel)tree.getModel()).removeNodeFromParent(n); } } } @Override public void propertyChange(final PropertyChangeEvent evt) { if(MapLayer.STYLE_PROPERTY.equals(evt.getPropertyName())){ //must regenerate style node elements resetStructure(); }else{ ((DefaultTreeModel)tree.getModel()).nodeChanged(this); } } private void fillStyleNodes(final MapLayer layer){ final GraphicBuilder gb = layer.getGraphicBuilder(GraphicJ2D.class); if(gb != null){ //this kind of layer have there own style systems we rely on it try { final Image img = gb.getLegend(layer); final DefaultMutableTreeNode imgNode = new DefaultMutableTreeNode(img); this.add(imgNode); } catch (PortrayalException ex) { Logging.getLogger("org.geotoolkit.gui.swing.contexttree").log(Level.WARNING, null, ex); } }else{ final List<MutableFeatureTypeStyle> ftss = layer.getStyle().featureTypeStyles(); if(ftss.size() == 1){ for(FeatureTypeStyle fts : layer.getStyle().featureTypeStyles()){ for(Rule rule : fts.rules()){ final DefaultMutableTreeNode ruleNode = new DefaultMutableTreeNode(rule); this.add(ruleNode); } } }else{ for(FeatureTypeStyle fts : layer.getStyle().featureTypeStyles()){ final DefaultMutableTreeNode ftsNode = new DefaultMutableTreeNode(fts); for(Rule rule : fts.rules()){ final DefaultMutableTreeNode ruleNode = new DefaultMutableTreeNode(rule); ftsNode.add(ruleNode); } this.add(ftsNode); } } } } @Override public void structureChanged(FeatureStoreManagementEvent event) { } @Override public void contentChanged(FeatureStoreContentEvent event) { if(event.getType() == FeatureStoreContentEvent.Type.SESSION){ //pending changes, refresh node to add a small * to specify //some changes are made ((DefaultTreeModel)tree.getModel()).nodeChanged(this); } } } }