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

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

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

  • 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 * <p>
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 * <p>
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 * <p>
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.