001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.conflict.pair.tags;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Adjustable;
007import java.awt.event.ActionEvent;
008import java.awt.event.AdjustmentEvent;
009import java.awt.event.AdjustmentListener;
010import java.awt.event.MouseAdapter;
011import java.awt.event.MouseEvent;
012import java.util.Arrays;
013import java.util.HashSet;
014import java.util.List;
015import java.util.Set;
016
017import javax.swing.AbstractAction;
018import javax.swing.Action;
019import javax.swing.JButton;
020import javax.swing.JComponent;
021import javax.swing.JScrollPane;
022import javax.swing.JTable;
023import javax.swing.event.ListSelectionEvent;
024import javax.swing.event.ListSelectionListener;
025
026import org.openstreetmap.josm.data.conflict.Conflict;
027import org.openstreetmap.josm.data.osm.OsmPrimitive;
028import org.openstreetmap.josm.gui.conflict.pair.AbstractMergePanel;
029import org.openstreetmap.josm.gui.conflict.pair.IConflictResolver;
030import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
031import org.openstreetmap.josm.gui.tagging.TagTableColumnModelBuilder;
032import org.openstreetmap.josm.tools.GBC;
033import org.openstreetmap.josm.tools.ImageProvider;
034import org.openstreetmap.josm.tools.ImageResource;
035
036/**
037 * UI component for resolving conflicts in the tag sets of two {@link OsmPrimitive}s.
038 * @since 1622
039 */
040public class TagMerger extends AbstractMergePanel implements IConflictResolver {
041    private static final String[] KEY_VALUE = {tr("Key"), tr("Value")};
042
043    private final TagMergeModel model = new TagMergeModel();
044
045    /**
046     * the table for my tag set
047     */
048    private final JTable mineTable = generateTable(new MineTableCellRenderer());
049    /**
050     * the table for the merged tag set
051     */
052    private final JTable mergedTable = generateTable(new MergedTableCellRenderer());
053    /**
054     * the table for their tag set
055     */
056    private final JTable theirTable = generateTable(new TheirTableCellRenderer());
057
058    /**
059     * Constructs a new {@code TagMerger}.
060     */
061    public TagMerger() {
062        mineTable.setName("table.my");
063        theirTable.setName("table.their");
064        mergedTable.setName("table.merged");
065
066        DoubleClickAdapter dblClickAdapter = new DoubleClickAdapter();
067        mineTable.addMouseListener(dblClickAdapter);
068        theirTable.addMouseListener(dblClickAdapter);
069
070        buildRows();
071    }
072
073    private JTable generateTable(TagMergeTableCellRenderer renderer) {
074        return new JTable(model, new TagTableColumnModelBuilder(renderer, KEY_VALUE).build());
075    }
076
077    @Override
078    protected List<? extends MergeRow> getRows() {
079        return Arrays.asList(new TitleRow(), new TagTableRow(), new UndecidedRow());
080    }
081
082    /**
083     * replies the model used by this tag merger
084     *
085     * @return the model
086     */
087    public TagMergeModel getModel() {
088        return model;
089    }
090
091    private void selectNextConflict(int... rows) {
092        int max = rows[0];
093        for (int row: rows) {
094            if (row > max) {
095                max = row;
096            }
097        }
098        int index = model.getFirstUndecided(max+1);
099        if (index == -1) {
100            index = model.getFirstUndecided(0);
101        }
102        mineTable.getSelectionModel().setSelectionInterval(index, index);
103        theirTable.getSelectionModel().setSelectionInterval(index, index);
104    }
105
106    private final class TagTableRow extends MergeRow {
107        private final AdjustmentSynchronizer adjustmentSynchronizer = new AdjustmentSynchronizer();
108
109        /**
110         * embeds table in a new {@link JScrollPane} and returns th scroll pane
111         *
112         * @param table the table
113         * @return the scroll pane embedding the table
114         */
115        JScrollPane embeddInScrollPane(JTable table) {
116            JScrollPane pane = new JScrollPane(table);
117            adjustmentSynchronizer.synchronizeAdjustment(pane.getVerticalScrollBar());
118            return pane;
119        }
120
121        @Override
122        protected JComponent mineField() {
123            return embeddInScrollPane(mineTable);
124        }
125
126        @Override
127        protected JComponent mineButton() {
128            KeepMineAction keepMineAction = new KeepMineAction();
129            mineTable.getSelectionModel().addListSelectionListener(keepMineAction);
130            JButton btnKeepMine = new JButton(keepMineAction);
131            btnKeepMine.setName("button.keepmine");
132            return btnKeepMine;
133        }
134
135        @Override
136        protected JComponent merged() {
137            return embeddInScrollPane(mergedTable);
138        }
139
140        @Override
141        protected JComponent theirsButton() {
142            KeepTheirAction keepTheirAction = new KeepTheirAction();
143            theirTable.getSelectionModel().addListSelectionListener(keepTheirAction);
144            JButton btnKeepTheir = new JButton(keepTheirAction);
145            btnKeepTheir.setName("button.keeptheir");
146            return btnKeepTheir;
147        }
148
149        @Override
150        protected JComponent theirsField() {
151            return embeddInScrollPane(theirTable);
152        }
153
154        @Override
155        protected void addConstraints(GBC constraints, int columnIndex) {
156            super.addConstraints(constraints, columnIndex);
157            // Fill to bottom
158            constraints.weighty = 1;
159        }
160    }
161
162    private final class UndecidedRow extends AbstractUndecideRow {
163        @Override
164        protected AbstractAction createAction() {
165            UndecideAction undecidedAction = new UndecideAction();
166            mergedTable.getSelectionModel().addListSelectionListener(undecidedAction);
167            return undecidedAction;
168        }
169
170        @Override
171        protected String getButtonName() {
172            return "button.undecide";
173        }
174    }
175
176    /**
177     * Keeps the currently selected tags in my table in the list of merged tags.
178     *
179     */
180    class KeepMineAction extends AbstractAction implements ListSelectionListener {
181        KeepMineAction() {
182            ImageResource icon = new ImageProvider("dialogs/conflict", "tagkeepmine").getResource();
183            if (icon != null) {
184                icon.attachImageIcon(this, true);
185                putValue(Action.NAME, "");
186            } else {
187                putValue(Action.NAME, ">");
188            }
189            putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the local dataset"));
190            setEnabled(false);
191        }
192
193        @Override
194        public void actionPerformed(ActionEvent arg0) {
195            int[] rows = mineTable.getSelectedRows();
196            if (rows.length == 0)
197                return;
198            model.decide(rows, MergeDecisionType.KEEP_MINE);
199            selectNextConflict(rows);
200        }
201
202        @Override
203        public void valueChanged(ListSelectionEvent e) {
204            setEnabled(mineTable.getSelectedRowCount() > 0);
205        }
206    }
207
208    /**
209     * Keeps the currently selected tags in their table in the list of merged tags.
210     *
211     */
212    class KeepTheirAction extends AbstractAction implements ListSelectionListener {
213        KeepTheirAction() {
214            ImageResource icon = new ImageProvider("dialogs/conflict", "tagkeeptheir").getResource();
215            if (icon != null) {
216                icon.attachImageIcon(this, true);
217                putValue(Action.NAME, "");
218            } else {
219                putValue(Action.NAME, ">");
220            }
221            putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the server dataset"));
222            setEnabled(false);
223        }
224
225        @Override
226        public void actionPerformed(ActionEvent arg0) {
227            int[] rows = theirTable.getSelectedRows();
228            if (rows.length == 0)
229                return;
230            model.decide(rows, MergeDecisionType.KEEP_THEIR);
231            selectNextConflict(rows);
232        }
233
234        @Override
235        public void valueChanged(ListSelectionEvent e) {
236            setEnabled(theirTable.getSelectedRowCount() > 0);
237        }
238    }
239
240    /**
241     * Synchronizes scrollbar adjustments between a set of
242     * {@link Adjustable}s. Whenever the adjustment of one of
243     * the registerd Adjustables is updated the adjustment of
244     * the other registered Adjustables is adjusted too.
245     *
246     */
247    static class AdjustmentSynchronizer implements AdjustmentListener {
248        private final Set<Adjustable> synchronizedAdjustables;
249
250        AdjustmentSynchronizer() {
251            synchronizedAdjustables = new HashSet<>();
252        }
253
254        public void synchronizeAdjustment(Adjustable adjustable) {
255            if (adjustable == null)
256                return;
257            if (synchronizedAdjustables.contains(adjustable))
258                return;
259            synchronizedAdjustables.add(adjustable);
260            adjustable.addAdjustmentListener(this);
261        }
262
263        @Override
264        public void adjustmentValueChanged(AdjustmentEvent e) {
265            for (Adjustable a : synchronizedAdjustables) {
266                if (a != e.getAdjustable()) {
267                    a.setValue(e.getValue());
268                }
269            }
270        }
271    }
272
273    /**
274     * Handler for double clicks on entries in the three tag tables.
275     *
276     */
277    class DoubleClickAdapter extends MouseAdapter {
278
279        @Override
280        public void mouseClicked(MouseEvent e) {
281            if (e.getClickCount() != 2)
282                return;
283            JTable table;
284            MergeDecisionType mergeDecision;
285
286            if (e.getSource() == mineTable) {
287                table = mineTable;
288                mergeDecision = MergeDecisionType.KEEP_MINE;
289            } else if (e.getSource() == theirTable) {
290                table = theirTable;
291                mergeDecision = MergeDecisionType.KEEP_THEIR;
292            } else if (e.getSource() == mergedTable) {
293                table = mergedTable;
294                mergeDecision = MergeDecisionType.UNDECIDED;
295            } else
296                // double click in another component; shouldn't happen,
297                // but just in case
298                return;
299            int row = table.rowAtPoint(e.getPoint());
300            model.decide(row, mergeDecision);
301        }
302    }
303
304    /**
305     * Sets the currently selected tags in the table of merged tags to state
306     * {@link MergeDecisionType#UNDECIDED}
307     *
308     */
309    class UndecideAction extends AbstractAction implements ListSelectionListener {
310
311        UndecideAction() {
312            ImageResource icon = new ImageProvider("dialogs/conflict", "tagundecide").getResource();
313            if (icon != null) {
314                icon.attachImageIcon(this, true);
315                putValue(Action.NAME, "");
316            } else {
317                putValue(Action.NAME, tr("Undecide"));
318            }
319            putValue(SHORT_DESCRIPTION, tr("Mark the selected tags as undecided"));
320            setEnabled(false);
321        }
322
323        @Override
324        public void actionPerformed(ActionEvent arg0) {
325            int[] rows = mergedTable.getSelectedRows();
326            if (rows.length == 0)
327                return;
328            model.decide(rows, MergeDecisionType.UNDECIDED);
329        }
330
331        @Override
332        public void valueChanged(ListSelectionEvent e) {
333            setEnabled(mergedTable.getSelectedRowCount() > 0);
334        }
335    }
336
337    @Override
338    public void deletePrimitive(boolean deleted) {
339        // Use my entries, as it doesn't really matter
340        MergeDecisionType decision = deleted ? MergeDecisionType.KEEP_MINE : MergeDecisionType.UNDECIDED;
341        for (int i = 0; i < model.getRowCount(); i++) {
342            model.decide(i, decision);
343        }
344    }
345
346    @Override
347    public void populate(Conflict<? extends OsmPrimitive> conflict) {
348        model.populate(conflict.getMy(), conflict.getTheir());
349        for (JTable table : new JTable[]{mineTable, theirTable}) {
350            int index = table.getRowCount() > 0 ? 0 : -1;
351            table.getSelectionModel().setSelectionInterval(index, index);
352        }
353    }
354
355    @Override
356    public void decideRemaining(MergeDecisionType decision) {
357        model.decideRemaining(decision);
358    }
359}