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

Last change on this file since 10357 was 10357, checked in by stoecker, 8 years ago

see #9995, see #10684 - remove more hardcoded places of images

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