// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.conflict.pair.tags;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Adjustable;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashSet;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.openstreetmap.josm.data.conflict.Conflict;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.gui.conflict.pair.IConflictResolver;
import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
import org.openstreetmap.josm.gui.tagging.TagTableColumnModelBuilder;
import org.openstreetmap.josm.tools.ImageProvider;
/**
* UI component for resolving conflicts in the tag sets of two {@link OsmPrimitive}s.
* @since 1622
*/
public class TagMerger extends JPanel implements IConflictResolver {
private JTable mineTable;
private JTable mergedTable;
private JTable theirTable;
private final TagMergeModel model;
private final String[] keyvalue;
private transient AdjustmentSynchronizer adjustmentSynchronizer;
/**
* Constructs a new {@code TagMerger}.
*/
public TagMerger() {
model = new TagMergeModel();
keyvalue = new String[]{tr("Key"), tr("Value")};
build();
}
/**
* embeds table in a new {@link JScrollPane} and returns th scroll pane
*
* @param table the table
* @return the scroll pane embedding the table
*/
protected JScrollPane embeddInScrollPane(JTable table) {
JScrollPane pane = new JScrollPane(table);
adjustmentSynchronizer.synchronizeAdjustment(pane.getVerticalScrollBar());
return pane;
}
/**
* builds the table for my tag set (table already embedded in a scroll pane)
*
* @return the table (embedded in a scroll pane)
*/
protected JScrollPane buildMineTagTable() {
mineTable = new JTable(model, new TagTableColumnModelBuilder(new MineTableCellRenderer(), keyvalue).build());
mineTable.setName("table.my");
return embeddInScrollPane(mineTable);
}
/**
* builds the table for their tag set (table already embedded in a scroll pane)
*
* @return the table (embedded in a scroll pane)
*/
protected JScrollPane buildTheirTable() {
theirTable = new JTable(model, new TagTableColumnModelBuilder(new TheirTableCellRenderer(), keyvalue).build());
theirTable.setName("table.their");
return embeddInScrollPane(theirTable);
}
/**
* builds the table for the merged tag set (table already embedded in a scroll pane)
*
* @return the table (embedded in a scroll pane)
*/
protected JScrollPane buildMergedTable() {
mergedTable = new JTable(model, new TagTableColumnModelBuilder(new MergedTableCellRenderer(), keyvalue).build());
mergedTable.setName("table.merged");
return embeddInScrollPane(mergedTable);
}
/**
* build the user interface
*/
protected final void build() {
GridBagConstraints gc = new GridBagConstraints();
setLayout(new GridBagLayout());
adjustmentSynchronizer = new AdjustmentSynchronizer();
gc.gridx = 0;
gc.gridy = 0;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.NONE;
gc.anchor = GridBagConstraints.CENTER;
gc.weightx = 0.0;
gc.weighty = 0.0;
gc.insets = new Insets(10, 0, 10, 0);
JLabel lblMy = new JLabel(tr("My version (local dataset)"));
add(lblMy, gc);
gc.gridx = 2;
gc.gridy = 0;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.NONE;
gc.anchor = GridBagConstraints.CENTER;
gc.weightx = 0.0;
gc.weighty = 0.0;
JLabel lblMerge = new JLabel(tr("Merged version"));
add(lblMerge, gc);
gc.gridx = 4;
gc.gridy = 0;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.NONE;
gc.anchor = GridBagConstraints.CENTER;
gc.weightx = 0.0;
gc.weighty = 0.0;
gc.insets = new Insets(0, 0, 0, 0);
JLabel lblTheir = new JLabel(tr("Their version (server dataset)"));
add(lblTheir, gc);
gc.gridx = 0;
gc.gridy = 1;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.BOTH;
gc.anchor = GridBagConstraints.FIRST_LINE_START;
gc.weightx = 0.3;
gc.weighty = 1.0;
JScrollPane tabMy = buildMineTagTable();
lblMy.setLabelFor(tabMy);
add(tabMy, gc);
gc.gridx = 1;
gc.gridy = 1;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.NONE;
gc.anchor = GridBagConstraints.CENTER;
gc.weightx = 0.0;
gc.weighty = 0.0;
KeepMineAction keepMineAction = new KeepMineAction();
mineTable.getSelectionModel().addListSelectionListener(keepMineAction);
JButton btnKeepMine = new JButton(keepMineAction);
btnKeepMine.setName("button.keepmine");
add(btnKeepMine, gc);
gc.gridx = 2;
gc.gridy = 1;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.BOTH;
gc.anchor = GridBagConstraints.FIRST_LINE_START;
gc.weightx = 0.3;
gc.weighty = 1.0;
JScrollPane tabMerge = buildMergedTable();
lblMerge.setLabelFor(tabMerge);
add(tabMerge, gc);
gc.gridx = 3;
gc.gridy = 1;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.NONE;
gc.anchor = GridBagConstraints.CENTER;
gc.weightx = 0.0;
gc.weighty = 0.0;
KeepTheirAction keepTheirAction = new KeepTheirAction();
JButton btnKeepTheir = new JButton(keepTheirAction);
btnKeepTheir.setName("button.keeptheir");
add(btnKeepTheir, gc);
gc.gridx = 4;
gc.gridy = 1;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.BOTH;
gc.anchor = GridBagConstraints.FIRST_LINE_START;
gc.weightx = 0.3;
gc.weighty = 1.0;
JScrollPane tabTheir = buildTheirTable();
lblTheir.setLabelFor(tabTheir);
add(tabTheir, gc);
theirTable.getSelectionModel().addListSelectionListener(keepTheirAction);
DoubleClickAdapter dblClickAdapter = new DoubleClickAdapter();
mineTable.addMouseListener(dblClickAdapter);
theirTable.addMouseListener(dblClickAdapter);
gc.gridx = 2;
gc.gridy = 2;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.NONE;
gc.anchor = GridBagConstraints.CENTER;
gc.weightx = 0.0;
gc.weighty = 0.0;
UndecideAction undecidedAction = new UndecideAction();
mergedTable.getSelectionModel().addListSelectionListener(undecidedAction);
JButton btnUndecide = new JButton(undecidedAction);
btnUndecide.setName("button.undecide");
add(btnUndecide, gc);
}
/**
* replies the model used by this tag merger
*
* @return the model
*/
public TagMergeModel getModel() {
return model;
}
private void selectNextConflict(int... rows) {
int max = rows[0];
for (int row: rows) {
if (row > max) {
max = row;
}
}
int index = model.getFirstUndecided(max+1);
if (index == -1) {
index = model.getFirstUndecided(0);
}
mineTable.getSelectionModel().setSelectionInterval(index, index);
theirTable.getSelectionModel().setSelectionInterval(index, index);
}
/**
* Keeps the currently selected tags in my table in the list of merged tags.
*
*/
class KeepMineAction extends AbstractAction implements ListSelectionListener {
KeepMineAction() {
ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagkeepmine");
if (icon != null) {
putValue(Action.SMALL_ICON, icon);
putValue(Action.NAME, "");
} else {
putValue(Action.NAME, ">");
}
putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the local dataset"));
setEnabled(false);
}
@Override
public void actionPerformed(ActionEvent arg0) {
int[] rows = mineTable.getSelectedRows();
if (rows.length == 0)
return;
model.decide(rows, MergeDecisionType.KEEP_MINE);
selectNextConflict(rows);
}
@Override
public void valueChanged(ListSelectionEvent e) {
setEnabled(mineTable.getSelectedRowCount() > 0);
}
}
/**
* Keeps the currently selected tags in their table in the list of merged tags.
*
*/
class KeepTheirAction extends AbstractAction implements ListSelectionListener {
KeepTheirAction() {
ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagkeeptheir");
if (icon != null) {
putValue(Action.SMALL_ICON, icon);
putValue(Action.NAME, "");
} else {
putValue(Action.NAME, ">");
}
putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the server dataset"));
setEnabled(false);
}
@Override
public void actionPerformed(ActionEvent arg0) {
int[] rows = theirTable.getSelectedRows();
if (rows.length == 0)
return;
model.decide(rows, MergeDecisionType.KEEP_THEIR);
selectNextConflict(rows);
}
@Override
public void valueChanged(ListSelectionEvent e) {
setEnabled(theirTable.getSelectedRowCount() > 0);
}
}
/**
* Synchronizes scrollbar adjustments between a set of
* {@link Adjustable}s. Whenever the adjustment of one of
* the registerd Adjustables is updated the adjustment of
* the other registered Adjustables is adjusted too.
*
*/
static class AdjustmentSynchronizer implements AdjustmentListener {
private final Set<Adjustable> synchronizedAdjustables;
AdjustmentSynchronizer() {
synchronizedAdjustables = new HashSet<>();
}
public void synchronizeAdjustment(Adjustable adjustable) {
if (adjustable == null)
return;
if (synchronizedAdjustables.contains(adjustable))
return;
synchronizedAdjustables.add(adjustable);
adjustable.addAdjustmentListener(this);
}
@Override
public void adjustmentValueChanged(AdjustmentEvent e) {
for (Adjustable a : synchronizedAdjustables) {
if (a != e.getAdjustable()) {
a.setValue(e.getValue());
}
}
}
}
/**
* Handler for double clicks on entries in the three tag tables.
*
*/
class DoubleClickAdapter extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() != 2)
return;
JTable table;
MergeDecisionType mergeDecision;
if (e.getSource() == mineTable) {
table = mineTable;
mergeDecision = MergeDecisionType.KEEP_MINE;
} else if (e.getSource() == theirTable) {
table = theirTable;
mergeDecision = MergeDecisionType.KEEP_THEIR;
} else if (e.getSource() == mergedTable) {
table = mergedTable;
mergeDecision = MergeDecisionType.UNDECIDED;
} else
// double click in another component; shouldn't happen,
// but just in case
return;
int row = table.rowAtPoint(e.getPoint());
model.decide(row, mergeDecision);
}
}
/**
* Sets the currently selected tags in the table of merged tags to state
* {@link MergeDecisionType#UNDECIDED}
*
*/
class UndecideAction extends AbstractAction implements ListSelectionListener {
UndecideAction() {
ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagundecide");
if (icon != null) {
putValue(Action.SMALL_ICON, icon);
putValue(Action.NAME, "");
} else {
putValue(Action.NAME, tr("Undecide"));
}
putValue(SHORT_DESCRIPTION, tr("Mark the selected tags as undecided"));
setEnabled(false);
}
@Override
public void actionPerformed(ActionEvent arg0) {
int[] rows = mergedTable.getSelectedRows();
if (rows.length == 0)
return;
model.decide(rows, MergeDecisionType.UNDECIDED);
}
@Override
public void valueChanged(ListSelectionEvent e) {
setEnabled(mergedTable.getSelectedRowCount() > 0);
}
}
@Override
public void deletePrimitive(boolean deleted) {
// Use my entries, as it doesn't really matter
MergeDecisionType decision = deleted ? MergeDecisionType.KEEP_MINE : MergeDecisionType.UNDECIDED;
for (int i = 0; i < model.getRowCount(); i++) {
model.decide(i, decision);
}
}
@Override
public void populate(Conflict<? extends OsmPrimitive> conflict) {
model.populate(conflict.getMy(), conflict.getTheir());
for (JTable table : new JTable[]{mineTable, theirTable}) {
int index = table.getRowCount() > 0 ? 0 : -1;
table.getSelectionModel().setSelectionInterval(index, index);
}
}
@Override
public void decideRemaining(MergeDecisionType decision) {
model.decideRemaining(decision);
}
}