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

Last change on this file since 10869 was 10869, checked in by Don-vip, 8 years ago

fix #13402 - Shift+P (make parallel ways copy) causes crash (patch by michael2402, modified) - regression - gsoc-core

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