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

Last change on this file since 19050 was 19050, checked in by taylor.smock, 15 months ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

  • Property svn:eol-style set to native
File size: 20.4 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.InputEvent;
12import java.awt.event.KeyEvent;
13import java.beans.PropertyChangeEvent;
14import java.beans.PropertyChangeListener;
15import java.util.Collections;
16import java.util.EventObject;
17import java.util.concurrent.CopyOnWriteArrayList;
18
19import javax.swing.AbstractAction;
20import javax.swing.CellEditor;
21import javax.swing.JComponent;
22import javax.swing.JTable;
23import javax.swing.KeyStroke;
24import javax.swing.ListSelectionModel;
25import javax.swing.SwingUtilities;
26import javax.swing.event.ListSelectionEvent;
27import javax.swing.event.ListSelectionListener;
28import javax.swing.text.JTextComponent;
29
30import org.openstreetmap.josm.data.osm.Relation;
31import org.openstreetmap.josm.data.osm.TagMap;
32import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler;
33import org.openstreetmap.josm.gui.tagging.TagEditorModel.EndEditListener;
34import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
35import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
36import org.openstreetmap.josm.gui.widgets.JosmTable;
37import org.openstreetmap.josm.tools.ImageProvider;
38import org.openstreetmap.josm.tools.Logging;
39import org.openstreetmap.josm.tools.Utils;
40
41/**
42 * This is the tabular editor component for OSM tags.
43 * @since 1762
44 */
45public class TagTable extends JosmTable implements EndEditListener {
46 /** the table cell editor used by this table */
47 private TagCellEditor editor;
48 private final TagEditorModel model;
49 private Component nextFocusComponent;
50
51 /** a list of components to which focus can be transferred without stopping
52 * cell editing this table.
53 */
54 private final CopyOnWriteArrayList<Component> doNotStopCellEditingWhenFocused = new CopyOnWriteArrayList<>();
55 private transient CellEditorRemover editorRemover;
56
57 /**
58 * Action to be run when the user navigates to the next cell in the table,
59 * for instance by pressing TAB or ENTER. The action alters the standard
60 * navigation path from cell to cell:
61 * <ul>
62 * <li>it jumps over cells in the first column</li>
63 * <li>it automatically add a new empty row when the user leaves the
64 * last cell in the table</li>
65 * </ul>
66 */
67 class SelectNextColumnCellAction extends AbstractAction {
68 @Override
69 public void actionPerformed(ActionEvent e) {
70 run();
71 }
72
73 public void run() {
74 int col = getSelectedColumn();
75 int row = getSelectedRow();
76 if (getCellEditor() != null) {
77 getCellEditor().stopCellEditing();
78 }
79
80 if (row == -1 && col == -1) {
81 requestFocusInCell(0, 0);
82 return;
83 }
84
85 if (col == 0) {
86 col++;
87 } else if (col == 1 && row < getRowCount()-1) {
88 col = 0;
89 row++;
90 } else if (col == 1 && row == getRowCount()-1) {
91 // we are at the end. Append an empty row and move the focus to its second column
92 String key = ((TagModel) model.getValueAt(row, 0)).getName();
93 if (!Utils.isStripEmpty(key)) {
94 model.appendNewTag();
95 col = 0;
96 row++;
97 } else {
98 clearSelection();
99 if (nextFocusComponent != null)
100 nextFocusComponent.requestFocusInWindow();
101 return;
102 }
103 }
104 requestFocusInCell(row, col);
105 }
106 }
107
108 /**
109 * Action to be run when the user navigates to the previous cell in the table,
110 * for instance by pressing Shift-TAB
111 */
112 class SelectPreviousColumnCellAction extends AbstractAction {
113
114 @Override
115 public void actionPerformed(ActionEvent e) {
116 int col = getSelectedColumn();
117 int row = getSelectedRow();
118 if (getCellEditor() != null) {
119 getCellEditor().stopCellEditing();
120 }
121
122 if (col <= 0 && row <= 0) {
123 // change nothing
124 } else if (col == 1) {
125 col--;
126 } else {
127 col = 1;
128 row--;
129 }
130 requestFocusInCell(row, col);
131 }
132 }
133
134 /**
135 * Action to be run when the user invokes a delete action on the table, for
136 * instance by pressing DEL.
137 * <p>
138 * Depending on the shape on the current selection the action deletes individual
139 * values or entire tags from the model.
140 * <p>
141 * If the current selection consists of cells in the second column only, the keys of
142 * the selected tags are set to the empty string.
143 * <p>
144 * If the current selection consists of cell in the third column only, the values of the
145 * selected tags are set to the empty string.
146 * <p>
147 * If the current selection consists of cells in the second and the third column,
148 * the selected tags are removed from the model.
149 * <p>
150 * This action listens to the table selection. It becomes enabled when the selection
151 * is non-empty, otherwise it is disabled.
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 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 || !Utils.isStripEmpty(((TagModel) model.getValueAt(rowIdx, 0)).getName())) {
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"));
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, InputEvent.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 * <p>
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.