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

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

code refactoring

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