/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun * Microsystems, Inc. All Rights Reserved. */ package org.openide.explorer.view; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.FocusListener; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.Collections; import javax.swing.*; import javax.swing.tree.*; import javax.swing.event.*; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.plaf.basic.BasicTableUI; import java.util.EventObject; import javax.swing.plaf.TableUI; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellEditor; import org.openide.nodes.Node.Property; import org.openide.nodes.Node; import org.openide.ErrorManager; import org.openide.util.NbBundle; import org.openide.awt.MouseUtils; /** * TreeTable implementation. * * @author Jan Rojcek */ class TreeTable extends JTable { /** A subclass of JTree. */ private TreeTableCellRenderer tree; private NodeTableModel tableModel; private int treeColumnIndex = -1; /** Tree editor stuff. */ private int lastRow = -1; private boolean canEdit; private boolean ignoreScrolling = false; /** Action key for up/down focus action */ private static final String ACTION_FOCUS_NEXT = "focusNext"; //NOI18N /** Flag to ignore clearSelection() called from super.tableChanged(). */ private boolean ignoreClearSelection = false; /** Position of tree renderer, used for horizontal scrolling. */ private int positionX; /** If true, horizontal scrolling of tree column is enabled in TreeTableView */ private boolean treeHScrollingEnabled = true; public TreeTable(NodeTreeModel treeModel, NodeTableModel tableModel) { super(); setSurrendersFocusOnKeystroke(true); this.tree = new TreeTableCellRenderer(treeModel); this.tableModel = new TreeTableModelAdapter(tree, tableModel); NodeRenderer rend = NodeRenderer.sharedInstance (); tree.setCellRenderer(rend); // Install a tableModel representing the visible rows in the tree. setModel(this.tableModel); // Force the JTable and JTree to share their row selection models. ListToTreeSelectionModelWrapper selectionWrapper = new ListToTreeSelectionModelWrapper(); tree.setSelectionModel(selectionWrapper); setSelectionModel(selectionWrapper.getListSelectionModel()); getTableHeader().setReorderingAllowed(false); // Install the tree editor renderer and editor. setDefaultRenderer(TreeTableModelAdapter.class, tree); // Install property renderer and editor. TableSheetCell tableCell = new TableSheetCell(this.tableModel); tableCell.setFlat(true); setDefaultRenderer(Property.class, tableCell); setDefaultEditor(Property.class, tableCell); getTableHeader().setDefaultRenderer(tableCell); getAccessibleContext().setAccessibleName( NbBundle.getBundle(TreeTable.class).getString("ACSN_TreeTable")); // NOI18N getAccessibleContext().setAccessibleDescription( // NOI18N NbBundle.getBundle(TreeTable.class).getString("ACSD_TreeTable")); // NOI18N setFocusCycleRoot(true); setFocusTraversalPolicy(new STPolicy()); putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); putClientProperty("JTable.autoStartsEdit", Boolean.FALSE); initKeysAndActions(); } private void initKeysAndActions() { setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, Collections.EMPTY_SET); setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, Collections.EMPTY_SET); //Next two lines do not work using inputmap/actionmap, but do work //using the older API. We will process ENTER to skip to next row, //not next cell unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)); unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, Event.SHIFT_MASK)); InputMap imp = getInputMap(WHEN_FOCUSED); InputMap imp2 = getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); ActionMap am = getActionMap(); /* fix of #23873, then removed - davidjon request getActionMap().put("selectNextColumnExtendSelection", getActionMap().get("selectNextColumn")); getActionMap().put("selectPreviousColumnExtendSelection", getActionMap().get("selectPreviousColumn")); */ //Issue 37919, reinstate support for up/down cycle focus transfer. //being focus cycle root mangles this in some dialogs imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, KeyEvent.CTRL_MASK | KeyEvent.SHIFT_MASK, false), ACTION_FOCUS_NEXT); imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, KeyEvent.CTRL_MASK, false), ACTION_FOCUS_NEXT); Action ctrlTab = new CTRLTabAction(); am.put(ACTION_FOCUS_NEXT, ctrlTab); getActionMap().put("selectNextColumn", // NOI18N new TreeTableAction(tree.getActionMap().get("selectChild"), // NOI18N getActionMap().get("selectNextColumn"))); // NOI18N getActionMap().put("selectPreviousColumn", // NOI18N new TreeTableAction(tree.getActionMap().get("selectParent"), // NOI18N getActionMap().get("selectPreviousColumn"))); // NOI18N getAccessibleContext ().setAccessibleName ( NbBundle.getBundle (TreeTable.class).getString ("ACSN_TreeTable")); // NOI18N getAccessibleContext ().setAccessibleDescription ( // NOI18N NbBundle.getBundle (TreeTable.class).getString ("ACSD_TreeTable")); // NOI18N imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, false), "beginEdit"); getActionMap().put("beginEdit", new EditAction()); imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "cancelEdit"); getActionMap().put("cancelEdit", new CancelEditAction()); imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "enter"); getActionMap().put("enter", new EnterAction()); imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), "next"); imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, KeyEvent.SHIFT_DOWN_MASK), "previous"); am.put("next", new NavigationAction(true)); am.put("previous", new NavigationAction(false)); } private boolean edCreated = false; public TableCellEditor getDefaultEditor(Class columnClass) { if (!edCreated && columnClass == TreeTableModelAdapter.class) { //Creating this editor in the constructor can take > 100ms even //on a very fast machine, so do it lazily here to improve //performance of creating a TreeTable setDefaultEditor(TreeTableModelAdapter.class, new TreeTableCellEditor()); edCreated = true; } return super.getDefaultEditor(columnClass); } /* * Overridden to message super and forward the method to the tree. */ public void updateUI() { super.updateUI(); if(tree != null) { tree.updateUI(); } // Use the tree's default foreground and background colors in the // table. LookAndFeel.installColorsAndFont(this, "Tree.background", // NOI18N "Tree.foreground", "Tree.font"); // NOI18N if (UIManager.getColor ("Table.selectionBackground") == null) { // NOI18N UIManager.put ("Table.selectionBackground", new JTable ().getSelectionBackground ()); // NOI18N } if (UIManager.getColor ("Table.selectionForeground") == null) { // NOI18N UIManager.put ("Table.selectionForeground", new JTable ().getSelectionForeground ()); // NOI18N } if (UIManager.getColor ("Table.gridColor") == null) { // NOI18N UIManager.put ("Table.gridColor", new JTable ().getGridColor ()); // NOI18N } setUI(new TreeTableUI()); needCalcRowHeight = true; } /* Workaround for BasicTableUI anomaly. Make sure the UI never tries to * paint the editor. The UI currently uses different techniques to * paint the renderers and editors and overriding setBounds() below * is not the right thing to do for an editor. Returning -1 for the * editing row in this case, ensures the editor is never painted. */ public int getEditingRow() { return (getColumnClass(editingColumn) == TreeTableModelAdapter.class) ? -1 : editingRow; } /** Overridden - JTable's implementation of the method will * actually attach (and leave behind) a gratuitous border * on the enclosing scroll pane. */ protected final void configureEnclosingScrollPane() { Container p = getParent(); if (p instanceof JViewport) { Container gp = p.getParent(); if (gp instanceof JScrollPane) { JScrollPane scrollPane = (JScrollPane)gp; JViewport viewport = scrollPane.getViewport(); if (viewport == null || viewport.getView() != this) { return; } JTableHeader jth = getTableHeader(); if (jth != null) { jth.setBorder(null); } scrollPane.setColumnHeaderView(jth); } } } private boolean needCalcRowHeight = true; public void paint (Graphics g) { if (needCalcRowHeight) { calcRowHeight(g); return; } /* long time = perf.highResCounter(); */ super.paint(g); /* double dur = perf.highResCounter()-time; total += dur; System.err.println("Paint time: " + total + " ticks = " + (total / perf.highResFrequency()) + " ms. "); */ } // private static final sun.misc.Perf perf = sun.misc.Perf.getPerf(); // private static double total = 0; /** Calculate the height of rows based on the current font. This is * done when the first paint occurs, to ensure that a valid Graphics * object is available. * @since 1.25 */ private void calcRowHeight(Graphics g) { Font f = getFont(); FontMetrics fm = g.getFontMetrics(f); int rowHeight = fm.getHeight() + 4; needCalcRowHeight = false; rowHeight = Math.min(20, rowHeight); setRowHeight (rowHeight); } /* * Overridden to pass the new rowHeight to the tree. */ public void setRowHeight(int rowHeight) { super.setRowHeight(rowHeight); if (tree != null && tree.getRowHeight() != rowHeight) { tree.setRowHeight(getRowHeight()); } } /** * Returns the tree that is being shared between the model. */ JTree getTree() { return tree; } /** * Returns table column index of the column displaying the tree. */ int getTreeColumnIndex() { return treeColumnIndex; } /** * Sets tree column index and fires property change. */ void setTreeColumnIndex(int index) { if (treeColumnIndex == index) return; int old = treeColumnIndex; treeColumnIndex = index; firePropertyChange("treeColumnIndex", old, treeColumnIndex); } /* Overriden to do not clear a selection upon model changes. */ public void clearSelection() { if (!ignoreClearSelection) { super.clearSelection(); } } /* Updates tree column name and sets ignoreClearSelection flag. */ public void tableChanged(TableModelEvent e) { // update tree column name int modelColumn = getTreeColumnIndex(); if (e.getFirstRow() <= 0 && modelColumn != -1 && getColumnCount() > 0) { String columnName = getModel().getColumnName(modelColumn); TableColumn aColumn = getColumnModel().getColumn(modelColumn); aColumn.setHeaderValue(columnName); } ignoreClearSelection = true; try { super.tableChanged(e); } finally { ignoreClearSelection = false; } } public void processKeyEvent(KeyEvent e) { //Manually hook in the bindings for tab - does not seem to get called //automatically if (isEditing() && (e.getKeyCode() == e.VK_DOWN || e.getKeyCode() == e.VK_UP)) { return; //XXX } //Bypass standard tab and escape handling, and use our registered //actions instead if ((e.getKeyCode() != e.VK_TAB && e.getKeyCode() != e.VK_ESCAPE) || (e.getModifiers() & e.CTRL_MASK) != 0) { super.processKeyEvent(e); } else { processKeyBinding(KeyStroke.getKeyStroke( e.getKeyCode(), e.getModifiersEx(), e.getID() == e.KEY_RELEASED), e, JComponent.WHEN_FOCUSED, e.getID() == e.KEY_PRESSED); } } boolean inEditRequest = false; boolean inEditorChangeRequest=false; int editRow = -1; /* Performs horizontal scrolling of the tree when editing is started. */ public boolean editCellAt(int row, int column, EventObject e) { if (e instanceof MouseEvent && column != 0) { MouseEvent me = (MouseEvent) e; if (!SwingUtilities.isLeftMouseButton(me) || me.getID() != me.MOUSE_PRESSED) { return false; } } if (row >= getRowCount() || row < 0 || column > getColumnCount() || column < 0) { //I don't want to know why this happens, but it does. return false; } inEditRequest = true; editRow = row; if (editingRow == row && editingColumn == column && isEditing()) { //discard edit requests if we're already editing that cell inEditRequest =false; return false; } if (isEditing()) { inEditorChangeRequest = true; try { removeEditor(); changeSelection(row, column, false, false); } finally { inEditorChangeRequest = false; } } //Treat a keyEvent request to edit on a non-editable //column as a request to edit the nearest column that is //editable boolean editable = getModel().isCellEditable(row, column); //We never want to invoke node name editing from the keyboard, //it doesn't work anyway - better to look for an editable property if (editable && (e == null || e instanceof KeyEvent) && column == 0) { editable = false; column = 1; } if (!editable && (e instanceof KeyEvent || e == null)) { for (int i=column; i < getColumnCount(); i++) { if (getModel().isCellEditable(row, i)) { column = i; changeSelection(row, column, false, false); break; } } } try { canEdit = (lastRow == row); Object o = getValueAt(row, column); if (o instanceof Property) { // && (e == null || e instanceof KeyEvent)) { //Toggle booleans without instantiating an editor Property p = (Property) o; if (p.getValueType() == Boolean.class || p.getValueType() == Boolean.TYPE) { if (!p.canWrite()) { return false; } try { Boolean val = (Boolean) p.getValue(); if (Boolean.FALSE.equals(val)) { p.setValue(Boolean.TRUE); } else { //This covers null multi-selections too p.setValue(Boolean.FALSE); } Rectangle r = getCellRect (row, column, true); repaint (r.x, r.y, r.width, r.height); return false; } catch (Exception e1) { ErrorManager.getDefault().notify(ErrorManager.WARNING, e1); return false; } } } boolean ret = super.editCellAt(row, column, e); if (ret) { editorComp.requestFocus(); } if (ret && column == getTreeColumnIndex()) { ignoreScrolling = true; tree.scrollRectToVisible(tree.getRowBounds(row)); ignoreScrolling = false; } return ret; } finally { inEditRequest = false; } } /* */ public void valueChanged(ListSelectionEvent e) { if (getSelectedRowCount() == 1) lastRow = getSelectedRow(); else lastRow = -1; super.valueChanged(e); } /* Updates tree column index */ public void columnAdded(TableColumnModelEvent e) { super.columnAdded(e); updateTreeColumnIndex(); } /* Updates tree column index */ public void columnRemoved(TableColumnModelEvent e) { super.columnRemoved(e); updateTreeColumnIndex(); } /* Updates tree column index */ public void columnMoved(TableColumnModelEvent e) { super.columnMoved(e); updateTreeColumnIndex(); int from = e.getFromIndex(); int to = e.getToIndex(); if ( from != to ) firePropertyChange( "column_moved", from, to ); // NOI18N } /* Updates tree column index */ private void updateTreeColumnIndex() { for (int i = getColumnCount() - 1; i >= 0; i--) { if (getColumnClass(i) == TreeTableModelAdapter.class) { setTreeColumnIndex(i); return; } } setTreeColumnIndex(-1); } /** Returns x coordinate of tree renderer. */ public int getPositionX() { return positionX; } /** Sets x position. */ public void setPositionX(int x) { if (x == positionX || !treeHScrollingEnabled) return; int old = positionX; positionX = x; firePropertyChange("positionX", old, x); if (isEditing() && getEditingColumn() == getTreeColumnIndex()) { CellEditor editor = getCellEditor(); if (ignoreScrolling && editor instanceof TreeTableCellEditor) { ((TreeTableCellEditor)editor).revalidateTextField(); } else { removeEditor(); } } repaint(); } /** Overridden to manually draw the focused rectangle for the tree column */ public void paintComponent(Graphics g) { super.paintComponent(g); if (hasFocus() && getSelectedColumn() == 0 && getSelectedRow() > 0) { Color bdr = UIManager.getColor("Tree.selectionBorderColor"); //NOI18N if (bdr == null) { //Button focus color doesn't work on win classic - better to //get the color from a value we know will work - Tim if (getForeground().equals(Color.BLACK)) { //typical bdr = getBackground().darker(); } else { bdr = getForeground().darker(); } } g.setColor(bdr); Rectangle r = getCellRect (getSelectedRow(), getSelectedColumn(), false); g.drawRect(r.x+1, r.y+1, r.width - 3, r.height - 3); } } /** Enables horizontal scrolling of tree column */ void setTreeHScrollingEnabled(boolean enabled) { treeHScrollingEnabled = enabled; } /** * A TreeCellRenderer that displays a JTree. */ class TreeTableCellRenderer extends JTree implements TableCellRenderer { /** Last table/tree row asked to renderer. */ protected int visibleRow; /* Last width of the tree. */ private int oldWidth; public TreeTableCellRenderer(TreeModel model) { super(model); setRowHeight(getRowHeight()); setToggleClickCount(0); putClientProperty("JTree.lineStyle", "None"); // NOI18N } public void validate() { //do nothing } public void repaint(long tm, int x, int y, int width, int height) { //do nothing } public void addHierarchyListener (java.awt.event.HierarchyListener hl) { //do nothing } public void addComponentListener (java.awt.event.ComponentListener cl) { //do nothing } /** * Sets the row height of the tree, and forwards the row height to * the table. */ public void setRowHeight(int rowHeight) { if (rowHeight > 0) { synchronized (getTreeLock()) { super.setRowHeight(rowHeight); if (TreeTable.this != null && TreeTable.this.getRowHeight() != rowHeight) { TreeTable.this.setRowHeight(getRowHeight()); } } } } /** * Overridden to always set the size to the height of the TreeTable * and the width of column 0. The paint() method will translate the * coordinates to the correct position. */ public void setBounds(int x, int y, int w, int h) { transY = -y; super.setBounds(0,0, TreeTable.this.getColumnModel().getColumn(0).getWidth(), TreeTable.this.getHeight()); } private int transY = 0; /* Fire width property change so that we can revalidate horizontal scrollbar in TreeTableView. */ public void reshape(int x, int y, int w, int h) { int oldWidth = getWidth(); super.reshape(x, y, w, h); if (oldWidth != w) { firePropertyChange("width", oldWidth, w); } } public void paint (Graphics g) { g.translate(-getPositionX(), transY); super.paint(g); } public Rectangle getVisibleRect() { Rectangle visibleRect = TreeTable.this.getVisibleRect(); visibleRect.x = positionX; visibleRect.width = TreeTable.this.getColumnModel().getColumn(getTreeColumnIndex()).getWidth(); return visibleRect; } /* Overriden to use this call for moving tree renderer. */ public void scrollRectToVisible(Rectangle aRect) { Rectangle rect = getVisibleRect(); rect.y = aRect.y; rect.height = aRect.height; TreeTable.this.scrollRectToVisible(rect); int x = rect.x; if (aRect.width > rect.width) { x = aRect.x; } else if (aRect.x < rect.x) { x = aRect.x; } else if (aRect.x + aRect.width > rect.x + rect.width) { x = aRect.x + aRect.width - rect.width; } TreeTable.this.setPositionX(x); } public String getToolTipText(MouseEvent event) { if(event != null) { Point p = event.getPoint(); p.translate(positionX, visibleRow * getRowHeight()); int selRow = getRowForLocation(p.x, p.y); if(selRow != -1) { TreePath path = getPathForRow(selRow); VisualizerNode v = (VisualizerNode)path.getLastPathComponent(); String tooltip = v.getShortDescription(); String displayName = v.getDisplayName (); if ((tooltip != null) && !tooltip.equals (displayName)) return tooltip; } } return null; } /** * TreeCellRenderer method. Overridden to update the visible row. */ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if(isSelected) { Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager(). getFocusOwner(); boolean tableHasFocus = focusOwner == this || focusOwner == TreeTable.this || TreeTable.this.isAncestorOf(focusOwner); setBackground(tableHasFocus ? table.getSelectionBackground() : NodeRenderer.getNoFocusSelectionBackground()); setForeground(tableHasFocus ? table.getSelectionForeground() : NodeRenderer.getNoFocusSelectionForeground()); } else { setBackground(table.getBackground()); setForeground(table.getForeground()); } visibleRow = row; return this; } protected TreeModelListener createTreeModelListener() { return new JTree.TreeModelHandler() { public void treeNodesRemoved(TreeModelEvent e) { if (tree.getSelectionCount () == 0) { TreePath path = TreeView.findSiblingTreePath (e.getTreePath (), e.getChildIndices ()); if (path != null && path.getPathCount () > 0) { tree.setSelectionPath (path); } } } }; } } boolean isKnownComponent (Component c) { if (c == null) return false; if (isAncestorOf (c)) { return true; } if (c == editorComp) { return true; } if (editorComp != null && (editorComp instanceof Container) && ((Container) editorComp).isAncestorOf(c)) { return true; } return false; } public boolean isValidationRoot() { return true; } public void paintImmediately (int x, int y, int w, int h) { //Eliminate duplicate repaints in an editor change request if (inEditorChangeRequest) { return; } super.paintImmediately(x, y, w, h); } protected void processFocusEvent (FocusEvent fe) { super.processFocusEvent(fe); //Remove the editor here if the new focus owner is not //known to the table & the focus event is not temporary if (fe.getID() == fe.FOCUS_LOST && !fe.isTemporary() && !inRemoveRequest && !inEditRequest) { boolean stopEditing = (fe.getOppositeComponent() != getParent() && !isKnownComponent(fe.getOppositeComponent()) && fe.getOppositeComponent() != null); if (stopEditing) { removeEditor(); } } //The UI will only repaint the lead selection, but we need to //paint all selected rows for the color to change when focus //is lost/gained if (!inRemoveRequest && !inEditRequest) { repaintSelection(fe.getID() == fe.FOCUS_GAINED); } } private boolean inRemoveRequest=false; public void removeEditor() { inRemoveRequest = true; try { synchronized (getTreeLock()) { super.removeEditor(); } } finally { inRemoveRequest = false; } } /** Repaint the selected row */ private void repaintSelection(boolean focused) { int start = getSelectionModel().getMinSelectionIndex(); int end = getSelectionModel().getMaxSelectionIndex(); if (end != -1) { if (end != start) { Rectangle begin = getCellRect(start, 0, false); Rectangle r = getCellRect(end, 0, false); r.y =begin.y; r.x = 0; r.width = getWidth(); r.height = r.y + r.height - begin.y; repaint (r.x, r.y, r.width, r.height); } else { Rectangle r = getCellRect(start, 0, false); r.width = getWidth(); r.x = 0; repaint (r.x, r.y, r.width, r.height); } } if (isEditing() && editorComp != null) { editorComp.setBackground(focused ? getSelectionBackground() : NodeRenderer.getNoFocusSelectionBackground()); editorComp.setForeground(focused ? getSelectionForeground() : NodeRenderer.getNoFocusSelectionForeground()); } } /** * TreeTableCellEditor implementation. */ class TreeTableCellEditor extends DefaultCellEditor implements TreeSelectionListener, ActionListener, FocusListener, CellEditorListener { /** Used in editing. Indicates x position to place editingComponent. */ protected transient int offset; /** Used before starting the editing session. */ protected transient Timer timer; public TreeTableCellEditor() { super(new TreeTableTextField()); tree.addTreeSelectionListener(this); addCellEditorListener(this); super.getComponent().addFocusListener(this); } /** * Overridden to determine an offset that tree would place the * editor at. The offset is determined from the * getRowBounds JTree method, and additionally * from the icon DefaultTreeCellRenderer will use. *

The offset is then set on the TreeTableTextField component * created in the constructor, and returned. */ public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int r, int c) { Component component = super.getTableCellEditorComponent (table, value, isSelected, r, c); determineOffset(value, isSelected, r); ((TreeTableTextField)getComponent()).offset = offset; return component; } /** * This is overridden to forward the event to the tree and start editor timer. */ public boolean isCellEditable(EventObject e) { if (lastRow != -1) { org.openide.nodes.Node n = Visualizer.findNode (tree.getPathForRow(lastRow).getLastPathComponent()); if (n == null || !n.canRename ()) { //return false; canEdit = false; } } if (canEdit && e != null && ( e.getSource() instanceof Timer )) return true; if (canEdit && shouldStartEditingTimer(e)) { startEditingTimer(); } else if (shouldStopEditingTimer(e)) { timer.stop(); } if (e instanceof MouseEvent) { MouseEvent me = (MouseEvent)e; int column = getTreeColumnIndex(); if ( MouseUtils.isLeftMouseButton(me) && me.getClickCount() == 2 ) { TreePath path = tree.getPathForRow(TreeTable.this.rowAtPoint(me.getPoint())); Rectangle r = tree.getPathBounds(path); if ( me.getX() < r.x - positionX || me.getX() > r.x - positionX + r.width ) { me.translatePoint( r.x - me.getX(), 0 ); } } MouseEvent newME = new MouseEvent (TreeTable.this.tree, me.getID(), me.getWhen(), me.getModifiers(), me.getX() - getCellRect(0, column, true).x + positionX, me.getY(), me.getClickCount(), me.isPopupTrigger()); TreeTable.this.tree.dispatchEvent(newME); } return false; } /* Stop timer when selection has been changed. */ public void valueChanged(TreeSelectionEvent e) { if (timer != null) { timer.stop(); } } /* Timer performer. */ public void actionPerformed(java.awt.event.ActionEvent e) { if (lastRow != -1) { editCellAt(lastRow, getTreeColumnIndex(), new EventObject( timer )); } } /* Start editing timer only on certain conditions. */ private boolean shouldStartEditingTimer(EventObject event) { if ((event instanceof MouseEvent) && SwingUtilities.isLeftMouseButton((MouseEvent)event)) { MouseEvent me = (MouseEvent)event; return (me.getID() == me.MOUSE_PRESSED && me.getClickCount() == 1 && inHitRegion(me)); } return false; } /* Stop editing timer only on certain conditions. */ private boolean shouldStopEditingTimer(EventObject event) { if (timer == null) return false; if (event instanceof MouseEvent) { MouseEvent me = (MouseEvent)event; return (!SwingUtilities.isLeftMouseButton(me) || me.getClickCount() > 1); } return false; } /** * Starts the editing timer. */ private void startEditingTimer() { if(timer == null) { timer = new Timer(1200, this); timer.setRepeats(false); } timer.start(); } /* Does a click go into node's label? */ private boolean inHitRegion(MouseEvent me) { determineOffset(me); if (me.getX() <= offset) { return false; } return true; } /* Determines offset of node's label from left edge of the table. */ private void determineOffset(MouseEvent me) { int row = TreeTable.this.rowAtPoint(me.getPoint()); if (row == -1) { offset = 0; return; } determineOffset(tree.getPathForRow(row).getLastPathComponent(), TreeTable.this.isRowSelected(row), row); } /* Determines offset of node's label from left edge of the table. */ private void determineOffset(Object value, boolean isSelected, int row) { JTree t = getTree(); boolean rv = t.isRootVisible(); int offsetRow = row; if ( !rv && row > 0 ) offsetRow--; Rectangle bounds = t.getRowBounds(offsetRow); offset = bounds.x; TreeCellRenderer tcr = t.getCellRenderer(); Object node = t.getPathForRow(offsetRow).getLastPathComponent(); Component comp = tcr.getTreeCellRendererComponent( t, node, isSelected, t.isExpanded(offsetRow), t.getModel().isLeaf(node), offsetRow, false); if (comp instanceof JLabel) { Icon icon = ((JLabel)comp).getIcon(); if (icon != null) { offset += ((JLabel)comp).getIconTextGap() + icon.getIconWidth(); } } offset -= positionX; } /* Revalidates text field upon change of x position of renderer */ private void revalidateTextField() { int row = TreeTable.this.editingRow; if (row == -1) { offset = 0; return; } determineOffset(tree.getPathForRow(row).getLastPathComponent(), TreeTable.this.isRowSelected(row), row); ((TreeTableTextField)super.getComponent()).offset = offset; getComponent().setBounds(TreeTable.this.getCellRect(row, getTreeColumnIndex(), false)); } // Focus listener /* Cancel editing when text field loses focus */ public void focusLost (java.awt.event.FocusEvent evt) { /* to allow Escape functionality if (!stopCellEditing()) cancelCellEditing(); */ } /* Select a text in text field when it gets focus. */ public void focusGained (java.awt.event.FocusEvent evt) { ((TreeTableTextField)super.getComponent()).selectAll(); } // Cell editor listener - copied from TreeViewCellEditor /** Implements CellEditorListener interface method. */ public void editingStopped(ChangeEvent e) { TreePath lastP = tree.getPathForRow(lastRow); if (lastP != null) { Node n = Visualizer.findNode (lastP.getLastPathComponent()); if (n != null && n.canRename ()) { String newStr = (String) getCellEditorValue(); try { // bugfix #21589 don't update name if there is not any change if (!n.getName ().equals (newStr)) { n.setName (newStr); } } catch (IllegalArgumentException exc) { boolean needToAnnotate = true; ErrorManager em = ErrorManager.getDefault (); ErrorManager.Annotation[] ann = em.findAnnotations(exc); // determine if "new annotation" of this exception is needed if (ann!=null && ann.length>0) { for (int i=0; iCellEditorListener interface method. */ public void editingCanceled(ChangeEvent e) { } } /** * Component used by TreeTableCellEditor. The only thing this does * is to override the reshape method, and to ALWAYS * make the x location be offset. */ static class TreeTableTextField extends JTextField { public int offset; public void reshape(int x, int y, int w, int h) { int newX = Math.max(x, offset); super.reshape(newX, y, w - (newX - x), h); } public void addNotify() { super.addNotify(); //requestFocus(); //no longer necessary, STPolicy will do it - Tim } } /** * ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel * to listen for changes in the ListSelectionModel it maintains. Once * a change in the ListSelectionModel happens, the paths are updated * in the DefaultTreeSelectionModel. */ class ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel { /** Set to true when we are updating the ListSelectionModel. */ protected boolean updatingListSelectionModel; public ListToTreeSelectionModelWrapper() { super(); getListSelectionModel().addListSelectionListener (createListSelectionListener()); } /** * Returns the list selection model. ListToTreeSelectionModelWrapper * listens for changes to this model and updates the selected paths * accordingly. */ ListSelectionModel getListSelectionModel() { return listSelectionModel; } /** * This is overridden to set updatingListSelectionModel * and message super. This is the only place DefaultTreeSelectionModel * alters the ListSelectionModel. */ public void resetRowSelection() { if(!updatingListSelectionModel) { updatingListSelectionModel = true; try { super.resetRowSelection(); } finally { updatingListSelectionModel = false; } } // Notice how we don't message super if // updatingListSelectionModel is true. If // updatingListSelectionModel is true, it implies the // ListSelectionModel has already been updated and the // paths are the only thing that needs to be updated. } /** * Creates and returns an instance of ListSelectionHandler. */ protected ListSelectionListener createListSelectionListener() { return new ListSelectionHandler(); } /** * If updatingListSelectionModel is false, this will * reset the selected paths from the selected rows in the list * selection model. */ protected void updateSelectedPathsFromSelectedRows() { if(!updatingListSelectionModel) { updatingListSelectionModel = true; try { // This is way expensive, ListSelectionModel needs an // enumerator for iterating. int min = listSelectionModel.getMinSelectionIndex(); int max = listSelectionModel.getMaxSelectionIndex(); this.clearSelection (); java.util.Vector v = new java.util.Vector(); if(min != -1 && max != -1) { for(int counter = min; counter <= max; counter++) { if(listSelectionModel.isSelectedIndex(counter)) { TreePath selPath = tree.getPathForRow (counter); if(selPath != null) { v.add( selPath ); // addSelectionPath(selPath); } } } if( v.size() > 0 ) { addSelectionPaths( (TreePath[])v.toArray( new TreePath[v.size()]) ); } } } finally { updatingListSelectionModel = false; } } } /** * Class responsible for calling updateSelectedPathsFromSelectedRows * when the selection of the list changes. */ class ListSelectionHandler implements ListSelectionListener { public void valueChanged(ListSelectionEvent e) { updateSelectedPathsFromSelectedRows(); } } } /* This is overriden to handle mouse events especially. E.g. do not change selection * when it was clicked on tree's expand/collapse toggles. */ class TreeTableUI extends BasicTableUI { /** * Creates the mouse listener for the JTable. */ protected MouseInputListener createMouseInputListener() { return new TreeTableMouseInputHandler(); } public class TreeTableMouseInputHandler extends MouseInputHandler { // Component recieving mouse events during editing. May not be editorComponent. private Component dispatchComponent; // The Table's mouse listener methods. public void mouseClicked(MouseEvent e) { processMouseEvent(e); } public void mousePressed(MouseEvent e) { processMouseEvent(e); } public void mouseReleased(MouseEvent e) { if (shouldIgnore(e)) { return; } repostEvent(e); dispatchComponent = null; setValueIsAdjusting(false); if (!TreeTable.this.isEditing()) processMouseEvent(e); } public void mouseDragged(MouseEvent e) { return; } private void setDispatchComponent(MouseEvent e) { Component editorComponent = table.getEditorComponent(); Point p = e.getPoint(); Point p2 = SwingUtilities.convertPoint(table, p, editorComponent); dispatchComponent = SwingUtilities.getDeepestComponentAt(editorComponent, p2.x, p2.y); } private boolean repostEvent(MouseEvent e) { if (dispatchComponent == null) { return false; } MouseEvent e2 = SwingUtilities.convertMouseEvent(table, e, dispatchComponent); dispatchComponent.dispatchEvent(e2); return true; } private void setValueIsAdjusting(boolean flag) { table.getSelectionModel().setValueIsAdjusting(flag); table.getColumnModel().getSelectionModel().setValueIsAdjusting(flag); } private boolean shouldIgnore(MouseEvent e) { return !table.isEnabled() || ( e.getButton() == MouseEvent.BUTTON3 && e.getClickCount() == 1 && ! e.isPopupTrigger() ) ; } private boolean isTreeColumn(int column) { return TreeTable.this.getColumnClass(column) == TreeTableModelAdapter.class; } /** Forwards mouse events to a renderer (tree). */ private void processMouseEvent(MouseEvent e) { if (shouldIgnore(e)) { return; } Point p = e.getPoint(); int row = table.rowAtPoint(p); int column = table.columnAtPoint(p); // The autoscroller can generate drag events outside the Table's range. if ((column == -1) || (row == -1)) { return; } // for automatic jemmy testing purposes if ( getEditingColumn() == column && getEditingRow() == row ) { return; } boolean changeSelection = true; if (isTreeColumn(column)) { TreePath path = tree.getPathForRow(TreeTable.this.rowAtPoint(e.getPoint())); Rectangle r = tree.getPathBounds(path); if (e.getX() >= r.x - positionX && e.getX() <= r.x - positionX + r.width) { changeSelection = false; } } if ( table.getSelectionModel().isSelectedIndex( row ) && e.isPopupTrigger() ) return; if (table.editCellAt(row, column, e)) { setDispatchComponent(e); repostEvent(e); } else { table.requestFocus(); } CellEditor editor = table.getCellEditor(); if (changeSelection && (editor == null || editor.shouldSelectCell(e))) { setValueIsAdjusting(true); table.changeSelection(row, column, e.isControlDown(), e.isShiftDown()); } } } } /* When selected column is tree column then call tree's action otherwise call table's. */ class TreeTableAction extends AbstractAction { Action treeAction; Action tableAction; TreeTableAction(Action treeAction, Action tableAction) { this.treeAction = treeAction; this.tableAction = tableAction; } public void actionPerformed(ActionEvent e) { if (TreeTable.this.getSelectedColumn() == getTreeColumnIndex()) { //Issue 40075, on JDK 1.5, BasicTreeUI remarkably expects //that action events performed on trees actually come from //trees e.setSource(getTree()); treeAction.actionPerformed(e); } } } /** Focus transfer policy that retains focus after closing an editor. * Copied wholesale from org.openide.explorer.propertysheet.SheetTable */ private class STPolicy extends ContainerOrderFocusTraversalPolicy { public Component getComponentAfter(Container focusCycleRoot, Component aComponent) { if (inRemoveRequest) { return TreeTable.this; } else { Component result = super.getComponentAfter(focusCycleRoot, aComponent); return result; } } public Component getComponentBefore(Container focusCycleRoot, Component aComponent) { if (inRemoveRequest) { return TreeTable.this; } else { return super.getComponentBefore(focusCycleRoot, aComponent); } } public Component getFirstComponent(Container focusCycleRoot) { if (!inRemoveRequest && isEditing()) { return editorComp; } else { return TreeTable.this; } } public Component getDefaultComponent(Container focusCycleRoot) { if (inRemoveRequest && isEditing() && editorComp.isShowing()) { return editorComp; } else { return TreeTable.this; } } protected boolean accept(Component aComponent) { //Do not allow focus to go to a child of the editor we're using if //we are in the process of removing the editor if (isEditing() && inEditRequest) { return isKnownComponent (aComponent); } return super.accept(aComponent) && aComponent.isShowing(); } } /** Enables tab keys to navigate between rows but also exit the table * to the next focusable component in either direction */ private final class NavigationAction extends AbstractAction { private boolean direction; public NavigationAction(boolean direction) { this.direction = direction; } public void actionPerformed(ActionEvent e) { if (isEditing()) { removeEditor(); } int targetRow; int targetColumn; if (direction) { if (getSelectedColumn() == getColumnCount()-1) { targetColumn=0; targetRow = getSelectedRow()+1; } else { targetColumn = getSelectedColumn()+1; targetRow = getSelectedRow(); } } else { if (getSelectedColumn() == 0) { targetColumn = getColumnCount()-1; targetRow = getSelectedRow()-1; } else { targetRow = getSelectedRow(); targetColumn = getSelectedColumn() -1; } } //if we're off the end, try to find a sibling component to pass //focus to if (targetRow >= getRowCount() || targetRow < 0) { //This code is a bit ugly, but works Container ancestor = getFocusCycleRootAncestor(); //Find the next component in our parent's focus cycle Component sibling = direction ? ancestor.getFocusTraversalPolicy().getComponentAfter(ancestor, TreeTable.this.getParent()) : ancestor.getFocusTraversalPolicy().getComponentBefore(ancestor, TreeTable.this); //Often LayoutFocusTranferPolicy will return ourselves if we're //the last. First try to find a parent focus cycle root that //will be a little more polite if (sibling == TreeTable.this) { Container grandcestor = ancestor.getFocusCycleRootAncestor(); if (grandcestor != null) { sibling = direction ? grandcestor.getFocusTraversalPolicy().getComponentAfter(grandcestor, ancestor) : grandcestor.getFocusTraversalPolicy().getComponentBefore(grandcestor, ancestor); ancestor = grandcestor; } } //Okay, we still ended up with ourselves, or there is only one focus //cycle root ancestor. Try to find the first component according to //the policy if (sibling == TreeTable.this) { if (ancestor.getFocusTraversalPolicy().getFirstComponent(ancestor) != null) { sibling = ancestor.getFocusTraversalPolicy().getFirstComponent(ancestor); } } //If we're *still* getting ourselves, find the default button and punt if (sibling == TreeTable.this) { JRootPane rp = getRootPane(); JButton jb = rp.getDefaultButton(); if (jb != null) { sibling = jb; } } //See if it's us, or something we know about, and if so, just //loop around to the top or bottom row - there's noplace //interesting for focus to go to if (sibling != null) { if (sibling == TreeTable.this) { //set the selection if there's nothing else to do changeSelection(direction ? 0 : getRowCount()-1, direction ? 0 : getColumnCount()-1,false,false); } else { //Request focus on the sibling sibling.requestFocus(); } return; } } changeSelection (targetRow, targetColumn, false, false); } } /** Used to explicitly invoke editing from the keyboard */ private class EditAction extends AbstractAction { public void actionPerformed(ActionEvent e) { int row = getSelectedRow(); int col = getSelectedColumn(); if (col == 0) { col = 1; } editCellAt(row, col, null); } public boolean isEnabled() { return getSelectedRow() != -1 && getSelectedColumn() != -1 && !isEditing(); } } /** Either cancels an edit, or closes the enclosing dialog if present */ private class CancelEditAction extends AbstractAction { public void actionPerformed(ActionEvent e) { if (isEditing() || editorComp != null) { removeEditor(); return; } else { Component c = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); InputMap imp = getRootPane().getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); ActionMap am = getRootPane().getActionMap(); KeyStroke escape = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false); Object key = imp.get(escape); if (key == null) { //Default for NbDialog key = "Cancel"; } if (key != null) { Action a = am.get(key); if (a != null) { String commandKey = (String)a.getValue(Action.ACTION_COMMAND_KEY); if (commandKey == null) { commandKey = key.toString(); } a.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, commandKey)); //NOI18N } } } } public boolean isEnabled() { // return isEditing(); return true; } } private class EnterAction extends AbstractAction { public void actionPerformed(ActionEvent e) { JRootPane jrp = getRootPane(); if (jrp != null) { JButton b = getRootPane().getDefaultButton(); if (b != null && b.isEnabled()) { b.doClick(); } } } public boolean isEnabled() { return !isEditing() && !inRemoveRequest; } } private class CTRLTabAction extends AbstractAction { public void actionPerformed(ActionEvent e) { setFocusCycleRoot(false); try { Container con = TreeTable.this.getFocusCycleRootAncestor(); if (con != null) { Component target = TreeTable.this; if (getParent() instanceof JViewport) { target = getParent().getParent(); if (target == con) { target = TreeTable.this; } } EventObject eo = EventQueue.getCurrentEvent(); boolean backward = false; if (eo instanceof KeyEvent) { backward = (((KeyEvent) eo).getModifiers() & KeyEvent.SHIFT_MASK) != 0 && (((KeyEvent) eo).getModifiersEx() & KeyEvent.SHIFT_DOWN_MASK) != 0; } Component to = backward ? con.getFocusTraversalPolicy().getComponentAfter( con, TreeTable.this) : con.getFocusTraversalPolicy().getComponentAfter( con, TreeTable.this); if (to == TreeTable.this) { to = backward ? con.getFocusTraversalPolicy().getFirstComponent(con) : con.getFocusTraversalPolicy().getLastComponent(con); } to.requestFocus(); } } finally { setFocusCycleRoot(true); } } } }