source: osm/applications/editors/josm/plugins/merge-overlap/src/mergeoverlap/MyCombinePrimitiveResolverDialog.java@ 30034

Last change on this file since 30034 was 30034, checked in by donvip, 11 years ago

[josm_plugins] update to [josm6340]

File size: 56.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package mergeoverlap;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trc;
7
8import java.awt.BorderLayout;
9import java.awt.Component;
10import java.awt.Dimension;
11import java.awt.FlowLayout;
12import java.awt.GridBagConstraints;
13import java.awt.GridBagLayout;
14import java.awt.Insets;
15import java.awt.event.ActionEvent;
16import java.awt.event.FocusAdapter;
17import java.awt.event.FocusEvent;
18import java.awt.event.HierarchyBoundsListener;
19import java.awt.event.HierarchyEvent;
20import java.awt.event.KeyEvent;
21import java.awt.event.WindowAdapter;
22import java.awt.event.WindowEvent;
23import java.beans.PropertyChangeEvent;
24import java.beans.PropertyChangeListener;
25import java.beans.PropertyChangeSupport;
26import java.util.ArrayList;
27import java.util.Collection;
28import java.util.Collections;
29import java.util.Comparator;
30import java.util.HashMap;
31import java.util.HashSet;
32import java.util.LinkedList;
33import java.util.List;
34import java.util.Map;
35import java.util.Set;
36
37import javax.swing.AbstractAction;
38import javax.swing.AbstractButton;
39import javax.swing.Action;
40import javax.swing.BorderFactory;
41import javax.swing.BoxLayout;
42import javax.swing.ButtonModel;
43import javax.swing.JButton;
44import javax.swing.JCheckBox;
45import javax.swing.JComboBox;
46import javax.swing.JComponent;
47import javax.swing.JDialog;
48import javax.swing.JLabel;
49import javax.swing.JOptionPane;
50import javax.swing.JPanel;
51import javax.swing.JScrollPane;
52import javax.swing.JSplitPane;
53import javax.swing.JTable;
54import javax.swing.KeyStroke;
55import javax.swing.ListSelectionModel;
56import javax.swing.UIManager;
57import javax.swing.event.ChangeEvent;
58import javax.swing.event.ChangeListener;
59import javax.swing.table.DefaultTableModel;
60
61import org.openstreetmap.josm.Main;
62import org.openstreetmap.josm.command.ChangePropertyCommand;
63import org.openstreetmap.josm.command.Command;
64import org.openstreetmap.josm.data.osm.Node;
65import org.openstreetmap.josm.data.osm.OsmPrimitive;
66import org.openstreetmap.josm.data.osm.Relation;
67import org.openstreetmap.josm.data.osm.RelationMember;
68import org.openstreetmap.josm.data.osm.RelationToChildReference;
69import org.openstreetmap.josm.data.osm.TagCollection;
70import org.openstreetmap.josm.data.osm.Way;
71import org.openstreetmap.josm.gui.DefaultNameFormatter;
72import org.openstreetmap.josm.gui.SideButton;
73import org.openstreetmap.josm.gui.conflict.tags.MultiValueCellEditor;
74import org.openstreetmap.josm.gui.conflict.tags.MultiValueDecisionType;
75import org.openstreetmap.josm.gui.conflict.tags.MultiValueResolutionDecision;
76import org.openstreetmap.josm.gui.conflict.tags.RelationMemberConflictDecision;
77import org.openstreetmap.josm.gui.conflict.tags.RelationMemberConflictDecisionType;
78import org.openstreetmap.josm.gui.conflict.tags.RelationMemberConflictResolverColumnModel;
79import org.openstreetmap.josm.gui.conflict.tags.TagConflictResolverColumnModel;
80import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
81import org.openstreetmap.josm.gui.help.HelpUtil;
82import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
83import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
84import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
85import org.openstreetmap.josm.tools.CheckParameterUtil;
86import org.openstreetmap.josm.tools.ImageProvider;
87import org.openstreetmap.josm.tools.WindowGeometry;
88
89/**
90 * This dialog helps to resolve conflicts occurring when ways are combined or
91 * nodes are merged.
92 *
93 * There is a singleton instance of this dialog which can be retrieved using
94 * {@see #getInstance()}.
95 *
96 * The dialog uses two models: one for resolving tag conflicts, the other
97 * for resolving conflicts in relation memberships. For both models there are accessors,
98 * i.e {@see #getTagConflictResolverModel()} and {@see #getRelationMemberConflictResolverModel()}.
99 *
100 * Models have to be <strong>populated</strong> before the dialog is launched. Example:
101 * <pre>
102 * CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
103 * dialog.getTagConflictResolverModel().populate(aTagCollection);
104 * dialog.getRelationMemberConflictResolverModel().populate(aRelationLinkCollection);
105 * dialog.prepareDefaultDecisions();
106 * </pre>
107 *
108 * You should also set the target primitive which other primitives (ways or nodes) are
109 * merged to, see {@see #setTargetPrimitive(OsmPrimitive)}.
110 *
111 * After the dialog is closed use {@see #isCancelled()} to check whether the user canceled
112 * the dialog. If it wasn't canceled you may build a collection of {@see Command} objects
113 * which reflect the conflict resolution decisions the user made in the dialog:
114 * see {@see #buildResolutionCommands()}
115 *
116 *
117 */
118public class MyCombinePrimitiveResolverDialog extends JDialog {
119
120 /** the unique instance of the dialog */
121 static private MyCombinePrimitiveResolverDialog instance;
122
123 /**
124 * Replies the unique instance of the dialog
125 *
126 * @return the unique instance of the dialog
127 */
128 public static MyCombinePrimitiveResolverDialog getInstance() {
129 if (instance == null) {
130 instance = new MyCombinePrimitiveResolverDialog(Main.parent);
131 }
132 return instance;
133 }
134
135 private AutoAdjustingSplitPane spTagConflictTypes;
136 private MyTagConflictResolver pnlTagConflictResolver;
137 private MyRelationMemberConflictResolver pnlRelationMemberConflictResolver;
138 private boolean cancelled;
139 private JPanel pnlButtons;
140 private OsmPrimitive targetPrimitive;
141
142 /** the private help action */
143 private ContextSensitiveHelpAction helpAction;
144 /** the apply button */
145 private SideButton btnApply;
146
147 /**
148 * Replies the target primitive the collection of primitives is merged
149 * or combined to.
150 *
151 * @return the target primitive
152 */
153 public OsmPrimitive getTargetPrimitmive() {
154 return targetPrimitive;
155 }
156
157 /**
158 * Sets the primitive the collection of primitives is merged or combined
159 * to.
160 *
161 * @param primitive the target primitive
162 */
163 public void setTargetPrimitive(OsmPrimitive primitive) {
164 this.targetPrimitive = primitive;
165 updateTitle();
166 if (primitive instanceof Way) {
167 pnlRelationMemberConflictResolver.initForWayCombining();
168 } else if (primitive instanceof Node) {
169 pnlRelationMemberConflictResolver.initForNodeMerging();
170 }
171 }
172
173 protected void updateTitle() {
174 if (targetPrimitive == null) {
175 setTitle(tr("Conflicts when combining primitives"));
176 return;
177 }
178 if (targetPrimitive instanceof Way) {
179 setTitle(tr("Conflicts when combining ways - combined way is ''{0}''", targetPrimitive
180 .getDisplayName(DefaultNameFormatter.getInstance())));
181 helpAction.setHelpTopic(ht("/Action/CombineWay#ResolvingConflicts"));
182 getRootPane().putClientProperty("help", ht("/Action/CombineWay#ResolvingConflicts"));
183 } else if (targetPrimitive instanceof Node) {
184 setTitle(tr("Conflicts when merging nodes - target node is ''{0}''", targetPrimitive
185 .getDisplayName(DefaultNameFormatter.getInstance())));
186 helpAction.setHelpTopic(ht("/Action/MergeNodes#ResolvingConflicts"));
187 getRootPane().putClientProperty("help", ht("/Action/MergeNodes#ResolvingConflicts"));
188 }
189 }
190
191 protected void build() {
192 getContentPane().setLayout(new BorderLayout());
193 updateTitle();
194 spTagConflictTypes = new AutoAdjustingSplitPane(JSplitPane.VERTICAL_SPLIT);
195 spTagConflictTypes.setTopComponent(buildTagConflictResolverPanel());
196 spTagConflictTypes.setBottomComponent(buildRelationMemberConflictResolverPanel());
197 getContentPane().add(pnlButtons = buildButtonPanel(), BorderLayout.SOUTH);
198 addWindowListener(new AdjustDividerLocationAction());
199 HelpUtil.setHelpContext(getRootPane(), ht("/"));
200 }
201
202 protected JPanel buildTagConflictResolverPanel() {
203 pnlTagConflictResolver = new MyTagConflictResolver();
204 return pnlTagConflictResolver;
205 }
206
207 protected JPanel buildRelationMemberConflictResolverPanel() {
208 pnlRelationMemberConflictResolver = new MyRelationMemberConflictResolver();
209 return pnlRelationMemberConflictResolver;
210 }
211
212 protected JPanel buildButtonPanel() {
213 JPanel pnl = new JPanel();
214 pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
215
216 // -- apply button
217 ApplyAction applyAction = new ApplyAction();
218 pnlTagConflictResolver.getModel().addPropertyChangeListener(applyAction);
219 pnlRelationMemberConflictResolver.getModel().addPropertyChangeListener(applyAction);
220 btnApply = new SideButton(applyAction);
221 btnApply.setFocusable(true);
222 pnl.add(btnApply);
223
224 // -- cancel button
225 CancelAction cancelAction = new CancelAction();
226 pnl.add(new SideButton(cancelAction));
227
228 // -- help button
229 helpAction = new ContextSensitiveHelpAction();
230 pnl.add(new SideButton(helpAction));
231
232 return pnl;
233 }
234
235 public MyCombinePrimitiveResolverDialog(Component owner) {
236 super(JOptionPane.getFrameForComponent(owner), ModalityType.DOCUMENT_MODAL);
237 build();
238 }
239
240 public MyTagConflictResolverModel getTagConflictResolverModel() {
241 return pnlTagConflictResolver.getModel();
242 }
243
244 public MyRelationMemberConflictResolverModel getRelationMemberConflictResolverModel() {
245 return pnlRelationMemberConflictResolver.getModel();
246 }
247
248 protected List<Command> buildTagChangeCommand(OsmPrimitive primitive, TagCollection tc) {
249 LinkedList<Command> cmds = new LinkedList<Command>();
250 for (String key : tc.getKeys()) {
251 if (tc.hasUniqueEmptyValue(key)) {
252 if (primitive.get(key) != null) {
253 cmds.add(new ChangePropertyCommand(primitive, key, null));
254 }
255 } else {
256 String value = tc.getJoinedValues(key);
257 if (!value.equals(primitive.get(key))) {
258 cmds.add(new ChangePropertyCommand(primitive, key, value));
259 }
260 }
261 }
262 return cmds;
263 }
264
265 public List<Command> buildWayResolutionCommands() {
266 List<Command> cmds = new LinkedList<Command>();
267
268 TagCollection allResolutions = getTagConflictResolverModel().getAllResolutions();
269 if (allResolutions.size() > 0) {
270 cmds.addAll(buildTagChangeCommand(targetPrimitive, allResolutions));
271 }
272 if (targetPrimitive.get("created_by") != null) {
273 cmds.add(new ChangePropertyCommand(targetPrimitive, "created_by", null));
274 }
275
276 Command cmd = pnlRelationMemberConflictResolver.buildTagApplyCommands(getRelationMemberConflictResolverModel()
277 .getModifiedRelations(targetPrimitive));
278 if (cmd != null) {
279 cmds.add(cmd);
280 }
281 return cmds;
282 }
283
284 public void buildRelationCorrespondance(Map<Relation, Relation> newRelations, Map<Way, Way> oldWays) {
285 getRelationMemberConflictResolverModel().buildRelationCorrespondance(targetPrimitive, newRelations, oldWays);
286 }
287
288 protected void prepareDefaultTagDecisions() {
289 MyTagConflictResolverModel model = getTagConflictResolverModel();
290 for (int i = 0; i < model.getRowCount(); i++) {
291 MultiValueResolutionDecision decision = model.getDecision(i);
292 List<String> values = decision.getValues();
293 values.remove("");
294 if (values.size() == 1) {
295 decision.keepOne(values.get(0));
296 } else {
297 decision.keepAll();
298 }
299 }
300 model.rebuild();
301 }
302
303 protected void prepareDefaultRelationDecisions() {
304 MyRelationMemberConflictResolverModel model = getRelationMemberConflictResolverModel();
305 Set<Relation> relations = new HashSet<Relation>();
306 for (int i = 0; i < model.getNumDecisions(); i++) {
307 RelationMemberConflictDecision decision = model.getDecision(i);
308 if (!relations.contains(decision.getRelation())) {
309 decision.decide(RelationMemberConflictDecisionType.KEEP);
310 relations.add(decision.getRelation());
311 } else {
312 decision.decide(RelationMemberConflictDecisionType.REMOVE);
313 }
314 }
315 model.refresh();
316 }
317
318 public void prepareDefaultDecisions() {
319 prepareDefaultTagDecisions();
320 prepareDefaultRelationDecisions();
321 }
322
323 protected JPanel buildEmptyConflictsPanel() {
324 JPanel pnl = new JPanel();
325 pnl.setLayout(new BorderLayout());
326 pnl.add(new JLabel(tr("No conflicts to resolve")));
327 return pnl;
328 }
329
330 protected void prepareGUIBeforeConflictResolutionStarts() {
331 MyRelationMemberConflictResolverModel relModel = getRelationMemberConflictResolverModel();
332 MyTagConflictResolverModel tagModel = getTagConflictResolverModel();
333 getContentPane().removeAll();
334
335 if (relModel.getNumDecisions() > 0 && tagModel.getNumDecisions() > 0) {
336 // display both, the dialog for resolving relation conflicts and for resolving
337 // tag conflicts
338 spTagConflictTypes.setTopComponent(pnlTagConflictResolver);
339 spTagConflictTypes.setBottomComponent(pnlRelationMemberConflictResolver);
340 getContentPane().add(spTagConflictTypes, BorderLayout.CENTER);
341 } else if (relModel.getNumDecisions() > 0) {
342 // relation conflicts only
343 //
344 getContentPane().add(pnlRelationMemberConflictResolver, BorderLayout.CENTER);
345 } else if (tagModel.getNumDecisions() > 0) {
346 // tag conflicts only
347 //
348 getContentPane().add(pnlTagConflictResolver, BorderLayout.CENTER);
349 } else {
350 getContentPane().add(buildEmptyConflictsPanel(), BorderLayout.CENTER);
351 }
352
353 getContentPane().add(pnlButtons, BorderLayout.SOUTH);
354 validate();
355 int numTagDecisions = getTagConflictResolverModel().getNumDecisions();
356 int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions();
357 if (numTagDecisions > 0 && numRelationDecisions > 0) {
358 spTagConflictTypes.setDividerLocation(0.5);
359 }
360 pnlRelationMemberConflictResolver.prepareForEditing();
361 }
362
363 protected void setCancelled(boolean cancelled) {
364 this.cancelled = cancelled;
365 }
366
367 public boolean isCancelled() {
368 return cancelled;
369 }
370
371 @Override
372 public void setVisible(boolean visible) {
373 if (visible) {
374 prepareGUIBeforeConflictResolutionStarts();
375 new WindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent,
376 new Dimension(600, 400))).applySafe(this);
377 setCancelled(false);
378 btnApply.requestFocusInWindow();
379 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
380 new WindowGeometry(this).remember(getClass().getName() + ".geometry");
381 }
382 super.setVisible(visible);
383 }
384
385 class CancelAction extends AbstractAction {
386
387 public CancelAction() {
388 putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution"));
389 putValue(Action.NAME, tr("Cancel"));
390 putValue(Action.SMALL_ICON, ImageProvider.get("", "cancel"));
391 setEnabled(true);
392 }
393
394 @Override
395 public void actionPerformed(ActionEvent arg0) {
396 setCancelled(true);
397 setVisible(false);
398 }
399 }
400
401 class ApplyAction extends AbstractAction implements PropertyChangeListener {
402
403 public ApplyAction() {
404 putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts"));
405 putValue(Action.NAME, tr("Apply"));
406 putValue(Action.SMALL_ICON, ImageProvider.get("ok"));
407 updateEnabledState();
408 }
409
410 @Override
411 public void actionPerformed(ActionEvent arg0) {
412 setVisible(false);
413 pnlTagConflictResolver.rememberPreferences();
414 }
415
416 protected void updateEnabledState() {
417 setEnabled(pnlTagConflictResolver.getModel().getNumConflicts() == 0
418 && pnlRelationMemberConflictResolver.getModel().getNumConflicts() == 0);
419 }
420
421 @Override
422 public void propertyChange(PropertyChangeEvent evt) {
423 if (evt.getPropertyName().equals(MyTagConflictResolverModel.NUM_CONFLICTS_PROP)) {
424 updateEnabledState();
425 }
426 if (evt.getPropertyName().equals(MyRelationMemberConflictResolverModel.NUM_CONFLICTS_PROP)) {
427 updateEnabledState();
428 }
429 }
430 }
431
432 class AdjustDividerLocationAction extends WindowAdapter {
433 @Override
434 public void windowOpened(WindowEvent e) {
435 int numTagDecisions = getTagConflictResolverModel().getNumDecisions();
436 int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions();
437 if (numTagDecisions > 0 && numRelationDecisions > 0) {
438 spTagConflictTypes.setDividerLocation(0.5);
439 }
440 }
441 }
442
443 static class AutoAdjustingSplitPane extends JSplitPane implements PropertyChangeListener, HierarchyBoundsListener {
444 private double dividerLocation;
445
446 public AutoAdjustingSplitPane(int newOrientation) {
447 super(newOrientation);
448 addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, this);
449 addHierarchyBoundsListener(this);
450 }
451
452 @Override
453 public void ancestorResized(HierarchyEvent e) {
454 setDividerLocation((int) (dividerLocation * getHeight()));
455 }
456
457 @Override
458 public void ancestorMoved(HierarchyEvent e) {
459 // do nothing
460 }
461
462 @Override
463 public void propertyChange(PropertyChangeEvent evt) {
464 if (evt.getPropertyName().equals(JSplitPane.DIVIDER_LOCATION_PROPERTY)) {
465 int newVal = (Integer) evt.getNewValue();
466 if (getHeight() != 0) {
467 dividerLocation = (double) newVal / (double) getHeight();
468 }
469 }
470 }
471 }
472
473 /**
474 * This model manages a list of conflicting relation members.
475 *
476 * It can be used as {@see TableModel}.
477 *
478 *
479 */
480 public static class MyRelationMemberConflictResolverModel extends DefaultTableModel {
481 /** the property name for the number conflicts managed by this model */
482 static public final String NUM_CONFLICTS_PROP = MyRelationMemberConflictResolverModel.class.getName() + ".numConflicts";
483
484 /** the list of conflict decisions */
485 private List<RelationMemberConflictDecision> decisions;
486 /** the collection of relations for which we manage conflicts */
487 private Collection<Relation> relations;
488 /** the number of conflicts */
489 private int numConflicts;
490 private PropertyChangeSupport support;
491
492 /**
493 * Replies the current number of conflicts
494 *
495 * @return the current number of conflicts
496 */
497 public int getNumConflicts() {
498 return numConflicts;
499 }
500
501 /**
502 * Updates the current number of conflicts from list of decisions and emits
503 * a property change event if necessary.
504 *
505 */
506 protected void updateNumConflicts() {
507 int count = 0;
508 for (RelationMemberConflictDecision decision: decisions) {
509 if (!decision.isDecided()) {
510 count++;
511 }
512 }
513 int oldValue = numConflicts;
514 numConflicts = count;
515 if (numConflicts != oldValue) {
516 support.firePropertyChange(NUM_CONFLICTS_PROP, oldValue, numConflicts);
517 }
518 }
519
520 public void addPropertyChangeListener(PropertyChangeListener l) {
521 support.addPropertyChangeListener(l);
522 }
523
524 public void removePropertyChangeListener(PropertyChangeListener l) {
525 support.removePropertyChangeListener(l);
526 }
527
528 public MyRelationMemberConflictResolverModel() {
529 decisions = new ArrayList<RelationMemberConflictDecision>();
530 support = new PropertyChangeSupport(this);
531 }
532
533 @Override
534 public int getRowCount() {
535 if (decisions == null) return 0;
536 return decisions.size();
537 }
538
539 @Override
540 public Object getValueAt(int row, int column) {
541 if (decisions == null) return null;
542
543 RelationMemberConflictDecision d = decisions.get(row);
544 switch(column) {
545 case 0: /* relation */ return d.getRelation();
546 case 1: /* pos */ return Integer.toString(d.getPos() + 1); // position in "user space" starting at 1
547 case 2: /* role */ return d.getRole();
548 case 3: /* original */ return d.getOriginalPrimitive();
549 case 4: /* decision */ return d.getDecision();
550 }
551 return null;
552 }
553
554 @Override
555 public void setValueAt(Object value, int row, int column) {
556 RelationMemberConflictDecision d = decisions.get(row);
557 switch(column) {
558 case 2: /* role */
559 d.setRole((String)value);
560 break;
561 case 4: /* decision */
562 d.decide((RelationMemberConflictDecisionType)value);
563 refresh();
564 break;
565 }
566 fireTableDataChanged();
567 }
568
569 /**
570 * Populates the model with the members of the relation <code>relation</code>
571 * referring to <code>primitive</code>.
572 *
573 * @param relation the parent relation
574 * @param primitive the child primitive
575 */
576 protected void populate(Relation relation, OsmPrimitive primitive, Map<Way, Way> oldWays) {
577 for (int i = 0; i<relation.getMembersCount(); i++) {
578 if (MergeOverlapAction.getOld(relation.getMember(i).getWay(), oldWays) == MergeOverlapAction.getOld((Way)primitive, oldWays)) {
579 decisions.add(new RelationMemberConflictDecision(relation, i));
580 }
581 }
582 }
583
584 /**
585 * Populates the model with the relation members belonging to one of the relations in <code>relations</code>
586 * and referring to one of the primitives in <code>memberPrimitives</code>.
587 *
588 * @param relations the parent relations. Empty list assumed if null.
589 * @param memberPrimitives the child primitives. Empty list assumed if null.
590 */
591 public void populate(Collection<Relation> relations, Collection<? extends OsmPrimitive> memberPrimitives, Map<Way, Way> oldWays) {
592 decisions.clear();
593
594 relations = relations == null ? new LinkedList<Relation>() : relations;
595 memberPrimitives = memberPrimitives == null ? new LinkedList<OsmPrimitive>() : memberPrimitives;
596 for (Relation r : relations) {
597 for (OsmPrimitive p: memberPrimitives) {
598 populate(r, p, oldWays);
599 }
600 }
601 this.relations = relations;
602 refresh();
603 }
604
605 /**
606 * Populates the model with the relation members represented as a collection of
607 * {@see RelationToChildReference}s.
608 *
609 * @param references the references. Empty list assumed if null.
610 */
611 public void populate(Collection<RelationToChildReference> references) {
612 references = references == null ? new LinkedList<RelationToChildReference>() : references;
613 decisions.clear();
614 this.relations = new HashSet<Relation>(references.size());
615 for (RelationToChildReference reference: references) {
616 decisions.add(new RelationMemberConflictDecision(reference.getParent(), reference.getPosition()));
617 relations.add(reference.getParent());
618 }
619 refresh();
620 }
621
622 /**
623 * Replies the decision at position <code>row</code>
624 *
625 * @param row
626 * @return the decision at position <code>row</code>
627 */
628 public RelationMemberConflictDecision getDecision(int row) {
629 return decisions.get(row);
630 }
631
632 /**
633 * Replies the number of decisions managed by this model
634 *
635 * @return the number of decisions managed by this model
636 */
637 public int getNumDecisions() {
638 return getRowCount();
639 }
640
641 /**
642 * Refreshes the model state. Invoke this method to trigger necessary change
643 * events after an update of the model data.
644 *
645 */
646 public void refresh() {
647 updateNumConflicts();
648 fireTableDataChanged();
649 }
650
651 /**
652 * Apply a role to all member managed by this model.
653 *
654 * @param role the role. Empty string assumed if null.
655 */
656 public void applyRole(String role) {
657 role = role == null ? "" : role;
658 for (RelationMemberConflictDecision decision : decisions) {
659 decision.setRole(role);
660 }
661 refresh();
662 }
663
664 protected RelationMemberConflictDecision getDecision(Relation relation, int pos) {
665 for(RelationMemberConflictDecision decision: decisions) {
666 if (decision.matches(relation, pos)) return decision;
667 }
668 return null;
669 }
670
671 protected void buildResolveCorrespondance(Relation relation, OsmPrimitive newPrimitive, Map<Relation, Relation> newRelations, Map<Way, Way> oldWays) {
672
673 List<RelationMember> relationsMembers = relation.getMembers();
674 Relation modifiedRelation = MergeOverlapAction.getNew(relation, newRelations);
675 modifiedRelation.setMembers(null);
676// boolean isChanged = false;
677 for (int i=0; i < relationsMembers.size(); i++) {
678 RelationMember rm = relationsMembers.get(i);
679// RelationMember rm = relation.getMember(i);
680// RelationMember rmNew;
681 RelationMemberConflictDecision decision = getDecision(relation, i);
682 if (decision == null) {
683 modifiedRelation.addMember(rm);
684 } else {
685 System.out.println(modifiedRelation);
686 System.out.println(111);
687 switch(decision.getDecision()) {
688 case KEEP:
689// modifiedRelation.removeMembersFor(newPrimitive);
690 System.out.println(222);
691 if (newPrimitive instanceof Way) {
692 modifiedRelation.addMember(new RelationMember(decision.getRole(), MergeOverlapAction.getOld((Way)newPrimitive, oldWays)));
693 }
694 else {
695 modifiedRelation.addMember(new RelationMember(decision.getRole(), newPrimitive));
696 }
697// modifiedRelation.addMember(new RelationMember(decision.getRole(), newPrimitive));
698 break;
699 case REMOVE:
700 System.out.println(333);
701// modifiedRelation.removeMembersFor(rm.getMember());
702// isChanged = true;
703 // do nothing
704 break;
705 case UNDECIDED:
706 // FIXME: this is an error
707 break;
708 }
709 }
710 }
711 }
712
713 /**
714 * Builds a collection of commands executing the decisions made in this model.
715 *
716 * @param newPrimitive the primitive which members shall refer to if the
717 * decision is {@see RelationMemberConflictDecisionType#REPLACE}
718 * @return a list of commands
719 */
720 public void buildRelationCorrespondance(OsmPrimitive newPrimitive, Map<Relation, Relation> newRelations, Map<Way, Way> oldWays) {
721 for (Relation relation : relations) {
722 buildResolveCorrespondance(relation, newPrimitive, newRelations, oldWays);
723 }
724 }
725
726 protected boolean isChanged(Relation relation, OsmPrimitive newPrimitive) {
727 for (int i=0; i < relation.getMembersCount(); i++) {
728 RelationMemberConflictDecision decision = getDecision(relation, i);
729 if (decision == null) {
730 continue;
731 }
732 switch(decision.getDecision()) {
733 case REMOVE: return true;
734 case KEEP:
735 if (!relation.getMember(i).getRole().equals(decision.getRole()))
736 return true;
737 if (relation.getMember(i).getMember() != newPrimitive)
738 return true;
739 case UNDECIDED:
740 // FIXME: handle error
741 }
742 }
743 return false;
744 }
745
746 /**
747 * Replies the set of relations which have to be modified according
748 * to the decisions managed by this model.
749 *
750 * @param newPrimitive the primitive which members shall refer to if the
751 * decision is {@see RelationMemberConflictDecisionType#REPLACE}
752 *
753 * @return the set of relations which have to be modified according
754 * to the decisions managed by this model
755 */
756 public Set<Relation> getModifiedRelations(OsmPrimitive newPrimitive) {
757 HashSet<Relation> ret = new HashSet<Relation>();
758 for (Relation relation: relations) {
759 if (isChanged(relation, newPrimitive)) {
760 ret.add(relation);
761 }
762 }
763 return ret;
764 }
765 }
766
767 public class MyRelationMemberConflictResolver extends JPanel {
768
769 private AutoCompletingTextField tfRole;
770 private AutoCompletingTextField tfKey;
771 private AutoCompletingTextField tfValue;
772 private JCheckBox cbTagRelations;
773 private MyRelationMemberConflictResolverModel model;
774 private MyRelationMemberConflictResolverTable tblResolver;
775 private JMultilineLabel lblHeader;
776
777 protected void build() {
778 setLayout(new GridBagLayout());
779 JPanel pnl = new JPanel();
780 pnl.setLayout(new BorderLayout());
781 pnl.add(lblHeader = new JMultilineLabel(""));
782 GridBagConstraints gc = new GridBagConstraints();
783 gc.fill = GridBagConstraints.HORIZONTAL;
784 gc.weighty = 0.0;
785 gc.weightx = 1.0;
786 gc.insets = new Insets(5,5,5,5);
787 add(pnl, gc);
788 model = new MyRelationMemberConflictResolverModel();
789
790 gc.gridy = 1;
791 gc.weighty = 1.0;
792 gc.fill = GridBagConstraints.BOTH;
793 gc.insets = new Insets(0,0,0,0);
794 add(new JScrollPane(tblResolver = new MyRelationMemberConflictResolverTable(model)), gc);
795 pnl = new JPanel();
796 pnl.setLayout(new BoxLayout(pnl, BoxLayout.Y_AXIS));
797 pnl.add(buildRoleEditingPanel());
798 pnl.add(buildTagRelationsPanel());
799 gc.gridy = 2;
800 gc.weighty = 0.0;
801 gc.fill = GridBagConstraints.HORIZONTAL;
802 add(pnl,gc);
803 }
804
805 protected JPanel buildRoleEditingPanel() {
806 JPanel pnl = new JPanel();
807 pnl.setLayout(new FlowLayout(FlowLayout.LEFT));
808 pnl.add(new JLabel(tr("Role:")));
809 pnl.add(tfRole = new AutoCompletingTextField(10));
810 tfRole.setToolTipText(tr("Enter a role for all relation memberships"));
811 pnl.add(new JButton(new ApplyRoleAction()));
812 tfRole.addActionListener(new ApplyRoleAction());
813 tfRole.addFocusListener(
814 new FocusAdapter() {
815 @Override
816 public void focusGained(FocusEvent e) {
817 tfRole.selectAll();
818 }
819 }
820 );
821 return pnl;
822 }
823
824 protected JPanel buildTagRelationsPanel() {
825 JPanel pnl = new JPanel();
826 pnl.setLayout(new FlowLayout(FlowLayout.LEFT));
827 cbTagRelations = new JCheckBox(tr("Tag modified relations with "));
828 cbTagRelations.addChangeListener(new ToggleTagRelationsAction());
829 cbTagRelations.setToolTipText(
830 tr("<html>Select to enable entering a tag which will be applied<br>"
831 + "to all modified relations.</html>"));
832 pnl.add(cbTagRelations);
833 pnl.add(new JLabel(trc("tag", "Key:")));
834 pnl.add(tfKey = new AutoCompletingTextField(10));
835 tfKey.setToolTipText(tr("<html>Enter a tag key, i.e. <strong><tt>fixme</tt></strong></html>"));
836 pnl.add(new JLabel(tr("Value:")));
837 pnl.add(tfValue = new AutoCompletingTextField(10));
838 tfValue.setToolTipText(tr("<html>Enter a tag value, i.e. <strong><tt>check members</tt></strong></html>"));
839 cbTagRelations.setSelected(false);
840 tfKey.setEnabled(false);
841 tfValue.setEnabled(false);
842 return pnl;
843 }
844
845 public MyRelationMemberConflictResolver() {
846 build();
847 }
848
849 public void initForWayCombining() {
850 lblHeader.setText(tr("<html>The combined ways are members in one ore more relations. "
851 + "Please decide whether you want to <strong>keep</strong> these memberships "
852 + "for the combined way or whether you want to <strong>remove</strong> them.<br>"
853 + "The default is to <strong>keep</strong> the first way and <strong>remove</strong> "
854 + "the other ways that are members of the same relation: the combined way will "
855 + "take the place of the original way in the relation."
856 + "</html>"));
857 invalidate();
858 }
859
860 public void initForNodeMerging() {
861 lblHeader.setText(tr("<html>The merged nodes are members in one ore more relations. "
862 + "Please decide whether you want to <strong>keep</strong> these memberships "
863 + "for the target node or whether you want to <strong>remove</strong> them.<br>"
864 + "The default is to <strong>keep</strong> the first node and <strong>remove</strong> "
865 + "the other nodes that are members of the same relation: the target node will "
866 + "take the place of the original node in the relation."
867 + "</html>"));
868 invalidate();
869 }
870
871 class ApplyRoleAction extends AbstractAction {
872 public ApplyRoleAction() {
873 putValue(NAME, tr("Apply"));
874 putValue(SMALL_ICON, ImageProvider.get("ok"));
875 putValue(SHORT_DESCRIPTION, tr("Apply this role to all members"));
876 }
877
878 @Override
879 public void actionPerformed(ActionEvent e) {
880 model.applyRole(tfRole.getText());
881 }
882 }
883
884 class ToggleTagRelationsAction implements ChangeListener {
885 @Override
886 public void stateChanged(ChangeEvent e) {
887 ButtonModel buttonModel = ((AbstractButton) e.getSource()).getModel();
888 tfKey.setEnabled(buttonModel.isSelected());
889 tfValue.setEnabled(buttonModel.isSelected());
890 tfKey.setBackground(buttonModel.isSelected() ? UIManager.getColor("TextField.background") : UIManager
891 .getColor("Panel.background"));
892 tfValue.setBackground(buttonModel.isSelected() ? UIManager.getColor("TextField.background") : UIManager
893 .getColor("Panel.background"));
894 }
895 }
896
897 public MyRelationMemberConflictResolverModel getModel() {
898 return model;
899 }
900
901 public Command buildTagApplyCommands(Collection<? extends OsmPrimitive> primitives) {
902 if (!cbTagRelations.isSelected())
903 return null;
904 if (tfKey.getText().trim().equals(""))
905 return null;
906 if (tfValue.getText().trim().equals(""))
907 return null;
908 if (primitives == null || primitives.isEmpty())
909 return null;
910 return new ChangePropertyCommand(primitives, tfKey.getText(), tfValue.getText());
911 }
912
913 public void prepareForEditing() {
914 AutoCompletionList acList = new AutoCompletionList();
915 Main.main.getEditLayer().data.getAutoCompletionManager().populateWithMemberRoles(acList);
916 tfRole.setAutoCompletionList(acList);
917 AutoCompletingTextField editor = (AutoCompletingTextField) tblResolver.getColumnModel().getColumn(2).getCellEditor();
918 if (editor != null) {
919 editor.setAutoCompletionList(acList);
920 }
921 AutoCompletionList acList2 = new AutoCompletionList();
922 Main.main.getEditLayer().data.getAutoCompletionManager().populateWithKeys(acList2);
923 tfKey.setAutoCompletionList(acList2);
924 }
925 }
926
927
928 public class MyRelationMemberConflictResolverTable extends JTable implements MultiValueCellEditor.NavigationListener {
929
930 private SelectNextColumnCellAction selectNextColumnCellAction;
931 private SelectPreviousColumnCellAction selectPreviousColumnCellAction;
932
933 public MyRelationMemberConflictResolverTable(MyRelationMemberConflictResolverModel model) {
934 super(model, new RelationMemberConflictResolverColumnModel());
935 build();
936 }
937
938 protected void build() {
939 setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
940 setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
941 putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
942
943 // make ENTER behave like TAB
944 //
945 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
946 KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
947
948 // install custom navigation actions
949 //
950 selectNextColumnCellAction = new SelectNextColumnCellAction();
951 selectPreviousColumnCellAction = new SelectPreviousColumnCellAction();
952 getActionMap().put("selectNextColumnCell", selectNextColumnCellAction);
953 getActionMap().put("selectPreviousColumnCell", selectPreviousColumnCellAction);
954
955 setRowHeight((int)new JComboBox().getPreferredSize().getHeight());
956 }
957
958 /**
959 * Action to be run when the user navigates to the next cell in the table, for instance by
960 * pressing TAB or ENTER. The action alters the standard navigation path from cell to cell: <ul>
961 * <li>it jumps over cells in the first column</li> <li>it automatically add a new empty row
962 * when the user leaves the last cell in the table</li> <ul>
963 *
964 *
965 */
966 class SelectNextColumnCellAction extends AbstractAction {
967 @Override
968 public void actionPerformed(ActionEvent e) {
969 run();
970 }
971
972 public void run() {
973 int col = getSelectedColumn();
974 int row = getSelectedRow();
975 if (getCellEditor() != null) {
976 getCellEditor().stopCellEditing();
977 }
978
979 if (col == 2 && row < getRowCount() - 1) {
980 row++;
981 } else if (row < getRowCount() - 1) {
982 col = 2;
983 row++;
984 }
985 changeSelection(row, col, false, false);
986 editCellAt(getSelectedRow(), getSelectedColumn());
987 getEditorComponent().requestFocusInWindow();
988 }
989 }
990
991 /**
992 * Action to be run when the user navigates to the previous cell in the table, for instance by
993 * pressing Shift-TAB
994 *
995 */
996 class SelectPreviousColumnCellAction extends AbstractAction {
997
998 @Override
999 public void actionPerformed(ActionEvent e) {
1000 run();
1001 }
1002
1003 public void run() {
1004 int col = getSelectedColumn();
1005 int row = getSelectedRow();
1006 if (getCellEditor() != null) {
1007 getCellEditor().stopCellEditing();
1008 }
1009
1010 if (col <= 0 && row <= 0) {
1011 // change nothing
1012 } else if (row > 0) {
1013 col = 2;
1014 row--;
1015 }
1016 changeSelection(row, col, false, false);
1017 editCellAt(getSelectedRow(), getSelectedColumn());
1018 getEditorComponent().requestFocusInWindow();
1019 }
1020 }
1021
1022 @Override
1023 public void gotoNextDecision() {
1024 selectNextColumnCellAction.run();
1025 }
1026
1027 @Override
1028 public void gotoPreviousDecision() {
1029 selectPreviousColumnCellAction.run();
1030 }
1031 }
1032
1033
1034 public static class MyTagConflictResolverModel extends DefaultTableModel {
1035 static public final String NUM_CONFLICTS_PROP = MyTagConflictResolverModel.class.getName() + ".numConflicts";
1036
1037 private TagCollection tags;
1038 private List<String> displayedKeys;
1039 private Set<String> keysWithConflicts;
1040 private HashMap<String, MultiValueResolutionDecision> decisions;
1041 private int numConflicts;
1042 private PropertyChangeSupport support;
1043 private boolean showTagsWithConflictsOnly = false;
1044 private boolean showTagsWithMultiValuesOnly = false;
1045
1046 public MyTagConflictResolverModel() {
1047 numConflicts = 0;
1048 support = new PropertyChangeSupport(this);
1049 }
1050
1051 public void addPropertyChangeListener(PropertyChangeListener listener) {
1052 support.addPropertyChangeListener(listener);
1053 }
1054
1055 public void removePropertyChangeListener(PropertyChangeListener listener) {
1056 support.removePropertyChangeListener(listener);
1057 }
1058
1059 protected void setNumConflicts(int numConflicts) {
1060 int oldValue = this.numConflicts;
1061 this.numConflicts = numConflicts;
1062 if (oldValue != this.numConflicts) {
1063 support.firePropertyChange(NUM_CONFLICTS_PROP, oldValue, this.numConflicts);
1064 }
1065 }
1066
1067 protected void refreshNumConflicts() {
1068 int count = 0;
1069 for (MultiValueResolutionDecision d : decisions.values()) {
1070 if (!d.isDecided()) {
1071 count++;
1072 }
1073 }
1074 setNumConflicts(count);
1075 }
1076
1077 protected void sort() {
1078 Collections.sort(
1079 displayedKeys,
1080 new Comparator<String>() {
1081 @Override
1082 public int compare(String key1, String key2) {
1083 if (decisions.get(key1).isDecided() && ! decisions.get(key2).isDecided())
1084 return 1;
1085 else if (!decisions.get(key1).isDecided() && decisions.get(key2).isDecided())
1086 return -1;
1087 return key1.compareTo(key2);
1088 }
1089 }
1090 );
1091 }
1092
1093 /**
1094 * initializes the model from the current tags
1095 *
1096 */
1097 protected void rebuild() {
1098 if (tags == null) return;
1099 for(String key: tags.getKeys()) {
1100 MultiValueResolutionDecision decision = new MultiValueResolutionDecision(tags.getTagsFor(key));
1101 if (decisions.get(key) == null) {
1102 decisions.put(key,decision);
1103 }
1104 }
1105 displayedKeys.clear();
1106 Set<String> keys = tags.getKeys();
1107 if (showTagsWithConflictsOnly) {
1108 keys.retainAll(keysWithConflicts);
1109 if (showTagsWithMultiValuesOnly) {
1110 Set<String> keysWithMultiValues = new HashSet<String>();
1111 for (String key: keys) {
1112 if (decisions.get(key).canKeepAll()) {
1113 keysWithMultiValues.add(key);
1114 }
1115 }
1116 keys.retainAll(keysWithMultiValues);
1117 }
1118 for (String key: tags.getKeys()) {
1119 if (!decisions.get(key).isDecided() && !keys.contains(key)) {
1120 keys.add(key);
1121 }
1122 }
1123 }
1124 displayedKeys.addAll(keys);
1125 refreshNumConflicts();
1126 sort();
1127 fireTableDataChanged();
1128 }
1129
1130 /**
1131 * Populates the model with the tags for which conflicts are to be resolved.
1132 *
1133 * @param tags the tag collection with the tags. Must not be null.
1134 * @param keysWithConflicts the set of tag keys with conflicts
1135 * @throws IllegalArgumentException thrown if tags is null
1136 */
1137 public void populate(TagCollection tags, Set<String> keysWithConflicts) {
1138 CheckParameterUtil.ensureParameterNotNull(tags, "tags");
1139 this.tags = tags;
1140 displayedKeys = new ArrayList<String>();
1141 this.keysWithConflicts = keysWithConflicts == null ? new HashSet<String>() : keysWithConflicts;
1142 decisions = new HashMap<String, MultiValueResolutionDecision>();
1143 rebuild();
1144 }
1145
1146 @Override
1147 public int getRowCount() {
1148 if (displayedKeys == null) return 0;
1149 return displayedKeys.size();
1150 }
1151
1152 @Override
1153 public Object getValueAt(int row, int column) {
1154 return decisions.get(displayedKeys.get(row));
1155 }
1156
1157 @Override
1158 public boolean isCellEditable(int row, int column) {
1159 return column == 2;
1160 }
1161
1162 @Override
1163 public void setValueAt(Object value, int row, int column) {
1164 MultiValueResolutionDecision decision = decisions.get(displayedKeys.get(row));
1165 if (value instanceof String) {
1166 decision.keepOne((String)value);
1167 } else if (value instanceof MultiValueDecisionType) {
1168 MultiValueDecisionType type = (MultiValueDecisionType)value;
1169 switch(type) {
1170 case KEEP_NONE:
1171 decision.keepNone();
1172 break;
1173 case KEEP_ALL:
1174 decision.keepAll();
1175 break;
1176 }
1177 }
1178 fireTableDataChanged();
1179 refreshNumConflicts();
1180 }
1181
1182 /**
1183 * Replies true if each {@see MultiValueResolutionDecision} is decided.
1184 *
1185 * @return true if each {@see MultiValueResolutionDecision} is decided; false
1186 * otherwise
1187 */
1188 public boolean isResolvedCompletely() {
1189 return numConflicts == 0;
1190 }
1191
1192 public int getNumConflicts() {
1193 return numConflicts;
1194 }
1195
1196 public int getNumDecisions() {
1197 return getRowCount();
1198 }
1199
1200 //TODO Should this method work with all decisions or only with displayed decisions? For MergeNodes it should be
1201 //all decisions, but this method is also used on other places, so I've made new method just for MergeNodes
1202 public TagCollection getResolution() {
1203 TagCollection tc = new TagCollection();
1204 for (String key: displayedKeys) {
1205 tc.add(decisions.get(key).getResolution());
1206 }
1207 return tc;
1208 }
1209
1210 public TagCollection getAllResolutions() {
1211 TagCollection tc = new TagCollection();
1212 for (MultiValueResolutionDecision value: decisions.values()) {
1213 tc.add(value.getResolution());
1214 }
1215 return tc;
1216 }
1217
1218 public MultiValueResolutionDecision getDecision(int row) {
1219 return decisions.get(displayedKeys.get(row));
1220 }
1221
1222 /**
1223 * Sets whether all tags or only tags with conflicts are displayed
1224 *
1225 * @param showTagsWithConflictsOnly if true, only tags with conflicts are displayed
1226 */
1227 public void setShowTagsWithConflictsOnly(boolean showTagsWithConflictsOnly) {
1228 this.showTagsWithConflictsOnly = showTagsWithConflictsOnly;
1229 rebuild();
1230 }
1231
1232 /**
1233 * Sets whether all conflicts or only conflicts with multiple values are displayed
1234 *
1235 * @param showTagsWithMultiValuesOnly if true, only tags with multiple values are displayed
1236 */
1237 public void setShowTagsWithMultiValuesOnly(boolean showTagsWithMultiValuesOnly) {
1238 this.showTagsWithMultiValuesOnly = showTagsWithMultiValuesOnly;
1239 rebuild();
1240 }
1241
1242 /**
1243 * Prepare the default decisions for the current model
1244 *
1245 */
1246 public void prepareDefaultTagDecisions() {
1247 for (MultiValueResolutionDecision decision: decisions.values()) {
1248 List<String> values = decision.getValues();
1249 values.remove("");
1250 if (values.size() == 1) {
1251 decision.keepOne(values.get(0));
1252 } else {
1253 decision.keepAll();
1254 }
1255 }
1256 rebuild();
1257 }
1258
1259 }
1260
1261
1262 /**
1263 * This is a UI widget for resolving tag conflicts, i.e. differences of the tag values
1264 * of multiple {@see OsmPrimitive}s.
1265 *
1266 *
1267 */
1268 public class MyTagConflictResolver extends JPanel {
1269
1270 /** the model for the tag conflict resolver */
1271 private MyTagConflictResolverModel model;
1272 /** selects wheter only tags with conflicts are displayed */
1273 private JCheckBox cbShowTagsWithConflictsOnly;
1274 private JCheckBox cbShowTagsWithMultiValuesOnly;
1275
1276 protected JPanel buildInfoPanel() {
1277 JPanel pnl = new JPanel();
1278 pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
1279 pnl.setLayout(new GridBagLayout());
1280 GridBagConstraints gc = new GridBagConstraints();
1281 gc.fill = GridBagConstraints.BOTH;
1282 gc.weighty = 1.0;
1283 gc.weightx = 1.0;
1284 gc.anchor = GridBagConstraints.LINE_START;
1285 pnl.add(new JLabel(tr("<html>Please select the values to keep for the following tags.</html>")), gc);
1286
1287 gc.gridy = 1;
1288 gc.fill = GridBagConstraints.HORIZONTAL;
1289 gc.weighty = 0.0;
1290 pnl.add(cbShowTagsWithConflictsOnly = new JCheckBox(tr("Show tags with conflicts only")), gc);
1291 pnl.add(cbShowTagsWithMultiValuesOnly = new JCheckBox(tr("Show tags with multiple values only")), gc);
1292 cbShowTagsWithConflictsOnly.addChangeListener(
1293 new ChangeListener() {
1294 @Override
1295 public void stateChanged(ChangeEvent e) {
1296 model.setShowTagsWithConflictsOnly(cbShowTagsWithConflictsOnly.isSelected());
1297 cbShowTagsWithMultiValuesOnly.setEnabled(cbShowTagsWithConflictsOnly.isSelected());
1298 }
1299 }
1300 );
1301 cbShowTagsWithConflictsOnly.setSelected(
1302 Main.pref.getBoolean(getClass().getName() + ".showTagsWithConflictsOnly", false)
1303 );
1304 cbShowTagsWithMultiValuesOnly.addChangeListener(
1305 new ChangeListener() {
1306 @Override
1307 public void stateChanged(ChangeEvent e) {
1308 model.setShowTagsWithMultiValuesOnly(cbShowTagsWithMultiValuesOnly.isSelected());
1309 }
1310 }
1311 );
1312 cbShowTagsWithMultiValuesOnly.setSelected(
1313 Main.pref.getBoolean(getClass().getName() + ".showTagsWithMultiValuesOnly", false)
1314 );
1315 cbShowTagsWithMultiValuesOnly.setEnabled(cbShowTagsWithConflictsOnly.isSelected());
1316 return pnl;
1317 }
1318
1319 /**
1320 * Remembers the current settings in the global preferences
1321 *
1322 */
1323 public void rememberPreferences() {
1324 Main.pref.put(getClass().getName() + ".showTagsWithConflictsOnly", cbShowTagsWithConflictsOnly.isSelected());
1325 Main.pref.put(getClass().getName() + ".showTagsWithMultiValuesOnly", cbShowTagsWithMultiValuesOnly.isSelected());
1326 }
1327
1328 protected void build() {
1329 setLayout(new BorderLayout());
1330 add(buildInfoPanel(), BorderLayout.NORTH);
1331 add(new JScrollPane(new MyTagConflictResolverTable(model)), BorderLayout.CENTER);
1332 }
1333
1334 public MyTagConflictResolver() {
1335 this.model = new MyTagConflictResolverModel();
1336 build();
1337 }
1338
1339 /**
1340 * Replies the model used by this dialog
1341 *
1342 * @return the model
1343 */
1344 public MyTagConflictResolverModel getModel() {
1345 return model;
1346 }
1347 }
1348
1349 public class MyTagConflictResolverTable extends JTable implements MultiValueCellEditor.NavigationListener {
1350
1351 private SelectNextColumnCellAction selectNextColumnCellAction;
1352 private SelectPreviousColumnCellAction selectPreviousColumnCellAction;
1353
1354 public MyTagConflictResolverTable(MyTagConflictResolverModel model) {
1355 super(model, new TagConflictResolverColumnModel());
1356 build();
1357 }
1358
1359 protected void build() {
1360 setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
1361 setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
1362 putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
1363
1364 // make ENTER behave like TAB
1365 //
1366 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
1367 KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
1368
1369 // install custom navigation actions
1370 //
1371 selectNextColumnCellAction = new SelectNextColumnCellAction();
1372 selectPreviousColumnCellAction = new SelectPreviousColumnCellAction();
1373 getActionMap().put("selectNextColumnCell", selectNextColumnCellAction);
1374 getActionMap().put("selectPreviousColumnCell", selectPreviousColumnCellAction);
1375
1376 ((MultiValueCellEditor)getColumnModel().getColumn(2).getCellEditor()).addNavigationListeners(this);
1377
1378 setRowHeight((int)new JComboBox().getPreferredSize().getHeight());
1379 }
1380
1381 /**
1382 * Action to be run when the user navigates to the next cell in the table, for instance by
1383 * pressing TAB or ENTER. The action alters the standard navigation path from cell to cell: <ul>
1384 * <li>it jumps over cells in the first column</li> <li>it automatically add a new empty row
1385 * when the user leaves the last cell in the table</li> <ul>
1386 *
1387 *
1388 */
1389 class SelectNextColumnCellAction extends AbstractAction {
1390 @Override
1391 public void actionPerformed(ActionEvent e) {
1392 run();
1393 }
1394
1395 public void run() {
1396 int col = getSelectedColumn();
1397 int row = getSelectedRow();
1398 if (getCellEditor() != null) {
1399 getCellEditor().stopCellEditing();
1400 }
1401
1402 if (col == 2 && row < getRowCount() - 1) {
1403 row++;
1404 } else if (row < getRowCount() - 1) {
1405 col = 2;
1406 row++;
1407 }
1408 changeSelection(row, col, false, false);
1409 editCellAt(getSelectedRow(), getSelectedColumn());
1410 getEditorComponent().requestFocusInWindow();
1411 }
1412 }
1413
1414 /**
1415 * Action to be run when the user navigates to the previous cell in the table, for instance by
1416 * pressing Shift-TAB
1417 *
1418 */
1419 class SelectPreviousColumnCellAction extends AbstractAction {
1420
1421 @Override
1422 public void actionPerformed(ActionEvent e) {
1423 run();
1424 }
1425
1426 public void run() {
1427 int col = getSelectedColumn();
1428 int row = getSelectedRow();
1429 if (getCellEditor() != null) {
1430 getCellEditor().stopCellEditing();
1431 }
1432
1433 if (col <= 0 && row <= 0) {
1434 // change nothing
1435 } else if (row > 0) {
1436 col = 2;
1437 row--;
1438 }
1439 changeSelection(row, col, false, false);
1440 editCellAt(getSelectedRow(), getSelectedColumn());
1441 getEditorComponent().requestFocusInWindow();
1442 }
1443 }
1444
1445 @Override
1446 public void gotoNextDecision() {
1447 selectNextColumnCellAction.run();
1448 }
1449
1450 @Override
1451 public void gotoPreviousDecision() {
1452 selectPreviousColumnCellAction.run();
1453 }
1454 }
1455}
Note: See TracBrowser for help on using the repository browser.