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