// 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.HashMap; import java.util.Observable; import java.util.logging.Logger; import javax.swing.AbstractListModel; import javax.swing.ComboBoxModel; import javax.swing.DefaultListSelectionModel; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableModel; /** * 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: *
    *
  1. the list of my entries
  2. *
  3. the list of their entries
  4. *
  5. the list of merged entries
  6. *
* * A ListMergeModel is a factory for three {@see TableModel}s and three {@see ListSelectionModel}s: *
    *
  1. the table model and the list selection for for a {@see JTable} which shows my entries. * See {@see #getMyTableModel()}
  2. and {@see ListMergeModel#getMySelectionModel()} *
  3. dito for their entries and merged entries
  4. *
* * A ListMergeModel can be ''frozen''. If it's frozen, it doesn't accept additional merge * decisions. {@see PropertyChangeListener}s can register for property value changes of * {@see #PROP_FROZEN}. * * ListMergeModel is an abstract class. Three methods have to be implemented by subclasses: * * A ListMergeModel is used in combination with a {@see ListMerger}. * * @param the type of the list entries * @see ListMerger */ public abstract class ListMergeModel extends Observable { //private static final Logger logger = Logger.getLogger(ListMergeModel.class.getName()); public static final String FROZEN_PROP = ListMergeModel.class.getName() + ".frozen"; protected HashMap> entries; protected DefaultTableModel myEntriesTableModel; protected DefaultTableModel theirEntriesTableModel; protected DefaultTableModel mergedEntriesTableModel; protected EntriesSelectionModel myEntriesSelectionModel; protected EntriesSelectionModel theirEntriesSelectionModel; protected EntriesSelectionModel mergedEntriesSelectionModel; private final ArrayList listeners; private boolean isFrozen = false; private final ComparePairListModel comparePairListModel; /** * 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 {@see 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); 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 ArrayList getMergedEntries() { return entries.get(MERGED_ENTRIES); } protected ArrayList getMyEntries() { return entries.get(MY_ENTRIES); } protected ArrayList getTheirEntries() { return entries.get(THEIR_ENTRIES); } public int getMyEntriesSize() { return getMyEntries().size(); } public int getMergedEntriesSize() { return getMergedEntries().size(); } public int getTheirEntriesSize() { return getTheirEntries().size(); } public ListMergeModel() { entries = new HashMap>(); for (ListRole role : ListRole.values()) { entries.put(role, new ArrayList()); } 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 ArrayList(); 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); for (PropertyChangeListener listener: listeners) { listener.propertyChange(evt); } } } public void setFrozen(boolean isFrozen) { boolean oldValue = this.isFrozen; this.isFrozen = isFrozen; fireFrozenChanged(oldValue, this.isFrozen); } public boolean isFrozen() { return isFrozen; } public TableModel getMyTableModel() { return myEntriesTableModel; } public TableModel getTheirTableModel() { return theirEntriesTableModel; } public TableModel 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(); setChanged(); notifyObservers(); } protected void copyToTop(ListRole role, int []rows) { if (rows == null || rows.length == 0) return; for (int i = rows.length - 1; i >= 0; i--) { int row = rows[i]; T n = entries.get(role).get(row); entries.get(MERGED_ENTRIES).add(0, cloneEntryForMergedList(n)); } fireModelDataChanged(); 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) { if (rows == null || rows.length == 0) return; ArrayList mergedEntries = getMergedEntries(); for (int row : rows) { T n = entries.get(source).get(row); mergedEntries.add(cloneEntryForMergedList(n)); } fireModelDataChanged(); mergedEntriesSelectionModel.setSelectionInterval(mergedEntries.size()-rows.length, mergedEntries.size() -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); } /** * Copies the nodes given by indices in rows from the list of nodes source 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 * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes * */ protected void copyBeforeCurrent(ListRole source, int [] rows, int current) { if (rows == null || rows.length == 0) return; ArrayList mergedEntries = getMergedEntries(); if (current < 0 || current >= mergedEntries.size()) throw new IllegalArgumentException(tr("Parameter current out of range. Got {0}.", current)); for (int i=rows.length -1; i>=0; i--) { int row = rows[i]; T n = entries.get(source).get(row); mergedEntries.add(current, cloneEntryForMergedList(n)); } fireModelDataChanged(); 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 * @exception IllegalArgumentException thrown, 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 * @exception IllegalArgumentException thrown, 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 source 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 * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes * */ protected void copyAfterCurrent(ListRole source, int [] rows, int current) { if (rows == null || rows.length == 0) return; ArrayList mergedEntries = getMergedEntries(); if (current < 0 || current >= mergedEntries.size()) throw new IllegalArgumentException(tr("Parameter current out of range. Got {0}.", current)); if (current == mergedEntries.size() -1) { copyToEnd(source, rows); } else { for (int i=rows.length -1; i>=0; i--) { int row = rows[i]; T n = entries.get(source).get(row); mergedEntries.add(current+1, cloneEntryForMergedList(n)); } } fireModelDataChanged(); mergedEntriesSelectionModel.setSelectionInterval(current+1, current + rows.length-1); notifyObservers(); } /** * 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 * @exception IllegalArgumentException thrown, 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 * @exception IllegalArgumentException thrown, 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; ArrayList mergedEntries = getMergedEntries(); for (int row: rows) { T n = mergedEntries.get(row); mergedEntries.remove(row); mergedEntries.add(row -1, n); } fireModelDataChanged(); notifyObservers(); mergedEntriesSelectionModel.clearSelection(); for (int row: rows) { mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1); } } /** * 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; ArrayList 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(); notifyObservers(); mergedEntriesSelectionModel.clearSelection(); for (int row: rows) { mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1); } } /** * 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; ArrayList mergedEntries = getMergedEntries(); for (int i = rows.length-1; i>=0;i--) { mergedEntries.remove(rows[i]); } fireModelDataChanged(); notifyObservers(); 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 (getMyEntries().size() != getTheirEntries().size()) return false; for (int i=0; i < getMyEntries().size(); i++) { if (! isEqualEntry(getMyEntries().get(i), getTheirEntries().get(i))) return false; } return true; } /** * This an adapter between a {@see JTable} and one of the three entry lists * in the role {@see ListRole} managed by the {@see ListMergeModel}. * * From the point of view of the {@see JTable} it is a {@see TableModel}. * * @param * @see ListMergeModel#getMyTableModel() * @see ListMergeModel#getTheirTableModel() * @see ListMergeModel#getMergedTableModel() */ public class EntriesTableModel extends DefaultTableModel { 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()); count = Math.max(count, getTheirEntries().size()); return count; } @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) { ListMergeModel.this.setValueAt(this, value,row,col); } public ListMergeModel getListMergeModel() { return ListMergeModel.this; } /** * replies true if the {@see ListRole} of this {@see EntriesTableModel} * participates in the current {@see ComparePairType} * * @return true, if the if the {@see ListRole} of this {@see EntriesTableModel} * participates in the current {@see ComparePairType} * * @see ComparePairListModel#getSelectedComparePair() */ public boolean isParticipatingInCurrentComparePair() { return getComparePairListModel() .getSelectedComparePair() .isParticipatingIn(role); } /** * replies true if the entry at row is equal to the entry at the * same position in the opposite list of the current {@see ComparePairType}. * * @param row the row number * @return true if the entry at row is equal to the entry at the * same position in the opposite list of the current {@see ComparePairType} * @exception IllegalStateException thrown, if this model is not participating in the * current {@see 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 {@see ComparePairType}. * * @param row the current row * @return true if the entry at the current position is present in the opposite list * of the current {@see ComparePairType}. * @exception IllegalStateException thrown, if this model is not participating in the * current {@see 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); for (T e2: getOppositeEntries()) { if (isEqualEntry(e1, e2)) return true; } return false; } protected ArrayList getEntries() { return entries.get(role); } /** * replies the opposite list of entries with respect to the current {@see ComparePairType} * * @return the opposite list of entries */ protected ArrayList getOppositeEntries() { ListRole opposite = getComparePairListModel().getSelectedComparePair().getOppositeRole(role); return entries.get(opposite); } public ListRole getRole() { return role; } } /** * This is the selection model to be used in a {@see JTable} which displays * an entry list managed by {@see ListMergeModel}. * * The model ensures that only rows displaying an entry in the entry list * can be selected. "Empty" rows can't be selected. * * @see ListMergeModel#getMySelectionModel() * @see ListMergeModel#getMergedSelectionModel() * @see ListMergeModel#getTheirSelectionModel() * */ protected class EntriesSelectionModel extends DefaultListSelectionModel { private final ArrayList entries; public EntriesSelectionModel(ArrayList 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 implements ComboBoxModel { private int selectedIdx; private final ArrayList compareModes; public ComparePairListModel() { this.compareModes = new ArrayList(); compareModes.add(MY_WITH_THEIR); compareModes.add(MY_WITH_MERGED); compareModes.add(THEIR_WITH_MERGED); selectedIdx = 0; } public Object getElementAt(int index) { if (index < compareModes.size()) return compareModes.get(index); throw new IllegalArgumentException(tr("Unexpected value of parameter ''index''. Got {0}.", index)); } public int getSize() { return compareModes.size(); } public Object getSelectedItem() { return compareModes.get(selectedIdx); } 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); } } }