// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.conflict.pair;
import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_MERGED;
import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_THEIR;
import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.THEIR_WITH_MERGED;
import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MERGED_ENTRIES;
import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MY_ENTRIES;
import static org.openstreetmap.josm.gui.conflict.pair.ListRole.THEIR_ENTRIES;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.AbstractListModel;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultListSelectionModel;
import javax.swing.JOptionPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.command.conflict.ConflictResolveCommand;
import org.openstreetmap.josm.data.conflict.Conflict;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.PrimitiveId;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.gui.HelpAwareOptionPane;
import org.openstreetmap.josm.gui.help.HelpUtil;
import org.openstreetmap.josm.gui.util.ChangeNotifier;
import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTableModel;
import org.openstreetmap.josm.tools.CheckParameterUtil;
import org.openstreetmap.josm.tools.Utils;
/**
* ListMergeModel is a model for interactively comparing and merging two list of entries
* of type T. It maintains three lists of entries of type T:
* <ol>
* <li>the list of <em>my</em> entries</li>
* <li>the list of <em>their</em> entries</li>
* <li>the list of <em>merged</em> entries</li>
* </ol>
*
* A ListMergeModel is a factory for three {@link TableModel}s and three {@link ListSelectionModel}s:
* <ol>
* <li>the table model and the list selection for for a {@link JTable} which shows my entries.
* See {@link #getMyTableModel()} and {@link AbstractListMergeModel#getMySelectionModel()}</li>
* <li>dito for their entries and merged entries</li>
* </ol>
*
* A ListMergeModel can be ''frozen''. If it's frozen, it doesn't accept additional merge
* decisions. {@link PropertyChangeListener}s can register for property value changes of
* {@link #FROZEN_PROP}.
*
* ListMergeModel is an abstract class. Three methods have to be implemented by subclasses:
* <ul>
* <li>{@link AbstractListMergeModel#cloneEntryForMergedList} - clones an entry of type T</li>
* <li>{@link AbstractListMergeModel#isEqualEntry} - checks whether two entries are equals </li>
* <li>{@link AbstractListMergeModel#setValueAt(DefaultTableModel, Object, int, int)} - handles values edited in
* a JTable, dispatched from {@link TableModel#setValueAt(Object, int, int)} </li>
* </ul>
* A ListMergeModel is used in combination with a {@link AbstractListMerger}.
*
* @param <T> the type of the list entries
* @param <C> the type of conflict resolution command
* @see AbstractListMerger
*/
public abstract class AbstractListMergeModel<T extends PrimitiveId, C extends ConflictResolveCommand> extends ChangeNotifier {
public static final String FROZEN_PROP = AbstractListMergeModel.class.getName() + ".frozen";
private static final int MAX_DELETED_PRIMITIVE_IN_DIALOG = 5;
protected Map<ListRole, ArrayList<T>> entries;
protected EntriesTableModel myEntriesTableModel;
protected EntriesTableModel theirEntriesTableModel;
protected EntriesTableModel mergedEntriesTableModel;
protected EntriesSelectionModel myEntriesSelectionModel;
protected EntriesSelectionModel theirEntriesSelectionModel;
protected EntriesSelectionModel mergedEntriesSelectionModel;
private final Set<PropertyChangeListener> listeners;
private boolean isFrozen;
private final ComparePairListModel comparePairListModel;
private DataSet myDataset;
private Map<PrimitiveId, PrimitiveId> mergedMap;
/**
* Creates a clone of an entry of type T suitable to be included in the
* list of merged entries
*
* @param entry the entry
* @return the cloned entry
*/
protected abstract T cloneEntryForMergedList(T entry);
/**
* checks whether two entries are equal. This is not necessarily the same as
* e1.equals(e2).
*
* @param e1 the first entry
* @param e2 the second entry
* @return true, if the entries are equal, false otherwise.
*/
public abstract boolean isEqualEntry(T e1, T e2);
/**
* Handles method dispatches from {@link TableModel#setValueAt(Object, int, int)}.
*
* @param model the table model
* @param value the value to be set
* @param row the row index
* @param col the column index
*
* @see TableModel#setValueAt(Object, int, int)
*/
protected abstract void setValueAt(DefaultTableModel model, Object value, int row, int col);
/**
* Replies primitive from my dataset referenced by entry
* @param entry entry
* @return Primitive from my dataset referenced by entry
*/
public OsmPrimitive getMyPrimitive(T entry) {
return getMyPrimitiveById(entry);
}
public final OsmPrimitive getMyPrimitiveById(PrimitiveId entry) {
OsmPrimitive result = myDataset.getPrimitiveById(entry);
if (result == null && mergedMap != null) {
PrimitiveId id = mergedMap.get(entry);
if (id == null && entry instanceof OsmPrimitive) {
id = mergedMap.get(((OsmPrimitive) entry).getPrimitiveId());
}
if (id != null) {
result = myDataset.getPrimitiveById(id);
}
}
return result;
}
protected void buildMyEntriesTableModel() {
myEntriesTableModel = new EntriesTableModel(MY_ENTRIES);
}
protected void buildTheirEntriesTableModel() {
theirEntriesTableModel = new EntriesTableModel(THEIR_ENTRIES);
}
protected void buildMergedEntriesTableModel() {
mergedEntriesTableModel = new EntriesTableModel(MERGED_ENTRIES);
}
protected List<T> getMergedEntries() {
return entries.get(MERGED_ENTRIES);
}
protected List<T> getMyEntries() {
return entries.get(MY_ENTRIES);
}
protected List<T> getTheirEntries() {
return entries.get(THEIR_ENTRIES);
}
public int getMyEntriesSize() {
return getMyEntries().size();
}
public int getMergedEntriesSize() {
return getMergedEntries().size();
}
public int getTheirEntriesSize() {
return getTheirEntries().size();
}
/**
* Constructs a new {@code ListMergeModel}.
*/
public AbstractListMergeModel() {
entries = new EnumMap<>(ListRole.class);
for (ListRole role : ListRole.values()) {
entries.put(role, new ArrayList<T>());
}
buildMyEntriesTableModel();
buildTheirEntriesTableModel();
buildMergedEntriesTableModel();
myEntriesSelectionModel = new EntriesSelectionModel(entries.get(MY_ENTRIES));
theirEntriesSelectionModel = new EntriesSelectionModel(entries.get(THEIR_ENTRIES));
mergedEntriesSelectionModel = new EntriesSelectionModel(entries.get(MERGED_ENTRIES));
listeners = new HashSet<>();
comparePairListModel = new ComparePairListModel();
setFrozen(true);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
synchronized (listeners) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener);
}
}
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
synchronized (listeners) {
if (listener != null && listeners.contains(listener)) {
listeners.remove(listener);
}
}
}
protected void fireFrozenChanged(boolean oldValue, boolean newValue) {
synchronized (listeners) {
PropertyChangeEvent evt = new PropertyChangeEvent(this, FROZEN_PROP, oldValue, newValue);
listeners.forEach(listener -> listener.propertyChange(evt));
}
}
public final void setFrozen(boolean isFrozen) {
boolean oldValue = this.isFrozen;
this.isFrozen = isFrozen;
fireFrozenChanged(oldValue, this.isFrozen);
}
public final boolean isFrozen() {
return isFrozen;
}
public OsmPrimitivesTableModel getMyTableModel() {
return myEntriesTableModel;
}
public OsmPrimitivesTableModel getTheirTableModel() {
return theirEntriesTableModel;
}
public OsmPrimitivesTableModel getMergedTableModel() {
return mergedEntriesTableModel;
}
public EntriesSelectionModel getMySelectionModel() {
return myEntriesSelectionModel;
}
public EntriesSelectionModel getTheirSelectionModel() {
return theirEntriesSelectionModel;
}
public EntriesSelectionModel getMergedSelectionModel() {
return mergedEntriesSelectionModel;
}
protected void fireModelDataChanged() {
myEntriesTableModel.fireTableDataChanged();
theirEntriesTableModel.fireTableDataChanged();
mergedEntriesTableModel.fireTableDataChanged();
fireStateChanged();
}
protected void copyToTop(ListRole role, int... rows) {
copy(role, rows, 0);
mergedEntriesSelectionModel.setSelectionInterval(0, rows.length -1);
}
/**
* Copies the nodes given by indices in rows from the list of my nodes to the
* list of merged nodes. Inserts the nodes at the top of the list of merged
* nodes.
*
* @param rows the indices
*/
public void copyMyToTop(int... rows) {
copyToTop(MY_ENTRIES, rows);
}
/**
* Copies the nodes given by indices in rows from the list of their nodes to the
* list of merged nodes. Inserts the nodes at the top of the list of merged
* nodes.
*
* @param rows the indices
*/
public void copyTheirToTop(int... rows) {
copyToTop(THEIR_ENTRIES, rows);
}
/**
* Copies the nodes given by indices in rows from the list of nodes in source to the
* list of merged nodes. Inserts the nodes at the end of the list of merged
* nodes.
*
* @param source the list of nodes to copy from
* @param rows the indices
*/
public void copyToEnd(ListRole source, int... rows) {
copy(source, rows, getMergedEntriesSize());
mergedEntriesSelectionModel.setSelectionInterval(getMergedEntriesSize()-rows.length, getMergedEntriesSize() -1);
}
/**
* Copies the nodes given by indices in rows from the list of my nodes to the
* list of merged nodes. Inserts the nodes at the end of the list of merged
* nodes.
*
* @param rows the indices
*/
public void copyMyToEnd(int... rows) {
copyToEnd(MY_ENTRIES, rows);
}
/**
* Copies the nodes given by indices in rows from the list of their nodes to the
* list of merged nodes. Inserts the nodes at the end of the list of merged
* nodes.
*
* @param rows the indices
*/
public void copyTheirToEnd(int... rows) {
copyToEnd(THEIR_ENTRIES, rows);
}
public void clearMerged() {
getMergedEntries().clear();
fireModelDataChanged();
}
protected final void initPopulate(OsmPrimitive my, OsmPrimitive their, Map<PrimitiveId, PrimitiveId> mergedMap) {
CheckParameterUtil.ensureParameterNotNull(my, "my");
CheckParameterUtil.ensureParameterNotNull(their, "their");
this.myDataset = my.getDataSet();
this.mergedMap = mergedMap;
getMergedEntries().clear();
getMyEntries().clear();
getTheirEntries().clear();
}
protected void alertCopyFailedForDeletedPrimitives(List<PrimitiveId> deletedIds) {
List<String> items = new ArrayList<>();
for (int i = 0; i < Math.min(MAX_DELETED_PRIMITIVE_IN_DIALOG, deletedIds.size()); i++) {
items.add(deletedIds.get(i).toString());
}
if (deletedIds.size() > MAX_DELETED_PRIMITIVE_IN_DIALOG) {
items.add(tr("{0} more...", deletedIds.size() - MAX_DELETED_PRIMITIVE_IN_DIALOG));
}
StringBuilder sb = new StringBuilder();
sb.append("<html>")
.append(tr("The following objects could not be copied to the target object<br>because they are deleted in the target dataset:"))
.append(Utils.joinAsHtmlUnorderedList(items))
.append("</html>");
HelpAwareOptionPane.showOptionDialog(
Main.parent,
sb.toString(),
tr("Merging deleted objects failed"),
JOptionPane.WARNING_MESSAGE,
HelpUtil.ht("/Dialog/Conflict#MergingDeletedPrimitivesFailed")
);
}
private void copy(ListRole sourceRole, int[] rows, int position) {
if (position < 0 || position > getMergedEntriesSize())
throw new IllegalArgumentException("Position must be between 0 and "+getMergedEntriesSize()+" but is "+position);
List<T> newItems = new ArrayList<>(rows.length);
List<T> source = entries.get(sourceRole);
List<PrimitiveId> deletedIds = new ArrayList<>();
for (int row: rows) {
T entry = source.get(row);
OsmPrimitive primitive = getMyPrimitive(entry);
if (!primitive.isDeleted()) {
T clone = cloneEntryForMergedList(entry);
newItems.add(clone);
} else {
deletedIds.add(primitive.getPrimitiveId());
}
}
getMergedEntries().addAll(position, newItems);
fireModelDataChanged();
if (!deletedIds.isEmpty()) {
alertCopyFailedForDeletedPrimitives(deletedIds);
}
}
public void copyAll(ListRole source) {
getMergedEntries().clear();
int[] rows = new int[entries.get(source).size()];
for (int i = 0; i < rows.length; i++) {
rows[i] = i;
}
copy(source, rows, 0);
}
/**
* Copies the nodes given by indices in rows from the list of nodes <code>source</code> to the
* list of merged nodes. Inserts the nodes before row given by current.
*
* @param source the list of nodes to copy from
* @param rows the indices
* @param current the row index before which the nodes are inserted
* @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes
*/
protected void copyBeforeCurrent(ListRole source, int[] rows, int current) {
copy(source, rows, current);
mergedEntriesSelectionModel.setSelectionInterval(current, current + rows.length-1);
}
/**
* Copies the nodes given by indices in rows from the list of my nodes to the
* list of merged nodes. Inserts the nodes before row given by current.
*
* @param rows the indices
* @param current the row index before which the nodes are inserted
* @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes
*/
public void copyMyBeforeCurrent(int[] rows, int current) {
copyBeforeCurrent(MY_ENTRIES, rows, current);
}
/**
* Copies the nodes given by indices in rows from the list of their nodes to the
* list of merged nodes. Inserts the nodes before row given by current.
*
* @param rows the indices
* @param current the row index before which the nodes are inserted
* @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes
*/
public void copyTheirBeforeCurrent(int[] rows, int current) {
copyBeforeCurrent(THEIR_ENTRIES, rows, current);
}
/**
* Copies the nodes given by indices in rows from the list of nodes <code>source</code> to the
* list of merged nodes. Inserts the nodes after the row given by current.
*
* @param source the list of nodes to copy from
* @param rows the indices
* @param current the row index after which the nodes are inserted
* @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes
*/
protected void copyAfterCurrent(ListRole source, int[] rows, int current) {
copy(source, rows, current + 1);
mergedEntriesSelectionModel.setSelectionInterval(current+1, current + rows.length-1);
fireStateChanged();
}
/**
* Copies the nodes given by indices in rows from the list of my nodes to the
* list of merged nodes. Inserts the nodes after the row given by current.
*
* @param rows the indices
* @param current the row index after which the nodes are inserted
* @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes
*/
public void copyMyAfterCurrent(int[] rows, int current) {
copyAfterCurrent(MY_ENTRIES, rows, current);
}
/**
* Copies the nodes given by indices in rows from the list of my nodes to the
* list of merged nodes. Inserts the nodes after the row given by current.
*
* @param rows the indices
* @param current the row index after which the nodes are inserted
* @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes
*/
public void copyTheirAfterCurrent(int[] rows, int current) {
copyAfterCurrent(THEIR_ENTRIES, rows, current);
}
/**
* Moves the nodes given by indices in rows up by one position in the list
* of merged nodes.
*
* @param rows the indices
*
*/
public void moveUpMerged(int... rows) {
if (rows == null || rows.length == 0)
return;
if (rows[0] == 0)
// can't move up
return;
List<T> mergedEntries = getMergedEntries();
for (int row: rows) {
T n = mergedEntries.get(row);
mergedEntries.remove(row);
mergedEntries.add(row -1, n);
}
fireModelDataChanged();
mergedEntriesSelectionModel.setValueIsAdjusting(true);
mergedEntriesSelectionModel.clearSelection();
for (int row: rows) {
mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1);
}
mergedEntriesSelectionModel.setValueIsAdjusting(false);
}
/**
* Moves the nodes given by indices in rows down by one position in the list
* of merged nodes.
*
* @param rows the indices
*/
public void moveDownMerged(int... rows) {
if (rows == null || rows.length == 0)
return;
List<T> mergedEntries = getMergedEntries();
if (rows[rows.length -1] == mergedEntries.size() -1)
// can't move down
return;
for (int i = rows.length-1; i >= 0; i--) {
int row = rows[i];
T n = mergedEntries.get(row);
mergedEntries.remove(row);
mergedEntries.add(row +1, n);
}
fireModelDataChanged();
mergedEntriesSelectionModel.setValueIsAdjusting(true);
mergedEntriesSelectionModel.clearSelection();
for (int row: rows) {
mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1);
}
mergedEntriesSelectionModel.setValueIsAdjusting(false);
}
/**
* Removes the nodes given by indices in rows from the list
* of merged nodes.
*
* @param rows the indices
*/
public void removeMerged(int... rows) {
if (rows == null || rows.length == 0)
return;
List<T> mergedEntries = getMergedEntries();
for (int i = rows.length-1; i >= 0; i--) {
mergedEntries.remove(rows[i]);
}
fireModelDataChanged();
mergedEntriesSelectionModel.clearSelection();
}
/**
* Replies true if the list of my entries and the list of their
* entries are equal
*
* @return true, if the lists are equal; false otherwise
*/
protected boolean myAndTheirEntriesEqual() {
if (getMyEntriesSize() != getTheirEntriesSize())
return false;
for (int i = 0; i < getMyEntriesSize(); i++) {
if (!isEqualEntry(getMyEntries().get(i), getTheirEntries().get(i)))
return false;
}
return true;
}
/**
* This an adapter between a {@link JTable} and one of the three entry lists
* in the role {@link ListRole} managed by the {@link AbstractListMergeModel}.
*
* From the point of view of the {@link JTable} it is a {@link TableModel}.
*
* @see AbstractListMergeModel#getMyTableModel()
* @see AbstractListMergeModel#getTheirTableModel()
* @see AbstractListMergeModel#getMergedTableModel()
*/
public class EntriesTableModel extends DefaultTableModel implements OsmPrimitivesTableModel {
private final ListRole role;
/**
*
* @param role the role
*/
public EntriesTableModel(ListRole role) {
this.role = role;
}
@Override
public int getRowCount() {
int count = Math.max(getMyEntries().size(), getMergedEntries().size());
return Math.max(count, getTheirEntries().size());
}
@Override
public Object getValueAt(int row, int column) {
if (row < entries.get(role).size())
return entries.get(role).get(row);
return null;
}
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
@Override
public void setValueAt(Object value, int row, int col) {
AbstractListMergeModel.this.setValueAt(this, value, row, col);
}
/**
* Returns the list merge model.
* @return the list merge model
*/
public AbstractListMergeModel<T, C> getListMergeModel() {
return AbstractListMergeModel.this;
}
/**
* replies true if the {@link ListRole} of this {@link EntriesTableModel}
* participates in the current {@link ComparePairType}
*
* @return true, if the if the {@link ListRole} of this {@link EntriesTableModel}
* participates in the current {@link ComparePairType}
*
* @see AbstractListMergeModel.ComparePairListModel#getSelectedComparePair()
*/
public boolean isParticipatingInCurrentComparePair() {
return getComparePairListModel()
.getSelectedComparePair()
.isParticipatingIn(role);
}
/**
* replies true if the entry at <code>row</code> is equal to the entry at the
* same position in the opposite list of the current {@link ComparePairType}.
*
* @param row the row number
* @return true if the entry at <code>row</code> is equal to the entry at the
* same position in the opposite list of the current {@link ComparePairType}
* @throws IllegalStateException if this model is not participating in the
* current {@link ComparePairType}
* @see ComparePairType#getOppositeRole(ListRole)
* @see #getRole()
* @see #getOppositeEntries()
*/
public boolean isSamePositionInOppositeList(int row) {
if (!isParticipatingInCurrentComparePair())
throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString()));
if (row >= getEntries().size()) return false;
if (row >= getOppositeEntries().size()) return false;
T e1 = getEntries().get(row);
T e2 = getOppositeEntries().get(row);
return isEqualEntry(e1, e2);
}
/**
* replies true if the entry at the current position is present in the opposite list
* of the current {@link ComparePairType}.
*
* @param row the current row
* @return true if the entry at the current position is present in the opposite list
* of the current {@link ComparePairType}.
* @throws IllegalStateException if this model is not participating in the
* current {@link ComparePairType}
* @see ComparePairType#getOppositeRole(ListRole)
* @see #getRole()
* @see #getOppositeEntries()
*/
public boolean isIncludedInOppositeList(int row) {
if (!isParticipatingInCurrentComparePair())
throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString()));
if (row >= getEntries().size()) return false;
T e1 = getEntries().get(row);
return getOppositeEntries().stream().anyMatch(e2 -> isEqualEntry(e1, e2));
}
protected List<T> getEntries() {
return entries.get(role);
}
/**
* replies the opposite list of entries with respect to the current {@link ComparePairType}
*
* @return the opposite list of entries
*/
protected List<T> getOppositeEntries() {
ListRole opposite = getComparePairListModel().getSelectedComparePair().getOppositeRole(role);
return entries.get(opposite);
}
public ListRole getRole() {
return role;
}
@Override
public OsmPrimitive getReferredPrimitive(int idx) {
Object value = getValueAt(idx, 1);
if (value instanceof OsmPrimitive) {
return (OsmPrimitive) value;
} else if (value instanceof RelationMember) {
return ((RelationMember) value).getMember();
} else {
Main.error("Unknown object type: "+value);
return null;
}
}
}
/**
* This is the selection model to be used in a {@link JTable} which displays
* an entry list managed by {@link AbstractListMergeModel}.
*
* The model ensures that only rows displaying an entry in the entry list
* can be selected. "Empty" rows can't be selected.
*
* @see AbstractListMergeModel#getMySelectionModel()
* @see AbstractListMergeModel#getMergedSelectionModel()
* @see AbstractListMergeModel#getTheirSelectionModel()
*
*/
protected class EntriesSelectionModel extends DefaultListSelectionModel {
private final transient List<T> entries;
public EntriesSelectionModel(List<T> nodes) {
this.entries = nodes;
}
@Override
public void addSelectionInterval(int index0, int index1) {
if (entries.isEmpty()) return;
if (index0 > entries.size() - 1) return;
index0 = Math.min(entries.size()-1, index0);
index1 = Math.min(entries.size()-1, index1);
super.addSelectionInterval(index0, index1);
}
@Override
public void insertIndexInterval(int index, int length, boolean before) {
if (entries.isEmpty()) return;
if (before) {
int newindex = Math.min(entries.size()-1, index);
if (newindex < index - length) return;
length = length - (index - newindex);
super.insertIndexInterval(newindex, length, before);
} else {
if (index > entries.size() -1) return;
length = Math.min(entries.size()-1 - index, length);
super.insertIndexInterval(index, length, before);
}
}
@Override
public void moveLeadSelectionIndex(int leadIndex) {
if (entries.isEmpty()) return;
leadIndex = Math.max(0, leadIndex);
leadIndex = Math.min(entries.size() - 1, leadIndex);
super.moveLeadSelectionIndex(leadIndex);
}
@Override
public void removeIndexInterval(int index0, int index1) {
if (entries.isEmpty()) return;
index0 = Math.max(0, index0);
index0 = Math.min(entries.size() - 1, index0);
index1 = Math.max(0, index1);
index1 = Math.min(entries.size() - 1, index1);
super.removeIndexInterval(index0, index1);
}
@Override
public void removeSelectionInterval(int index0, int index1) {
if (entries.isEmpty()) return;
index0 = Math.max(0, index0);
index0 = Math.min(entries.size() - 1, index0);
index1 = Math.max(0, index1);
index1 = Math.min(entries.size() - 1, index1);
super.removeSelectionInterval(index0, index1);
}
@Override
public void setAnchorSelectionIndex(int anchorIndex) {
if (entries.isEmpty()) return;
anchorIndex = Math.min(entries.size() - 1, anchorIndex);
super.setAnchorSelectionIndex(anchorIndex);
}
@Override
public void setLeadSelectionIndex(int leadIndex) {
if (entries.isEmpty()) return;
leadIndex = Math.min(entries.size() - 1, leadIndex);
super.setLeadSelectionIndex(leadIndex);
}
@Override
public void setSelectionInterval(int index0, int index1) {
if (entries.isEmpty()) return;
index0 = Math.max(0, index0);
index0 = Math.min(entries.size() - 1, index0);
index1 = Math.max(0, index1);
index1 = Math.min(entries.size() - 1, index1);
super.setSelectionInterval(index0, index1);
}
}
public ComparePairListModel getComparePairListModel() {
return this.comparePairListModel;
}
public class ComparePairListModel extends AbstractListModel<ComparePairType> implements ComboBoxModel<ComparePairType> {
private int selectedIdx;
private final List<ComparePairType> compareModes;
/**
* Constructs a new {@code ComparePairListModel}.
*/
public ComparePairListModel() {
this.compareModes = new ArrayList<>();
compareModes.add(MY_WITH_THEIR);
compareModes.add(MY_WITH_MERGED);
compareModes.add(THEIR_WITH_MERGED);
selectedIdx = 0;
}
@Override
public ComparePairType getElementAt(int index) {
if (index < compareModes.size())
return compareModes.get(index);
throw new IllegalArgumentException(tr("Unexpected value of parameter ''index''. Got {0}.", index));
}
@Override
public int getSize() {
return compareModes.size();
}
@Override
public Object getSelectedItem() {
return compareModes.get(selectedIdx);
}
@Override
public void setSelectedItem(Object anItem) {
int i = compareModes.indexOf(anItem);
if (i < 0)
throw new IllegalStateException(tr("Item {0} not found in list.", anItem));
selectedIdx = i;
fireModelDataChanged();
}
public ComparePairType getSelectedComparePair() {
return compareModes.get(selectedIdx);
}
}
/**
* Builds the command to resolve conflicts in the list.
*
* @param conflict the conflict data set
* @return the command
* @throws IllegalStateException if the merge is not yet frozen
*/
public abstract C buildResolveCommand(Conflict<? extends OsmPrimitive> conflict);
}