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