package com.baselet.gwt.client.view; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import com.baselet.control.basics.geom.Point; import com.baselet.control.basics.geom.Rectangle; import com.baselet.control.constants.SharedConstants; import com.baselet.element.interfaces.GridElement; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.event.dom.client.ContextMenuEvent; import com.google.gwt.event.dom.client.ContextMenuHandler; import com.google.gwt.event.dom.client.DoubleClickEvent; import com.google.gwt.event.dom.client.DoubleClickHandler; import com.google.gwt.event.dom.client.HumanInputEvent; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.dom.client.KeyUpEvent; import com.google.gwt.event.dom.client.KeyUpHandler; import com.google.gwt.event.dom.client.MouseDownEvent; import com.google.gwt.event.dom.client.MouseDownHandler; import com.google.gwt.event.dom.client.MouseEvent; import com.google.gwt.event.dom.client.MouseMoveEvent; import com.google.gwt.event.dom.client.MouseMoveHandler; import com.google.gwt.event.dom.client.MouseOutEvent; import com.google.gwt.event.dom.client.MouseOutHandler; import com.google.gwt.event.dom.client.MouseOverEvent; import com.google.gwt.event.dom.client.MouseOverHandler; import com.google.gwt.event.dom.client.MouseUpEvent; import com.google.gwt.event.dom.client.MouseUpHandler; import com.google.gwt.event.dom.client.TouchEndEvent; import com.google.gwt.event.dom.client.TouchEndHandler; import com.google.gwt.event.dom.client.TouchEvent; import com.google.gwt.event.dom.client.TouchMoveEvent; import com.google.gwt.event.dom.client.TouchMoveHandler; import com.google.gwt.event.dom.client.TouchStartEvent; import com.google.gwt.event.dom.client.TouchStartHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.ui.FocusPanel; public class EventHandlingUtils { private static final List<DragStatus> DRAG_COMMANDS = Arrays.asList(DragStatus.FIRST, DragStatus.CONTINUOUS); public static interface EventHandlingTarget { HandlerRegistration addMouseOutHandler(MouseOutHandler mouseOutHandler); HandlerRegistration addMouseOverHandler(MouseOverHandler mouseOverHandler); void handleKeyDown(KeyDownEvent event); void handleKeyUp(KeyUpEvent event); void onMouseMoveDraggingScheduleDeferred(Point moveStart, int diffX, int diffY, GridElement elementToDrag, boolean shiftKeyDown, boolean controlKeyDown, boolean b); void onMouseMove(Point point); void onMouseDragEnd(GridElement elementToDrag, Point point); void onMouseDownScheduleDeferred(GridElement elementToDrag, boolean controlKeyDown); void onDoubleClick(GridElement gridElementOnPosition); void onShowMenu(Point p); Rectangle getVisibleBounds(); int getAbsoluteLeft(); int getAbsoluteTop(); void setFocus(boolean b); GridElement getGridElementOnPosition(Point p); Element getElement(); } private static enum DragStatus { FIRST, CONTINUOUS, NO } private static class DragCache { private DragStatus dragging = DragStatus.NO; private Point moveStart; private GridElement elementToDrag; private EventHandlingTarget activePanel; private EventHandlingTarget mouseContainingPanel; private List<HandlerRegistration> nonTouchHandlers = new ArrayList<HandlerRegistration>(); /** * doubleclicks are only handled if the mouse has moved into the canvas before * this is necessary to void unwanted propagation of suggestbox-selections via doubleclick * TODO: a better fix would be a custom SuggestDisplay which stops mouseevent propagation after handling them */ private boolean doubleClickEnabled = true; // private Timer menuShowTimer; //TODO doesn't really work at the moment (because some move and end events are not processed, therefore it's shown even if not wanted) } public static void addEventHandler(final FocusPanel handlerTarget, final EventHandlingTarget... panels) { final DragCache storage = new DragCache(); for (final EventHandlingTarget panel : panels) { storage.nonTouchHandlers.add(panel.addMouseOutHandler(new MouseOutHandler() { @Override public void onMouseOut(MouseOutEvent event) { storage.mouseContainingPanel = null; } })); storage.nonTouchHandlers.add(panel.addMouseOverHandler(new MouseOverHandler() { @Override public void onMouseOver(MouseOverEvent event) { storage.mouseContainingPanel = panel; } })); } handlerTarget.addTouchStartHandler(new TouchStartHandler() { @Override public void onTouchStart(final TouchStartEvent event) { // some mouseevents are interfering with touch events (eg: mousemove is triggered on each touchdown event) therefore they are removed as soon as a touch event is detected if (storage.nonTouchHandlers != null) { for (HandlerRegistration h : storage.nonTouchHandlers) { h.removeHandler(); } storage.nonTouchHandlers = null; } if (event.getTouches().length() == 1) { // only handle single finger touches (to allow zooming with 2 fingers) final Point absolutePos = getPointAbsolute(event); storage.activePanel = getPanelWhichContainsPoint(panels, absolutePos); if (storage.activePanel != null) { handleStart(panels, storage, handlerTarget, event, getPoint(storage.activePanel, event)); } // storage.menuShowTimer = new Timer() { // @Override // public void run() { // handleShowMenu(storage.activePanel, absolutePos); // } // }; // storage.menuShowTimer.schedule(1000); } } }); handlerTarget.addTouchEndHandler(new TouchEndHandler() { @Override public void onTouchEnd(TouchEndEvent event) { // storage.menuShowTimer.cancel(); if (storage.activePanel != null) { handleEnd(storage.activePanel, storage, event); } } }); handlerTarget.addTouchMoveHandler(new TouchMoveHandler() { @Override public void onTouchMove(TouchMoveEvent event) { // storage.menuShowTimer.cancel(); if (event.getTouches().length() == 1) { // only handle single finger touches (to allow zooming with 2 fingers) handleMove(storage.activePanel, storage, event); } } }); storage.nonTouchHandlers.add(handlerTarget.addMouseDownHandler(new MouseDownHandler() { @Override public void onMouseDown(MouseDownEvent event) { storage.activePanel = getPanelWhichContainsPoint(panels, getPointAbsolute(event)); if (storage.activePanel != null) { handleStart(panels, storage, handlerTarget, event, getPoint(storage.activePanel, event)); } } })); storage.nonTouchHandlers.add(handlerTarget.addMouseUpHandler(new MouseUpHandler() { @Override public void onMouseUp(MouseUpEvent event) { if (storage.activePanel != null) { handleEnd(storage.activePanel, storage, event); } } })); storage.nonTouchHandlers.add(handlerTarget.addMouseOutHandler(new MouseOutHandler() { @Override public void onMouseOut(MouseOutEvent event) { if (storage.activePanel != null) { handleEnd(storage.activePanel, storage, event); storage.doubleClickEnabled = false; } } })); storage.nonTouchHandlers.add(handlerTarget.addMouseOverHandler(new MouseOverHandler() { @Override public void onMouseOver(MouseOverEvent event) { storage.doubleClickEnabled = true; } })); storage.nonTouchHandlers.add(handlerTarget.addMouseMoveHandler(new MouseMoveHandler() { @Override public void onMouseMove(MouseMoveEvent event) { handleMove(storage.activePanel, storage, event); } })); // double tap on mobile devices is not easy to implement because browser zoom on double-tap is not an event which can be canceled storage.nonTouchHandlers.add(handlerTarget.addDoubleClickHandler(new DoubleClickHandler() { @Override public void onDoubleClick(DoubleClickEvent event) { if (storage.activePanel != null) { if (storage.doubleClickEnabled) { handleDoubleClick(storage.activePanel, getPoint(storage.activePanel, event)); } } } })); storage.nonTouchHandlers.add(handlerTarget.addDomHandler(new ContextMenuHandler() { @Override public void onContextMenu(ContextMenuEvent event) { if (storage.activePanel != null) { event.preventDefault(); // avoid default contextmenu event.stopPropagation(); handleShowMenu(storage.activePanel, new Point(event.getNativeEvent().getClientX(), event.getNativeEvent().getClientY())); } } }, ContextMenuEvent.getType())); storage.nonTouchHandlers.add(handlerTarget.addKeyDownHandler(new KeyDownHandler() { @Override public void onKeyDown(KeyDownEvent event) { if (storage.activePanel != null) { storage.activePanel.handleKeyDown(event); } } })); storage.nonTouchHandlers.add(handlerTarget.addKeyUpHandler(new KeyUpHandler() { @Override public void onKeyUp(KeyUpEvent event) { if (storage.activePanel != null) { storage.activePanel.handleKeyUp(event); } } })); Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { handlerTarget.setFocus(true); // set focus to enable keyboard shortcuts } }); } private static void handleEnd(EventHandlingTarget panel, final DragCache storage, HumanInputEvent<?> event) { // Notification.showInfo("UP"); if (DRAG_COMMANDS.contains(storage.dragging)) { panel.onMouseDragEnd(storage.elementToDrag, getPoint(storage.activePanel, event)); } storage.dragging = DragStatus.NO; } private static void handleStart(EventHandlingTarget[] panels, final DragCache storage, FocusPanel handlerTarget, HumanInputEvent<?> event, Point p) { // Notification.showInfo("DOWN " + p.x); handlerTarget.setFocus(true); event.preventDefault(); // necessary to avoid showing textcursor and selecting proppanel in chrome AND to avoid scrolling with touch move (problem is it also avoids scrolling with 2 fingers) storage.moveStart = new Point(p.x, p.y); storage.dragging = DragStatus.FIRST; storage.elementToDrag = storage.activePanel.getGridElementOnPosition(storage.moveStart); storage.activePanel.onMouseDownScheduleDeferred(storage.elementToDrag, event.isControlKeyDown()); } private static void handleMove(final EventHandlingTarget panel, final DragCache storage, HumanInputEvent<?> event) { // Notification.showInfo("MOVE " + getPointAbsolute(event)); if (storage.activePanel != null && DRAG_COMMANDS.contains(storage.dragging)) { Point p = getPoint(storage.activePanel, event); int diffX = p.x - storage.moveStart.getX(); int diffY = p.y - storage.moveStart.getY(); diffX -= diffX % SharedConstants.DEFAULT_GRID_SIZE; diffY -= diffY % SharedConstants.DEFAULT_GRID_SIZE; if (diffX != 0 || diffY != 0) { panel.onMouseMoveDraggingScheduleDeferred(storage.moveStart, diffX, diffY, storage.elementToDrag, event.isShiftKeyDown(), event.isControlKeyDown(), storage.dragging == DragStatus.FIRST); storage.dragging = DragStatus.CONTINUOUS; // after FIRST real drag switch to CONTINUOUS storage.moveStart = storage.moveStart.copy().move(diffX, diffY); // make copy because otherwise deferred action will act on wrong position } } else if (storage.mouseContainingPanel != null) { storage.mouseContainingPanel.onMouseMove(getPoint(storage.mouseContainingPanel, event)); } } private static void handleDoubleClick(final EventHandlingTarget panel, Point p) { panel.onDoubleClick(panel.getGridElementOnPosition(p)); } private static void handleShowMenu(final EventHandlingTarget panel, Point p) { panel.onShowMenu(p); } private static EventHandlingTarget getPanelWhichContainsPoint(EventHandlingTarget[] panels, Point p) { EventHandlingTarget returnPanel = null; for (EventHandlingTarget panel : panels) { Rectangle visibleBounds = panel.getVisibleBounds(); visibleBounds.move(panel.getAbsoluteLeft(), panel.getAbsoluteTop()); if (visibleBounds.contains(p)) { panel.setFocus(true); returnPanel = panel; } else { panel.setFocus(false); } } return returnPanel; } private static Point getPoint(EventHandlingTarget drawPanelCanvas, HumanInputEvent<?> event) { Element e = drawPanelCanvas.getElement(); if (event instanceof MouseEvent<?>) { return new Point(((MouseEvent<?>) event).getRelativeX(e), ((MouseEvent<?>) event).getRelativeY(e)); } else if (event instanceof TouchEndEvent) { return new Point(((TouchEvent<?>) event).getChangedTouches().get(0).getRelativeX(e), ((TouchEvent<?>) event).getChangedTouches().get(0).getRelativeY(e)); } else if (event instanceof TouchEvent<?>) { return new Point(((TouchEvent<?>) event).getTouches().get(0).getRelativeX(e), ((TouchEvent<?>) event).getTouches().get(0).getRelativeY(e)); } else { throw new RuntimeException("Unknown Event Type: " + event); } } private static Point getPointAbsolute(HumanInputEvent<?> event) { if (event instanceof MouseEvent<?>) { return new Point(((MouseEvent<?>) event).getClientX(), ((MouseEvent<?>) event).getClientY()); } else if (event instanceof TouchEndEvent) { return new Point(((TouchEvent<?>) event).getChangedTouches().get(0).getPageX(), ((TouchEvent<?>) event).getChangedTouches().get(0).getPageY()); } else if (event instanceof TouchEvent<?>) { return new Point(((TouchEvent<?>) event).getTouches().get(0).getPageX(), ((TouchEvent<?>) event).getTouches().get(0).getPageY()); } else { throw new RuntimeException("Unknown Event Type: " + event); } } }