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

Last change on this file since 18801 was 18801, checked in by taylor.smock, 10 months ago

Fix #22832: Code cleanup and some simplification, documentation fixes (patch by gaben)

There should not be any functional changes in this patch; it is intended to do
the following:

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