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