source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/TagTable.java@ 11717

Last change on this file since 11717 was 10604, checked in by Don-vip, 8 years ago

fix #12478, fix #12565, fix #11114 - Use ​Swing Copy/Paste instead of CopyAction/PasteAction with custom buffer (patch by michael2402, modified) - gsoc-core

  • Property svn:eol-style set to native
File size: 19.4 KB
RevLine 
[2512]1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.tagging;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
[2974]6import java.awt.Component;
[2512]7import java.awt.Dimension;
[2974]8import java.awt.KeyboardFocusManager;
9import java.awt.Window;
[2512]10import java.awt.event.ActionEvent;
11import java.awt.event.KeyEvent;
[2974]12import java.beans.PropertyChangeEvent;
13import java.beans.PropertyChangeListener;
[6092]14import java.util.Collections;
[2974]15import java.util.EventObject;
16import java.util.concurrent.CopyOnWriteArrayList;
[2512]17
18import javax.swing.AbstractAction;
[2974]19import javax.swing.CellEditor;
[2512]20import javax.swing.JComponent;
21import javax.swing.JTable;
22import javax.swing.KeyStroke;
23import javax.swing.ListSelectionModel;
24import javax.swing.SwingUtilities;
25import javax.swing.event.ListSelectionEvent;
26import javax.swing.event.ListSelectionListener;
[6063]27import javax.swing.text.JTextComponent;
[6248]28
[6092]29import org.openstreetmap.josm.Main;
30import org.openstreetmap.josm.data.osm.Relation;
[10604]31import org.openstreetmap.josm.data.osm.TagMap;
32import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler;
33import org.openstreetmap.josm.gui.tagging.TagEditorModel.EndEditListener;
[6248]34import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
[3210]35import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
[10070]36import org.openstreetmap.josm.gui.widgets.JosmTable;
[2974]37import org.openstreetmap.josm.tools.ImageProvider;
[2512]38
39/**
40 * This is the tabular editor component for OSM tags.
[9847]41 * @since 1762
[2512]42 */
[10604]43public class TagTable extends JosmTable implements EndEditListener {
[2512]44 /** the table cell editor used by this table */
[8840]45 private TagCellEditor editor;
[6064]46 private final TagEditorModel model;
47 private Component nextFocusComponent;
[6070]48
[4310]49 /** a list of components to which focus can be transferred without stopping
[2974]50 * cell editing this table.
51 */
[7005]52 private final CopyOnWriteArrayList<Component> doNotStopCellEditingWhenFocused = new CopyOnWriteArrayList<>();
[8308]53 private transient CellEditorRemover editorRemover;
[2974]54
[2512]55 /**
56 * Action to be run when the user navigates to the next cell in the table,
57 * for instance by pressing TAB or ENTER. The action alters the standard
58 * navigation path from cell to cell:
59 * <ul>
60 * <li>it jumps over cells in the first column</li>
61 * <li>it automatically add a new empty row when the user leaves the
62 * last cell in the table</li>
[6524]63 * </ul>
[2512]64 */
[10378]65 class SelectNextColumnCellAction extends AbstractAction {
[6064]66 @Override
[2512]67 public void actionPerformed(ActionEvent e) {
68 run();
69 }
70
71 public void run() {
72 int col = getSelectedColumn();
73 int row = getSelectedRow();
74 if (getCellEditor() != null) {
75 getCellEditor().stopCellEditing();
76 }
[6070]77
[8510]78 if (row == -1 && col == -1) {
[6064]79 requestFocusInCell(0, 0);
80 return;
81 }
[6070]82
[2512]83 if (col == 0) {
84 col++;
85 } else if (col == 1 && row < getRowCount()-1) {
[8510]86 col = 0;
[2512]87 row++;
[8510]88 } else if (col == 1 && row == getRowCount()-1) {
89 // we are at the end. Append an empty row and move the focus to its second column
90 String key = ((TagModel) model.getValueAt(row, 0)).getName();
[6064]91 if (!key.trim().isEmpty()) {
92 model.appendNewTag();
[8510]93 col = 0;
[6064]94 row++;
95 } else {
96 clearSelection();
[8510]97 if (nextFocusComponent != null)
[6064]98 nextFocusComponent.requestFocusInWindow();
99 return;
100 }
[2512]101 }
[8510]102 requestFocusInCell(row, col);
[2512]103 }
104 }
105
106 /**
107 * Action to be run when the user navigates to the previous cell in the table,
108 * for instance by pressing Shift-TAB
109 */
[10378]110 class SelectPreviousColumnCellAction extends AbstractAction {
[2512]111
[6063]112 @Override
[2512]113 public void actionPerformed(ActionEvent e) {
114 int col = getSelectedColumn();
115 int row = getSelectedRow();
116 if (getCellEditor() != null) {
117 getCellEditor().stopCellEditing();
118 }
119
120 if (col <= 0 && row <= 0) {
121 // change nothing
122 } else if (col == 1) {
123 col--;
124 } else {
125 col = 1;
126 row--;
127 }
[8510]128 requestFocusInCell(row, col);
[2512]129 }
130 }
131
132 /**
133 * Action to be run when the user invokes a delete action on the table, for
134 * instance by pressing DEL.
135 *
136 * Depending on the shape on the current selection the action deletes individual
137 * values or entire tags from the model.
138 *
139 * If the current selection consists of cells in the second column only, the keys of
140 * the selected tags are set to the empty string.
141 *
142 * If the current selection consists of cell in the third column only, the values of the
143 * selected tags are set to the empty string.
144 *
145 * If the current selection consists of cells in the second and the third column,
146 * the selected tags are removed from the model.
147 *
148 * This action listens to the table selection. It becomes enabled when the selection
149 * is non-empty, otherwise it is disabled.
150 *
151 *
152 */
[10082]153 class DeleteAction extends AbstractAction implements ListSelectionListener {
[2512]154
[8836]155 DeleteAction() {
[10369]156 new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this);
[2974]157 putValue(SHORT_DESCRIPTION, tr("Delete the selection in the tag table"));
158 getSelectionModel().addListSelectionListener(this);
159 getColumnModel().getSelectionModel().addListSelectionListener(this);
160 updateEnabledState();
161 }
162
[2512]163 /**
164 * delete a selection of tag names
165 */
166 protected void deleteTagNames() {
167 int[] rows = getSelectedRows();
168 model.deleteTagNames(rows);
169 }
170
171 /**
172 * delete a selection of tag values
173 */
174 protected void deleteTagValues() {
175 int[] rows = getSelectedRows();
176 model.deleteTagValues(rows);
177 }
178
179 /**
180 * delete a selection of tags
181 */
182 protected void deleteTags() {
183 int[] rows = getSelectedRows();
184 model.deleteTags(rows);
185 }
186
187 @Override
[10082]188 public void actionPerformed(ActionEvent e) {
[2512]189 if (!isEnabled())
190 return;
[2974]191 switch(getSelectedColumnCount()) {
192 case 1:
[3068]193 if (getSelectedColumn() == 0) {
[2512]194 deleteTagNames();
195 } else if (getSelectedColumn() == 1) {
196 deleteTagValues();
[3068]197 }
[2974]198 break;
199 case 2:
[2512]200 deleteTags();
[2974]201 break;
[10217]202 default: // Do nothing
[2512]203 }
[2974]204
[10604]205 endCellEditing();
[2974]206
[2512]207 if (model.getRowCount() == 0) {
208 model.ensureOneTag();
209 requestFocusInCell(0, 0);
210 }
211 }
212
213 /**
214 * listens to the table selection model
215 */
[6063]216 @Override
[2512]217 public void valueChanged(ListSelectionEvent e) {
[2974]218 updateEnabledState();
219 }
220
[6890]221 protected final void updateEnabledState() {
[10604]222 if (getSelectedColumnCount() >= 1 && getSelectedRowCount() >= 1) {
[2997]223 setEnabled(true);
[2512]224 } else {
225 setEnabled(false);
226 }
227 }
228 }
229
230 /**
231 * Action to be run when the user adds a new tag.
232 *
233 */
[10082]234 class AddAction extends AbstractAction implements PropertyChangeListener {
[8836]235 AddAction() {
[10369]236 new ImageProvider("dialogs", "add").getResource().attachImageIcon(this);
[2974]237 putValue(SHORT_DESCRIPTION, tr("Add a new tag"));
238 TagTable.this.addPropertyChangeListener(this);
239 updateEnabledState();
[2512]240 }
241
242 @Override
[10082]243 public void actionPerformed(ActionEvent e) {
[10217]244 CellEditor cEditor = getCellEditor();
245 if (cEditor != null) {
246 cEditor.stopCellEditing();
[2974]247 }
[6064]248 final int rowIdx = model.getRowCount()-1;
[9319]249 if (rowIdx < 0 || !((TagModel) model.getValueAt(rowIdx, 0)).getName().trim().isEmpty()) {
[6064]250 model.appendNewTag();
[6070]251 }
[6064]252 requestFocusInCell(model.getRowCount()-1, 0);
[2512]253 }
[6070]254
[6890]255 protected final void updateEnabledState() {
[2974]256 setEnabled(TagTable.this.isEnabled());
257 }
258
[6063]259 @Override
[2974]260 public void propertyChange(PropertyChangeEvent evt) {
261 updateEnabledState();
262 }
[2512]263 }
264
[9059]265 /**
[6092]266 * Action to be run when the user wants to paste tags from buffer
267 */
[10082]268 class PasteAction extends AbstractAction implements PropertyChangeListener {
[8836]269 PasteAction() {
[10369]270 new ImageProvider("pastetags").getResource().attachImageIcon(this);
[6092]271 putValue(SHORT_DESCRIPTION, tr("Paste tags from buffer"));
272 TagTable.this.addPropertyChangeListener(this);
273 updateEnabledState();
274 }
275
276 @Override
[10082]277 public void actionPerformed(ActionEvent e) {
[6092]278 Relation relation = new Relation();
279 model.applyToPrimitive(relation);
[10604]280 new OsmTransferHandler().pasteTags(Collections.singleton(relation));
281 model.updateTags(new TagMap(relation.getKeys()).getTags());
[6092]282 }
[7509]283
[6890]284 protected final void updateEnabledState() {
[6092]285 setEnabled(TagTable.this.isEnabled());
286 }
287
288 @Override
289 public void propertyChange(PropertyChangeEvent evt) {
290 updateEnabledState();
291 }
292 }
[7509]293
[2512]294 /** the delete action */
[10082]295 private DeleteAction deleteAction;
[2512]296
297 /** the add action */
[10082]298 private AddAction addAction;
[2512]299
[6092]300 /** the tag paste action */
[10082]301 private PasteAction pasteAction;
[6092]302
[2512]303 /**
[10082]304 * Returns the delete action.
[2512]305 * @return the delete action used by this table
306 */
[10082]307 public DeleteAction getDeleteAction() {
[2512]308 return deleteAction;
309 }
310
[10082]311 /**
312 * Returns the add action.
313 * @return the add action used by this table
314 */
315 public AddAction getAddAction() {
[2512]316 return addAction;
317 }
318
[10082]319 /**
320 * Returns the paste action.
321 * @return the paste action used by this table
322 */
323 public PasteAction getPasteAction() {
[6092]324 return pasteAction;
325 }
326
[2512]327 /**
328 * initialize the table
[8760]329 * @param maxCharacters maximum number of characters allowed for keys and values, 0 for unlimited
[2512]330 */
[8760]331 protected final void init(final int maxCharacters) {
[2512]332 setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
[2974]333 setRowSelectionAllowed(true);
334 setColumnSelectionAllowed(true);
[2512]335 setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
336
337 // make ENTER behave like TAB
338 //
339 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
340 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
341
342 // install custom navigation actions
343 //
344 getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction());
345 getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction());
346
347 // create a delete action. Installing this action in the input and action map
348 // didn't work. We therefore handle delete requests in processKeyBindings(...)
349 //
350 deleteAction = new DeleteAction();
351
352 // create the add action
353 //
354 addAction = new AddAction();
355 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
356 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_MASK), "addTag");
357 getActionMap().put("addTag", addAction);
358
[6092]359 pasteAction = new PasteAction();
360
[2512]361 // create the table cell editor and set it to key and value columns
362 //
[8760]363 TagCellEditor tmpEditor = new TagCellEditor(maxCharacters);
[4120]364 setRowHeight(tmpEditor.getEditor().getPreferredSize().height);
365 setTagCellEditor(tmpEditor);
[2512]366 }
367
368 /**
[3141]369 * Creates a new tag table
[2512]370 *
[3141]371 * @param model the tag editor model
[8760]372 * @param maxCharacters maximum number of characters allowed for keys and values, 0 for unlimited
[2512]373 */
[8760]374 public TagTable(TagEditorModel model, final int maxCharacters) {
[9847]375 super(model, new TagTableColumnModelBuilder(new TagCellRenderer(), tr("Key"), tr("Value"))
376 .setSelectionModel(model.getColumnSelectionModel()).build(),
377 model.getRowSelectionModel());
[6064]378 this.model = model;
[10604]379 model.setEndEditListener(this);
[8760]380 init(maxCharacters);
[2512]381 }
382
383 @Override
[8510]384 public Dimension getPreferredSize() {
[10070]385 return getPreferredFullWidthSize();
[2512]386 }
387
[10070]388 @Override
389 protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
[2512]390
391 // handle delete key
392 //
393 if (e.getKeyCode() == KeyEvent.VK_DELETE) {
[3068]394 if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1)
395 // if DEL was pressed and only the currently edited cell is selected,
396 // don't run the delete action. DEL is handled by the CellEditor as normal
397 // DEL in the text input.
398 //
399 return super.processKeyBinding(ks, e, condition, pressed);
[10082]400 getDeleteAction().actionPerformed(null);
[2512]401 }
402 return super.processKeyBinding(ks, e, condition, pressed);
403 }
404
405 /**
[8470]406 * Sets the editor autocompletion list
407 * @param autoCompletionList autocompletion list
[2512]408 */
409 public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
410 if (autoCompletionList == null)
411 return;
412 if (editor != null) {
413 editor.setAutoCompletionList(autoCompletionList);
414 }
415 }
416
[3210]417 public void setAutoCompletionManager(AutoCompletionManager autocomplete) {
418 if (autocomplete == null) {
[6248]419 Main.warn("argument autocomplete should not be null. Aborting.");
[3214]420 Thread.dumpStack();
[2512]421 return;
422 }
423 if (editor != null) {
[3210]424 editor.setAutoCompletionManager(autocomplete);
[2512]425 }
426 }
427
428 public AutoCompletionList getAutoCompletionList() {
429 if (editor != null)
430 return editor.getAutoCompletionList();
431 else
432 return null;
433 }
434
[9231]435 /**
436 * Sets the next component to request focus after navigation (with tab or enter).
437 * @param nextFocusComponent next component to request focus after navigation (with tab or enter)
438 */
[6064]439 public void setNextFocusComponent(Component nextFocusComponent) {
440 this.nextFocusComponent = nextFocusComponent;
441 }
[6070]442
[2512]443 public TagCellEditor getTableCellEditor() {
444 return editor;
445 }
446
[3015]447 /**
448 * Inject a tag cell editor in the tag table
[3530]449 *
[8470]450 * @param editor tag cell editor
[3015]451 */
452 public void setTagCellEditor(TagCellEditor editor) {
[10604]453 endCellEditing();
[3015]454 this.editor = editor;
455 getColumnModel().getColumn(0).setCellEditor(editor);
456 getColumnModel().getColumn(1).setCellEditor(editor);
457 }
458
[2512]459 public void requestFocusInCell(final int row, final int col) {
[6064]460 changeSelection(row, col, false, false);
[6063]461 editCellAt(row, col);
462 Component c = getEditorComponent();
[8510]463 if (c != null) {
[6063]464 c.requestFocusInWindow();
[8444]465 if (c instanceof JTextComponent) {
[8510]466 ((JTextComponent) c).selectAll();
[6063]467 }
[2512]468 }
[6063]469 // there was a bug here - on older 1.6 Java versions Tab was not working
[6070]470 // after such activation. In 1.7 it works OK,
[8444]471 // previous solution of using awt.Robot was resetting mouse speed on Windows
[2512]472 }
[2974]473
474 public void addComponentNotStoppingCellEditing(Component component) {
475 if (component == null) return;
476 doNotStopCellEditingWhenFocused.addIfAbsent(component);
477 }
478
479 public void removeComponentNotStoppingCellEditing(Component component) {
480 if (component == null) return;
481 doNotStopCellEditingWhenFocused.remove(component);
482 }
483
484 @Override
[8444]485 public boolean editCellAt(int row, int column, EventObject e) {
[2974]486
487 // a snipped copied from the Java 1.5 implementation of JTable
488 //
489 if (cellEditor != null && !cellEditor.stopCellEditing())
490 return false;
491
492 if (row < 0 || row >= getRowCount() ||
493 column < 0 || column >= getColumnCount())
494 return false;
495
496 if (!isCellEditable(row, column))
497 return false;
498
499 // make sure our custom implementation of CellEditorRemover is created
500 if (editorRemover == null) {
501 KeyboardFocusManager fm =
502 KeyboardFocusManager.getCurrentKeyboardFocusManager();
503 editorRemover = new CellEditorRemover(fm);
504 fm.addPropertyChangeListener("permanentFocusOwner", editorRemover);
505 }
506
507 // delegate to the default implementation
[8510]508 return super.editCellAt(row, column, e);
[2974]509 }
510
511 @Override
[10604]512 public void endCellEditing() {
513 if (isEditing()) {
514 CellEditor cEditor = getCellEditor();
515 if (cEditor != null) {
516 // First attempt to commit. If this does not work, cancel.
517 cEditor.stopCellEditing();
518 cEditor.cancelCellEditing();
519 }
520 }
521 }
522
523 @Override
[2974]524 public void removeEditor() {
525 // make sure we unregister our custom implementation of CellEditorRemover
526 KeyboardFocusManager.getCurrentKeyboardFocusManager().
527 removePropertyChangeListener("permanentFocusOwner", editorRemover);
528 editorRemover = null;
529 super.removeEditor();
530 }
531
532 @Override
533 public void removeNotify() {
534 // make sure we unregister our custom implementation of CellEditorRemover
535 KeyboardFocusManager.getCurrentKeyboardFocusManager().
536 removePropertyChangeListener("permanentFocusOwner", editorRemover);
537 editorRemover = null;
538 super.removeNotify();
539 }
540
541 /**
542 * This is a custom implementation of the CellEditorRemover used in JTable
543 * to handle the client property <tt>terminateEditOnFocusLost</tt>.
[3530]544 *
[2974]545 * This implementation also checks whether focus is transferred to one of a list
[5266]546 * of dedicated components, see {@link TagTable#doNotStopCellEditingWhenFocused}.
547 * A typical example for such a component is a button in {@link TagEditorPanel}
548 * which isn't a child component of {@link TagTable} but which should respond to
[2974]549 * to focus transfer in a similar way to a child of TagTable.
550 *
551 */
552 class CellEditorRemover implements PropertyChangeListener {
[9078]553 private final KeyboardFocusManager focusManager;
[2974]554
[8836]555 CellEditorRemover(KeyboardFocusManager fm) {
[2974]556 this.focusManager = fm;
557 }
558
[6063]559 @Override
[2974]560 public void propertyChange(PropertyChangeEvent ev) {
561 if (!isEditing())
562 return;
563
564 Component c = focusManager.getPermanentFocusOwner();
565 while (c != null) {
566 if (c == TagTable.this)
567 // focus remains inside the table
568 return;
569 if (doNotStopCellEditingWhenFocused.contains(c))
570 // focus remains on one of the associated components
571 return;
[7026]572 else if (c instanceof Window) {
[10217]573 if (c == SwingUtilities.getRoot(TagTable.this) && !getCellEditor().stopCellEditing()) {
574 getCellEditor().cancelCellEditing();
[2974]575 }
576 break;
577 }
578 c = c.getParent();
579 }
580 }
581 }
[2512]582}
Note: See TracBrowser for help on using the repository browser.