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