source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/TagEditorModel.java@ 10604

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

fix #12478, fix #12565, fix #11114 - Use ​Swing Copy/Paste instead of CopyAction/PasteAction with custom buffer (patch by michael2402, modified) - gsoc-core

  • Property svn:eol-style set to native
File size: 21.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.tagging;
3
4import static org.openstreetmap.josm.tools.I18n.trn;
5
6import java.beans.PropertyChangeListener;
7import java.beans.PropertyChangeSupport;
8import java.util.ArrayList;
9import java.util.Collection;
10import java.util.Collections;
11import java.util.Comparator;
12import java.util.EnumSet;
13import java.util.HashMap;
14import java.util.Iterator;
15import java.util.List;
16import java.util.Map;
17import java.util.Map.Entry;
18
19import javax.swing.DefaultListSelectionModel;
20import javax.swing.table.AbstractTableModel;
21
22import org.openstreetmap.josm.command.ChangePropertyCommand;
23import org.openstreetmap.josm.command.Command;
24import org.openstreetmap.josm.command.SequenceCommand;
25import org.openstreetmap.josm.data.osm.OsmPrimitive;
26import org.openstreetmap.josm.data.osm.Tag;
27import org.openstreetmap.josm.data.osm.TagCollection;
28import org.openstreetmap.josm.data.osm.TagMap;
29import org.openstreetmap.josm.data.osm.Tagged;
30import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
31import org.openstreetmap.josm.tools.CheckParameterUtil;
32
33/**
34 * TagEditorModel is a table model to use with {@link TagEditorPanel}.
35 * @since 1762
36 */
37public class TagEditorModel extends AbstractTableModel {
38 public static final String PROP_DIRTY = TagEditorModel.class.getName() + ".dirty";
39
40 /** the list holding the tags */
41 protected final transient List<TagModel> tags = new ArrayList<>();
42
43 /** indicates whether the model is dirty */
44 private boolean dirty;
45 private final PropertyChangeSupport propChangeSupport = new PropertyChangeSupport(this);
46
47 private final DefaultListSelectionModel rowSelectionModel;
48 private final DefaultListSelectionModel colSelectionModel;
49
50 private transient OsmPrimitive primitive;
51
52 private EndEditListener endEditListener;
53
54 /**
55 * Creates a new tag editor model. Internally allocates two selection models
56 * for row selection and column selection.
57 *
58 * To create a {@link javax.swing.JTable} with this model:
59 * <pre>
60 * TagEditorModel model = new TagEditorModel();
61 * TagTable tbl = new TagTabel(model);
62 * </pre>
63 *
64 * @see #getRowSelectionModel()
65 * @see #getColumnSelectionModel()
66 */
67 public TagEditorModel() {
68 this(new DefaultListSelectionModel(), new DefaultListSelectionModel());
69 }
70
71 /**
72 * Creates a new tag editor model.
73 *
74 * @param rowSelectionModel the row selection model. Must not be null.
75 * @param colSelectionModel the column selection model. Must not be null.
76 * @throws IllegalArgumentException if {@code rowSelectionModel} is null
77 * @throws IllegalArgumentException if {@code colSelectionModel} is null
78 */
79 public TagEditorModel(DefaultListSelectionModel rowSelectionModel, DefaultListSelectionModel colSelectionModel) {
80 CheckParameterUtil.ensureParameterNotNull(rowSelectionModel, "rowSelectionModel");
81 CheckParameterUtil.ensureParameterNotNull(colSelectionModel, "colSelectionModel");
82 this.rowSelectionModel = rowSelectionModel;
83 this.colSelectionModel = colSelectionModel;
84 }
85
86 /**
87 * Adds property change listener.
88 * @param listener property change listener to add
89 */
90 public void addPropertyChangeListener(PropertyChangeListener listener) {
91 propChangeSupport.addPropertyChangeListener(listener);
92 }
93
94 /**
95 * Replies the row selection model used by this tag editor model
96 *
97 * @return the row selection model used by this tag editor model
98 */
99 public DefaultListSelectionModel getRowSelectionModel() {
100 return rowSelectionModel;
101 }
102
103 /**
104 * Replies the column selection model used by this tag editor model
105 *
106 * @return the column selection model used by this tag editor model
107 */
108 public DefaultListSelectionModel getColumnSelectionModel() {
109 return colSelectionModel;
110 }
111
112 /**
113 * Removes property change listener.
114 * @param listener property change listener to remove
115 */
116 public void removePropertyChangeListener(PropertyChangeListener listener) {
117 propChangeSupport.removePropertyChangeListener(listener);
118 }
119
120 protected void fireDirtyStateChanged(final boolean oldValue, final boolean newValue) {
121 propChangeSupport.firePropertyChange(PROP_DIRTY, oldValue, newValue);
122 }
123
124 protected void setDirty(boolean newValue) {
125 boolean oldValue = dirty;
126 dirty = newValue;
127 if (oldValue != newValue) {
128 fireDirtyStateChanged(oldValue, newValue);
129 }
130 }
131
132 @Override
133 public int getColumnCount() {
134 return 2;
135 }
136
137 @Override
138 public int getRowCount() {
139 return tags.size();
140 }
141
142 @Override
143 public Object getValueAt(int rowIndex, int columnIndex) {
144 if (rowIndex >= getRowCount())
145 throw new IndexOutOfBoundsException("unexpected rowIndex: rowIndex=" + rowIndex);
146
147 return tags.get(rowIndex);
148 }
149
150 @Override
151 public void setValueAt(Object value, int row, int col) {
152 TagModel tag = get(row);
153 if (tag != null) {
154 switch(col) {
155 case 0:
156 updateTagName(tag, (String) value);
157 break;
158 case 1:
159 String v = (String) value;
160 if ((tag.getValueCount() > 1 && !v.isEmpty()) || tag.getValueCount() <= 1) {
161 updateTagValue(tag, v);
162 }
163 break;
164 default: // Do nothing
165 }
166 }
167 }
168
169 /**
170 * removes all tags in the model
171 */
172 public void clear() {
173 commitPendingEdit();
174 boolean wasEmpty = tags.isEmpty();
175 tags.clear();
176 if (!wasEmpty) {
177 setDirty(true);
178 fireTableDataChanged();
179 }
180 }
181
182 /**
183 * adds a tag to the model
184 *
185 * @param tag the tag. Must not be null.
186 *
187 * @throws IllegalArgumentException if tag is null
188 */
189 public void add(TagModel tag) {
190 commitPendingEdit();
191 CheckParameterUtil.ensureParameterNotNull(tag, "tag");
192 tags.add(tag);
193 setDirty(true);
194 fireTableDataChanged();
195 }
196
197 /**
198 * Add a tag at the beginning of the table.
199 *
200 * @param tag The tag to add
201 *
202 * @throws IllegalArgumentException if tag is null
203 *
204 * @see #add(TagModel)
205 */
206 public void prepend(TagModel tag) {
207 commitPendingEdit();
208 CheckParameterUtil.ensureParameterNotNull(tag, "tag");
209 tags.add(0, tag);
210 setDirty(true);
211 fireTableDataChanged();
212 }
213
214 /**
215 * adds a tag given by a name/value pair to the tag editor model.
216 *
217 * If there is no tag with name <code>name</code> yet, a new {@link TagModel} is created
218 * and append to this model.
219 *
220 * If there is a tag with name <code>name</code>, <code>value</code> is merged to the list
221 * of values for this tag.
222 *
223 * @param name the name; converted to "" if null
224 * @param value the value; converted to "" if null
225 */
226 public void add(String name, String value) {
227 commitPendingEdit();
228 String key = (name == null) ? "" : name;
229 String val = (value == null) ? "" : value;
230
231 TagModel tag = get(key);
232 if (tag == null) {
233 tag = new TagModel(key, val);
234 int index = tags.size();
235 while (index >= 1 && tags.get(index - 1).getName().isEmpty() && tags.get(index - 1).getValue().isEmpty()) {
236 index--; // If last line(s) is empty, add new tag before it
237 }
238 tags.add(index, tag);
239 } else {
240 tag.addValue(val);
241 }
242 setDirty(true);
243 fireTableDataChanged();
244 }
245
246 /**
247 * replies the tag with name <code>name</code>; null, if no such tag exists
248 * @param name the tag name
249 * @return the tag with name <code>name</code>; null, if no such tag exists
250 */
251 public TagModel get(String name) {
252 String key = (name == null) ? "" : name;
253 for (TagModel tag : tags) {
254 if (tag.getName().equals(key))
255 return tag;
256 }
257 return null;
258 }
259
260 public TagModel get(int idx) {
261 return idx >= tags.size() ? null : tags.get(idx);
262 }
263
264 @Override
265 public boolean isCellEditable(int row, int col) {
266 // all cells are editable
267 return true;
268 }
269
270 /**
271 * deletes the names of the tags given by tagIndices
272 *
273 * @param tagIndices a list of tag indices
274 */
275 public void deleteTagNames(int[] tagIndices) {
276 if (tags == null)
277 return;
278 commitPendingEdit();
279 for (int tagIdx : tagIndices) {
280 TagModel tag = tags.get(tagIdx);
281 if (tag != null) {
282 tag.setName("");
283 }
284 }
285 fireTableDataChanged();
286 setDirty(true);
287 }
288
289 /**
290 * deletes the values of the tags given by tagIndices
291 *
292 * @param tagIndices the lit of tag indices
293 */
294 public void deleteTagValues(int[] tagIndices) {
295 if (tags == null)
296 return;
297 commitPendingEdit();
298 for (int tagIdx : tagIndices) {
299 TagModel tag = tags.get(tagIdx);
300 if (tag != null) {
301 tag.setValue("");
302 }
303 }
304 fireTableDataChanged();
305 setDirty(true);
306 }
307
308 /**
309 * Deletes all tags with name <code>name</code>
310 *
311 * @param name the name. Ignored if null.
312 */
313 public void delete(String name) {
314 commitPendingEdit();
315 if (name == null)
316 return;
317 Iterator<TagModel> it = tags.iterator();
318 boolean changed = false;
319 while (it.hasNext()) {
320 TagModel tm = it.next();
321 if (tm.getName().equals(name)) {
322 changed = true;
323 it.remove();
324 }
325 }
326 if (changed) {
327 fireTableDataChanged();
328 setDirty(true);
329 }
330 }
331
332 /**
333 * deletes the tags given by tagIndices
334 *
335 * @param tagIndices the list of tag indices
336 */
337 public void deleteTags(int[] tagIndices) {
338 if (tags == null)
339 return;
340 commitPendingEdit();
341 List<TagModel> toDelete = new ArrayList<>();
342 for (int tagIdx : tagIndices) {
343 TagModel tag = tags.get(tagIdx);
344 if (tag != null) {
345 toDelete.add(tag);
346 }
347 }
348 for (TagModel tag : toDelete) {
349 tags.remove(tag);
350 }
351 fireTableDataChanged();
352 setDirty(true);
353 }
354
355 /**
356 * creates a new tag and appends it to the model
357 */
358 public void appendNewTag() {
359 TagModel tag = new TagModel();
360 tags.add(tag);
361 fireTableDataChanged();
362 }
363
364 /**
365 * makes sure the model includes at least one (empty) tag
366 */
367 public void ensureOneTag() {
368 if (tags.isEmpty()) {
369 appendNewTag();
370 }
371 }
372
373 /**
374 * initializes the model with the tags of an OSM primitive
375 *
376 * @param primitive the OSM primitive
377 */
378 public void initFromPrimitive(Tagged primitive) {
379 commitPendingEdit();
380 this.tags.clear();
381 for (String key : primitive.keySet()) {
382 String value = primitive.get(key);
383 this.tags.add(new TagModel(key, value));
384 }
385 sort();
386 TagModel tag = new TagModel();
387 tags.add(tag);
388 setDirty(false);
389 fireTableDataChanged();
390 }
391
392 /**
393 * Initializes the model with the tags of an OSM primitive
394 *
395 * @param tags the tags of an OSM primitive
396 */
397 public void initFromTags(Map<String, String> tags) {
398 commitPendingEdit();
399 this.tags.clear();
400 for (Entry<String, String> entry : tags.entrySet()) {
401 this.tags.add(new TagModel(entry.getKey(), entry.getValue()));
402 }
403 sort();
404 TagModel tag = new TagModel();
405 this.tags.add(tag);
406 setDirty(false);
407 }
408
409 /**
410 * Initializes the model with the tags in a tag collection. Removes
411 * all tags if {@code tags} is null.
412 *
413 * @param tags the tags
414 */
415 public void initFromTags(TagCollection tags) {
416 commitPendingEdit();
417 this.tags.clear();
418 if (tags == null) {
419 setDirty(false);
420 return;
421 }
422 for (String key : tags.getKeys()) {
423 String value = tags.getJoinedValues(key);
424 this.tags.add(new TagModel(key, value));
425 }
426 sort();
427 // add an empty row
428 TagModel tag = new TagModel();
429 this.tags.add(tag);
430 setDirty(false);
431 }
432
433 /**
434 * applies the current state of the tag editor model to a primitive
435 *
436 * @param primitive the primitive
437 *
438 */
439 public void applyToPrimitive(Tagged primitive) {
440 primitive.setKeys(applyToTags(false));
441 }
442
443 /**
444 * applies the current state of the tag editor model to a map of tags
445 * @param keepEmpty {@code true} to keep empty tags
446 *
447 * @return the map of key/value pairs
448 */
449 private Map<String, String> applyToTags(boolean keepEmpty) {
450 // TagMap preserves the order of tags.
451 TagMap result = new TagMap();
452 for (TagModel tag: this.tags) {
453 // tag still holds an unchanged list of different values for the same key.
454 // no property change command required
455 if (tag.getValueCount() > 1) {
456 continue;
457 }
458
459 // tag name holds an empty key. Don't apply it to the selection.
460 if (!keepEmpty && (tag.getName().trim().isEmpty() || tag.getValue().trim().isEmpty())) {
461 continue;
462 }
463 result.put(tag.getName().trim(), tag.getValue().trim());
464 }
465 return result;
466 }
467
468 /**
469 * Returns tags, without empty ones.
470 * @return not-empty tags
471 */
472 public Map<String, String> getTags() {
473 return getTags(false);
474 }
475
476 /**
477 * Returns tags.
478 * @param keepEmpty {@code true} to keep empty tags
479 * @return tags
480 */
481 public Map<String, String> getTags(boolean keepEmpty) {
482 return applyToTags(keepEmpty);
483 }
484
485 /**
486 * Replies the tags in this tag editor model as {@link TagCollection}.
487 *
488 * @return the tags in this tag editor model as {@link TagCollection}
489 */
490 public TagCollection getTagCollection() {
491 return TagCollection.from(getTags());
492 }
493
494 /**
495 * checks whether the tag model includes a tag with a given key
496 *
497 * @param key the key
498 * @return true, if the tag model includes the tag; false, otherwise
499 */
500 public boolean includesTag(String key) {
501 if (key != null) {
502 for (TagModel tag : tags) {
503 if (tag.getName().equals(key))
504 return true;
505 }
506 }
507 return false;
508 }
509
510 protected Command createUpdateTagCommand(Collection<OsmPrimitive> primitives, TagModel tag) {
511
512 // tag still holds an unchanged list of different values for the same key.
513 // no property change command required
514 if (tag.getValueCount() > 1)
515 return null;
516
517 // tag name holds an empty key. Don't apply it to the selection.
518 //
519 if (tag.getName().trim().isEmpty())
520 return null;
521
522 return new ChangePropertyCommand(primitives, tag.getName(), tag.getValue());
523 }
524
525 protected Command createDeleteTagsCommand(Collection<OsmPrimitive> primitives) {
526
527 List<String> currentkeys = getKeys();
528 List<Command> commands = new ArrayList<>();
529
530 for (OsmPrimitive prim : primitives) {
531 for (String oldkey : prim.keySet()) {
532 if (!currentkeys.contains(oldkey)) {
533 ChangePropertyCommand deleteCommand =
534 new ChangePropertyCommand(prim, oldkey, null);
535 commands.add(deleteCommand);
536 }
537 }
538 }
539
540 return new SequenceCommand(
541 trn("Remove old keys from up to {0} object", "Remove old keys from up to {0} objects", primitives.size(), primitives.size()),
542 commands
543 );
544 }
545
546 /**
547 * replies the list of keys of the tags managed by this model
548 *
549 * @return the list of keys managed by this model
550 */
551 public List<String> getKeys() {
552 List<String> keys = new ArrayList<>();
553 for (TagModel tag: tags) {
554 if (!tag.getName().trim().isEmpty()) {
555 keys.add(tag.getName());
556 }
557 }
558 return keys;
559 }
560
561 /**
562 * sorts the current tags according alphabetical order of names
563 */
564 protected void sort() {
565 Collections.sort(
566 tags,
567 new Comparator<TagModel>() {
568 @Override
569 public int compare(TagModel self, TagModel other) {
570 return self.getName().compareTo(other.getName());
571 }
572 }
573 );
574 }
575
576 /**
577 * updates the name of a tag and sets the dirty state to true if
578 * the new name is different from the old name.
579 *
580 * @param tag the tag
581 * @param newName the new name
582 */
583 public void updateTagName(TagModel tag, String newName) {
584 String oldName = tag.getName();
585 tag.setName(newName);
586 if (!newName.equals(oldName)) {
587 setDirty(true);
588 }
589 SelectionStateMemento memento = new SelectionStateMemento();
590 fireTableDataChanged();
591 memento.apply();
592 }
593
594 /**
595 * updates the value value of a tag and sets the dirty state to true if the
596 * new name is different from the old name
597 *
598 * @param tag the tag
599 * @param newValue the new value
600 */
601 public void updateTagValue(TagModel tag, String newValue) {
602 String oldValue = tag.getValue();
603 tag.setValue(newValue);
604 if (!newValue.equals(oldValue)) {
605 setDirty(true);
606 }
607 SelectionStateMemento memento = new SelectionStateMemento();
608 fireTableDataChanged();
609 memento.apply();
610 }
611
612 /**
613 * Load tags from given list
614 * @param tags - the list
615 */
616 public void updateTags(List<Tag> tags) {
617 if (tags.isEmpty())
618 return;
619
620 commitPendingEdit();
621 Map<String, TagModel> modelTags = new HashMap<>();
622 for (int i = 0; i < getRowCount(); i++) {
623 TagModel tagModel = get(i);
624 modelTags.put(tagModel.getName(), tagModel);
625 }
626 for (Tag tag: tags) {
627 TagModel existing = modelTags.get(tag.getKey());
628
629 if (tag.getValue().isEmpty()) {
630 if (existing != null) {
631 delete(tag.getKey());
632 }
633 } else {
634 if (existing != null) {
635 updateTagValue(existing, tag.getValue());
636 } else {
637 add(tag.getKey(), tag.getValue());
638 }
639 }
640 }
641 }
642
643 /**
644 * replies true, if this model has been updated
645 *
646 * @return true, if this model has been updated
647 */
648 public boolean isDirty() {
649 return dirty;
650 }
651
652 /**
653 * Returns the list of tagging presets types to consider when updating the presets list panel.
654 * By default returns type of associated primitive or empty set.
655 * @return the list of tagging presets types to consider when updating the presets list panel
656 * @see #forPrimitive
657 * @see TaggingPresetType#forPrimitive
658 * @since 9588
659 */
660 public Collection<TaggingPresetType> getTaggingPresetTypes() {
661 return primitive == null ? EnumSet.noneOf(TaggingPresetType.class) : EnumSet.of(TaggingPresetType.forPrimitive(primitive));
662 }
663
664 /**
665 * Makes this TagEditorModel specific to a given OSM primitive.
666 * @param primitive primitive to consider
667 * @return {@code this}
668 * @since 9588
669 */
670 public TagEditorModel forPrimitive(OsmPrimitive primitive) {
671 this.primitive = primitive;
672 return this;
673 }
674
675 /**
676 * Sets the listener that is notified when an edit should be aborted.
677 * @param endEditListener The listener to be notified when editing should be aborted.
678 */
679 public void setEndEditListener(EndEditListener endEditListener) {
680 this.endEditListener = endEditListener;
681 }
682
683 private void commitPendingEdit() {
684 if (endEditListener != null) {
685 endEditListener.endCellEditing();
686 }
687 }
688
689 class SelectionStateMemento {
690 private final int rowMin;
691 private final int rowMax;
692 private final int colMin;
693 private final int colMax;
694
695 SelectionStateMemento() {
696 rowMin = rowSelectionModel.getMinSelectionIndex();
697 rowMax = rowSelectionModel.getMaxSelectionIndex();
698 colMin = colSelectionModel.getMinSelectionIndex();
699 colMax = colSelectionModel.getMaxSelectionIndex();
700 }
701
702 void apply() {
703 rowSelectionModel.setValueIsAdjusting(true);
704 colSelectionModel.setValueIsAdjusting(true);
705 if (rowMin >= 0 && rowMax >= 0) {
706 rowSelectionModel.setSelectionInterval(rowMin, rowMax);
707 }
708 if (colMin >= 0 && colMax >= 0) {
709 colSelectionModel.setSelectionInterval(colMin, colMax);
710 }
711 rowSelectionModel.setValueIsAdjusting(false);
712 colSelectionModel.setValueIsAdjusting(false);
713 }
714 }
715
716 /**
717 * A listener that is called whenever the cells may be updated from outside the editor and the editor should thus be commited.
718 * @since 10604
719 */
720 @FunctionalInterface
721 public interface EndEditListener {
722 /**
723 * Requests to end the editing of any cells on this model
724 */
725 void endCellEditing();
726 }
727}
Note: See TracBrowser for help on using the repository browser.