source: josm/trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMerger.java@ 18845

Last change on this file since 18845 was 18845, checked in by taylor.smock, 9 months ago

Fix #23189: Conflict tag tables should resize with the conflict window

  • Property svn:eol-style set to native
File size: 11.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.conflict.pair.tags;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Adjustable;
7import java.awt.GridBagConstraints;
8import java.awt.GridBagLayout;
9import java.awt.event.ActionEvent;
10import java.awt.event.AdjustmentEvent;
11import java.awt.event.AdjustmentListener;
12import java.awt.event.MouseAdapter;
13import java.awt.event.MouseEvent;
14import java.util.Arrays;
15import java.util.HashSet;
16import java.util.List;
17import java.util.Set;
18
19import javax.swing.AbstractAction;
20import javax.swing.Action;
21import javax.swing.JButton;
22import javax.swing.JComponent;
23import javax.swing.JPanel;
24import javax.swing.JScrollPane;
25import javax.swing.JTable;
26import javax.swing.event.ListSelectionEvent;
27import javax.swing.event.ListSelectionListener;
28
29import org.openstreetmap.josm.data.conflict.Conflict;
30import org.openstreetmap.josm.data.osm.OsmPrimitive;
31import org.openstreetmap.josm.gui.conflict.pair.AbstractMergePanel;
32import org.openstreetmap.josm.gui.conflict.pair.IConflictResolver;
33import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
34import org.openstreetmap.josm.gui.tagging.TagTableColumnModelBuilder;
35import org.openstreetmap.josm.gui.util.GuiHelper;
36import org.openstreetmap.josm.tools.GBC;
37import org.openstreetmap.josm.tools.ImageProvider;
38
39/**
40 * UI component for resolving conflicts in the tag sets of two {@link OsmPrimitive}s.
41 * @since 1622
42 */
43public class TagMerger extends AbstractMergePanel implements IConflictResolver {
44 private static final String[] KEY_VALUE = {tr("Key"), tr("Value")};
45
46 private final TagMergeModel model = new TagMergeModel();
47
48 /**
49 * the table for my tag set
50 */
51 private final JTable mineTable = generateTable(new MineTableCellRenderer());
52 /**
53 * the table for the merged tag set
54 */
55 private final JTable mergedTable = generateTable(new MergedTableCellRenderer());
56 /**
57 * the table for their tag set
58 */
59 private final JTable theirTable = generateTable(new TheirTableCellRenderer());
60
61 /**
62 * Constructs a new {@code TagMerger}.
63 */
64 public TagMerger() {
65 mineTable.setName("table.my");
66 theirTable.setName("table.their");
67 mergedTable.setName("table.merged");
68
69 DoubleClickAdapter dblClickAdapter = new DoubleClickAdapter();
70 mineTable.addMouseListener(dblClickAdapter);
71 theirTable.addMouseListener(dblClickAdapter);
72
73 buildRows();
74 }
75
76 private JTable generateTable(TagMergeTableCellRenderer renderer) {
77 return new JTable(model, new TagTableColumnModelBuilder(renderer, KEY_VALUE).build());
78 }
79
80 @Override
81 protected List<? extends MergeRow> getRows() {
82 return Arrays.asList(new TitleRow(), new TagTableRow(), new UndecidedRow());
83 }
84
85 /**
86 * replies the model used by this tag merger
87 *
88 * @return the model
89 */
90 public TagMergeModel getModel() {
91 return model;
92 }
93
94 private void selectNextConflict(int... rows) {
95 int max = rows[0];
96 for (int row: rows) {
97 if (row > max) {
98 max = row;
99 }
100 }
101 int index = model.getFirstUndecided(max+1);
102 if (index == -1) {
103 index = model.getFirstUndecided(0);
104 }
105 mineTable.getSelectionModel().setSelectionInterval(index, index);
106 theirTable.getSelectionModel().setSelectionInterval(index, index);
107 }
108
109 private final class TagTableRow extends MergeRow {
110 private final AdjustmentSynchronizer adjustmentSynchronizer = new AdjustmentSynchronizer();
111
112 /**
113 * embeds table in a new {@link JScrollPane} and returns th scroll pane
114 *
115 * @param table the table
116 * @return the scroll pane embedding the table
117 */
118 JScrollPane embeddInScrollPane(JTable table) {
119 // See #23189: Tag tables should resize (where possible) with the window
120 final JPanel panel = new JPanel(new GridBagLayout());
121 panel.add(table.getTableHeader(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
122 panel.add(table, GBC.eol().fill(GridBagConstraints.BOTH));
123 final JScrollPane pane = GuiHelper.embedInVerticalScrollPane(panel);
124 adjustmentSynchronizer.synchronizeAdjustment(pane.getVerticalScrollBar());
125 return pane;
126 }
127
128 @Override
129 protected JComponent mineField() {
130 return embeddInScrollPane(mineTable);
131 }
132
133 @Override
134 protected JComponent mineButton() {
135 KeepMineAction keepMineAction = new KeepMineAction();
136 mineTable.getSelectionModel().addListSelectionListener(keepMineAction);
137 JButton btnKeepMine = new JButton(keepMineAction);
138 btnKeepMine.setName("button.keepmine");
139 return btnKeepMine;
140 }
141
142 @Override
143 protected JComponent merged() {
144 return embeddInScrollPane(mergedTable);
145 }
146
147 @Override
148 protected JComponent theirsButton() {
149 KeepTheirAction keepTheirAction = new KeepTheirAction();
150 theirTable.getSelectionModel().addListSelectionListener(keepTheirAction);
151 JButton btnKeepTheir = new JButton(keepTheirAction);
152 btnKeepTheir.setName("button.keeptheir");
153 return btnKeepTheir;
154 }
155
156 @Override
157 protected JComponent theirsField() {
158 return embeddInScrollPane(theirTable);
159 }
160
161 @Override
162 protected void addConstraints(GBC constraints, int columnIndex) {
163 super.addConstraints(constraints, columnIndex);
164 // Fill to bottom
165 constraints.weighty = 1;
166 }
167 }
168
169 private final class UndecidedRow extends AbstractUndecideRow {
170 @Override
171 protected AbstractAction createAction() {
172 UndecideAction undecidedAction = new UndecideAction();
173 mergedTable.getSelectionModel().addListSelectionListener(undecidedAction);
174 return undecidedAction;
175 }
176
177 @Override
178 protected String getButtonName() {
179 return "button.undecide";
180 }
181 }
182
183 /**
184 * Keeps the currently selected tags in my table in the list of merged tags.
185 *
186 */
187 class KeepMineAction extends AbstractAction implements ListSelectionListener {
188 KeepMineAction() {
189 new ImageProvider("dialogs/conflict", "tagkeepmine").getResource().attachImageIcon(this, true);
190 putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the local dataset"));
191 setEnabled(false);
192 }
193
194 @Override
195 public void actionPerformed(ActionEvent arg0) {
196 int[] rows = mineTable.getSelectedRows();
197 if (rows.length == 0)
198 return;
199 model.decide(rows, MergeDecisionType.KEEP_MINE);
200 selectNextConflict(rows);
201 }
202
203 @Override
204 public void valueChanged(ListSelectionEvent e) {
205 setEnabled(mineTable.getSelectedRowCount() > 0);
206 }
207 }
208
209 /**
210 * Keeps the currently selected tags in their table in the list of merged tags.
211 *
212 */
213 class KeepTheirAction extends AbstractAction implements ListSelectionListener {
214 KeepTheirAction() {
215 new ImageProvider("dialogs/conflict", "tagkeeptheir").getResource().attachImageIcon(this, true);
216 putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the server dataset"));
217 setEnabled(false);
218 }
219
220 @Override
221 public void actionPerformed(ActionEvent arg0) {
222 int[] rows = theirTable.getSelectedRows();
223 if (rows.length == 0)
224 return;
225 model.decide(rows, MergeDecisionType.KEEP_THEIR);
226 selectNextConflict(rows);
227 }
228
229 @Override
230 public void valueChanged(ListSelectionEvent e) {
231 setEnabled(theirTable.getSelectedRowCount() > 0);
232 }
233 }
234
235 /**
236 * Synchronizes scrollbar adjustments between a set of
237 * {@link Adjustable}s. Whenever the adjustment of one of
238 * the registered Adjustables is updated the adjustment of
239 * the other registered Adjustables is adjusted too.
240 *
241 */
242 static class AdjustmentSynchronizer implements AdjustmentListener {
243 private final Set<Adjustable> synchronizedAdjustables;
244
245 AdjustmentSynchronizer() {
246 synchronizedAdjustables = new HashSet<>();
247 }
248
249 public void synchronizeAdjustment(Adjustable adjustable) {
250 if (adjustable == null)
251 return;
252 if (synchronizedAdjustables.contains(adjustable))
253 return;
254 synchronizedAdjustables.add(adjustable);
255 adjustable.addAdjustmentListener(this);
256 }
257
258 @Override
259 public void adjustmentValueChanged(AdjustmentEvent e) {
260 for (Adjustable a : synchronizedAdjustables) {
261 if (a != e.getAdjustable()) {
262 a.setValue(e.getValue());
263 }
264 }
265 }
266 }
267
268 /**
269 * Handler for double clicks on entries in the three tag tables.
270 *
271 */
272 class DoubleClickAdapter extends MouseAdapter {
273
274 @Override
275 public void mouseClicked(MouseEvent e) {
276 if (e.getClickCount() != 2)
277 return;
278 JTable table;
279 MergeDecisionType mergeDecision;
280
281 if (e.getSource() == mineTable) {
282 table = mineTable;
283 mergeDecision = MergeDecisionType.KEEP_MINE;
284 } else if (e.getSource() == theirTable) {
285 table = theirTable;
286 mergeDecision = MergeDecisionType.KEEP_THEIR;
287 } else if (e.getSource() == mergedTable) {
288 table = mergedTable;
289 mergeDecision = MergeDecisionType.UNDECIDED;
290 } else
291 // double click in another component; shouldn't happen,
292 // but just in case
293 return;
294 int row = table.rowAtPoint(e.getPoint());
295 model.decide(row, mergeDecision);
296 }
297 }
298
299 /**
300 * Sets the currently selected tags in the table of merged tags to state
301 * {@link MergeDecisionType#UNDECIDED}
302 *
303 */
304 class UndecideAction extends AbstractAction implements ListSelectionListener {
305
306 UndecideAction() {
307 new ImageProvider("dialogs/conflict", "tagundecide").getResource().attachImageIcon(this, true);
308 putValue(SHORT_DESCRIPTION, tr("Mark the selected tags as undecided"));
309 setEnabled(false);
310 }
311
312 @Override
313 public void actionPerformed(ActionEvent arg0) {
314 int[] rows = mergedTable.getSelectedRows();
315 if (rows.length == 0)
316 return;
317 model.decide(rows, MergeDecisionType.UNDECIDED);
318 }
319
320 @Override
321 public void valueChanged(ListSelectionEvent e) {
322 setEnabled(mergedTable.getSelectedRowCount() > 0);
323 }
324 }
325
326 @Override
327 public void deletePrimitive(boolean deleted) {
328 // Use my entries, as it doesn't really matter
329 MergeDecisionType decision = deleted ? MergeDecisionType.KEEP_MINE : MergeDecisionType.UNDECIDED;
330 for (int i = 0; i < model.getRowCount(); i++) {
331 model.decide(i, decision);
332 }
333 }
334
335 @Override
336 public void populate(Conflict<? extends OsmPrimitive> conflict) {
337 model.populate(conflict.getMy(), conflict.getTheir());
338 for (JTable table : new JTable[]{mineTable, theirTable}) {
339 int index = table.getRowCount() > 0 ? 0 : -1;
340 table.getSelectionModel().setSelectionInterval(index, index);
341 }
342 }
343
344 @Override
345 public void decideRemaining(MergeDecisionType decision) {
346 model.decideRemaining(decision);
347 }
348}
Note: See TracBrowser for help on using the repository browser.