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