001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.conflict.pair.tags; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Adjustable; 007import java.awt.event.ActionEvent; 008import java.awt.event.AdjustmentEvent; 009import java.awt.event.AdjustmentListener; 010import java.awt.event.MouseAdapter; 011import java.awt.event.MouseEvent; 012import java.util.Arrays; 013import java.util.HashSet; 014import java.util.List; 015import java.util.Set; 016 017import javax.swing.AbstractAction; 018import javax.swing.Action; 019import javax.swing.JButton; 020import javax.swing.JComponent; 021import javax.swing.JScrollPane; 022import javax.swing.JTable; 023import javax.swing.event.ListSelectionEvent; 024import javax.swing.event.ListSelectionListener; 025 026import org.openstreetmap.josm.data.conflict.Conflict; 027import org.openstreetmap.josm.data.osm.OsmPrimitive; 028import org.openstreetmap.josm.gui.conflict.pair.AbstractMergePanel; 029import org.openstreetmap.josm.gui.conflict.pair.IConflictResolver; 030import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType; 031import org.openstreetmap.josm.gui.tagging.TagTableColumnModelBuilder; 032import org.openstreetmap.josm.tools.GBC; 033import org.openstreetmap.josm.tools.ImageProvider; 034 035/** 036 * UI component for resolving conflicts in the tag sets of two {@link OsmPrimitive}s. 037 * @since 1622 038 */ 039public class TagMerger extends AbstractMergePanel implements IConflictResolver { 040 private static final String[] KEY_VALUE = {tr("Key"), tr("Value")}; 041 042 private final TagMergeModel model = new TagMergeModel(); 043 044 /** 045 * the table for my tag set 046 */ 047 private final JTable mineTable = generateTable(new MineTableCellRenderer()); 048 /** 049 * the table for the merged tag set 050 */ 051 private final JTable mergedTable = generateTable(new MergedTableCellRenderer()); 052 /** 053 * the table for their tag set 054 */ 055 private final JTable theirTable = generateTable(new TheirTableCellRenderer()); 056 057 /** 058 * Constructs a new {@code TagMerger}. 059 */ 060 public TagMerger() { 061 mineTable.setName("table.my"); 062 theirTable.setName("table.their"); 063 mergedTable.setName("table.merged"); 064 065 DoubleClickAdapter dblClickAdapter = new DoubleClickAdapter(); 066 mineTable.addMouseListener(dblClickAdapter); 067 theirTable.addMouseListener(dblClickAdapter); 068 069 buildRows(); 070 } 071 072 private JTable generateTable(TagMergeTableCellRenderer renderer) { 073 return new JTable(model, new TagTableColumnModelBuilder(renderer, KEY_VALUE).build()); 074 } 075 076 @Override 077 protected List<? extends MergeRow> getRows() { 078 return Arrays.asList(new TitleRow(), new TagTableRow(), new UndecidedRow()); 079 } 080 081 /** 082 * replies the model used by this tag merger 083 * 084 * @return the model 085 */ 086 public TagMergeModel getModel() { 087 return model; 088 } 089 090 private void selectNextConflict(int... rows) { 091 int max = rows[0]; 092 for (int row: rows) { 093 if (row > max) { 094 max = row; 095 } 096 } 097 int index = model.getFirstUndecided(max+1); 098 if (index == -1) { 099 index = model.getFirstUndecided(0); 100 } 101 mineTable.getSelectionModel().setSelectionInterval(index, index); 102 theirTable.getSelectionModel().setSelectionInterval(index, index); 103 } 104 105 private final class TagTableRow extends MergeRow { 106 private final AdjustmentSynchronizer adjustmentSynchronizer = new AdjustmentSynchronizer(); 107 108 /** 109 * embeds table in a new {@link JScrollPane} and returns th scroll pane 110 * 111 * @param table the table 112 * @return the scroll pane embedding the table 113 */ 114 JScrollPane embeddInScrollPane(JTable table) { 115 JScrollPane pane = new JScrollPane(table); 116 adjustmentSynchronizer.synchronizeAdjustment(pane.getVerticalScrollBar()); 117 return pane; 118 } 119 120 @Override 121 protected JComponent mineField() { 122 return embeddInScrollPane(mineTable); 123 } 124 125 @Override 126 protected JComponent mineButton() { 127 KeepMineAction keepMineAction = new KeepMineAction(); 128 mineTable.getSelectionModel().addListSelectionListener(keepMineAction); 129 JButton btnKeepMine = new JButton(keepMineAction); 130 btnKeepMine.setName("button.keepmine"); 131 return btnKeepMine; 132 } 133 134 @Override 135 protected JComponent merged() { 136 return embeddInScrollPane(mergedTable); 137 } 138 139 @Override 140 protected JComponent theirsButton() { 141 KeepTheirAction keepTheirAction = new KeepTheirAction(); 142 theirTable.getSelectionModel().addListSelectionListener(keepTheirAction); 143 JButton btnKeepTheir = new JButton(keepTheirAction); 144 btnKeepTheir.setName("button.keeptheir"); 145 return btnKeepTheir; 146 } 147 148 @Override 149 protected JComponent theirsField() { 150 return embeddInScrollPane(theirTable); 151 } 152 153 @Override 154 protected void addConstraints(GBC constraints, int columnIndex) { 155 super.addConstraints(constraints, columnIndex); 156 // Fill to bottom 157 constraints.weighty = 1; 158 } 159 } 160 161 private final class UndecidedRow extends AbstractUndecideRow { 162 @Override 163 protected AbstractAction createAction() { 164 UndecideAction undecidedAction = new UndecideAction(); 165 mergedTable.getSelectionModel().addListSelectionListener(undecidedAction); 166 return undecidedAction; 167 } 168 169 @Override 170 protected String getButtonName() { 171 return "button.undecide"; 172 } 173 } 174 175 /** 176 * Keeps the currently selected tags in my table in the list of merged tags. 177 * 178 */ 179 class KeepMineAction extends AbstractAction implements ListSelectionListener { 180 KeepMineAction() { 181 new ImageProvider("dialogs/conflict", "tagkeepmine").getResource().attachImageIcon(this, true); 182 putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the local dataset")); 183 setEnabled(false); 184 } 185 186 @Override 187 public void actionPerformed(ActionEvent arg0) { 188 int[] rows = mineTable.getSelectedRows(); 189 if (rows.length == 0) 190 return; 191 model.decide(rows, MergeDecisionType.KEEP_MINE); 192 selectNextConflict(rows); 193 } 194 195 @Override 196 public void valueChanged(ListSelectionEvent e) { 197 setEnabled(mineTable.getSelectedRowCount() > 0); 198 } 199 } 200 201 /** 202 * Keeps the currently selected tags in their table in the list of merged tags. 203 * 204 */ 205 class KeepTheirAction extends AbstractAction implements ListSelectionListener { 206 KeepTheirAction() { 207 new ImageProvider("dialogs/conflict", "tagkeeptheir").getResource().attachImageIcon(this, true); 208 putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the server dataset")); 209 setEnabled(false); 210 } 211 212 @Override 213 public void actionPerformed(ActionEvent arg0) { 214 int[] rows = theirTable.getSelectedRows(); 215 if (rows.length == 0) 216 return; 217 model.decide(rows, MergeDecisionType.KEEP_THEIR); 218 selectNextConflict(rows); 219 } 220 221 @Override 222 public void valueChanged(ListSelectionEvent e) { 223 setEnabled(theirTable.getSelectedRowCount() > 0); 224 } 225 } 226 227 /** 228 * Synchronizes scrollbar adjustments between a set of 229 * {@link Adjustable}s. Whenever the adjustment of one of 230 * the registered Adjustables is updated the adjustment of 231 * the other registered Adjustables is adjusted too. 232 * 233 */ 234 static class AdjustmentSynchronizer implements AdjustmentListener { 235 private final Set<Adjustable> synchronizedAdjustables; 236 237 AdjustmentSynchronizer() { 238 synchronizedAdjustables = new HashSet<>(); 239 } 240 241 public void synchronizeAdjustment(Adjustable adjustable) { 242 if (adjustable == null) 243 return; 244 if (synchronizedAdjustables.contains(adjustable)) 245 return; 246 synchronizedAdjustables.add(adjustable); 247 adjustable.addAdjustmentListener(this); 248 } 249 250 @Override 251 public void adjustmentValueChanged(AdjustmentEvent e) { 252 for (Adjustable a : synchronizedAdjustables) { 253 if (a != e.getAdjustable()) { 254 a.setValue(e.getValue()); 255 } 256 } 257 } 258 } 259 260 /** 261 * Handler for double clicks on entries in the three tag tables. 262 * 263 */ 264 class DoubleClickAdapter extends MouseAdapter { 265 266 @Override 267 public void mouseClicked(MouseEvent e) { 268 if (e.getClickCount() != 2) 269 return; 270 JTable table; 271 MergeDecisionType mergeDecision; 272 273 if (e.getSource() == mineTable) { 274 table = mineTable; 275 mergeDecision = MergeDecisionType.KEEP_MINE; 276 } else if (e.getSource() == theirTable) { 277 table = theirTable; 278 mergeDecision = MergeDecisionType.KEEP_THEIR; 279 } else if (e.getSource() == mergedTable) { 280 table = mergedTable; 281 mergeDecision = MergeDecisionType.UNDECIDED; 282 } else 283 // double click in another component; shouldn't happen, 284 // but just in case 285 return; 286 int row = table.rowAtPoint(e.getPoint()); 287 model.decide(row, mergeDecision); 288 } 289 } 290 291 /** 292 * Sets the currently selected tags in the table of merged tags to state 293 * {@link MergeDecisionType#UNDECIDED} 294 * 295 */ 296 class UndecideAction extends AbstractAction implements ListSelectionListener { 297 298 UndecideAction() { 299 new ImageProvider("dialogs/conflict", "tagundecide").getResource().attachImageIcon(this, true); 300 putValue(SHORT_DESCRIPTION, tr("Mark the selected tags as undecided")); 301 setEnabled(false); 302 } 303 304 @Override 305 public void actionPerformed(ActionEvent arg0) { 306 int[] rows = mergedTable.getSelectedRows(); 307 if (rows.length == 0) 308 return; 309 model.decide(rows, MergeDecisionType.UNDECIDED); 310 } 311 312 @Override 313 public void valueChanged(ListSelectionEvent e) { 314 setEnabled(mergedTable.getSelectedRowCount() > 0); 315 } 316 } 317 318 @Override 319 public void deletePrimitive(boolean deleted) { 320 // Use my entries, as it doesn't really matter 321 MergeDecisionType decision = deleted ? MergeDecisionType.KEEP_MINE : MergeDecisionType.UNDECIDED; 322 for (int i = 0; i < model.getRowCount(); i++) { 323 model.decide(i, decision); 324 } 325 } 326 327 @Override 328 public void populate(Conflict<? extends OsmPrimitive> conflict) { 329 model.populate(conflict.getMy(), conflict.getTheir()); 330 for (JTable table : new JTable[]{mineTable, theirTable}) { 331 int index = table.getRowCount() > 0 ? 0 : -1; 332 table.getSelectionModel().setSelectionInterval(index, index); 333 } 334 } 335 336 @Override 337 public void decideRemaining(MergeDecisionType decision) { 338 model.decideRemaining(decision); 339 } 340}