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.JButton;
021import javax.swing.JCheckBox;
022import javax.swing.JLabel;
023import javax.swing.JPanel;
024import javax.swing.JScrollPane;
025import javax.swing.JTable;
026import javax.swing.JToggleButton;
027import javax.swing.event.ChangeEvent;
028import javax.swing.event.ChangeListener;
029import javax.swing.event.ListSelectionEvent;
030import javax.swing.event.ListSelectionListener;
031
032import org.openstreetmap.josm.command.conflict.ConflictResolveCommand;
033import org.openstreetmap.josm.data.osm.OsmPrimitive;
034import org.openstreetmap.josm.data.osm.PrimitiveId;
035import org.openstreetmap.josm.data.osm.Relation;
036import org.openstreetmap.josm.data.osm.Way;
037import org.openstreetmap.josm.gui.MainApplication;
038import org.openstreetmap.josm.gui.layer.OsmDataLayer;
039import org.openstreetmap.josm.gui.util.AdjustmentSynchronizer;
040import org.openstreetmap.josm.gui.widgets.JosmComboBox;
041import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTable;
042import org.openstreetmap.josm.tools.ImageProvider;
043import org.openstreetmap.josm.tools.ImageResource;
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            ImageResource icon = new ImageProvider("dialogs/conflict", iconName).getResource();
425            if (icon == null) {
426                putValue(Action.NAME, actionName);
427            } else {
428                icon.attachImageIcon(this, true);
429            }
430            putValue(Action.SHORT_DESCRIPTION, shortDescription);
431            setEnabled(false);
432        }
433    }
434
435    /**
436     * Action for copying selected nodes in the list of my nodes to the list of merged
437     * nodes. Inserts the nodes at the beginning of the list of merged nodes.
438     */
439    class CopyStartLeftAction extends CopyAction {
440
441        CopyStartLeftAction() {
442            super(/* ICON(dialogs/conflict/)*/ "copystartleft", tr("> top"),
443                tr("Copy my selected nodes to the start of the merged node list"));
444        }
445
446        @Override
447        public void actionPerformed(ActionEvent e) {
448            model.copyMyToTop(myEntriesTable.getSelectedRows());
449        }
450
451        @Override
452        public void valueChanged(ListSelectionEvent e) {
453            setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
454        }
455    }
456
457    /**
458     * Action for copying selected nodes in the list of my nodes to the list of merged
459     * nodes. Inserts the nodes at the end of the list of merged nodes.
460     */
461    class CopyEndLeftAction extends CopyAction {
462
463        CopyEndLeftAction() {
464            super(/* ICON(dialogs/conflict/)*/ "copyendleft", tr("> bottom"),
465                tr("Copy my selected elements to the end of the list of merged elements."));
466        }
467
468        @Override
469        public void actionPerformed(ActionEvent e) {
470            model.copyMyToEnd(myEntriesTable.getSelectedRows());
471        }
472
473        @Override
474        public void valueChanged(ListSelectionEvent e) {
475            setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
476        }
477    }
478
479    /**
480     * Action for copying selected nodes in the list of my nodes to the list of merged
481     * nodes. Inserts the nodes before the first selected row in the list of merged nodes.
482     */
483    class CopyBeforeCurrentLeftAction extends CopyAction {
484
485        CopyBeforeCurrentLeftAction() {
486            super(/* ICON(dialogs/conflict/)*/ "copybeforecurrentleft", tr("> before"),
487                    tr("Copy my selected elements before the first selected element in the list of merged elements."));
488        }
489
490        @Override
491        public void actionPerformed(ActionEvent e) {
492            int[] mergedRows = mergedEntriesTable.getSelectedRows();
493            if (mergedRows.length == 0)
494                return;
495            int[] myRows = myEntriesTable.getSelectedRows();
496            int current = mergedRows[0];
497            model.copyMyBeforeCurrent(myRows, current);
498        }
499
500        @Override
501        public void valueChanged(ListSelectionEvent e) {
502            setEnabled(
503                    !myEntriesTable.getSelectionModel().isSelectionEmpty()
504                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
505            );
506        }
507    }
508
509    /**
510     * Action for copying selected nodes in the list of my nodes to the list of merged
511     * nodes. Inserts the nodes after the first selected row in the list of merged nodes.
512     */
513    class CopyAfterCurrentLeftAction extends CopyAction {
514
515        CopyAfterCurrentLeftAction() {
516            super(/* ICON(dialogs/conflict/)*/ "copyaftercurrentleft", tr("> after"),
517                    tr("Copy my selected elements after the first selected element in the list of merged elements."));
518        }
519
520        @Override
521        public void actionPerformed(ActionEvent e) {
522            int[] mergedRows = mergedEntriesTable.getSelectedRows();
523            if (mergedRows.length == 0)
524                return;
525            int[] myRows = myEntriesTable.getSelectedRows();
526            int current = mergedRows[0];
527            model.copyMyAfterCurrent(myRows, current);
528        }
529
530        @Override
531        public void valueChanged(ListSelectionEvent e) {
532            setEnabled(
533                    !myEntriesTable.getSelectionModel().isSelectionEmpty()
534                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
535            );
536        }
537    }
538
539    class CopyStartRightAction extends CopyAction {
540
541        CopyStartRightAction() {
542            super(/* ICON(dialogs/conflict/)*/ "copystartright", tr("< top"),
543                tr("Copy their selected element to the start of the list of merged elements."));
544        }
545
546        @Override
547        public void actionPerformed(ActionEvent e) {
548            model.copyTheirToTop(theirEntriesTable.getSelectedRows());
549        }
550
551        @Override
552        public void valueChanged(ListSelectionEvent e) {
553            setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
554        }
555    }
556
557    class CopyEndRightAction extends CopyAction {
558
559        CopyEndRightAction() {
560            super(/* ICON(dialogs/conflict/)*/ "copyendright", tr("< bottom"),
561                tr("Copy their selected elements to the end of the list of merged elements."));
562        }
563
564        @Override
565        public void actionPerformed(ActionEvent arg0) {
566            model.copyTheirToEnd(theirEntriesTable.getSelectedRows());
567        }
568
569        @Override
570        public void valueChanged(ListSelectionEvent e) {
571            setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
572        }
573    }
574
575    class CopyBeforeCurrentRightAction extends CopyAction {
576
577        CopyBeforeCurrentRightAction() {
578            super(/* ICON(dialogs/conflict/)*/ "copybeforecurrentright", tr("< before"),
579                    tr("Copy their selected elements before the first selected element in the list of merged elements."));
580        }
581
582        @Override
583        public void actionPerformed(ActionEvent e) {
584            int[] mergedRows = mergedEntriesTable.getSelectedRows();
585            if (mergedRows.length == 0)
586                return;
587            int[] myRows = theirEntriesTable.getSelectedRows();
588            int current = mergedRows[0];
589            model.copyTheirBeforeCurrent(myRows, current);
590        }
591
592        @Override
593        public void valueChanged(ListSelectionEvent e) {
594            setEnabled(
595                    !theirEntriesTable.getSelectionModel().isSelectionEmpty()
596                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
597            );
598        }
599    }
600
601    class CopyAfterCurrentRightAction extends CopyAction {
602
603        CopyAfterCurrentRightAction() {
604            super(/* ICON(dialogs/conflict/)*/ "copyaftercurrentright", tr("< after"),
605                    tr("Copy their selected element after the first selected element in the list of merged elements"));
606        }
607
608        @Override
609        public void actionPerformed(ActionEvent e) {
610            int[] mergedRows = mergedEntriesTable.getSelectedRows();
611            if (mergedRows.length == 0)
612                return;
613            int[] myRows = theirEntriesTable.getSelectedRows();
614            int current = mergedRows[0];
615            model.copyTheirAfterCurrent(myRows, current);
616        }
617
618        @Override
619        public void valueChanged(ListSelectionEvent e) {
620            setEnabled(
621                    !theirEntriesTable.getSelectionModel().isSelectionEmpty()
622                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
623            );
624        }
625    }
626
627    class CopyAllLeft extends AbstractAction implements ChangeListener, PropertyChangeListener {
628
629        CopyAllLeft() {
630            new ImageProvider("dialogs/conflict", "useallleft").getResource().attachImageIcon(this, true);
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            new ImageProvider("dialogs/conflict", "useallright").getResource().attachImageIcon(this, true);
659            putValue(Action.SHORT_DESCRIPTION, tr("Copy all their elements to the target"));
660        }
661
662        @Override
663        public void actionPerformed(ActionEvent arg0) {
664            model.copyAll(ListRole.THEIR_ENTRIES);
665            model.setFrozen(true);
666        }
667
668        private void updateEnabledState() {
669            setEnabled(model.getMergedEntries().isEmpty() && !model.isFrozen());
670        }
671
672        @Override
673        public void stateChanged(ChangeEvent e) {
674            updateEnabledState();
675        }
676
677        @Override
678        public void propertyChange(PropertyChangeEvent evt) {
679            updateEnabledState();
680        }
681    }
682
683    class MoveUpMergedAction extends AbstractAction implements ListSelectionListener {
684
685        MoveUpMergedAction() {
686            ImageResource icon = new ImageProvider("dialogs/conflict", "moveup").getResource();
687            if (icon == null) {
688                putValue(Action.NAME, tr("Up"));
689            } else {
690                icon.attachImageIcon(this, true);
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            ImageResource icon = new ImageProvider("dialogs/conflict", "movedown").getResource();
720            if (icon == null) {
721                putValue(Action.NAME, tr("Down"));
722            } else {
723                icon.attachImageIcon(this, true);
724            }
725            putValue(Action.SHORT_DESCRIPTION, tr("Move down the selected entries by one position."));
726            setEnabled(false);
727        }
728
729        @Override
730        public void actionPerformed(ActionEvent arg0) {
731            int[] rows = mergedEntriesTable.getSelectedRows();
732            model.moveDownMerged(rows);
733        }
734
735        @Override
736        public void valueChanged(ListSelectionEvent e) {
737            int[] rows = mergedEntriesTable.getSelectedRows();
738            setEnabled(rows.length > 0
739                    && rows[rows.length -1] != mergedEntriesTable.getRowCount() -1
740            );
741        }
742    }
743
744    /**
745     * Action for removing the selected entries in the list of merged entries
746     * from the list of merged entries.
747     *
748     */
749    class RemoveMergedAction extends AbstractAction implements ListSelectionListener {
750
751        RemoveMergedAction() {
752            ImageResource icon = new ImageProvider("dialogs/conflict", "remove").getResource();
753            if (icon == null) {
754                putValue(Action.NAME, tr("Remove"));
755            } else {
756                icon.attachImageIcon(this, true);
757            }
758            putValue(Action.SHORT_DESCRIPTION, tr("Remove the selected entries from the list of merged elements."));
759            setEnabled(false);
760        }
761
762        @Override
763        public void actionPerformed(ActionEvent arg0) {
764            int[] rows = mergedEntriesTable.getSelectedRows();
765            model.removeMerged(rows);
766        }
767
768        @Override
769        public void valueChanged(ListSelectionEvent e) {
770            int[] rows = mergedEntriesTable.getSelectedRows();
771            setEnabled(rows.length > 0);
772        }
773    }
774
775    private 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    private final class FreezeAction extends AbstractAction implements ItemListener, FreezeActionProperties {
784
785        private FreezeAction() {
786            putValue(Action.NAME, tr("Freeze"));
787            putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements."));
788            putValue(PROP_SELECTED, Boolean.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(evt -> {
807                    if (evt.getPropertyName().equals(PROP_SELECTED)) {
808                        btn.setSelected((Boolean) evt.getNewValue());
809                    }
810                });
811        }
812
813        @Override
814        public void itemStateChanged(ItemEvent e) {
815            int state = e.getStateChange();
816            if (state == ItemEvent.SELECTED) {
817                putValue(Action.NAME, tr("Unfreeze"));
818                putValue(Action.SHORT_DESCRIPTION, tr("Unfreeze the list of merged elements and start merging."));
819                model.setFrozen(true);
820            } else if (state == ItemEvent.DESELECTED) {
821                putValue(Action.NAME, tr("Freeze"));
822                putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements."));
823                model.setFrozen(false);
824            }
825            boolean isSelected = (Boolean) getValue(PROP_SELECTED);
826            if (isSelected != (e.getStateChange() == ItemEvent.SELECTED)) {
827                putValue(PROP_SELECTED, e.getStateChange() == ItemEvent.SELECTED);
828            }
829
830        }
831    }
832
833    protected void handlePropertyChangeFrozen(boolean newValue) {
834        myEntriesTable.getSelectionModel().clearSelection();
835        myEntriesTable.setEnabled(!newValue);
836        theirEntriesTable.getSelectionModel().clearSelection();
837        theirEntriesTable.setEnabled(!newValue);
838        mergedEntriesTable.getSelectionModel().clearSelection();
839        mergedEntriesTable.setEnabled(!newValue);
840        freezeAction.putValue(FreezeActionProperties.PROP_SELECTED, newValue);
841        if (newValue) {
842            lblFrozenState.setText(
843                    tr("<html>Click <strong>{0}</strong> to start merging my and their entries.</html>",
844                            freezeAction.getValue(Action.NAME))
845            );
846        } else {
847            lblFrozenState.setText(
848                    tr("<html>Click <strong>{0}</strong> to finish merging my and their entries.</html>",
849                            freezeAction.getValue(Action.NAME))
850            );
851        }
852    }
853
854    @Override
855    public void propertyChange(PropertyChangeEvent evt) {
856        if (evt.getPropertyName().equals(AbstractListMergeModel.FROZEN_PROP)) {
857            handlePropertyChangeFrozen((Boolean) evt.getNewValue());
858        }
859    }
860
861    /**
862     * Returns the model.
863     * @return the model
864     */
865    public AbstractListMergeModel<T, C> getModel() {
866        return model;
867    }
868
869    @Override
870    public void stateChanged(ChangeEvent e) {
871        lblMyVersion.setText(
872                trn("My version ({0} entry)", "My version ({0} entries)", model.getMyEntriesSize(), model.getMyEntriesSize())
873        );
874        lblMergedVersion.setText(
875                trn("Merged version ({0} entry)", "Merged version ({0} entries)", model.getMergedEntriesSize(), model.getMergedEntriesSize())
876        );
877        lblTheirVersion.setText(
878                trn("Their version ({0} entry)", "Their version ({0} entries)", model.getTheirEntriesSize(), model.getTheirEntriesSize())
879        );
880    }
881
882    /**
883     * Adds all registered listeners by this merger
884     * @see #unregisterListeners()
885     * @since 10454
886     */
887    public void registerListeners() {
888        myEntriesTable.registerListeners();
889        mergedEntriesTable.registerListeners();
890        theirEntriesTable.registerListeners();
891    }
892
893    /**
894     * Removes all registered listeners by this merger
895     * @since 10454
896     */
897    public void unregisterListeners() {
898        myEntriesTable.unregisterListeners();
899        mergedEntriesTable.unregisterListeners();
900        theirEntriesTable.unregisterListeners();
901    }
902
903    protected final <P extends OsmPrimitive> OsmDataLayer findLayerFor(P primitive) {
904        if (primitive != null) {
905            Iterable<OsmDataLayer> layers = MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class);
906            // Find layer with same dataset
907            for (OsmDataLayer layer : layers) {
908                if (layer.data == primitive.getDataSet()) {
909                    return layer;
910                }
911            }
912            // Conflict after merging layers: a dataset could be no more in any layer, try to find another layer with same primitive
913            for (OsmDataLayer layer : layers) {
914                final Collection<? extends OsmPrimitive> collection;
915                if (primitive instanceof Way) {
916                    collection = layer.data.getWays();
917                } else if (primitive instanceof Relation) {
918                    collection = layer.data.getRelations();
919                } else {
920                    collection = layer.data.allPrimitives();
921                }
922                for (OsmPrimitive p : collection) {
923                    if (p.getPrimitiveId().equals(primitive.getPrimitiveId())) {
924                        return layer;
925                    }
926                }
927            }
928        }
929        return null;
930    }
931
932    @Override
933    public void decideRemaining(MergeDecisionType decision) {
934        if (!model.isFrozen()) {
935            model.copyAll(MergeDecisionType.KEEP_MINE == decision ? ListRole.MY_ENTRIES : ListRole.THEIR_ENTRIES);
936            model.setFrozen(true);
937        }
938    }
939}