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