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

Last change on this file since 14877 was 14877, checked in by GerdP, 5 years ago

fix sonar issue: Replace this if-then-else statement by a single method invocation.

  • Property svn:eol-style set to native
File size: 20.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.tagging;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Component;
7import java.awt.Dimension;
8import java.awt.KeyboardFocusManager;
9import java.awt.Window;
10import java.awt.event.ActionEvent;
11import java.awt.event.KeyEvent;
12import java.beans.PropertyChangeEvent;
13import java.beans.PropertyChangeListener;
14import java.util.Collections;
15import java.util.EventObject;
16import java.util.concurrent.CopyOnWriteArrayList;
17
18import javax.swing.AbstractAction;
19import javax.swing.CellEditor;
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;
27import javax.swing.text.JTextComponent;
28
29import org.openstreetmap.josm.data.osm.Relation;
30import org.openstreetmap.josm.data.osm.TagMap;
31import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler;
32import org.openstreetmap.josm.gui.tagging.TagEditorModel.EndEditListener;
33import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
34import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
35import org.openstreetmap.josm.gui.widgets.JosmTable;
36import org.openstreetmap.josm.tools.ImageProvider;
37import org.openstreetmap.josm.tools.Logging;
38
39/**
40 * This is the tabular editor component for OSM tags.
41 * @since 1762
42 */
43public class TagTable extends JosmTable implements EndEditListener {
44 /** the table cell editor used by this table */
45 private TagCellEditor editor;
46 private final TagEditorModel model;
47 private Component nextFocusComponent;
48
49 /** a list of components to which focus can be transferred without stopping
50 * cell editing this table.
51 */
52 private final CopyOnWriteArrayList<Component> doNotStopCellEditingWhenFocused = new CopyOnWriteArrayList<>();
53 private transient CellEditorRemover editorRemover;
54
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>
63 * </ul>
64 */
65 class SelectNextColumnCellAction extends AbstractAction {
66 @Override
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 }
77
78 if (row == -1 && col == -1) {
79 requestFocusInCell(0, 0);
80 return;
81 }
82
83 if (col == 0) {
84 col++;
85 } else if (col == 1 && row < getRowCount()-1) {
86 col = 0;
87 row++;
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();
91 if (!key.trim().isEmpty()) {
92 model.appendNewTag();
93 col = 0;
94 row++;
95 } else {
96 clearSelection();
97 if (nextFocusComponent != null)
98 nextFocusComponent.requestFocusInWindow();
99 return;
100 }
101 }
102 requestFocusInCell(row, col);
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 */
110 class SelectPreviousColumnCellAction extends AbstractAction {
111
112 @Override
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 }
128 requestFocusInCell(row, col);
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 */
153 class DeleteAction extends AbstractAction implements ListSelectionListener {
154
155 DeleteAction() {
156 new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this);
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
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
188 public void actionPerformed(ActionEvent e) {
189 if (!isEnabled())
190 return;
191 switch(getSelectedColumnCount()) {
192 case 1:
193 if (getSelectedColumn() == 0) {
194 deleteTagNames();
195 } else if (getSelectedColumn() == 1) {
196 deleteTagValues();
197 }
198 break;
199 case 2:
200 deleteTags();
201 break;
202 default: // Do nothing
203 }
204
205 endCellEditing();
206
207 if (model.getRowCount() == 0) {
208 model.ensureOneTag();
209 requestFocusInCell(0, 0);
210 }
211 }
212
213 /**
214 * listens to the table selection model
215 */
216 @Override
217 public void valueChanged(ListSelectionEvent e) {
218 updateEnabledState();
219 }
220
221 protected final void updateEnabledState() {
222 setEnabled(getSelectedColumnCount() >= 1 && getSelectedRowCount() >= 1);
223 }
224 }
225
226 /**
227 * Action to be run when the user adds a new tag.
228 *
229 */
230 class AddAction extends AbstractAction implements PropertyChangeListener {
231 AddAction() {
232 new ImageProvider("dialogs", "add").getResource().attachImageIcon(this);
233 putValue(SHORT_DESCRIPTION, tr("Add a new tag"));
234 TagTable.this.addPropertyChangeListener(this);
235 updateEnabledState();
236 }
237
238 @Override
239 public void actionPerformed(ActionEvent e) {
240 CellEditor cEditor = getCellEditor();
241 if (cEditor != null) {
242 cEditor.stopCellEditing();
243 }
244 final int rowIdx = model.getRowCount()-1;
245 if (rowIdx < 0 || !((TagModel) model.getValueAt(rowIdx, 0)).getName().trim().isEmpty()) {
246 model.appendNewTag();
247 }
248 requestFocusInCell(model.getRowCount()-1, 0);
249 }
250
251 protected final void updateEnabledState() {
252 setEnabled(TagTable.this.isEnabled());
253 }
254
255 @Override
256 public void propertyChange(PropertyChangeEvent evt) {
257 updateEnabledState();
258 }
259 }
260
261 /**
262 * Action to be run when the user wants to paste tags from buffer
263 */
264 class PasteAction extends AbstractAction implements PropertyChangeListener {
265 PasteAction() {
266 new ImageProvider("pastetags").getResource().attachImageIcon(this);
267 putValue(SHORT_DESCRIPTION, tr("Paste tags from buffer"));
268 TagTable.this.addPropertyChangeListener(this);
269 updateEnabledState();
270 }
271
272 @Override
273 public void actionPerformed(ActionEvent e) {
274 Relation relation = new Relation();
275 model.applyToPrimitive(relation);
276 new OsmTransferHandler().pasteTags(Collections.singleton(relation));
277 model.updateTags(new TagMap(relation.getKeys()).getTags());
278 }
279
280 protected final void updateEnabledState() {
281 setEnabled(TagTable.this.isEnabled());
282 }
283
284 @Override
285 public void propertyChange(PropertyChangeEvent evt) {
286 updateEnabledState();
287 }
288 }
289
290 /** the delete action */
291 private DeleteAction deleteAction;
292
293 /** the add action */
294 private AddAction addAction;
295
296 /** the tag paste action */
297 private PasteAction pasteAction;
298
299 /**
300 * Returns the delete action.
301 * @return the delete action used by this table
302 */
303 public DeleteAction getDeleteAction() {
304 return deleteAction;
305 }
306
307 /**
308 * Returns the add action.
309 * @return the add action used by this table
310 */
311 public AddAction getAddAction() {
312 return addAction;
313 }
314
315 /**
316 * Returns the paste action.
317 * @return the paste action used by this table
318 */
319 public PasteAction getPasteAction() {
320 return pasteAction;
321 }
322
323 /**
324 * initialize the table
325 * @param maxCharacters maximum number of characters allowed for keys and values, 0 for unlimited
326 */
327 protected final void init(final int maxCharacters) {
328 setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
329 setRowSelectionAllowed(true);
330 setColumnSelectionAllowed(true);
331 setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
332
333 // make ENTER behave like TAB
334 //
335 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
336 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
337
338 // install custom navigation actions
339 //
340 getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction());
341 getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction());
342
343 // create a delete action. Installing this action in the input and action map
344 // didn't work. We therefore handle delete requests in processKeyBindings(...)
345 //
346 deleteAction = new DeleteAction();
347
348 // create the add action
349 //
350 addAction = new AddAction();
351 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
352 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_DOWN_MASK), "addTag");
353 getActionMap().put("addTag", addAction);
354
355 pasteAction = new PasteAction();
356
357 // create the table cell editor and set it to key and value columns
358 //
359 TagCellEditor tmpEditor = new TagCellEditor(maxCharacters);
360 setRowHeight(tmpEditor.getEditor().getPreferredSize().height);
361 setTagCellEditor(tmpEditor);
362 }
363
364 /**
365 * Creates a new tag table
366 *
367 * @param model the tag editor model
368 * @param maxCharacters maximum number of characters allowed for keys and values, 0 for unlimited
369 */
370 public TagTable(TagEditorModel model, final int maxCharacters) {
371 super(model, new TagTableColumnModelBuilder(new TagCellRenderer(), tr("Key"), tr("Value"))
372 .setSelectionModel(model.getColumnSelectionModel()).build(),
373 model.getRowSelectionModel());
374 this.model = model;
375 model.setEndEditListener(this);
376 init(maxCharacters);
377 }
378
379 @Override
380 public Dimension getPreferredSize() {
381 return getPreferredFullWidthSize();
382 }
383
384 @Override
385 protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
386
387 // handle delete key
388 //
389 if (e.getKeyCode() == KeyEvent.VK_DELETE) {
390 if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1)
391 // if DEL was pressed and only the currently edited cell is selected,
392 // don't run the delete action. DEL is handled by the CellEditor as normal
393 // DEL in the text input.
394 //
395 return super.processKeyBinding(ks, e, condition, pressed);
396 getDeleteAction().actionPerformed(null);
397 }
398 return super.processKeyBinding(ks, e, condition, pressed);
399 }
400
401 /**
402 * Sets the editor autocompletion list
403 * @param autoCompletionList autocompletion list
404 */
405 public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
406 if (autoCompletionList == null)
407 return;
408 if (editor != null) {
409 editor.setAutoCompletionList(autoCompletionList);
410 }
411 }
412
413 /**
414 * Sets the autocompletion manager that should be used for editing the cells
415 * @param autocomplete The {@link AutoCompletionManager}
416 */
417 public void setAutoCompletionManager(AutoCompletionManager autocomplete) {
418 if (autocomplete == null) {
419 Logging.warn("argument autocomplete should not be null. Aborting.");
420 Logging.error(new Exception());
421 return;
422 }
423 if (editor != null) {
424 editor.setAutoCompletionManager(autocomplete);
425 }
426 }
427
428 /**
429 * Gets the {@link AutoCompletionList} the cell editor is synchronized with
430 * @return The list
431 */
432 public AutoCompletionList getAutoCompletionList() {
433 if (editor != null)
434 return editor.getAutoCompletionList();
435 else
436 return null;
437 }
438
439 /**
440 * Sets the next component to request focus after navigation (with tab or enter).
441 * @param nextFocusComponent next component to request focus after navigation (with tab or enter)
442 */
443 public void setNextFocusComponent(Component nextFocusComponent) {
444 this.nextFocusComponent = nextFocusComponent;
445 }
446
447 /**
448 * Gets the editor that is used for the table cells
449 * @return The editor that is used when the user wants to enter text into a cell
450 */
451 public TagCellEditor getTableCellEditor() {
452 return editor;
453 }
454
455 /**
456 * Inject a tag cell editor in the tag table
457 *
458 * @param editor tag cell editor
459 */
460 public void setTagCellEditor(TagCellEditor editor) {
461 endCellEditing();
462 this.editor = editor;
463 getColumnModel().getColumn(0).setCellEditor(editor);
464 getColumnModel().getColumn(1).setCellEditor(editor);
465 }
466
467 /**
468 * Request the focus in a specific cell
469 * @param row The row index
470 * @param col The column index
471 */
472 public void requestFocusInCell(final int row, final int col) {
473 changeSelection(row, col, false, false);
474 editCellAt(row, col);
475 Component c = getEditorComponent();
476 if (c != null) {
477 if (!c.requestFocusInWindow()) {
478 Logging.warn("Unable to request focus for " + c);
479 }
480 if (c instanceof JTextComponent) {
481 ((JTextComponent) c).selectAll();
482 }
483 }
484 // there was a bug here - on older 1.6 Java versions Tab was not working
485 // after such activation. In 1.7 it works OK,
486 // previous solution of using awt.Robot was resetting mouse speed on Windows
487 }
488
489 /**
490 * Marks a component that may be focused without stopping the cell editing
491 * @param component The component
492 */
493 public void addComponentNotStoppingCellEditing(Component component) {
494 if (component == null) return;
495 doNotStopCellEditingWhenFocused.addIfAbsent(component);
496 }
497
498 /**
499 * Removes a component added with {@link #addComponentNotStoppingCellEditing(Component)}
500 * @param component The component
501 */
502 public void removeComponentNotStoppingCellEditing(Component component) {
503 if (component == null) return;
504 doNotStopCellEditingWhenFocused.remove(component);
505 }
506
507 @Override
508 public boolean editCellAt(int row, int column, EventObject e) {
509
510 // a snipped copied from the Java 1.5 implementation of JTable
511 //
512 if (cellEditor != null && !cellEditor.stopCellEditing())
513 return false;
514
515 if (row < 0 || row >= getRowCount() ||
516 column < 0 || column >= getColumnCount())
517 return false;
518
519 if (!isCellEditable(row, column))
520 return false;
521
522 // make sure our custom implementation of CellEditorRemover is created
523 if (editorRemover == null) {
524 KeyboardFocusManager fm =
525 KeyboardFocusManager.getCurrentKeyboardFocusManager();
526 editorRemover = new CellEditorRemover(fm);
527 fm.addPropertyChangeListener("permanentFocusOwner", editorRemover);
528 }
529
530 // delegate to the default implementation
531 return super.editCellAt(row, column, e);
532 }
533
534 @Override
535 public void endCellEditing() {
536 if (isEditing()) {
537 CellEditor cEditor = getCellEditor();
538 if (cEditor != null) {
539 // First attempt to commit. If this does not work, cancel.
540 cEditor.stopCellEditing();
541 cEditor.cancelCellEditing();
542 }
543 }
544 }
545
546 @Override
547 public void removeEditor() {
548 // make sure we unregister our custom implementation of CellEditorRemover
549 KeyboardFocusManager.getCurrentKeyboardFocusManager().
550 removePropertyChangeListener("permanentFocusOwner", editorRemover);
551 editorRemover = null;
552 super.removeEditor();
553 }
554
555 @Override
556 public void removeNotify() {
557 // make sure we unregister our custom implementation of CellEditorRemover
558 KeyboardFocusManager.getCurrentKeyboardFocusManager().
559 removePropertyChangeListener("permanentFocusOwner", editorRemover);
560 editorRemover = null;
561 super.removeNotify();
562 }
563
564 /**
565 * This is a custom implementation of the CellEditorRemover used in JTable
566 * to handle the client property <code>terminateEditOnFocusLost</code>.
567 *
568 * This implementation also checks whether focus is transferred to one of a list
569 * of dedicated components, see {@link TagTable#doNotStopCellEditingWhenFocused}.
570 * A typical example for such a component is a button in {@link TagEditorPanel}
571 * which isn't a child component of {@link TagTable} but which should respond to
572 * to focus transfer in a similar way to a child of TagTable.
573 *
574 */
575 class CellEditorRemover implements PropertyChangeListener {
576 private final KeyboardFocusManager focusManager;
577
578 CellEditorRemover(KeyboardFocusManager fm) {
579 this.focusManager = fm;
580 }
581
582 @Override
583 public void propertyChange(PropertyChangeEvent ev) {
584 if (!isEditing())
585 return;
586
587 Component c = focusManager.getPermanentFocusOwner();
588 while (c != null) {
589 if (c == TagTable.this)
590 // focus remains inside the table
591 return;
592 if (doNotStopCellEditingWhenFocused.contains(c))
593 // focus remains on one of the associated components
594 return;
595 else if (c instanceof Window) {
596 if (c == SwingUtilities.getRoot(TagTable.this) && !getCellEditor().stopCellEditing()) {
597 getCellEditor().cancelCellEditing();
598 }
599 break;
600 }
601 c = c.getParent();
602 }
603 }
604 }
605}
Note: See TracBrowser for help on using the repository browser.