001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.conflict.pair;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.FlowLayout;
008import java.awt.GridBagConstraints;
009import java.awt.GridBagLayout;
010import java.awt.Insets;
011import java.awt.event.ActionEvent;
012import java.awt.event.ItemEvent;
013import java.awt.event.ItemListener;
014import java.beans.PropertyChangeEvent;
015import java.beans.PropertyChangeListener;
016import java.util.Collection;
017import java.util.List;
018import java.util.Observable;
019import java.util.Observer;
020
021import javax.swing.AbstractAction;
022import javax.swing.Action;
023import javax.swing.ImageIcon;
024import javax.swing.JButton;
025import javax.swing.JCheckBox;
026import javax.swing.JLabel;
027import javax.swing.JPanel;
028import javax.swing.JScrollPane;
029import javax.swing.JTable;
030import javax.swing.JToggleButton;
031import javax.swing.event.ListSelectionEvent;
032import javax.swing.event.ListSelectionListener;
033
034import org.openstreetmap.josm.Main;
035import org.openstreetmap.josm.data.osm.OsmPrimitive;
036import org.openstreetmap.josm.data.osm.PrimitiveId;
037import org.openstreetmap.josm.data.osm.Relation;
038import org.openstreetmap.josm.data.osm.Way;
039import org.openstreetmap.josm.gui.layer.OsmDataLayer;
040import org.openstreetmap.josm.gui.util.AdjustmentSynchronizer;
041import org.openstreetmap.josm.gui.widgets.JosmComboBox;
042import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTable;
043import org.openstreetmap.josm.tools.ImageProvider;
044
045/**
046 * A UI component for resolving conflicts in two lists of entries of type T.
047 *
048 * @param <T>  the type of the entries
049 * @see ListMergeModel
050 */
051public abstract class ListMerger<T extends PrimitiveId> extends JPanel implements PropertyChangeListener, Observer {
052    protected OsmPrimitivesTable myEntriesTable;
053    protected OsmPrimitivesTable mergedEntriesTable;
054    protected OsmPrimitivesTable theirEntriesTable;
055
056    protected ListMergeModel<T> model;
057
058    private CopyStartLeftAction copyStartLeftAction;
059    private CopyBeforeCurrentLeftAction copyBeforeCurrentLeftAction;
060    private CopyAfterCurrentLeftAction copyAfterCurrentLeftAction;
061    private CopyEndLeftAction copyEndLeftAction;
062    private CopyAllLeft copyAllLeft;
063
064    private CopyStartRightAction copyStartRightAction;
065    private CopyBeforeCurrentRightAction copyBeforeCurrentRightAction;
066    private CopyAfterCurrentRightAction copyAfterCurrentRightAction;
067    private CopyEndRightAction copyEndRightAction;
068    private CopyAllRight copyAllRight;
069
070    private MoveUpMergedAction moveUpMergedAction;
071    private MoveDownMergedAction moveDownMergedAction;
072    private RemoveMergedAction removeMergedAction;
073    private FreezeAction freezeAction;
074
075    private AdjustmentSynchronizer adjustmentSynchronizer;
076
077    private  JLabel lblMyVersion;
078    private  JLabel lblMergedVersion;
079    private  JLabel lblTheirVersion;
080
081    private  JLabel lblFrozenState;
082
083    abstract protected JScrollPane buildMyElementsTable();
084    abstract protected JScrollPane buildMergedElementsTable();
085    abstract protected JScrollPane buildTheirElementsTable();
086
087    protected JScrollPane embeddInScrollPane(JTable table) {
088        JScrollPane pane = new JScrollPane(table);
089        pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
090        pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
091        if (adjustmentSynchronizer == null) {
092            adjustmentSynchronizer = new AdjustmentSynchronizer();
093        }
094        return pane;
095    }
096
097    protected void wireActionsToSelectionModels() {
098        myEntriesTable.getSelectionModel().addListSelectionListener(copyStartLeftAction);
099
100        myEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
101        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
102
103        myEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
104        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
105
106        myEntriesTable.getSelectionModel().addListSelectionListener(copyEndLeftAction);
107
108        theirEntriesTable.getSelectionModel().addListSelectionListener(copyStartRightAction);
109
110        theirEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
111        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
112
113        theirEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
114        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
115
116        theirEntriesTable.getSelectionModel().addListSelectionListener(copyEndRightAction);
117
118        mergedEntriesTable.getSelectionModel().addListSelectionListener(moveUpMergedAction);
119        mergedEntriesTable.getSelectionModel().addListSelectionListener(moveDownMergedAction);
120        mergedEntriesTable.getSelectionModel().addListSelectionListener(removeMergedAction);
121
122        model.addObserver(copyAllLeft);
123        model.addObserver(copyAllRight);
124        model.addPropertyChangeListener(copyAllLeft);
125        model.addPropertyChangeListener(copyAllRight);
126    }
127
128    protected JPanel buildLeftButtonPanel() {
129        JPanel pnl = new JPanel();
130        pnl.setLayout(new GridBagLayout());
131        GridBagConstraints gc = new GridBagConstraints();
132
133        gc.gridx = 0;
134        gc.gridy = 0;
135        copyStartLeftAction = new CopyStartLeftAction();
136        JButton btn = new JButton(copyStartLeftAction);
137        btn.setName("button.copystartleft");
138        pnl.add(btn, gc);
139
140        gc.gridx = 0;
141        gc.gridy = 1;
142        copyBeforeCurrentLeftAction = new CopyBeforeCurrentLeftAction();
143        btn = new JButton(copyBeforeCurrentLeftAction);
144        btn.setName("button.copybeforecurrentleft");
145        pnl.add(btn, gc);
146
147        gc.gridx = 0;
148        gc.gridy = 2;
149        copyAfterCurrentLeftAction = new CopyAfterCurrentLeftAction();
150        btn = new JButton(copyAfterCurrentLeftAction);
151        btn.setName("button.copyaftercurrentleft");
152        pnl.add(btn, gc);
153
154        gc.gridx = 0;
155        gc.gridy = 3;
156        copyEndLeftAction = new CopyEndLeftAction();
157        btn = new JButton(copyEndLeftAction);
158        btn.setName("button.copyendleft");
159        pnl.add(btn, gc);
160
161        gc.gridx = 0;
162        gc.gridy = 4;
163        copyAllLeft = new CopyAllLeft();
164        btn = new JButton(copyAllLeft);
165        btn.setName("button.copyallleft");
166        pnl.add(btn, gc);
167
168        return pnl;
169    }
170
171    protected JPanel buildRightButtonPanel() {
172        JPanel pnl = new JPanel();
173        pnl.setLayout(new GridBagLayout());
174        GridBagConstraints gc = new GridBagConstraints();
175
176        gc.gridx = 0;
177        gc.gridy = 0;
178        copyStartRightAction = new CopyStartRightAction();
179        pnl.add(new JButton(copyStartRightAction), gc);
180
181        gc.gridx = 0;
182        gc.gridy = 1;
183        copyBeforeCurrentRightAction = new CopyBeforeCurrentRightAction();
184        pnl.add(new JButton(copyBeforeCurrentRightAction), gc);
185
186        gc.gridx = 0;
187        gc.gridy = 2;
188        copyAfterCurrentRightAction = new CopyAfterCurrentRightAction();
189        pnl.add(new JButton(copyAfterCurrentRightAction), gc);
190
191        gc.gridx = 0;
192        gc.gridy = 3;
193        copyEndRightAction = new CopyEndRightAction();
194        pnl.add(new JButton(copyEndRightAction), gc);
195
196        gc.gridx = 0;
197        gc.gridy = 4;
198        copyAllRight = new CopyAllRight();
199        pnl.add(new JButton(copyAllRight), gc);
200
201        return pnl;
202    }
203
204    protected JPanel buildMergedListControlButtons() {
205        JPanel pnl = new JPanel();
206        pnl.setLayout(new GridBagLayout());
207        GridBagConstraints gc = new GridBagConstraints();
208
209        gc.gridx = 0;
210        gc.gridy = 0;
211        gc.gridwidth = 1;
212        gc.gridheight = 1;
213        gc.fill = GridBagConstraints.HORIZONTAL;
214        gc.anchor = GridBagConstraints.CENTER;
215        gc.weightx = 0.3;
216        gc.weighty = 0.0;
217        moveUpMergedAction = new MoveUpMergedAction();
218        pnl.add(new JButton(moveUpMergedAction), gc);
219
220        gc.gridx = 1;
221        gc.gridy = 0;
222        moveDownMergedAction = new MoveDownMergedAction();
223        pnl.add(new JButton(moveDownMergedAction), gc);
224
225        gc.gridx = 2;
226        gc.gridy = 0;
227        removeMergedAction = new RemoveMergedAction();
228        pnl.add(new JButton(removeMergedAction), gc);
229
230        return pnl;
231    }
232
233    protected JPanel buildAdjustmentLockControlPanel(JCheckBox cb) {
234        JPanel panel = new JPanel();
235        panel.setLayout(new FlowLayout(FlowLayout.RIGHT));
236        panel.add(new JLabel(tr("lock scrolling")));
237        panel.add(cb);
238        return panel;
239    }
240
241    protected JPanel buildComparePairSelectionPanel() {
242        JPanel p = new JPanel();
243        p.setLayout(new FlowLayout(FlowLayout.LEFT));
244        p.add(new JLabel(tr("Compare ")));
245        JosmComboBox cbComparePair = new JosmComboBox(model.getComparePairListModel());
246        cbComparePair.setRenderer(new ComparePairListCellRenderer());
247        p.add(cbComparePair);
248        return p;
249    }
250
251    protected JPanel buildFrozeStateControlPanel() {
252        JPanel p = new JPanel();
253        p.setLayout(new FlowLayout(FlowLayout.LEFT));
254        lblFrozenState = new JLabel();
255        p.add(lblFrozenState);
256        freezeAction = new FreezeAction();
257        JToggleButton btn = new JToggleButton(freezeAction);
258        freezeAction.adapt(btn);
259        btn.setName("button.freeze");
260        p.add(btn);
261
262        return p;
263    }
264
265    protected void build() {
266        setLayout(new GridBagLayout());
267        GridBagConstraints gc = new GridBagConstraints();
268
269        // ------------------
270        gc.gridx = 0;
271        gc.gridy = 0;
272        gc.gridwidth = 1;
273        gc.gridheight = 1;
274        gc.fill = GridBagConstraints.NONE;
275        gc.anchor = GridBagConstraints.CENTER;
276        gc.weightx = 0.0;
277        gc.weighty = 0.0;
278        gc.insets = new Insets(10,0,0,0);
279        lblMyVersion = new JLabel(tr("My version"));
280        lblMyVersion.setToolTipText(tr("List of elements in my dataset, i.e. the local dataset"));
281        add(lblMyVersion, gc);
282
283        gc.gridx = 2;
284        gc.gridy = 0;
285        lblMergedVersion = new JLabel(tr("Merged version"));
286        lblMergedVersion.setToolTipText(tr("List of merged elements. They will replace the list of my elements when the merge decisions are applied."));
287        add(lblMergedVersion, gc);
288
289        gc.gridx = 4;
290        gc.gridy = 0;
291        lblTheirVersion = new JLabel(tr("Their version"));
292        lblTheirVersion.setToolTipText(tr("List of elements in their dataset, i.e. the server dataset"));
293        add(lblTheirVersion, gc);
294
295        // ------------------------------
296        gc.gridx = 0;
297        gc.gridy = 1;
298        gc.gridwidth = 1;
299        gc.gridheight = 1;
300        gc.fill = GridBagConstraints.HORIZONTAL;
301        gc.anchor = GridBagConstraints.FIRST_LINE_START;
302        gc.weightx = 0.33;
303        gc.weighty = 0.0;
304        gc.insets = new Insets(0,0,0,0);
305        JCheckBox cbLockMyScrolling = new JCheckBox();
306        cbLockMyScrolling.setName("checkbox.lockmyscrolling");
307        add(buildAdjustmentLockControlPanel(cbLockMyScrolling), gc);
308
309        gc.gridx = 2;
310        gc.gridy = 1;
311        JCheckBox cbLockMergedScrolling = new JCheckBox();
312        cbLockMergedScrolling.setName("checkbox.lockmergedscrolling");
313        add(buildAdjustmentLockControlPanel(cbLockMergedScrolling), gc);
314
315        gc.gridx = 4;
316        gc.gridy = 1;
317        JCheckBox cbLockTheirScrolling = new JCheckBox();
318        cbLockTheirScrolling.setName("checkbox.locktheirscrolling");
319        add(buildAdjustmentLockControlPanel(cbLockTheirScrolling), gc);
320
321        // --------------------------------
322        gc.gridx = 0;
323        gc.gridy = 2;
324        gc.gridwidth = 1;
325        gc.gridheight = 1;
326        gc.fill = GridBagConstraints.BOTH;
327        gc.anchor = GridBagConstraints.FIRST_LINE_START;
328        gc.weightx = 0.33;
329        gc.weighty = 1.0;
330        gc.insets = new Insets(0,0,0,0);
331        JScrollPane pane = buildMyElementsTable();
332        adjustmentSynchronizer.adapt(cbLockMyScrolling, pane.getVerticalScrollBar());
333        add(pane, gc);
334
335        gc.gridx = 1;
336        gc.gridy = 2;
337        gc.fill = GridBagConstraints.NONE;
338        gc.anchor = GridBagConstraints.CENTER;
339        gc.weightx = 0.0;
340        gc.weighty = 0.0;
341        add(buildLeftButtonPanel(), gc);
342
343        gc.gridx = 2;
344        gc.gridy = 2;
345        gc.fill = GridBagConstraints.BOTH;
346        gc.anchor = GridBagConstraints.FIRST_LINE_START;
347        gc.weightx = 0.33;
348        gc.weighty = 0.0;
349        pane = buildMergedElementsTable();
350        adjustmentSynchronizer.adapt(cbLockMergedScrolling, pane.getVerticalScrollBar());
351        add(pane, gc);
352
353        gc.gridx = 3;
354        gc.gridy = 2;
355        gc.fill = GridBagConstraints.NONE;
356        gc.anchor = GridBagConstraints.CENTER;
357        gc.weightx = 0.0;
358        gc.weighty = 0.0;
359        add(buildRightButtonPanel(), gc);
360
361        gc.gridx = 4;
362        gc.gridy = 2;
363        gc.fill = GridBagConstraints.BOTH;
364        gc.anchor = GridBagConstraints.FIRST_LINE_START;
365        gc.weightx = 0.33;
366        gc.weighty = 0.0;
367        pane = buildTheirElementsTable();
368        adjustmentSynchronizer.adapt(cbLockTheirScrolling, pane.getVerticalScrollBar());
369        add(pane, gc);
370
371        // ----------------------------------
372        gc.gridx = 2;
373        gc.gridy = 3;
374        gc.gridwidth = 1;
375        gc.gridheight = 1;
376        gc.fill = GridBagConstraints.BOTH;
377        gc.anchor = GridBagConstraints.CENTER;
378        gc.weightx = 0.0;
379        gc.weighty = 0.0;
380        add(buildMergedListControlButtons(), gc);
381
382        // -----------------------------------
383        gc.gridx = 0;
384        gc.gridy = 4;
385        gc.gridwidth = 2;
386        gc.gridheight = 1;
387        gc.fill = GridBagConstraints.HORIZONTAL;
388        gc.anchor = GridBagConstraints.LINE_START;
389        gc.weightx = 0.0;
390        gc.weighty = 0.0;
391        add(buildComparePairSelectionPanel(), gc);
392
393        gc.gridx = 2;
394        gc.gridy = 4;
395        gc.gridwidth = 3;
396        gc.gridheight = 1;
397        gc.fill = GridBagConstraints.HORIZONTAL;
398        gc.anchor = GridBagConstraints.LINE_START;
399        gc.weightx = 0.0;
400        gc.weighty = 0.0;
401        add(buildFrozeStateControlPanel(), gc);
402
403        wireActionsToSelectionModels();
404    }
405
406    /**
407     * Constructs a new {@code ListMerger}.
408     * @param model
409     */
410    public ListMerger(ListMergeModel<T> model) {
411        this.model = model;
412        model.addObserver(this);
413        build();
414        model.addPropertyChangeListener(this);
415    }
416
417    /**
418     * Base class of all other Copy* inner classes.
419     */
420    abstract class CopyAction extends AbstractAction implements ListSelectionListener {
421        
422        protected CopyAction(String icon_name, String action_name, String short_description) {
423            ImageIcon icon = ImageProvider.get("dialogs/conflict", icon_name+".png");
424            putValue(Action.SMALL_ICON, icon);
425            if (icon == null) {
426                putValue(Action.NAME, action_name);
427            }
428            putValue(Action.SHORT_DESCRIPTION, short_description);
429            setEnabled(false);
430        }
431    }
432    
433    /**
434     * Action for copying selected nodes in the list of my nodes to the list of merged
435     * nodes. Inserts the nodes at the beginning of the list of merged nodes.
436     */
437    class CopyStartLeftAction extends CopyAction {
438
439        public CopyStartLeftAction() {
440            super("copystartleft", tr("> top"), tr("Copy my selected nodes to the start of the merged node list"));
441        }
442
443        @Override
444        public void actionPerformed(ActionEvent e) {
445            model.copyMyToTop(myEntriesTable.getSelectedRows());
446        }
447
448        @Override
449        public void valueChanged(ListSelectionEvent e) {
450            setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
451        }
452    }
453
454    /**
455     * Action for copying selected nodes in the list of my nodes to the list of merged
456     * nodes. Inserts the nodes at the end of the list of merged nodes.
457     */
458    class CopyEndLeftAction extends CopyAction {
459
460        public CopyEndLeftAction() {
461            super("copyendleft", tr("> bottom"), tr("Copy my selected elements to the end of the list of merged elements."));
462        }
463
464        @Override
465        public void actionPerformed(ActionEvent e) {
466            model.copyMyToEnd(myEntriesTable.getSelectedRows());
467        }
468
469        @Override
470        public void valueChanged(ListSelectionEvent e) {
471            setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
472        }
473    }
474
475    /**
476     * Action for copying selected nodes in the list of my nodes to the list of merged
477     * nodes. Inserts the nodes before the first selected row in the list of merged nodes.
478     */
479    class CopyBeforeCurrentLeftAction extends CopyAction {
480
481        public CopyBeforeCurrentLeftAction() {
482            super("copybeforecurrentleft", tr("> before"), 
483                    tr("Copy my selected elements before the first selected element in the list of merged elements."));
484        }
485
486        @Override
487        public void actionPerformed(ActionEvent e) {
488            int [] mergedRows = mergedEntriesTable.getSelectedRows();
489            if (mergedRows == null || mergedRows.length == 0)
490                return;
491            int [] myRows = myEntriesTable.getSelectedRows();
492            int current = mergedRows[0];
493            model.copyMyBeforeCurrent(myRows, current);
494        }
495
496        @Override
497        public void valueChanged(ListSelectionEvent e) {
498            setEnabled(
499                    !myEntriesTable.getSelectionModel().isSelectionEmpty()
500                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
501            );
502        }
503    }
504
505    /**
506     * Action for copying selected nodes in the list of my nodes to the list of merged
507     * nodes. Inserts the nodes after the first selected row in the list of merged nodes.
508     */
509    class CopyAfterCurrentLeftAction extends CopyAction {
510
511        public CopyAfterCurrentLeftAction() {
512            super("copyaftercurrentleft", tr("> after"), 
513                    tr("Copy my selected elements after the first selected element in the list of merged elements."));
514        }
515
516        @Override
517        public void actionPerformed(ActionEvent e) {
518            int [] mergedRows = mergedEntriesTable.getSelectedRows();
519            if (mergedRows == null || mergedRows.length == 0)
520                return;
521            int [] myRows = myEntriesTable.getSelectedRows();
522            int current = mergedRows[0];
523            model.copyMyAfterCurrent(myRows, current);
524        }
525
526        @Override
527        public void valueChanged(ListSelectionEvent e) {
528            setEnabled(
529                    !myEntriesTable.getSelectionModel().isSelectionEmpty()
530                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
531            );
532        }
533    }
534
535    class CopyStartRightAction extends CopyAction {
536
537        public CopyStartRightAction() {
538            super("copystartright", tr("< top"), tr("Copy their selected element to the start of the list of merged elements."));
539        }
540
541        @Override
542        public void actionPerformed(ActionEvent e) {
543            model.copyTheirToTop(theirEntriesTable.getSelectedRows());
544        }
545
546        @Override
547        public void valueChanged(ListSelectionEvent e) {
548            setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
549        }
550    }
551
552    class CopyEndRightAction extends CopyAction {
553
554        public CopyEndRightAction() {
555            super("copyendright", tr("< bottom"), tr("Copy their selected elements to the end of the list of merged elements."));
556        }
557
558        @Override
559        public void actionPerformed(ActionEvent arg0) {
560            model.copyTheirToEnd(theirEntriesTable.getSelectedRows());
561        }
562
563        @Override
564        public void valueChanged(ListSelectionEvent e) {
565            setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
566        }
567    }
568
569    class CopyBeforeCurrentRightAction extends CopyAction {
570
571        public CopyBeforeCurrentRightAction() {
572            super("copybeforecurrentright", tr("< before"), 
573                    tr("Copy their selected elements before the first selected element in the list of merged elements."));
574        }
575
576        @Override
577        public void actionPerformed(ActionEvent e) {
578            int [] mergedRows = mergedEntriesTable.getSelectedRows();
579            if (mergedRows == null || mergedRows.length == 0)
580                return;
581            int [] myRows = theirEntriesTable.getSelectedRows();
582            int current = mergedRows[0];
583            model.copyTheirBeforeCurrent(myRows, current);
584        }
585
586        @Override
587        public void valueChanged(ListSelectionEvent e) {
588            setEnabled(
589                    !theirEntriesTable.getSelectionModel().isSelectionEmpty()
590                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
591            );
592        }
593    }
594
595    class CopyAfterCurrentRightAction extends CopyAction {
596
597        public CopyAfterCurrentRightAction() {
598            super("copyaftercurrentright", tr("< after"), 
599                    tr("Copy their selected element after the first selected element in the list of merged elements"));
600        }
601
602        @Override
603        public void actionPerformed(ActionEvent e) {
604            int [] mergedRows = mergedEntriesTable.getSelectedRows();
605            if (mergedRows == null || mergedRows.length == 0)
606                return;
607            int [] myRows = theirEntriesTable.getSelectedRows();
608            int current = mergedRows[0];
609            model.copyTheirAfterCurrent(myRows, current);
610        }
611
612        @Override
613        public void valueChanged(ListSelectionEvent e) {
614            setEnabled(
615                    !theirEntriesTable.getSelectionModel().isSelectionEmpty()
616                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
617            );
618        }
619    }
620
621    class CopyAllLeft extends AbstractAction implements Observer, PropertyChangeListener {
622
623        public CopyAllLeft() {
624            ImageIcon icon = ImageProvider.get("dialogs/conflict", "useallleft.png");
625            putValue(Action.SMALL_ICON, icon);
626            putValue(Action.SHORT_DESCRIPTION, tr("Copy all my elements to the target"));
627        }
628
629        @Override
630        public void actionPerformed(ActionEvent arg0) {
631            model.copyAll(ListRole.MY_ENTRIES);
632            model.setFrozen(true);
633        }
634
635        private void updateEnabledState() {
636            setEnabled(model.getMergedEntries().isEmpty() && !model.isFrozen());
637        }
638
639        @Override
640        public void update(Observable o, Object arg) {
641            updateEnabledState();
642        }
643
644        @Override
645        public void propertyChange(PropertyChangeEvent evt) {
646            updateEnabledState();
647        }
648    }
649
650    class CopyAllRight extends AbstractAction implements Observer, PropertyChangeListener {
651
652        public CopyAllRight() {
653            ImageIcon icon = ImageProvider.get("dialogs/conflict", "useallright.png");
654            putValue(Action.SMALL_ICON, icon);
655            putValue(Action.SHORT_DESCRIPTION, tr("Copy all their elements to the target"));
656        }
657
658        @Override
659        public void actionPerformed(ActionEvent arg0) {
660            model.copyAll(ListRole.THEIR_ENTRIES);
661            model.setFrozen(true);
662        }
663
664        private void updateEnabledState() {
665            setEnabled(model.getMergedEntries().isEmpty() && !model.isFrozen());
666        }
667
668        @Override
669        public void update(Observable o, Object arg) {
670            updateEnabledState();
671        }
672
673        @Override
674        public void propertyChange(PropertyChangeEvent evt) {
675            updateEnabledState();
676        }
677    }
678
679    class MoveUpMergedAction extends AbstractAction implements ListSelectionListener {
680
681        public MoveUpMergedAction() {
682            ImageIcon icon = ImageProvider.get("dialogs/conflict", "moveup.png");
683            putValue(Action.SMALL_ICON, icon);
684            if (icon == null) {
685                putValue(Action.NAME, tr("Up"));
686            }
687            putValue(Action.SHORT_DESCRIPTION, tr("Move up the selected entries by one position."));
688            setEnabled(false);
689        }
690
691        @Override
692        public void actionPerformed(ActionEvent arg0) {
693            int [] rows = mergedEntriesTable.getSelectedRows();
694            model.moveUpMerged(rows);
695        }
696
697        @Override
698        public void valueChanged(ListSelectionEvent e) {
699            int [] rows = mergedEntriesTable.getSelectedRows();
700            setEnabled(
701                    rows != null
702                    && rows.length > 0
703                    && rows[0] != 0
704            );
705        }
706    }
707
708    /**
709     * Action for moving the currently selected entries in the list of merged entries
710     * one position down
711     *
712     */
713    class MoveDownMergedAction extends AbstractAction implements ListSelectionListener {
714
715        public MoveDownMergedAction() {
716            ImageIcon icon = ImageProvider.get("dialogs/conflict", "movedown.png");
717            putValue(Action.SMALL_ICON, icon);
718            if (icon == null) {
719                putValue(Action.NAME, tr("Down"));
720            }
721            putValue(Action.SHORT_DESCRIPTION, tr("Move down the selected entries by one position."));
722            setEnabled(false);
723        }
724
725        @Override
726        public void actionPerformed(ActionEvent arg0) {
727            int [] rows = mergedEntriesTable.getSelectedRows();
728            model.moveDownMerged(rows);
729        }
730
731        @Override
732        public void valueChanged(ListSelectionEvent e) {
733            int [] rows = mergedEntriesTable.getSelectedRows();
734            setEnabled(
735                    rows != null
736                    && rows.length > 0
737                    && rows[rows.length -1] != mergedEntriesTable.getRowCount() -1
738            );
739        }
740    }
741
742    /**
743     * Action for removing the selected entries in the list of merged entries
744     * from the list of merged entries.
745     *
746     */
747    class RemoveMergedAction extends AbstractAction implements ListSelectionListener {
748
749        public RemoveMergedAction() {
750            ImageIcon icon = ImageProvider.get("dialogs/conflict", "remove.png");
751            putValue(Action.SMALL_ICON, icon);
752            if (icon == null) {
753                putValue(Action.NAME, tr("Remove"));
754            }
755            putValue(Action.SHORT_DESCRIPTION, tr("Remove the selected entries from the list of merged elements."));
756            setEnabled(false);
757        }
758
759        @Override
760        public void actionPerformed(ActionEvent arg0) {
761            int [] rows = mergedEntriesTable.getSelectedRows();
762            model.removeMerged(rows);
763        }
764
765        @Override
766        public void valueChanged(ListSelectionEvent e) {
767            int [] rows = mergedEntriesTable.getSelectedRows();
768            setEnabled(
769                    rows != null
770                    && rows.length > 0
771            );
772        }
773    }
774
775    static public interface FreezeActionProperties {
776        String PROP_SELECTED = FreezeActionProperties.class.getName() + ".selected";
777    }
778
779    /**
780     * Action for freezing the current state of the list merger
781     *
782     */
783    class FreezeAction extends AbstractAction implements ItemListener, FreezeActionProperties  {
784
785        public FreezeAction() {
786            putValue(Action.NAME, tr("Freeze"));
787            putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements."));
788            putValue(PROP_SELECTED, false);
789            setEnabled(true);
790        }
791
792        @Override
793        public void actionPerformed(ActionEvent arg0) {
794            // do nothing
795        }
796
797        /**
798         * Java 1.5 doesn't known Action.SELECT_KEY. Wires a toggle button to this action
799         * such that the action gets notified about item state changes and the button gets
800         * notified about selection state changes of the action.
801         *
802         * @param btn a toggle button
803         */
804        public void adapt(final JToggleButton btn) {
805            btn.addItemListener(this);
806            addPropertyChangeListener(
807                    new PropertyChangeListener() {
808                        @Override
809                        public void propertyChange(PropertyChangeEvent evt) {
810                            if (evt.getPropertyName().equals(PROP_SELECTED)) {
811                                btn.setSelected((Boolean)evt.getNewValue());
812                            }
813                        }
814                    }
815            );
816        }
817
818        @Override
819        public void itemStateChanged(ItemEvent e) {
820            int state = e.getStateChange();
821            if (state == ItemEvent.SELECTED) {
822                putValue(Action.NAME, tr("Unfreeze"));
823                putValue(Action.SHORT_DESCRIPTION, tr("Unfreeze the list of merged elements and start merging."));
824                model.setFrozen(true);
825            } else if (state == ItemEvent.DESELECTED) {
826                putValue(Action.NAME, tr("Freeze"));
827                putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements."));
828                model.setFrozen(false);
829            }
830            boolean isSelected = (Boolean)getValue(PROP_SELECTED);
831            if (isSelected != (e.getStateChange() == ItemEvent.SELECTED)) {
832                putValue(PROP_SELECTED, e.getStateChange() == ItemEvent.SELECTED);
833            }
834
835        }
836    }
837
838    protected void handlePropertyChangeFrozen(boolean oldValue, boolean newValue) {
839        myEntriesTable.getSelectionModel().clearSelection();
840        myEntriesTable.setEnabled(!newValue);
841        theirEntriesTable.getSelectionModel().clearSelection();
842        theirEntriesTable.setEnabled(!newValue);
843        mergedEntriesTable.getSelectionModel().clearSelection();
844        mergedEntriesTable.setEnabled(!newValue);
845        freezeAction.putValue(FreezeActionProperties.PROP_SELECTED, newValue);
846        if (newValue) {
847            lblFrozenState.setText(
848                    tr("<html>Click <strong>{0}</strong> to start merging my and their entries.</html>",
849                            freezeAction.getValue(Action.NAME))
850            );
851        } else {
852            lblFrozenState.setText(
853                    tr("<html>Click <strong>{0}</strong> to finish merging my and their entries.</html>",
854                            freezeAction.getValue(Action.NAME))
855            );
856        }
857    }
858
859    @Override
860    public void propertyChange(PropertyChangeEvent evt) {
861        if (evt.getPropertyName().equals(ListMergeModel.FROZEN_PROP)) {
862            handlePropertyChangeFrozen((Boolean)evt.getOldValue(), (Boolean)evt.getNewValue());
863        }
864    }
865
866    public ListMergeModel<T> getModel() {
867        return model;
868    }
869
870    @Override
871    public void update(Observable o, Object arg) {
872        lblMyVersion.setText(
873                trn("My version ({0} entry)", "My version ({0} entries)", model.getMyEntriesSize(), model.getMyEntriesSize())
874        );
875        lblMergedVersion.setText(
876                trn("Merged version ({0} entry)", "Merged version ({0} entries)", model.getMergedEntriesSize(), model.getMergedEntriesSize())
877        );
878        lblTheirVersion.setText(
879                trn("Their version ({0} entry)", "Their version ({0} entries)", model.getTheirEntriesSize(), model.getTheirEntriesSize())
880        );
881    }
882
883    public void unlinkAsListener() {
884        myEntriesTable.unlinkAsListener();
885        mergedEntriesTable.unlinkAsListener();
886        theirEntriesTable.unlinkAsListener();
887    }
888
889    protected final <P extends OsmPrimitive> OsmDataLayer findLayerFor(P primitive) {
890        if (primitive != null) {
891            List<OsmDataLayer> layers = Main.map.mapView.getLayersOfType(OsmDataLayer.class);
892            // Find layer with same dataset
893            for (OsmDataLayer layer : layers) {
894                if (layer.data == primitive.getDataSet()) {
895                    return layer;
896                }
897            }
898            // Conflict after merging layers: a dataset could be no more in any layer, try to find another layer with same primitive
899            for (OsmDataLayer layer : layers) {
900                final Collection<? extends OsmPrimitive> collection;
901                if (primitive instanceof Way) {
902                    collection = layer.data.getWays();
903                } else if (primitive instanceof Relation) {
904                    collection = layer.data.getRelations();
905                } else {
906                    collection = layer.data.allPrimitives();
907                }
908                for (OsmPrimitive p : collection) {
909                    if (p.getPrimitiveId().equals(primitive.getPrimitiveId())) {
910                        return layer;
911                    }
912                }
913            }
914        }
915        return null;
916    }
917}