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