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