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.GridBagConstraints; 008import java.awt.GridBagLayout; 009import java.awt.Insets; 010import java.awt.event.ActionEvent; 011import java.awt.event.AdjustmentEvent; 012import java.awt.event.AdjustmentListener; 013import java.awt.event.MouseAdapter; 014import java.awt.event.MouseEvent; 015import java.util.HashSet; 016import java.util.Set; 017 018import javax.swing.AbstractAction; 019import javax.swing.Action; 020import javax.swing.ImageIcon; 021import javax.swing.JButton; 022import javax.swing.JLabel; 023import javax.swing.JPanel; 024import javax.swing.JScrollPane; 025import javax.swing.JTable; 026import javax.swing.event.ListSelectionEvent; 027import javax.swing.event.ListSelectionListener; 028 029import org.openstreetmap.josm.data.conflict.Conflict; 030import org.openstreetmap.josm.data.osm.OsmPrimitive; 031import org.openstreetmap.josm.gui.conflict.pair.IConflictResolver; 032import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType; 033import org.openstreetmap.josm.gui.tagging.TagTableColumnModelBuilder; 034import org.openstreetmap.josm.tools.ImageProvider; 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 JPanel implements IConflictResolver { 041 042 private JTable mineTable; 043 private JTable mergedTable; 044 private JTable theirTable; 045 private final TagMergeModel model; 046 private final String[] keyvalue; 047 private transient AdjustmentSynchronizer adjustmentSynchronizer; 048 049 /** 050 * Constructs a new {@code TagMerger}. 051 */ 052 public TagMerger() { 053 model = new TagMergeModel(); 054 keyvalue = new String[]{tr("Key"), tr("Value")}; 055 build(); 056 } 057 058 /** 059 * embeds table in a new {@link JScrollPane} and returns th scroll pane 060 * 061 * @param table the table 062 * @return the scroll pane embedding the table 063 */ 064 protected JScrollPane embeddInScrollPane(JTable table) { 065 JScrollPane pane = new JScrollPane(table); 066 adjustmentSynchronizer.synchronizeAdjustment(pane.getVerticalScrollBar()); 067 return pane; 068 } 069 070 /** 071 * builds the table for my tag set (table already embedded in a scroll pane) 072 * 073 * @return the table (embedded in a scroll pane) 074 */ 075 protected JScrollPane buildMineTagTable() { 076 mineTable = new JTable(model, new TagTableColumnModelBuilder(new MineTableCellRenderer(), keyvalue).build()); 077 mineTable.setName("table.my"); 078 return embeddInScrollPane(mineTable); 079 } 080 081 /** 082 * builds the table for their tag set (table already embedded in a scroll pane) 083 * 084 * @return the table (embedded in a scroll pane) 085 */ 086 protected JScrollPane buildTheirTable() { 087 theirTable = new JTable(model, new TagTableColumnModelBuilder(new TheirTableCellRenderer(), keyvalue).build()); 088 theirTable.setName("table.their"); 089 return embeddInScrollPane(theirTable); 090 } 091 092 /** 093 * builds the table for the merged tag set (table already embedded in a scroll pane) 094 * 095 * @return the table (embedded in a scroll pane) 096 */ 097 098 protected JScrollPane buildMergedTable() { 099 mergedTable = new JTable(model, new TagTableColumnModelBuilder(new MergedTableCellRenderer(), keyvalue).build()); 100 mergedTable.setName("table.merged"); 101 return embeddInScrollPane(mergedTable); 102 } 103 104 /** 105 * build the user interface 106 */ 107 protected final void build() { 108 GridBagConstraints gc = new GridBagConstraints(); 109 setLayout(new GridBagLayout()); 110 111 adjustmentSynchronizer = new AdjustmentSynchronizer(); 112 113 gc.gridx = 0; 114 gc.gridy = 0; 115 gc.gridwidth = 1; 116 gc.gridheight = 1; 117 gc.fill = GridBagConstraints.NONE; 118 gc.anchor = GridBagConstraints.CENTER; 119 gc.weightx = 0.0; 120 gc.weighty = 0.0; 121 gc.insets = new Insets(10, 0, 10, 0); 122 JLabel lblMy = new JLabel(tr("My version (local dataset)")); 123 add(lblMy, gc); 124 125 gc.gridx = 2; 126 gc.gridy = 0; 127 gc.gridwidth = 1; 128 gc.gridheight = 1; 129 gc.fill = GridBagConstraints.NONE; 130 gc.anchor = GridBagConstraints.CENTER; 131 gc.weightx = 0.0; 132 gc.weighty = 0.0; 133 JLabel lblMerge = new JLabel(tr("Merged version")); 134 add(lblMerge, gc); 135 136 gc.gridx = 4; 137 gc.gridy = 0; 138 gc.gridwidth = 1; 139 gc.gridheight = 1; 140 gc.fill = GridBagConstraints.NONE; 141 gc.anchor = GridBagConstraints.CENTER; 142 gc.weightx = 0.0; 143 gc.weighty = 0.0; 144 gc.insets = new Insets(0, 0, 0, 0); 145 JLabel lblTheir = new JLabel(tr("Their version (server dataset)")); 146 add(lblTheir, gc); 147 148 gc.gridx = 0; 149 gc.gridy = 1; 150 gc.gridwidth = 1; 151 gc.gridheight = 1; 152 gc.fill = GridBagConstraints.BOTH; 153 gc.anchor = GridBagConstraints.FIRST_LINE_START; 154 gc.weightx = 0.3; 155 gc.weighty = 1.0; 156 JScrollPane tabMy = buildMineTagTable(); 157 lblMy.setLabelFor(tabMy); 158 add(tabMy, gc); 159 160 gc.gridx = 1; 161 gc.gridy = 1; 162 gc.gridwidth = 1; 163 gc.gridheight = 1; 164 gc.fill = GridBagConstraints.NONE; 165 gc.anchor = GridBagConstraints.CENTER; 166 gc.weightx = 0.0; 167 gc.weighty = 0.0; 168 KeepMineAction keepMineAction = new KeepMineAction(); 169 mineTable.getSelectionModel().addListSelectionListener(keepMineAction); 170 JButton btnKeepMine = new JButton(keepMineAction); 171 btnKeepMine.setName("button.keepmine"); 172 add(btnKeepMine, gc); 173 174 gc.gridx = 2; 175 gc.gridy = 1; 176 gc.gridwidth = 1; 177 gc.gridheight = 1; 178 gc.fill = GridBagConstraints.BOTH; 179 gc.anchor = GridBagConstraints.FIRST_LINE_START; 180 gc.weightx = 0.3; 181 gc.weighty = 1.0; 182 JScrollPane tabMerge = buildMergedTable(); 183 lblMerge.setLabelFor(tabMerge); 184 add(tabMerge, gc); 185 186 gc.gridx = 3; 187 gc.gridy = 1; 188 gc.gridwidth = 1; 189 gc.gridheight = 1; 190 gc.fill = GridBagConstraints.NONE; 191 gc.anchor = GridBagConstraints.CENTER; 192 gc.weightx = 0.0; 193 gc.weighty = 0.0; 194 KeepTheirAction keepTheirAction = new KeepTheirAction(); 195 JButton btnKeepTheir = new JButton(keepTheirAction); 196 btnKeepTheir.setName("button.keeptheir"); 197 add(btnKeepTheir, gc); 198 199 gc.gridx = 4; 200 gc.gridy = 1; 201 gc.gridwidth = 1; 202 gc.gridheight = 1; 203 gc.fill = GridBagConstraints.BOTH; 204 gc.anchor = GridBagConstraints.FIRST_LINE_START; 205 gc.weightx = 0.3; 206 gc.weighty = 1.0; 207 JScrollPane tabTheir = buildTheirTable(); 208 lblTheir.setLabelFor(tabTheir); 209 add(tabTheir, gc); 210 theirTable.getSelectionModel().addListSelectionListener(keepTheirAction); 211 212 DoubleClickAdapter dblClickAdapter = new DoubleClickAdapter(); 213 mineTable.addMouseListener(dblClickAdapter); 214 theirTable.addMouseListener(dblClickAdapter); 215 216 gc.gridx = 2; 217 gc.gridy = 2; 218 gc.gridwidth = 1; 219 gc.gridheight = 1; 220 gc.fill = GridBagConstraints.NONE; 221 gc.anchor = GridBagConstraints.CENTER; 222 gc.weightx = 0.0; 223 gc.weighty = 0.0; 224 UndecideAction undecidedAction = new UndecideAction(); 225 mergedTable.getSelectionModel().addListSelectionListener(undecidedAction); 226 JButton btnUndecide = new JButton(undecidedAction); 227 btnUndecide.setName("button.undecide"); 228 add(btnUndecide, gc); 229 } 230 231 /** 232 * replies the model used by this tag merger 233 * 234 * @return the model 235 */ 236 public TagMergeModel getModel() { 237 return model; 238 } 239 240 private void selectNextConflict(int[] rows) { 241 int max = rows[0]; 242 for (int row: rows) { 243 if (row > max) { 244 max = row; 245 } 246 } 247 int index = model.getFirstUndecided(max+1); 248 if (index == -1) { 249 index = model.getFirstUndecided(0); 250 } 251 mineTable.getSelectionModel().setSelectionInterval(index, index); 252 theirTable.getSelectionModel().setSelectionInterval(index, index); 253 } 254 255 /** 256 * Keeps the currently selected tags in my table in the list of merged tags. 257 * 258 */ 259 class KeepMineAction extends AbstractAction implements ListSelectionListener { 260 KeepMineAction() { 261 ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagkeepmine"); 262 if (icon != null) { 263 putValue(Action.SMALL_ICON, icon); 264 putValue(Action.NAME, ""); 265 } else { 266 putValue(Action.NAME, ">"); 267 } 268 putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the local dataset")); 269 setEnabled(false); 270 } 271 272 @Override 273 public void actionPerformed(ActionEvent arg0) { 274 int[] rows = mineTable.getSelectedRows(); 275 if (rows == null || rows.length == 0) 276 return; 277 model.decide(rows, MergeDecisionType.KEEP_MINE); 278 selectNextConflict(rows); 279 } 280 281 @Override 282 public void valueChanged(ListSelectionEvent e) { 283 setEnabled(mineTable.getSelectedRowCount() > 0); 284 } 285 } 286 287 /** 288 * Keeps the currently selected tags in their table in the list of merged tags. 289 * 290 */ 291 class KeepTheirAction extends AbstractAction implements ListSelectionListener { 292 KeepTheirAction() { 293 ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagkeeptheir"); 294 if (icon != null) { 295 putValue(Action.SMALL_ICON, icon); 296 putValue(Action.NAME, ""); 297 } else { 298 putValue(Action.NAME, ">"); 299 } 300 putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the server dataset")); 301 setEnabled(false); 302 } 303 304 @Override 305 public void actionPerformed(ActionEvent arg0) { 306 int[] rows = theirTable.getSelectedRows(); 307 if (rows == null || rows.length == 0) 308 return; 309 model.decide(rows, MergeDecisionType.KEEP_THEIR); 310 selectNextConflict(rows); 311 } 312 313 @Override 314 public void valueChanged(ListSelectionEvent e) { 315 setEnabled(theirTable.getSelectedRowCount() > 0); 316 } 317 } 318 319 /** 320 * Synchronizes scrollbar adjustments between a set of 321 * {@link Adjustable}s. Whenever the adjustment of one of 322 * the registerd Adjustables is updated the adjustment of 323 * the other registered Adjustables is adjusted too. 324 * 325 */ 326 static class AdjustmentSynchronizer implements AdjustmentListener { 327 private final Set<Adjustable> synchronizedAdjustables; 328 329 AdjustmentSynchronizer() { 330 synchronizedAdjustables = new HashSet<>(); 331 } 332 333 public void synchronizeAdjustment(Adjustable adjustable) { 334 if (adjustable == null) 335 return; 336 if (synchronizedAdjustables.contains(adjustable)) 337 return; 338 synchronizedAdjustables.add(adjustable); 339 adjustable.addAdjustmentListener(this); 340 } 341 342 @Override 343 public void adjustmentValueChanged(AdjustmentEvent e) { 344 for (Adjustable a : synchronizedAdjustables) { 345 if (a != e.getAdjustable()) { 346 a.setValue(e.getValue()); 347 } 348 } 349 } 350 } 351 352 /** 353 * Handler for double clicks on entries in the three tag tables. 354 * 355 */ 356 class DoubleClickAdapter extends MouseAdapter { 357 358 @Override 359 public void mouseClicked(MouseEvent e) { 360 if (e.getClickCount() != 2) 361 return; 362 JTable table = null; 363 MergeDecisionType mergeDecision; 364 365 if (e.getSource() == mineTable) { 366 table = mineTable; 367 mergeDecision = MergeDecisionType.KEEP_MINE; 368 } else if (e.getSource() == theirTable) { 369 table = theirTable; 370 mergeDecision = MergeDecisionType.KEEP_THEIR; 371 } else if (e.getSource() == mergedTable) { 372 table = mergedTable; 373 mergeDecision = MergeDecisionType.UNDECIDED; 374 } else 375 // double click in another component; shouldn't happen, 376 // but just in case 377 return; 378 int row = table.rowAtPoint(e.getPoint()); 379 model.decide(row, mergeDecision); 380 } 381 } 382 383 /** 384 * Sets the currently selected tags in the table of merged tags to state 385 * {@link MergeDecisionType#UNDECIDED} 386 * 387 */ 388 class UndecideAction extends AbstractAction implements ListSelectionListener { 389 390 UndecideAction() { 391 ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagundecide"); 392 if (icon != null) { 393 putValue(Action.SMALL_ICON, icon); 394 putValue(Action.NAME, ""); 395 } else { 396 putValue(Action.NAME, tr("Undecide")); 397 } 398 putValue(SHORT_DESCRIPTION, tr("Mark the selected tags as undecided")); 399 setEnabled(false); 400 } 401 402 @Override 403 public void actionPerformed(ActionEvent arg0) { 404 int[] rows = mergedTable.getSelectedRows(); 405 if (rows == null || rows.length == 0) 406 return; 407 model.decide(rows, MergeDecisionType.UNDECIDED); 408 } 409 410 @Override 411 public void valueChanged(ListSelectionEvent e) { 412 setEnabled(mergedTable.getSelectedRowCount() > 0); 413 } 414 } 415 416 @Override 417 public void deletePrimitive(boolean deleted) { 418 // Use my entries, as it doesn't really matter 419 MergeDecisionType decision = deleted ? MergeDecisionType.KEEP_MINE : MergeDecisionType.UNDECIDED; 420 for (int i = 0; i < model.getRowCount(); i++) { 421 model.decide(i, decision); 422 } 423 } 424 425 @Override 426 public void populate(Conflict<? extends OsmPrimitive> conflict) { 427 model.populate(conflict.getMy(), conflict.getTheir()); 428 for (JTable table : new JTable[]{mineTable, theirTable}) { 429 int index = table.getRowCount() > 0 ? 0 : -1; 430 table.getSelectionModel().setSelectionInterval(index, index); 431 } 432 } 433 434 @Override 435 public void decideRemaining(MergeDecisionType decision) { 436 model.decideRemaining(decision); 437 } 438}