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

Last change on this file since 14273 was 14273, checked in by stoecker, 6 years ago

fix typos - patch by naoliv - fix #16781 - Thanks a lot

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