source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java@ 11144

Last change on this file since 11144 was 11144, checked in by michael2402, 8 years ago

Fix #13793: Clip paths in a way that let's the dashes stay in place.

  • Property svn:eol-style set to native
File size: 24.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.marktr;
6import static org.openstreetmap.josm.tools.I18n.tr;
7
8import java.awt.BasicStroke;
9import java.awt.Color;
10import java.awt.Cursor;
11import java.awt.Graphics2D;
12import java.awt.Point;
13import java.awt.event.KeyEvent;
14import java.awt.event.MouseEvent;
15import java.util.Collection;
16import java.util.Collections;
17import java.util.EnumMap;
18import java.util.EnumSet;
19import java.util.LinkedHashSet;
20import java.util.Map;
21import java.util.Optional;
22import java.util.Set;
23import java.util.stream.Stream;
24
25import javax.swing.JOptionPane;
26
27import org.openstreetmap.josm.Main;
28import org.openstreetmap.josm.data.Bounds;
29import org.openstreetmap.josm.data.SystemOfMeasurement;
30import org.openstreetmap.josm.data.coor.EastNorth;
31import org.openstreetmap.josm.data.osm.Node;
32import org.openstreetmap.josm.data.osm.OsmPrimitive;
33import org.openstreetmap.josm.data.osm.Way;
34import org.openstreetmap.josm.data.osm.WaySegment;
35import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
36import org.openstreetmap.josm.data.preferences.AbstractToStringProperty;
37import org.openstreetmap.josm.data.preferences.BooleanProperty;
38import org.openstreetmap.josm.data.preferences.CachingProperty;
39import org.openstreetmap.josm.data.preferences.ColorProperty;
40import org.openstreetmap.josm.data.preferences.DoubleProperty;
41import org.openstreetmap.josm.data.preferences.IntegerProperty;
42import org.openstreetmap.josm.data.preferences.StrokeProperty;
43import org.openstreetmap.josm.gui.MapFrame;
44import org.openstreetmap.josm.gui.MapView;
45import org.openstreetmap.josm.gui.Notification;
46import org.openstreetmap.josm.gui.draw.MapViewPath;
47import org.openstreetmap.josm.gui.layer.Layer;
48import org.openstreetmap.josm.gui.layer.MapViewPaintable;
49import org.openstreetmap.josm.gui.layer.OsmDataLayer;
50import org.openstreetmap.josm.gui.util.ModifierListener;
51import org.openstreetmap.josm.tools.CheckParameterUtil;
52import org.openstreetmap.josm.tools.Geometry;
53import org.openstreetmap.josm.tools.ImageProvider;
54import org.openstreetmap.josm.tools.Shortcut;
55
56//// TODO: (list below)
57/* == Functionality ==
58 *
59 * 1. Use selected nodes as split points for the selected ways.
60 *
61 * The ways containing the selected nodes will be split and only the "inner"
62 * parts will be copied
63 *
64 * 2. Enter exact offset
65 *
66 * 3. Improve snapping
67 *
68 * 4. Visual cues could be better
69 *
70 * 5. (long term) Parallelize and adjust offsets of existing ways
71 *
72 * == Code quality ==
73 *
74 * a) The mode, flags, and modifiers might be updated more than necessary.
75 *
76 * Not a performance problem, but better if they where more centralized
77 *
78 * b) Extract generic MapMode services into a super class and/or utility class
79 *
80 * c) Maybe better to simply draw our own source way highlighting?
81 *
82 * Current code doesn't not take into account that ways might been highlighted
83 * by other than us. Don't think that situation should ever happen though.
84 */
85
86/**
87 * MapMode for making parallel ways.
88 *
89 * All calculations are done in projected coordinates.
90 *
91 * @author Ole Jørgen Brønner (olejorgenb)
92 */
93public class ParallelWayAction extends MapMode implements ModifierListener, MapViewPaintable {
94
95 private static final CachingProperty<BasicStroke> HELPER_LINE_STROKE = new StrokeProperty(prefKey("stroke.hepler-line"), "1").cached();
96 private static final CachingProperty<BasicStroke> REF_LINE_STROKE = new StrokeProperty(prefKey("stroke.ref-line"), "2 2 3").cached();
97
98 // @formatter:off
99 // CHECKSTYLE.OFF: SingleSpaceSeparator
100 private static final CachingProperty<Double> SNAP_THRESHOLD = new DoubleProperty(prefKey("snap-threshold-percent"), 0.70).cached();
101 private static final CachingProperty<Boolean> SNAP_DEFAULT = new BooleanProperty(prefKey("snap-default"), true).cached();
102 private static final CachingProperty<Boolean> COPY_TAGS_DEFAULT = new BooleanProperty(prefKey("copy-tags-default"), true).cached();
103 private static final CachingProperty<Integer> INITIAL_MOVE_DELAY = new IntegerProperty(prefKey("initial-move-delay"), 200).cached();
104 private static final CachingProperty<Double> SNAP_DISTANCE_METRIC = new DoubleProperty(prefKey("snap-distance-metric"), 0.5).cached();
105 private static final CachingProperty<Double> SNAP_DISTANCE_IMPERIAL = new DoubleProperty(prefKey("snap-distance-imperial"), 1).cached();
106 private static final CachingProperty<Double> SNAP_DISTANCE_CHINESE = new DoubleProperty(prefKey("snap-distance-chinese"), 1).cached();
107 private static final CachingProperty<Double> SNAP_DISTANCE_NAUTICAL = new DoubleProperty(prefKey("snap-distance-nautical"), 0.1).cached();
108 private static final CachingProperty<Color> MAIN_COLOR = new ColorProperty(marktr("make parallel helper line"), (Color) null).cached();
109
110 private static final CachingProperty<Map<Modifier, Boolean>> SNAP_MODIFIER_COMBO
111 = new KeyboardModifiersProperty(prefKey("snap-modifier-combo"), "?sC").cached();
112 private static final CachingProperty<Map<Modifier, Boolean>> COPY_TAGS_MODIFIER_COMBO
113 = new KeyboardModifiersProperty(prefKey("copy-tags-modifier-combo"), "As?").cached();
114 private static final CachingProperty<Map<Modifier, Boolean>> ADD_TO_SELECTION_MODIFIER_COMBO
115 = new KeyboardModifiersProperty(prefKey("add-to-selection-modifier-combo"), "aSc").cached();
116 private static final CachingProperty<Map<Modifier, Boolean>> TOGGLE_SELECTED_MODIFIER_COMBO
117 = new KeyboardModifiersProperty(prefKey("toggle-selection-modifier-combo"), "asC").cached();
118 private static final CachingProperty<Map<Modifier, Boolean>> SET_SELECTED_MODIFIER_COMBO
119 = new KeyboardModifiersProperty(prefKey("set-selection-modifier-combo"), "asc").cached();
120 // CHECKSTYLE.ON: SingleSpaceSeparator
121 // @formatter:on
122
123 private enum Mode {
124 DRAGGING, NORMAL
125 }
126
127 //// Preferences and flags
128 // See updateModeLocalPreferences for defaults
129 private Mode mode;
130 private boolean copyTags;
131
132 private boolean snap;
133
134 private final MapView mv;
135
136 // Mouse tracking state
137 private Point mousePressedPos;
138 private boolean mouseIsDown;
139 private long mousePressedTime;
140 private boolean mouseHasBeenDragged;
141
142 private transient WaySegment referenceSegment;
143 private transient ParallelWays pWays;
144 private transient Set<Way> sourceWays;
145 private EastNorth helperLineStart;
146 private EastNorth helperLineEnd;
147
148 /**
149 * Constructs a new {@code ParallelWayAction}.
150 * @param mapFrame Map frame
151 */
152 public ParallelWayAction(MapFrame mapFrame) {
153 super(tr("Parallel"), "parallel", tr("Make parallel copies of ways"),
154 Shortcut.registerShortcut("mapmode:parallel", tr("Mode: {0}",
155 tr("Parallel")), KeyEvent.VK_P, Shortcut.SHIFT),
156 mapFrame, ImageProvider.getCursor("normal", "parallel"));
157 putValue("help", ht("/Action/Parallel"));
158 mv = mapFrame.mapView;
159 }
160
161 @Override
162 public void enterMode() {
163 // super.enterMode() updates the status line and cursor so we need our state to be set correctly
164 setMode(Mode.NORMAL);
165 pWays = null;
166
167 super.enterMode();
168
169 mv.addMouseListener(this);
170 mv.addMouseMotionListener(this);
171 mv.addTemporaryLayer(this);
172
173 //// Needed to update the mouse cursor if modifiers are changed when the mouse is motionless
174 Main.map.keyDetector.addModifierListener(this);
175 sourceWays = new LinkedHashSet<>(getLayerManager().getEditDataSet().getSelectedWays());
176 for (Way w : sourceWays) {
177 w.setHighlighted(true);
178 }
179 mv.repaint();
180 }
181
182 @Override
183 public void exitMode() {
184 super.exitMode();
185 mv.removeMouseListener(this);
186 mv.removeMouseMotionListener(this);
187 mv.removeTemporaryLayer(this);
188 Main.map.statusLine.setDist(-1);
189 Main.map.statusLine.repaint();
190 Main.map.keyDetector.removeModifierListener(this);
191 removeWayHighlighting(sourceWays);
192 pWays = null;
193 sourceWays = null;
194 referenceSegment = null;
195 mv.repaint();
196 }
197
198 @Override
199 public String getModeHelpText() {
200 // TODO: add more detailed feedback based on modifier state.
201 // TODO: dynamic messages based on preferences. (Could be problematic translation wise)
202 switch (mode) {
203 case NORMAL:
204 // CHECKSTYLE.OFF: LineLength
205 return tr("Select ways as in Select mode. Drag selected ways or a single way to create a parallel copy (Alt toggles tag preservation)");
206 // CHECKSTYLE.ON: LineLength
207 case DRAGGING:
208 return tr("Hold Ctrl to toggle snapping");
209 }
210 return ""; // impossible ..
211 }
212
213 @Override
214 public boolean layerIsSupported(Layer layer) {
215 return layer instanceof OsmDataLayer;
216 }
217
218 @Override
219 public void modifiersChanged(int modifiers) {
220 if (Main.map == null || mv == null || !mv.isActiveLayerDrawable())
221 return;
222
223 // Should only get InputEvents due to the mask in enterMode
224 if (updateModifiersState(modifiers)) {
225 updateStatusLine();
226 updateCursor();
227 }
228 }
229
230 private boolean updateModifiersState(int modifiers) {
231 boolean oldAlt = alt, oldShift = shift, oldCtrl = ctrl;
232 updateKeyModifiers(modifiers);
233 return oldAlt != alt || oldShift != shift || oldCtrl != ctrl;
234 }
235
236 private void updateCursor() {
237 Cursor newCursor = null;
238 switch (mode) {
239 case NORMAL:
240 if (matchesCurrentModifiers(SET_SELECTED_MODIFIER_COMBO)) {
241 newCursor = ImageProvider.getCursor("normal", "parallel");
242 } else if (matchesCurrentModifiers(ADD_TO_SELECTION_MODIFIER_COMBO)) {
243 newCursor = ImageProvider.getCursor("normal", "parallel_add");
244 } else if (matchesCurrentModifiers(TOGGLE_SELECTED_MODIFIER_COMBO)) {
245 newCursor = ImageProvider.getCursor("normal", "parallel_remove");
246 }
247 break;
248 case DRAGGING:
249 newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
250 break;
251 default: throw new AssertionError();
252 }
253 if (newCursor != null) {
254 mv.setNewCursor(newCursor, this);
255 }
256 }
257
258 private void setMode(Mode mode) {
259 this.mode = mode;
260 updateCursor();
261 updateStatusLine();
262 }
263
264 private boolean sanityCheck() {
265 // @formatter:off
266 boolean areWeSane =
267 mv.isActiveLayerVisible() &&
268 mv.isActiveLayerDrawable() &&
269 ((Boolean) this.getValue("active"));
270 // @formatter:on
271 assert areWeSane; // mad == bad
272 return areWeSane;
273 }
274
275 @Override
276 public void mousePressed(MouseEvent e) {
277 requestFocusInMapView();
278 updateModifiersState(e.getModifiers());
279 // Other buttons are off limit, but we still get events.
280 if (e.getButton() != MouseEvent.BUTTON1)
281 return;
282
283 if (!sanityCheck())
284 return;
285
286 updateFlagsOnlyChangeableOnPress();
287 updateFlagsChangeableAlways();
288
289 // Since the created way is left selected, we need to unselect again here
290 if (pWays != null && pWays.getWays() != null) {
291 getLayerManager().getEditDataSet().clearSelection(pWays.getWays());
292 pWays = null;
293 }
294
295 mouseIsDown = true;
296 mousePressedPos = e.getPoint();
297 mousePressedTime = System.currentTimeMillis();
298
299 }
300
301 @Override
302 public void mouseReleased(MouseEvent e) {
303 updateModifiersState(e.getModifiers());
304 // Other buttons are off limit, but we still get events.
305 if (e.getButton() != MouseEvent.BUTTON1)
306 return;
307
308 if (!mouseHasBeenDragged) {
309 // use point from press or click event? (or are these always the same)
310 Way nearestWay = mv.getNearestWay(e.getPoint(), OsmPrimitive::isSelectable);
311 if (nearestWay == null) {
312 if (matchesCurrentModifiers(SET_SELECTED_MODIFIER_COMBO)) {
313 clearSourceWays();
314 }
315 resetMouseTrackingState();
316 return;
317 }
318 boolean isSelected = nearestWay.isSelected();
319 if (matchesCurrentModifiers(ADD_TO_SELECTION_MODIFIER_COMBO)) {
320 if (!isSelected) {
321 addSourceWay(nearestWay);
322 }
323 } else if (matchesCurrentModifiers(TOGGLE_SELECTED_MODIFIER_COMBO)) {
324 if (isSelected) {
325 removeSourceWay(nearestWay);
326 } else {
327 addSourceWay(nearestWay);
328 }
329 } else if (matchesCurrentModifiers(SET_SELECTED_MODIFIER_COMBO)) {
330 clearSourceWays();
331 addSourceWay(nearestWay);
332 } // else -> invalid modifier combination
333 } else if (mode == Mode.DRAGGING) {
334 clearSourceWays();
335 }
336
337 setMode(Mode.NORMAL);
338 resetMouseTrackingState();
339 mv.repaint();
340 }
341
342 private static void removeWayHighlighting(Collection<Way> ways) {
343 if (ways == null)
344 return;
345 for (Way w : ways) {
346 w.setHighlighted(false);
347 }
348 }
349
350 @Override
351 public void mouseDragged(MouseEvent e) {
352 // WTF.. the event passed here doesn't have button info?
353 // Since we get this event from other buttons too, we must check that
354 // _BUTTON1_ is down.
355 if (!mouseIsDown)
356 return;
357
358 boolean modifiersChanged = updateModifiersState(e.getModifiers());
359 updateFlagsChangeableAlways();
360
361 if (modifiersChanged) {
362 // Since this could be remotely slow, do it conditionally
363 updateStatusLine();
364 updateCursor();
365 }
366
367 if ((System.currentTimeMillis() - mousePressedTime) < INITIAL_MOVE_DELAY.get())
368 return;
369 // Assuming this event only is emitted when the mouse has moved
370 // Setting this after the check above means we tolerate clicks with some movement
371 mouseHasBeenDragged = true;
372
373 if (mode == Mode.NORMAL) {
374 // Should we ensure that the copyTags modifiers are still valid?
375
376 // Important to use mouse position from the press, since the drag
377 // event can come quite late
378 if (!isModifiersValidForDragMode())
379 return;
380 if (!initParallelWays(mousePressedPos, copyTags))
381 return;
382 setMode(Mode.DRAGGING);
383 }
384
385 // Calculate distance to the reference line
386 Point p = e.getPoint();
387 EastNorth enp = mv.getEastNorth((int) p.getX(), (int) p.getY());
388 EastNorth nearestPointOnRefLine = Geometry.closestPointToLine(referenceSegment.getFirstNode().getEastNorth(),
389 referenceSegment.getSecondNode().getEastNorth(), enp);
390
391 // Note: d is the distance in _projected units_
392 double d = enp.distance(nearestPointOnRefLine);
393 double realD = mv.getProjection().eastNorth2latlon(enp).greatCircleDistance(mv.getProjection().eastNorth2latlon(nearestPointOnRefLine));
394 double snappedRealD = realD;
395
396 boolean toTheRight = Geometry.angleIsClockwise(
397 referenceSegment.getFirstNode(), referenceSegment.getSecondNode(), new Node(enp));
398
399 if (snap) {
400 // TODO: Very simple snapping
401 // - Snap steps relative to the distance?
402 double snapDistance;
403 SystemOfMeasurement som = SystemOfMeasurement.getSystemOfMeasurement();
404 if (som.equals(SystemOfMeasurement.CHINESE)) {
405 snapDistance = SNAP_DISTANCE_CHINESE.get() * SystemOfMeasurement.CHINESE.aValue;
406 } else if (som.equals(SystemOfMeasurement.IMPERIAL)) {
407 snapDistance = SNAP_DISTANCE_IMPERIAL.get() * SystemOfMeasurement.IMPERIAL.aValue;
408 } else if (som.equals(SystemOfMeasurement.NAUTICAL_MILE)) {
409 snapDistance = SNAP_DISTANCE_NAUTICAL.get() * SystemOfMeasurement.NAUTICAL_MILE.aValue;
410 } else {
411 snapDistance = SNAP_DISTANCE_METRIC.get(); // Metric system by default
412 }
413 double closestWholeUnit;
414 double modulo = realD % snapDistance;
415 if (modulo < snapDistance/2.0) {
416 closestWholeUnit = realD - modulo;
417 } else {
418 closestWholeUnit = realD + (snapDistance-modulo);
419 }
420 if (Math.abs(closestWholeUnit - realD) < (SNAP_THRESHOLD.get() * snapDistance)) {
421 snappedRealD = closestWholeUnit;
422 } else {
423 snappedRealD = closestWholeUnit + Math.signum(realD - closestWholeUnit) * snapDistance;
424 }
425 }
426 d = snappedRealD * (d/realD); // convert back to projected distance. (probably ok on small scales)
427 helperLineStart = nearestPointOnRefLine;
428 helperLineEnd = enp;
429 if (toTheRight) {
430 d = -d;
431 }
432 pWays.changeOffset(d);
433
434 Main.map.statusLine.setDist(Math.abs(snappedRealD));
435 Main.map.statusLine.repaint();
436 mv.repaint();
437 }
438
439 private boolean matchesCurrentModifiers(CachingProperty<Map<Modifier, Boolean>> spec) {
440 return matchesCurrentModifiers(spec.get());
441 }
442
443 private boolean matchesCurrentModifiers(Map<Modifier, Boolean> spec) {
444 EnumSet<Modifier> modifiers = EnumSet.noneOf(Modifier.class);
445 if (ctrl) {
446 modifiers.add(Modifier.CTRL);
447 }
448 if (alt) {
449 modifiers.add(Modifier.ALT);
450 }
451 if (shift) {
452 modifiers.add(Modifier.SHIFT);
453 }
454 return spec.entrySet().stream().allMatch(entry -> modifiers.contains(entry.getKey()) == entry.getValue().booleanValue());
455 }
456
457 @Override
458 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
459 if (mode == Mode.DRAGGING) {
460 CheckParameterUtil.ensureParameterNotNull(mv, "mv");
461
462 Color mainColor = MAIN_COLOR.get();
463 if (mainColor == null) {
464 mainColor = PaintColors.SELECTED.get();
465 }
466
467 // FIXME: should clip the line (gets insanely slow when zoomed in on a very long line
468 g.setStroke(REF_LINE_STROKE.get());
469 g.setColor(mainColor);
470 MapViewPath line = new MapViewPath(mv);
471 line.moveTo(referenceSegment.getFirstNode());
472 line.lineTo(referenceSegment.getSecondNode());
473 g.draw(line.computeClippedLine(g.getStroke()));
474
475 g.setStroke(HELPER_LINE_STROKE.get());
476 g.setColor(mainColor);
477 line = new MapViewPath(mv);
478 line.moveTo(helperLineStart);
479 line.lineTo(helperLineEnd);
480 g.draw(line.computeClippedLine(g.getStroke()));
481 }
482 }
483
484 private boolean isModifiersValidForDragMode() {
485 return (!alt && !shift && !ctrl) || matchesCurrentModifiers(SNAP_MODIFIER_COMBO)
486 || matchesCurrentModifiers(COPY_TAGS_MODIFIER_COMBO);
487 }
488
489 private void updateFlagsOnlyChangeableOnPress() {
490 copyTags = COPY_TAGS_DEFAULT.get().booleanValue() != matchesCurrentModifiers(COPY_TAGS_MODIFIER_COMBO);
491 }
492
493 private void updateFlagsChangeableAlways() {
494 snap = SNAP_DEFAULT.get().booleanValue() != matchesCurrentModifiers(SNAP_MODIFIER_COMBO);
495 }
496
497 // We keep the source ways and the selection in sync so the user can see the source way's tags
498 private void addSourceWay(Way w) {
499 assert sourceWays != null;
500 getLayerManager().getEditDataSet().addSelected(w);
501 w.setHighlighted(true);
502 sourceWays.add(w);
503 }
504
505 private void removeSourceWay(Way w) {
506 assert sourceWays != null;
507 getLayerManager().getEditDataSet().clearSelection(w);
508 w.setHighlighted(false);
509 sourceWays.remove(w);
510 }
511
512 private void clearSourceWays() {
513 assert sourceWays != null;
514 getLayerManager().getEditDataSet().clearSelection(sourceWays);
515 for (Way w : sourceWays) {
516 w.setHighlighted(false);
517 }
518 sourceWays.clear();
519 }
520
521 private void resetMouseTrackingState() {
522 mouseIsDown = false;
523 mousePressedPos = null;
524 mouseHasBeenDragged = false;
525 }
526
527 // TODO: rename
528 private boolean initParallelWays(Point p, boolean copyTags) {
529 referenceSegment = mv.getNearestWaySegment(p, OsmPrimitive::isUsable, true);
530 if (referenceSegment == null)
531 return false;
532
533 if (!sourceWays.contains(referenceSegment.way)) {
534 clearSourceWays();
535 addSourceWay(referenceSegment.way);
536 }
537
538 try {
539 int referenceWayIndex = -1;
540 int i = 0;
541 for (Way w : sourceWays) {
542 if (w == referenceSegment.way) {
543 referenceWayIndex = i;
544 break;
545 }
546 i++;
547 }
548 pWays = new ParallelWays(sourceWays, copyTags, referenceWayIndex);
549 pWays.commit();
550 getLayerManager().getEditDataSet().setSelected(pWays.getWays());
551 return true;
552 } catch (IllegalArgumentException e) {
553 Main.debug(e);
554 new Notification(tr("ParallelWayAction\n" +
555 "The ways selected must form a simple branchless path"))
556 .setIcon(JOptionPane.INFORMATION_MESSAGE)
557 .show();
558 // The error dialog prevents us from getting the mouseReleased event
559 resetMouseTrackingState();
560 pWays = null;
561 return false;
562 }
563 }
564
565 private static String prefKey(String subKey) {
566 return "edit.make-parallel-way-action." + subKey;
567 }
568
569 /**
570 * A property that holds the keyboard modifiers.
571 * @author Michael Zangl
572 * @since 10869
573 */
574 private static class KeyboardModifiersProperty extends AbstractToStringProperty<Map<Modifier, Boolean>> {
575
576 KeyboardModifiersProperty(String key, String defaultValue) {
577 super(key, createFromString(defaultValue));
578 }
579
580 KeyboardModifiersProperty(String key, Map<Modifier, Boolean> defaultValue) {
581 super(key, defaultValue);
582 }
583
584 @Override
585 protected String toString(Map<Modifier, Boolean> t) {
586 StringBuilder sb = new StringBuilder();
587 for (Modifier mod : Modifier.values()) {
588 Boolean val = t.get(mod);
589 if (val == null) {
590 sb.append('?');
591 } else if (val) {
592 sb.append(Character.toUpperCase(mod.shortChar));
593 } else {
594 sb.append(mod.shortChar);
595 }
596 }
597 return sb.toString();
598 }
599
600 @Override
601 protected Map<Modifier, Boolean> fromString(String string) {
602 return createFromString(string);
603 }
604
605 private static Map<Modifier, Boolean> createFromString(String string) {
606 Map<Modifier, Boolean> ret = new EnumMap<>(Modifier.class);
607 for (char c : string.toCharArray()) {
608 if (c == '?') {
609 continue;
610 }
611 Optional<Modifier> mod = Modifier.findWithShortCode(c);
612 if (mod.isPresent()) {
613 ret.put(mod.get(), Character.isUpperCase(c));
614 } else {
615 Main.debug("Ignoring unknown modifier {0}", c);
616 }
617 }
618 return Collections.unmodifiableMap(ret);
619 }
620 }
621
622 private enum Modifier {
623 CTRL('c'),
624 ALT('a'),
625 SHIFT('s');
626
627 private final char shortChar;
628
629 Modifier(char shortChar) {
630 this.shortChar = Character.toLowerCase(shortChar);
631 }
632
633 /**
634 * Find the modifier with the given short code
635 * @param charCode The short code
636 * @return The modifier
637 */
638 public static Optional<Modifier> findWithShortCode(int charCode) {
639 return Stream.of(values()).filter(m -> m.shortChar == Character.toLowerCase(charCode)).findAny();
640 }
641 }
642}
Note: See TracBrowser for help on using the repository browser.