/******************************************************************************* * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink ******************************************************************************/ package org.eclipse.persistence.tools.workbench.framework.internal; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.prefs.Preferences; 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.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultTreeSelectionModel; import javax.swing.tree.TreeCellRenderer; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import org.eclipse.persistence.tools.workbench.framework.app.AccessibleNode; import org.eclipse.persistence.tools.workbench.framework.app.ApplicationNode; import org.eclipse.persistence.tools.workbench.framework.app.GroupContainerDescription; import org.eclipse.persistence.tools.workbench.framework.app.NavigatorSelectionModel; import org.eclipse.persistence.tools.workbench.framework.context.ApplicationContext; import org.eclipse.persistence.tools.workbench.framework.help.HelpManager; import org.eclipse.persistence.tools.workbench.framework.resources.ResourceRepository; import org.eclipse.persistence.tools.workbench.uitools.Displayable; import org.eclipse.persistence.tools.workbench.uitools.app.TreeNodeValueModel; import org.eclipse.persistence.tools.workbench.uitools.app.ValueModel; import org.eclipse.persistence.tools.workbench.uitools.app.swing.TreeModelAdapter; import org.eclipse.persistence.tools.workbench.uitools.cell.DisplayableTreeCellRenderer; import org.eclipse.persistence.tools.workbench.utility.CollectionTools; import org.eclipse.persistence.tools.workbench.utility.SimpleStack; import org.eclipse.persistence.tools.workbench.utility.Stack; import org.eclipse.persistence.tools.workbench.utility.iterators.EnumerationIterator; /** * This view is the "navigator" tree that displays all the various nodes * in a JTree and manages user interactions with the tree. */ final class NavigatorView { /** * The context provides us with the application-wide root node, * help manager, and resource repository (for our label). */ private ApplicationContext context; /** * The pop-up menu is rebuilt whenever the selection * menu description changes. */ private ValueModel selectionMenuDescriptionHolder; private PropertyChangeListener selectionMenuDescriptionListener; private JPopupMenu selectionPopupMenu; /** * We need to hold the navigator selection so the workspace view * can listen to it and coordinate the editor and problem views. */ private SelectionModel selectionModel; /** * We listen for the first node to be added so we can select it * and have it show up in the navigator; otherwise the root node * is hidden and un-expanded, making it impossible for the user * to see or manipulate anything in the navigator. * (Originally, we listened to the root node's children, but then * we would get notified before the TreeModelAdapter and the * selection would never occur.) */ private TreeModel treeModel; private TreeModelListener treeModelListener; private int rootNodeChildrenSize; /** This is the view's component, the tree. */ private JPanel component; private JTree tree; // ********** constructor/initialization ********** /** * We need access to the selection menu so we can display it * as a pop-up menu when the user either right-clicks with the * mouse or presses Shift-F10. */ NavigatorView(ApplicationContext context, ValueModel selectionMenuDescriptionHolder) { super(); this.context = context; this.selectionPopupMenu = new JPopupMenu(); this.selectionMenuDescriptionHolder = selectionMenuDescriptionHolder; this.selectionMenuDescriptionListener = this.buildSelectionMenuDescriptionListener(); this.selectionMenuDescriptionHolder.addPropertyChangeListener(ValueModel.VALUE, this.selectionMenuDescriptionListener); this.treeModel = this.buildTreeModel(); this.treeModelListener = this.buildTreeModelListener(); this.treeModel.addTreeModelListener(this.treeModelListener); this.rootNodeChildrenSize = this.rootNode().getChildrenModel().size(); this.component = this.buildComponent(); } private PropertyChangeListener buildSelectionMenuDescriptionListener() { return new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { NavigatorView.this.selectionMenuDescriptionChanged(); } }; } private JPanel buildComponent() { JPanel panel = new JPanel(new BorderLayout()); panel.setBorder(BorderFactory.createEtchedBorder()); panel.setMinimumSize(new Dimension(0, 0)); JLabel label = new JLabel(this.resourceRepository().getString("NAVIGATOR_LABEL")); label.setDisplayedMnemonic(this.resourceRepository().getMnemonic("NAVIGATOR_LABEL")); label.setIcon(this.resourceRepository().getIcon("navigator")); label.setBorder( BorderFactory.createCompoundBorder( BorderFactory.createMatteBorder(0, 0, 1, 0, panel.getBackground().darker()), BorderFactory.createEmptyBorder(2, 2, 2, 2) ) ); panel.add(label, BorderLayout.PAGE_START); // we need a circular reference between the tree and the tree selection model this.tree = this.buildTree(); this.selectionModel = new SelectionModel(this.tree); this.tree.setSelectionModel(this.selectionModel); JScrollPane scrollPane = new JScrollPane(this.tree); scrollPane.getVerticalScrollBar().setUnitIncrement(10); scrollPane.getHorizontalScrollBar().setUnitIncrement(10); scrollPane.setBorder(null); panel.add(scrollPane, BorderLayout.CENTER); label.setLabelFor(this.tree); return panel; } /** * Build a tree using the root node from the workbench context. * A custom selection model will be set later. */ private JTree buildTree() { JTree result = new NavigatorTree(this.treeModel); result.setShowsRootHandles(true); result.setRootVisible(false); result.setCellRenderer(this.buildTreeCellRenderer()); result.setRowHeight(0); // row height will be determined by the renderer result.addMouseListener(this.buildMouseListener()); result.addKeyListener(this.buildKeyListener()); return result; } /** * HACK: Build a new renderer every time or things get screwed * up when JAWS is running. */ private TreeCellRenderer buildTreeCellRenderer() { return new TreeCellRenderer() { public Component getTreeCellRendererComponent(JTree t, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { return new ApplicationNodeTreeCellRenderer().getTreeCellRendererComponent(t, value, selected, expanded, leaf, row, hasFocus); } }; } private MouseListener buildMouseListener() { return new MouseAdapter() { public void mousePressed(MouseEvent e) { this.handleMouseEvent(e); } public void mouseReleased(MouseEvent e) { this.handleMouseEvent(e); } private void handleMouseEvent(MouseEvent e) { if (e.isPopupTrigger()) { NavigatorView.this.displayPopupMenu((JTree) e.getSource(), e.getX(), e.getY()); } } }; } private KeyListener buildKeyListener() { return new KeyAdapter() { public void keyPressed(KeyEvent e) { NavigatorView.this.keyPressed(e); } }; } private TreeModel buildTreeModel() { return new TreeModelAdapter(this.rootNode()); } private TreeModelListener buildTreeModelListener() { return new TreeModelListener() { public void treeNodesChanged(TreeModelEvent e) { NavigatorView.this.treeChanged(); } public void treeNodesInserted(TreeModelEvent e) { NavigatorView.this.treeChanged(); } public void treeNodesRemoved(TreeModelEvent e) { NavigatorView.this.treeChanged(); } public void treeStructureChanged(TreeModelEvent e) { NavigatorView.this.treeChanged(); } }; } // ********** behavior ********** /** * Add a listener that will be notified of tree selection events. */ void addTreeSelectionListener(TreeSelectionListener listener) { this.selectionModel.addTreeSelectionListener(listener); } /** * Remove the specified listener. */ void removeTreeSelectionListener(TreeSelectionListener listener) { this.selectionModel.removeTreeSelectionListener(listener); } void selectionMenuDescriptionChanged() { this.selectionPopupMenu.removeAll(); GroupContainerDescription selectionMenuDescription = (GroupContainerDescription) this.selectionMenuDescriptionHolder.getValue(); for (Iterator stream = selectionMenuDescription.components(); stream.hasNext(); ) { this.selectionPopupMenu.add((Component) stream.next()); } } /** * mouse "right-click" */ void displayPopupMenu(JTree t, int x, int y) { TreePath path = t.getPathForLocation(x, y); if (path == null) { return; } TreePath[] selectedPaths = t.getSelectionPaths(); if ((selectedPaths == null) || ! CollectionTools.contains(selectedPaths, path)) { t.setSelectionPath(path); } if (this.selectionPopupMenu.getComponentCount() > 0) { this.selectionPopupMenu.show(t, x, y); } } void keyPressed(KeyEvent e) { if (e.isConsumed()) { return; } switch (e.getKeyCode()) { case KeyEvent.VK_F1: NavigatorView.this.displayHelp(); e.consume(); break; case KeyEvent.VK_F10: if (e.isShiftDown()) { NavigatorView.this.displayPopupMenu((JTree) e.getSource()); e.consume(); } break; default: break; } } /** * F1 pressed */ private void displayHelp() { ApplicationNode[] nodes = this.selectionModel.getSelectedNodes(); if (nodes.length != 1) { return; } ApplicationNode node = nodes[0]; this.helpManager().showTopic(node.helpTopicID()); } /** * Shift-F10 pressed */ private void displayPopupMenu(JTree t) { ApplicationNode[] nodes = this.selectionModel.getSelectedNodes(); if (nodes.length != 1) { return; } ApplicationNode node = nodes[0]; Rectangle rec = t.getPathBounds(new TreePath(node.path())); if (this.selectionPopupMenu.getComponentCount() > 0) { this.selectionPopupMenu.show(t, (int) rec.getCenterX(), (int) rec.getCenterY()); } } /** * Select the first node added to the root so that it is visible to the user. */ void treeChanged() { int oldSize = this.rootNodeChildrenSize; int newSize = this.rootNode().getChildrenModel().size(); if ((oldSize == 0) && (newSize != 0)) { this.selectionModel.setSelectedNode((ApplicationNode) this.rootNode().getChildrenModel().getItem(0)); } this.rootNodeChildrenSize = newSize; } void saveTreeExpansionState(Preferences windowsPreferences) { this.selectionModel.saveTreeExpansionState(windowsPreferences); } void restoreTreeExpansionState(Preferences windowsPreferences) { this.selectionModel.restoreTreeExpansionState(windowsPreferences); } /** * This is called when the window containing the navigator is closed. */ void close() { this.treeModel.removeTreeModelListener(this.treeModelListener); this.selectionMenuDescriptionHolder.removePropertyChangeListener(ValueModel.VALUE, this.selectionMenuDescriptionListener); this.tree.setModel(null); } // ********** queries ********** Component getComponent() { return this.component; } NavigatorSelectionModel getSelectionModel() { return this.selectionModel; } private ResourceRepository resourceRepository() { return this.context.getResourceRepository(); } private TreeNodeValueModel rootNode() { return this.context.getNodeManager().getRootNode(); } private HelpManager helpManager() { return this.context.getHelpManager(); } // ******************** member classes ******************** /** * Override JTree#convertValueToText() so JTree#getNextMatch() * works correctly, which is used by the TreeUIs to jump to a node * with a text value that begins with the letter typed by the user. */ private class NavigatorTree extends JTree { public NavigatorTree(TreeModel model) { super(model); } public String convertValueToText(Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { return (value == null) ? "" : ((Displayable) value).displayString(); } } /** * This class implements both the Swing TreeSelectionModel interface * (via inheritance) and the Framework NavigatorSelectionModel interface. * * This model needs a backpointer to the tree because there is no "model" * for a JTree's "expansion" state - we have to manipulate the tree directly * if we want to save and restore the tree's "expansion" state. */ private static class SelectionModel extends DefaultTreeSelectionModel implements NavigatorSelectionModel { /** backpointer to the tree; so we can push and pop its expansion state */ private JTree tree; /** stack of tree expansion states */ private Stack expansionStates; /** performance tweak */ private static final ApplicationNode[] EMPTY_SELECTED_NODES = new ApplicationNode[0]; // ********** constructor ********** public SelectionModel(JTree tree) { super(); this.tree = tree; this.expansionStates = new SimpleStack(); } // ********** NavigatorSelectionModel implementation ********** /** * Pull the last node off of each selection path. * @see org.eclipse.persistence.tools.workbench.framework.app.NavigatorSelectionModel#getSelectedNodes() */ public ApplicationNode[] getSelectedNodes() { TreePath[] paths = this.getSelectionPaths(); int len = (paths == null) ? 0 : paths.length; if (len == 0) { return EMPTY_SELECTED_NODES; } ApplicationNode[] nodes = new ApplicationNode[len]; for (int i = len; i-- > 0; ) { nodes[i] = (ApplicationNode) paths[i].getLastPathComponent(); } return nodes; } /** * Gather up the "project" nodes for all of the currently selected "leaf" nodes. * @see org.eclipse.persistence.tools.workbench.framework.app.NavigatorSelectionModel#getSelectedProjectNodes() */ public ApplicationNode[] getSelectedProjectNodes() { ApplicationNode[] selectedNodes = this.getSelectedNodes(); Set selectedRootNodes = new HashSet(selectedNodes.length); for (int i = selectedNodes.length; i-- > 0; ) { selectedRootNodes.add(selectedNodes[i].getProjectRoot()); } return (ApplicationNode[]) selectedRootNodes.toArray(new ApplicationNode[selectedRootNodes.size()]); } /** * @see org.eclipse.persistence.tools.workbench.framework.app.NavigatorSelectionModel#pushExpansionState() */ public void pushExpansionState() { this.expansionStates.push(this.currentExpansionState()); } /** * @see org.eclipse.persistence.tools.workbench.framework.app.NavigatorSelectionModel#popAndRestoreExpansionState() */ public void popAndRestoreExpansionState() { Collection expandedPaths = (Collection) this.expansionStates.pop(); for (Iterator stream = expandedPaths.iterator(); stream.hasNext(); ) { this.tree.expandPath((TreePath) stream.next()); } } public void popAndRestoreExpansionState(ApplicationNode oldNode, ApplicationNode morphedNode) { Collection expandedPaths = (Collection) this.expansionStates.pop(); for (Iterator stream = expandedPaths.iterator(); stream.hasNext(); ) { TreePath path = (TreePath) stream.next(); if (path.getLastPathComponent() == oldNode) { path = new TreePath(morphedNode.path()); } this.tree.expandPath(path); } } /** * Scroll so the node is visible once it is selected. * @see org.eclipse.persistence.tools.workbench.framework.app.NavigatorSelectionModel#setSelectedNode(org.eclipse.persistence.tools.workbench.framework.app.ApplicationNode) */ public void setSelectedNode(ApplicationNode node) { TreePath path = new TreePath(node.path()); this.setSelectionPath(path); this.tree.scrollPathToVisible(path); } /** * Scroll so the paths are visible once they are selected */ public void setSelectionPaths(TreePath[] paths) { super.setSelectionPaths(paths); for (int i = 0; i < paths.length; i++) { this.tree.scrollPathToVisible(paths[i]); } } /** * Scroll so the node is visible once it is selected. * @see org.eclipse.persistence.tools.workbench.framework.app.NavigatorSelectionModel#expandNode(org.eclipse.persistence.tools.workbench.framework.app.ApplicationNode) */ public void expandNode(ApplicationNode node) { this.tree.expandPath(new TreePath(node.path())); } // ********** behavior ********** void saveTreeExpansionState(Preferences windowsPreferences) { // TODO save expansion state } void restoreTreeExpansionState(Preferences windowsPreferences) { // TODO restore expansion state } // ********** queries ********** /** * Return a collection of the tree's currently expanded paths. */ private Collection currentExpansionState() { Enumeration stream = this.tree.getExpandedDescendants(new TreePath(this.tree.getModel().getRoot())); if (stream == null) { return Collections.EMPTY_LIST; } return CollectionTools.list(new EnumerationIterator(stream)); } } /** * Tweak the displayable tree cell renderer * to indicate a dirty node when appropriate. */ private static class ApplicationNodeTreeCellRenderer extends DisplayableTreeCellRenderer { /** * Ask the AccessibleNode for a description that can be different than * the regular text shown on the node. */ protected String buildAccessibleName(Object value) { return ((AccessibleNode) value).accessibleName(); } /** * Prepend the node's text with an asterisk if the node is dirty. */ protected String buildText(Object value) { String text = super.buildText(value); if (text == null) { return text; // the root node is NOT an app node... } return (((ApplicationNode) value).isDirty()) ? '*' + text : text; } /** * ask Paul what this is for... */ public Dimension getPreferredSize() { Dimension d = super.getPreferredSize(); return new Dimension(d.width, d.height + 1); } } }