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

Last change on this file since 27650 was 27650, checked in by stoecker, 13 years ago

fix some small bugs for new plugins

File size: 20.8 KB
Line 
1package mergeoverlap;
2
3import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.combineTigerTags;
4import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.completeTagCollectionForEditing;
5import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing;
6import static org.openstreetmap.josm.tools.I18n.tr;
7
8import java.awt.event.ActionEvent;
9import java.awt.event.KeyEvent;
10import java.util.ArrayList;
11import java.util.Arrays;
12import java.util.Collection;
13import java.util.Collections;
14import java.util.HashMap;
15import java.util.HashSet;
16import java.util.Iterator;
17import java.util.LinkedHashSet;
18import java.util.LinkedList;
19import java.util.List;
20import java.util.Map;
21import java.util.Set;
22
23import javax.swing.JOptionPane;
24
25import org.openstreetmap.josm.Main;
26import org.openstreetmap.josm.actions.JosmAction;
27import org.openstreetmap.josm.actions.SplitWayAction;
28import org.openstreetmap.josm.actions.CombineWayAction.NodeGraph;
29import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult;
30import org.openstreetmap.josm.command.AddCommand;
31import org.openstreetmap.josm.command.ChangeCommand;
32import org.openstreetmap.josm.command.Command;
33import org.openstreetmap.josm.command.DeleteCommand;
34import org.openstreetmap.josm.command.SequenceCommand;
35import org.openstreetmap.josm.corrector.ReverseWayTagCorrector;
36import org.openstreetmap.josm.corrector.UserCancelException;
37import org.openstreetmap.josm.data.osm.Node;
38import org.openstreetmap.josm.data.osm.OsmPrimitive;
39import org.openstreetmap.josm.data.osm.Relation;
40import org.openstreetmap.josm.data.osm.RelationMember;
41import org.openstreetmap.josm.data.osm.TagCollection;
42import org.openstreetmap.josm.data.osm.Way;
43import org.openstreetmap.josm.gui.layer.OsmDataLayer;
44import org.openstreetmap.josm.tools.Pair;
45import org.openstreetmap.josm.tools.Shortcut;
46
47/**
48 * Merge overlapping part of ways.
49 */
50@SuppressWarnings("serial")
51public class MergeOverlapAction extends JosmAction {
52
53 public MergeOverlapAction() {
54 super(tr("Merge overlap"), "merge_overlap",
55 tr("Merge overlap of ways."),
56 Shortcut.registerShortcut("tools:mergeoverlap",tr("Tool: {0}", tr("Merge overlap")), KeyEvent.VK_O,
57 Shortcut.GROUP_EDIT, Shortcut.SHIFT_DEFAULT)
58 , true);
59 }
60
61 Map<Way, List<Relation>> relations = new HashMap<Way, List<Relation>>();
62 Map<Way, Way> oldWays = new HashMap<Way, Way>();
63 Map<Relation, Relation> newRelations = new HashMap<Relation, Relation>();
64 Set<Way> deletes = new HashSet<Way>();
65
66 /**
67 * The action button has been clicked
68 *
69 * @param e
70 * Action Event
71 */
72 public void actionPerformed(ActionEvent e) {
73
74 // List of selected ways
75 List<Way> ways = new ArrayList<Way>();
76 relations.clear();
77 newRelations.clear();
78
79 // For every selected way
80 for (OsmPrimitive osm : Main.main.getCurrentDataSet().getSelected()) {
81 if (osm instanceof Way && !osm.isDeleted()) {
82 Way way = (Way) osm;
83 ways.add(way);
84 List<Relation> rels = new ArrayList<Relation>();
85 for (Relation r : OsmPrimitive.getFilteredList(way
86 .getReferrers(), Relation.class)) {
87 rels.add(r);
88 }
89 relations.put(way, rels);
90 }
91 }
92
93 List<Way> sel = new ArrayList<Way>(ways);
94 Collection<Command> cmds = new LinkedList<Command>();
95
96 // *****
97 // split
98 // *****
99 for (Way way : ways) {
100 Set<Node> nodes = new HashSet<Node>();
101 for (Way opositWay : ways) {
102 if (way != opositWay) {
103 List<NodePos> nodesPos = new LinkedList<NodePos>();
104
105 int pos = 0;
106 for (Node node : way.getNodes()) {
107 int opositPos = 0;
108 for (Node opositNode : opositWay.getNodes()) {
109 if (node == opositNode) {
110 if (opositWay.isClosed()) {
111 opositPos %= opositWay.getNodesCount() - 1;
112 }
113 nodesPos.add(new NodePos(node, pos, opositPos));
114 break;
115 }
116 opositPos++;
117 }
118 pos++;
119 }
120
121 NodePos start = null;
122 NodePos end = null;
123 int increment = 0;
124
125 boolean hasFirst = false;
126 for (NodePos node : nodesPos) {
127 if (start == null) {
128 start = node;
129 } else {
130 if (end == null) {
131 if (follows(way, opositWay, start, node, 1)) {
132 end = node;
133 increment = +1;
134 } else if (follows(way, opositWay, start, node,
135 -1)) {
136 end = node;
137 increment = -1;
138 } else {
139 start = node;
140 end = null;
141 }
142 } else {
143 if (follows(way, opositWay, end, node,
144 increment)) {
145 end = node;
146 } else {
147 hasFirst = addNodes(start, end, way, nodes,
148 hasFirst);
149 start = node;
150 end = null;
151 }
152 }
153 }
154 }
155
156 if (start != null && end != null) {
157 hasFirst = addNodes(start, end, way, nodes, hasFirst);
158 start = null;
159 end = null;
160 }
161 }
162 }
163 if (!nodes.isEmpty() && !way.isClosed() || nodes.size() >= 2) {
164 List<List<Node>> wayChunks = SplitWayAction.buildSplitChunks(
165 way, new ArrayList<Node>(nodes));
166 SplitWayResult result = splitWay(getEditLayer(), way, wayChunks);
167
168 cmds.add(result.getCommand());
169 sel.remove(way);
170 sel.add(result.getOriginalWay());
171 sel.addAll(result.getNewWays());
172 List<Relation> rels = relations.remove(way);
173 relations.put(result.getOriginalWay(), rels);
174 for (Way w : result.getNewWays()) {
175 relations.put(w, rels);
176 }
177 }
178 }
179
180 // *****
181 // merge
182 // *****
183 ways = new ArrayList<Way>(sel);
184 while (!ways.isEmpty()) {
185 Way way = ways.get(0);
186 List<Way> combine = new ArrayList<Way>();
187 combine.add(way);
188 for (Way opositWay : ways) {
189 if (way != opositWay
190 && way.getNodesCount() == opositWay.getNodesCount()) {
191 boolean equals1 = true;
192 for (int i = 0; i < way.getNodesCount(); i++) {
193 if (way.getNode(i) != opositWay.getNode(i)) {
194 equals1 = false;
195 break;
196 }
197 }
198 boolean equals2 = true;
199 for (int i = 0; i < way.getNodesCount(); i++) {
200 if (way.getNode(i) != opositWay.getNode(way
201 .getNodesCount()
202 - i - 1)) {
203 equals2 = false;
204 break;
205 }
206 }
207 if (equals1 || equals2) {
208 combine.add(opositWay);
209 }
210 }
211 }
212 ways.removeAll(combine);
213 if (combine.size() > 1) {
214 sel.removeAll(combine);
215 // combine
216 Pair<Way, List<Command>> combineResult;
217 try {
218 combineResult = combineWaysWorker(combine);
219 } catch (UserCancelException e1) {
220 return;
221 }
222 sel.add(combineResult.a);
223 cmds.addAll(combineResult.b);
224 }
225 }
226
227 for (Relation old : newRelations.keySet()) {
228 cmds.add(new ChangeCommand(old, newRelations.get(old)));
229 }
230
231 List<Way> del = new LinkedList<Way>();
232 for (Way w : deletes) {
233 if (!w.isDeleted()) {
234 del.add(w);
235 }
236 }
237 if (!del.isEmpty()) {
238 cmds.add(new DeleteCommand(del));
239 }
240
241 // Commit
242 Main.main.undoRedo.add(new SequenceCommand(
243 tr("Merge Overlap (combine)"), cmds));
244 getCurrentDataSet().setSelected(sel);
245 Main.map.repaint();
246
247 relations.clear();
248 newRelations.clear();
249 oldWays.clear();
250 }
251
252 private class NodePos {
253 Node node;
254 int pos;
255 int opositPos;
256
257 NodePos(Node n, int p, int op) {
258 node = n;
259 pos = p;
260 opositPos = op;
261 }
262
263 public String toString() {
264 return "NodePos: " + pos + ", " + opositPos + ", " + node;
265 }
266 }
267
268 private boolean addNodes(NodePos start, NodePos end, Way way,
269 Set<Node> nodes, boolean hasFirst) {
270 if (way.isClosed()
271 || (start.node != way.getNode(0) && start.node != way
272 .getNode(way.getNodesCount() - 1))) {
273 hasFirst = hasFirst || start.node == way.getNode(0);
274 nodes.add(start.node);
275 }
276 if (way.isClosed()
277 || (end.node != way.getNode(0) && end.node != way.getNode(way
278 .getNodesCount() - 1))) {
279 if (hasFirst && (end.node == way.getNode(way.getNodesCount() - 1))) {
280 nodes.remove(way.getNode(0));
281 } else {
282 nodes.add(end.node);
283 }
284 }
285 return hasFirst;
286 }
287
288 private boolean follows(Way way1, Way way2, NodePos np1, NodePos np2,
289 int incr) {
290 if (way2.isClosed() && incr == 1
291 && np1.opositPos == way2.getNodesCount() - 2) {
292 return np2.pos == np1.pos + 1 && np2.opositPos == 0;
293 } else if (way2.isClosed() && incr == 1 && np1.opositPos == 0) {
294 return np2.pos == np1.pos && np2.opositPos == 0
295 || np2.pos == np1.pos + 1 && np2.opositPos == 1;
296 } else if (way2.isClosed() && incr == -1 && np1.opositPos == 0) {
297 return np2.pos == np1.pos && np2.opositPos == 0
298 || np2.pos == np1.pos + 1
299 && np2.opositPos == way2.getNodesCount() - 2;
300 } else {
301 return np2.pos == np1.pos + 1
302 && np2.opositPos == np1.opositPos + incr;
303 }
304 }
305
306 /**
307 * Splits a way
308 *
309 * @param layer
310 * @param way
311 * @param wayChunks
312 * @return
313 */
314 private SplitWayResult splitWay(OsmDataLayer layer, Way way,
315 List<List<Node>> wayChunks) {
316 // build a list of commands, and also a new selection list
317 Collection<Command> commandList = new ArrayList<Command>(wayChunks
318 .size());
319
320 Iterator<List<Node>> chunkIt = wayChunks.iterator();
321 Collection<String> nowarnroles = Main.pref.getCollection(
322 "way.split.roles.nowarn", Arrays.asList(new String[] { "outer",
323 "inner", "forward", "backward" }));
324
325 // First, change the original way
326 Way changedWay = new Way(way);
327 oldWays.put(changedWay, way);
328 changedWay.setNodes(chunkIt.next());
329 commandList.add(new ChangeCommand(way, changedWay));
330
331 List<Way> newWays = new ArrayList<Way>();
332 // Second, create new ways
333 while (chunkIt.hasNext()) {
334 Way wayToAdd = new Way();
335 wayToAdd.setKeys(way.getKeys());
336 newWays.add(wayToAdd);
337 wayToAdd.setNodes(chunkIt.next());
338 commandList.add(new AddCommand(layer, wayToAdd));
339 }
340 boolean warnmerole = false;
341 boolean warnme = false;
342 // now copy all relations to new way also
343
344 for (Relation r : getParentRelations(way)) {
345 if (!r.isUsable()) {
346 continue;
347 }
348 Relation c = null;
349 String type = r.get("type");
350 if (type == null) {
351 type = "";
352 }
353
354 int i_c = 0, i_r = 0;
355 List<RelationMember> relationMembers = r.getMembers();
356 for (RelationMember rm : relationMembers) {
357 if (rm.isWay() && rm.getMember() == way) {
358 boolean insert = true;
359 if ("restriction".equals(type)) {
360 /*
361 * this code assumes the restriction is correct. No real
362 * error checking done
363 */
364 String role = rm.getRole();
365 if ("from".equals(role) || "to".equals(role)) {
366 OsmPrimitive via = null;
367 for (RelationMember rmv : r.getMembers()) {
368 if ("via".equals(rmv.getRole())) {
369 via = rmv.getMember();
370 }
371 }
372 List<Node> nodes = new ArrayList<Node>();
373 if (via != null) {
374 if (via instanceof Node) {
375 nodes.add((Node) via);
376 } else if (via instanceof Way) {
377 nodes.add(((Way) via).lastNode());
378 nodes.add(((Way) via).firstNode());
379 }
380 }
381 Way res = null;
382 for (Node n : nodes) {
383 if (changedWay.isFirstLastNode(n)) {
384 res = way;
385 }
386 }
387 if (res == null) {
388 for (Way wayToAdd : newWays) {
389 for (Node n : nodes) {
390 if (wayToAdd.isFirstLastNode(n)) {
391 res = wayToAdd;
392 }
393 }
394 }
395 if (res != null) {
396 if (c == null) {
397 c = getNew(r);
398 }
399 c.addMember(new RelationMember(role, res));
400 c.removeMembersFor(way);
401 insert = false;
402 }
403 } else {
404 insert = false;
405 }
406 } else if (!"via".equals(role)) {
407 warnme = true;
408 }
409 } else if (!("route".equals(type))
410 && !("multipolygon".equals(type))) {
411 warnme = true;
412 }
413 if (c == null) {
414 c = getNew(r);
415 }
416
417 if (insert) {
418 if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
419 warnmerole = true;
420 }
421
422 Boolean backwards = null;
423 int k = 1;
424 while (i_r - k >= 0 || i_r + k < relationMembers.size()) {
425 if ((i_r - k >= 0)
426 && relationMembers.get(i_r - k).isWay()) {
427 Way w = relationMembers.get(i_r - k).getWay();
428 if ((w.lastNode() == way.firstNode())
429 || w.firstNode() == way.firstNode()) {
430 backwards = false;
431 } else if ((w.firstNode() == way.lastNode())
432 || w.lastNode() == way.lastNode()) {
433 backwards = true;
434 }
435 break;
436 }
437 if ((i_r + k < relationMembers.size())
438 && relationMembers.get(i_r + k).isWay()) {
439 Way w = relationMembers.get(i_r + k).getWay();
440 if ((w.lastNode() == way.firstNode())
441 || w.firstNode() == way.firstNode()) {
442 backwards = true;
443 } else if ((w.firstNode() == way.lastNode())
444 || w.lastNode() == way.lastNode()) {
445 backwards = false;
446 }
447 break;
448 }
449 k++;
450 }
451
452 int j = i_c;
453 for (Way wayToAdd : newWays) {
454 RelationMember em = new RelationMember(
455 rm.getRole(), wayToAdd);
456 j++;
457 if ((backwards != null) && backwards) {
458 c.addMember(i_c, em);
459 } else {
460 c.addMember(j, em);
461 }
462 }
463 i_c = j;
464 }
465 }
466 i_c++;
467 i_r++;
468 }
469
470 if (c != null) {
471 // commandList.add(new ChangeCommand(layer, r, c));
472 newRelations.put(r, c);
473 }
474 }
475 if (warnmerole) {
476 JOptionPane
477 .showMessageDialog(
478 Main.parent,
479 tr("<html>A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.</html>"),
480 tr("Warning"), JOptionPane.WARNING_MESSAGE);
481 } else if (warnme) {
482 JOptionPane
483 .showMessageDialog(
484 Main.parent,
485 tr("<html>A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.</html>"),
486 tr("Warning"), JOptionPane.WARNING_MESSAGE);
487 }
488
489 return new SplitWayResult(
490 new SequenceCommand(tr("Split way"), commandList), null,
491 changedWay, newWays);
492 }
493
494 /**
495 * @param ways
496 * @return null if ways cannot be combined. Otherwise returns the combined
497 * ways and the commands to combine
498 * @throws UserCancelException
499 */
500 private Pair<Way, List<Command>> combineWaysWorker(Collection<Way> ways)
501 throws UserCancelException {
502
503 // prepare and clean the list of ways to combine
504 if (ways == null || ways.isEmpty())
505 return null;
506 ways.remove(null); // just in case - remove all null ways from the
507 // collection
508
509 // remove duplicates, preserving order
510 ways = new LinkedHashSet<Way>(ways);
511
512 // try to build a new way which includes all the combined ways
513 NodeGraph graph = NodeGraph.createUndirectedGraphFromNodeWays(ways);
514 List<Node> path = graph.buildSpanningPath();
515
516 // check whether any ways have been reversed in the process
517 // and build the collection of tags used by the ways to combine
518 TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways);
519
520 List<Way> reversedWays = new LinkedList<Way>();
521 List<Way> unreversedWays = new LinkedList<Way>();
522 for (Way w : ways) {
523 if ((path.indexOf(w.getNode(0)) + 1) == path.lastIndexOf(w
524 .getNode(1))) {
525 unreversedWays.add(w);
526 } else {
527 reversedWays.add(w);
528 }
529 }
530 // reverse path if all ways have been reversed
531 if (unreversedWays.isEmpty()) {
532 Collections.reverse(path);
533 unreversedWays = reversedWays;
534 reversedWays = null;
535 }
536 if ((reversedWays != null) && !reversedWays.isEmpty()) {
537 // filter out ways that have no direction-dependent tags
538 unreversedWays = ReverseWayTagCorrector
539 .irreversibleWays(unreversedWays);
540 reversedWays = ReverseWayTagCorrector
541 .irreversibleWays(reversedWays);
542 // reverse path if there are more reversed than unreversed ways with
543 // direction-dependent tags
544 if (reversedWays.size() > unreversedWays.size()) {
545 Collections.reverse(path);
546 List<Way> tempWays = unreversedWays;
547 unreversedWays = reversedWays;
548 reversedWays = tempWays;
549 }
550 // if there are still reversed ways with direction-dependent tags,
551 // reverse their tags
552 if (!reversedWays.isEmpty()) {
553 List<Way> unreversedTagWays = new ArrayList<Way>(ways);
554 unreversedTagWays.removeAll(reversedWays);
555 ReverseWayTagCorrector reverseWayTagCorrector = new ReverseWayTagCorrector();
556 List<Way> reversedTagWays = new ArrayList<Way>();
557 Collection<Command> changePropertyCommands = null;
558 for (Way w : reversedWays) {
559 Way wnew = new Way(w);
560 reversedTagWays.add(wnew);
561 changePropertyCommands = reverseWayTagCorrector.execute(w,
562 wnew);
563 }
564 if ((changePropertyCommands != null)
565 && !changePropertyCommands.isEmpty()) {
566 for (Command c : changePropertyCommands) {
567 c.executeCommand();
568 }
569 }
570 wayTags = TagCollection.unionOfAllPrimitives(reversedTagWays);
571 wayTags.add(TagCollection
572 .unionOfAllPrimitives(unreversedTagWays));
573 }
574 }
575
576 // create the new way and apply the new node list
577 Way targetWay = getTargetWay(ways);
578 Way modifiedTargetWay = new Way(targetWay);
579 modifiedTargetWay.setNodes(path);
580
581 TagCollection completeWayTags = new TagCollection(wayTags);
582 combineTigerTags(completeWayTags);
583 normalizeTagCollectionBeforeEditing(completeWayTags, ways);
584 TagCollection tagsToEdit = new TagCollection(completeWayTags);
585 completeTagCollectionForEditing(tagsToEdit);
586
587 MyCombinePrimitiveResolverDialog dialog = MyCombinePrimitiveResolverDialog
588 .getInstance();
589 dialog.getTagConflictResolverModel().populate(tagsToEdit,
590 completeWayTags.getKeysWithMultipleValues());
591 dialog.setTargetPrimitive(targetWay);
592 Set<Relation> parentRelations = getParentRelations(ways);
593 dialog.getRelationMemberConflictResolverModel().populate(
594 parentRelations, ways, oldWays);
595 dialog.prepareDefaultDecisions();
596
597 // resolve tag conflicts if necessary
598 if (askForMergeTag(ways) || duplicateParentRelations(ways)) {
599 dialog.setVisible(true);
600 if (dialog.isCancelled())
601 throw new UserCancelException();
602 }
603
604 LinkedList<Command> cmds = new LinkedList<Command>();
605 deletes.addAll(ways);
606 deletes.remove(targetWay);
607
608 cmds.add(new ChangeCommand(targetWay, modifiedTargetWay));
609 cmds.addAll(dialog.buildWayResolutionCommands());
610 dialog.buildRelationCorrespondance(newRelations, oldWays);
611
612 return new Pair<Way, List<Command>>(targetWay, cmds);
613 }
614
615 private static Way getTargetWay(Collection<Way> combinedWays) {
616 // init with an arbitrary way
617 Way targetWay = combinedWays.iterator().next();
618
619 // look for the first way already existing on
620 // the server
621 for (Way w : combinedWays) {
622 targetWay = w;
623 if (!w.isNew()) {
624 break;
625 }
626 }
627 return targetWay;
628 }
629
630 /**
631 * @return has tag to be merged (=> ask)
632 */
633 private static boolean askForMergeTag(Collection<Way> ways) {
634 for (Way way : ways) {
635 for (Way oposite : ways) {
636 for (String key : way.getKeys().keySet()) {
637 if (!"source".equals(key) && oposite.hasKey(key)
638 && !way.get(key).equals(oposite.get(key))) {
639 return true;
640 }
641 }
642 }
643 }
644 return false;
645 }
646
647 /**
648 * @return has duplicate parent relation
649 */
650 private boolean duplicateParentRelations(Collection<Way> ways) {
651 Set<Relation> relations = new HashSet<Relation>();
652 for (Way w : ways) {
653 List<Relation> rs = getParentRelations(w);
654 for (Relation r : rs) {
655 if (relations.contains(r)) {
656 return true;
657 }
658 }
659 relations.addAll(rs);
660 }
661 return false;
662 }
663
664 /**
665 * Replies the set of referring relations
666 *
667 * @return the set of referring relations
668 */
669 private List<Relation> getParentRelations(Way way) {
670 List<Relation> rels = new ArrayList<Relation>();
671 for (Relation r : relations.get(way)) {
672 if (newRelations.containsKey(r)) {
673 rels.add(newRelations.get(r));
674 } else {
675 rels.add(r);
676 }
677 }
678 return rels;
679 }
680
681 private Relation getNew(Relation r) {
682 return getNew(r, newRelations);
683 }
684
685 public static Relation getNew(Relation r,
686 Map<Relation, Relation> newRelations) {
687 if (newRelations.containsValue(r)) {
688 return r;
689 } else {
690 Relation c = new Relation(r);
691 newRelations.put(r, c);
692 return c;
693 }
694 }
695
696 private Way getOld(Way r) {
697 return getOld(r, oldWays);
698 }
699
700 public static Way getOld(Way w, Map<Way, Way> oldWays) {
701 if (oldWays.containsKey(w)) {
702 return oldWays.get(w);
703 } else {
704 return w;
705 }
706 }
707
708 /**
709 * Replies the set of referring relations
710 *
711 * @return the set of referring relations
712 */
713 private Set<Relation> getParentRelations(Collection<Way> ways) {
714 HashSet<Relation> ret = new HashSet<Relation>();
715 for (Way w : ways) {
716 ret.addAll(getParentRelations(w));
717 }
718 return ret;
719 }
720
721 /** Enable this action only if something is selected */
722 @Override
723 protected void updateEnabledState() {
724 if (getCurrentDataSet() == null) {
725 setEnabled(false);
726 } else {
727 updateEnabledState(getCurrentDataSet().getSelected());
728 }
729 }
730
731 /** Enable this action only if something is selected */
732 @Override
733 protected void updateEnabledState(
734 Collection<? extends OsmPrimitive> selection) {
735 if (selection == null) {
736 setEnabled(false);
737 return;
738 }
739 for (OsmPrimitive primitive : selection) {
740 if (!(primitive instanceof Way) || primitive.isDeleted()) {
741 setEnabled(false);
742 return;
743 }
744 }
745 setEnabled(selection.size() >= 2);
746 }
747}
Note: See TracBrowser for help on using the repository browser.