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

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

Checkstyle 6.19: enable SingleSpaceSeparator and fix violations

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