// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.tagging; import static org.openstreetmap.josm.tools.I18n.tr; import java.applet.Applet; import java.awt.AWTException; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.KeyboardFocusManager; import java.awt.MouseInfo; import java.awt.Point; import java.awt.Rectangle; import java.awt.Robot; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.EventObject; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.CellEditor; import javax.swing.DefaultListSelectionModel; import javax.swing.JComponent; import javax.swing.JTable; import javax.swing.JViewport; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableColumnModel; import javax.swing.table.TableColumn; import org.openstreetmap.josm.gui.dialogs.relation.RunnableAction; import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionCache; import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList; import org.openstreetmap.josm.tools.ImageProvider; /** * This is the tabular editor component for OSM tags. * */ public class TagTable extends JTable { private static final Logger logger = Logger.getLogger(TagTable.class.getName()); /** the table cell editor used by this table */ private TagCellEditor editor = null; /** a list of components to which focus can be transferred withouth stopping * cell editing this table. */ private final CopyOnWriteArrayList<Component> doNotStopCellEditingWhenFocused = new CopyOnWriteArrayList<Component>(); private CellEditorRemover editorRemover; /** * The table has two columns. The first column is used for editing rendering and * editing tag keys, the second for rendering and editing tag values. * */ static class TagTableColumnModel extends DefaultTableColumnModel { public TagTableColumnModel(DefaultListSelectionModel selectionModel) { setSelectionModel(selectionModel); TableColumn col = null; TagCellRenderer renderer = new TagCellRenderer(); // column 0 - tag key col = new TableColumn(0); col.setHeaderValue(tr("Key")); col.setResizable(true); col.setCellRenderer(renderer); addColumn(col); // column 1 - tag value col = new TableColumn(1); col.setHeaderValue(tr("Value")); col.setResizable(true); col.setCellRenderer(renderer); addColumn(col); } } /** * Action to be run when the user navigates to the next cell in the table, * for instance by pressing TAB or ENTER. The action alters the standard * navigation path from cell to cell: * <ul> * <li>it jumps over cells in the first column</li> * <li>it automatically add a new empty row when the user leaves the * last cell in the table</li> * <ul> * */ class SelectNextColumnCellAction extends AbstractAction { public void actionPerformed(ActionEvent e) { run(); } public void run() { int col = getSelectedColumn(); int row = getSelectedRow(); if (getCellEditor() != null) { getCellEditor().stopCellEditing(); } if (col == 0) { col++; } else if (col == 1 && row < getRowCount()-1) { col=0; row++; } else if (col == 1 && row == getRowCount()-1){ // we are at the end. Append an empty row and move the focus // to its second column TagEditorModel model = (TagEditorModel)getModel(); model.appendNewTag(); col=0; row++; } changeSelection(row, col, false, false); requestFocusInCell(row,col); } } /** * Action to be run when the user navigates to the previous cell in the table, * for instance by pressing Shift-TAB * */ class SelectPreviousColumnCellAction extends AbstractAction { public void actionPerformed(ActionEvent e) { int col = getSelectedColumn(); int row = getSelectedRow(); if (getCellEditor() != null) { getCellEditor().stopCellEditing(); } if (col <= 0 && row <= 0) { // change nothing } else if (col == 1) { col--; } else { col = 1; row--; } changeSelection(row, col, false, false); requestFocusInCell(row,col); } } /** * Action to be run when the user invokes a delete action on the table, for * instance by pressing DEL. * * Depending on the shape on the current selection the action deletes individual * values or entire tags from the model. * * If the current selection consists of cells in the second column only, the keys of * the selected tags are set to the empty string. * * If the current selection consists of cell in the third column only, the values of the * selected tags are set to the empty string. * * If the current selection consists of cells in the second and the third column, * the selected tags are removed from the model. * * This action listens to the table selection. It becomes enabled when the selection * is non-empty, otherwise it is disabled. * * */ class DeleteAction extends RunnableAction implements ListSelectionListener { public DeleteAction() { putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); putValue(SHORT_DESCRIPTION, tr("Delete the selection in the tag table")); getSelectionModel().addListSelectionListener(this); getColumnModel().getSelectionModel().addListSelectionListener(this); updateEnabledState(); } /** * delete a selection of tag names */ protected void deleteTagNames() { int[] rows = getSelectedRows(); TagEditorModel model = (TagEditorModel)getModel(); model.deleteTagNames(rows); } /** * delete a selection of tag values */ protected void deleteTagValues() { int[] rows = getSelectedRows(); TagEditorModel model = (TagEditorModel)getModel(); model.deleteTagValues(rows); } /** * delete a selection of tags */ protected void deleteTags() { int[] rows = getSelectedRows(); TagEditorModel model = (TagEditorModel)getModel(); model.deleteTags(rows); } @Override public void run() { if (!isEnabled()) return; switch(getSelectedColumnCount()) { case 1: if (getSelectedColumn() == 0) { deleteTagNames(); } else if (getSelectedColumn() == 1) { deleteTagValues(); } break; case 2: deleteTags(); break; } if (isEditing()) { CellEditor editor = getCellEditor(); if (editor != null) { editor.cancelCellEditing(); } } TagEditorModel model = (TagEditorModel)getModel(); if (model.getRowCount() == 0) { model.ensureOneTag(); requestFocusInCell(0, 0); } } /** * listens to the table selection model */ public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } protected void updateEnabledState() { if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) { setEnabled(true); } else if (!isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) { setEnabled(true); } else if (getSelectedColumnCount() > 1 || getSelectedRowCount() > 1) { setEnabled(true); } else { setEnabled(false); } } } /** * Action to be run when the user adds a new tag. * * */ class AddAction extends RunnableAction implements PropertyChangeListener{ public AddAction() { putValue(SMALL_ICON, ImageProvider.get("dialogs", "add")); putValue(SHORT_DESCRIPTION, tr("Add a new tag")); TagTable.this.addPropertyChangeListener(this); updateEnabledState(); } @Override public void run() { CellEditor editor = getCellEditor(); if (editor != null) { getCellEditor().stopCellEditing(); } ((TagEditorModel)getModel()).appendNewTag(); final int rowIdx = getModel().getRowCount()-1; requestFocusInCell(rowIdx, 0); } protected void updateEnabledState() { setEnabled(TagTable.this.isEnabled()); } public void propertyChange(PropertyChangeEvent evt) { updateEnabledState(); } } /** the delete action */ private RunnableAction deleteAction = null; /** the add action */ private RunnableAction addAction = null; /** * * @return the delete action used by this table */ public RunnableAction getDeleteAction() { return deleteAction; } public RunnableAction getAddAction() { return addAction; } /** * initialize the table */ protected void init() { setAutoResizeMode(JTable.AUTO_RESIZE_OFF); //setCellSelectionEnabled(true); setRowSelectionAllowed(true); setColumnSelectionAllowed(true); setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); // make ENTER behave like TAB // getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell"); // install custom navigation actions // getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction()); getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction()); // create a delete action. Installing this action in the input and action map // didn't work. We therefore handle delete requests in processKeyBindings(...) // deleteAction = new DeleteAction(); // create the add action // addAction = new AddAction(); getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) .put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_MASK), "addTag"); getActionMap().put("addTag", addAction); // create the table cell editor and set it to key and value columns // setTagCellEditor(new TagCellEditor()); } /** * Creates a new tag table * * @param model the tag editor model */ public TagTable(TagEditorModel model) { super(model, new TagTableColumnModel(model.getColumnSelectionModel()), model.getRowSelectionModel()); init(); } @Override public Dimension getPreferredSize(){ Container c = getParent(); while(c != null && ! (c instanceof JViewport)) { c = c.getParent(); } if (c != null) { Dimension d = super.getPreferredSize(); d.width = c.getSize().width; return d; } return super.getPreferredSize(); } @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) { // handle delete key // if (e.getKeyCode() == KeyEvent.VK_DELETE) { if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) // if DEL was pressed and only the currently edited cell is selected, // don't run the delete action. DEL is handled by the CellEditor as normal // DEL in the text input. // return super.processKeyBinding(ks, e, condition, pressed); getDeleteAction().run(); } return super.processKeyBinding(ks, e, condition, pressed); } /** * @param autoCompletionList */ public void setAutoCompletionList(AutoCompletionList autoCompletionList) { if (autoCompletionList == null) return; if (editor != null) { editor.setAutoCompletionList(autoCompletionList); } } public void setAutoCompletionCache(AutoCompletionCache acCache) { if (acCache == null) { logger.warning("argument acCache should not be null. Aborting."); return; } if (editor != null) { editor.setAutoCompletionCache(acCache); } } public AutoCompletionList getAutoCompletionList() { if (editor != null) return editor.getAutoCompletionList(); else return null; } public TagCellEditor getTableCellEditor() { return editor; } public void addOKAccelatorListener(KeyListener l) { addKeyListener(l); if (editor == null) { logger.warning("editor is null. cannot register OK accelator listener."); } else { editor.getEditor().addKeyListener(l); } } /** * Inject a tag cell editor in the tag table * * @param editor */ public void setTagCellEditor(TagCellEditor editor) { if (isEditing()) { this.editor.cancelCellEditing(); } this.editor = editor; getColumnModel().getColumn(0).setCellEditor(editor); getColumnModel().getColumn(1).setCellEditor(editor); } public void requestFocusInCell(final int row, final int col) { // the following code doesn't work reliably. If a table cell // gains focus using editCellAt() and requestFocusInWindow() // it isn't possible to tab to the next table cell using TAB or // ENTER. Don't know why. // // tblTagEditor.editCellAt(row, col); // if (tblTagEditor.getEditorComponent() != null) { // tblTagEditor.getEditorComponent().requestFocusInWindow(); // } // this is a workaround. We move the focus to the respective cell // using a simulated mouse click. In this case one can tab out of // the cell using TAB and ENTER. // Rectangle r = getCellRect(row,col, false); Point p = new Point(r.x + r.width/2, r.y + r.height/2); SwingUtilities.convertPointToScreen(p, this); Point before = MouseInfo.getPointerInfo().getLocation(); try { Robot robot = new Robot(); robot.mouseMove(p.x,p.y); robot.mousePress(InputEvent.BUTTON1_MASK); robot.mouseRelease(InputEvent.BUTTON1_MASK); robot.mouseMove(before.x, before.y); } catch(AWTException e) { logger.log(Level.SEVERE, "failed to simulate mouse click event at (" + r.x + "," + r.y + "). Exception: " + e.toString()); return; } } public void addComponentNotStoppingCellEditing(Component component) { if (component == null) return; doNotStopCellEditingWhenFocused.addIfAbsent(component); } public void removeComponentNotStoppingCellEditing(Component component) { if (component == null) return; doNotStopCellEditingWhenFocused.remove(component); } @Override public boolean editCellAt(int row, int column, EventObject e){ // a snipped copied from the Java 1.5 implementation of JTable // if (cellEditor != null && !cellEditor.stopCellEditing()) return false; if (row < 0 || row >= getRowCount() || column < 0 || column >= getColumnCount()) return false; if (!isCellEditable(row, column)) return false; // make sure our custom implementation of CellEditorRemover is created if (editorRemover == null) { KeyboardFocusManager fm = KeyboardFocusManager.getCurrentKeyboardFocusManager(); editorRemover = new CellEditorRemover(fm); fm.addPropertyChangeListener("permanentFocusOwner", editorRemover); } // delegate to the default implementation return super.editCellAt(row, column,e); } @Override public void removeEditor() { // make sure we unregister our custom implementation of CellEditorRemover KeyboardFocusManager.getCurrentKeyboardFocusManager(). removePropertyChangeListener("permanentFocusOwner", editorRemover); editorRemover = null; super.removeEditor(); } @Override public void removeNotify() { // make sure we unregister our custom implementation of CellEditorRemover KeyboardFocusManager.getCurrentKeyboardFocusManager(). removePropertyChangeListener("permanentFocusOwner", editorRemover); editorRemover = null; super.removeNotify(); } /** * This is a custom implementation of the CellEditorRemover used in JTable * to handle the client property <tt>terminateEditOnFocusLost</tt>. * * This implementation also checks whether focus is transferred to one of a list * of dedicated components, see {@see TagTable#doNotStopCellEditingWhenFocused}. * A typical example for such a component is a button in {@see TagEditorPanel} * which isn't a child component of {@see TagTable} but which should respond to * to focus transfer in a similar way to a child of TagTable. * */ class CellEditorRemover implements PropertyChangeListener { KeyboardFocusManager focusManager; public CellEditorRemover(KeyboardFocusManager fm) { this.focusManager = fm; } public void propertyChange(PropertyChangeEvent ev) { if (!isEditing()) return; Component c = focusManager.getPermanentFocusOwner(); while (c != null) { if (c == TagTable.this) // focus remains inside the table return; if (doNotStopCellEditingWhenFocused.contains(c)) // focus remains on one of the associated components return; else if ((c instanceof Window) || (c instanceof Applet && c.getParent() == null)) { if (c == SwingUtilities.getRoot(TagTable.this)) { if (!getCellEditor().stopCellEditing()) { getCellEditor().cancelCellEditing(); } } break; } c = c.getParent(); } } } }