/* GeoGebra - Dynamic Mathematics for Everyone http://www.geogebra.org This file is part of GeoGebra. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation. */ package org.geogebra.web.web.gui.view.algebra; import java.util.ArrayList; import org.geogebra.common.awt.GPoint; import org.geogebra.common.euclidian.EuclidianConstants; import org.geogebra.common.euclidian.EuclidianViewInterfaceCommon; import org.geogebra.common.euclidian.event.AbstractEvent; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.main.App; import org.geogebra.common.main.Feature; import org.geogebra.common.main.SelectionManager; import org.geogebra.common.util.debug.Log; import org.geogebra.web.html5.Browser; import org.geogebra.web.html5.event.PointerEvent; import org.geogebra.web.html5.event.ZeroOffset; import org.geogebra.web.html5.gui.tooltip.ToolTipManagerW; import org.geogebra.web.html5.gui.util.CancelEventTimer; import org.geogebra.web.html5.gui.util.LongTouchManager; import org.geogebra.web.html5.gui.util.LongTouchTimer.LongTouchHandler; import org.geogebra.web.html5.main.AppW; import org.geogebra.web.html5.util.EventUtil; import org.geogebra.web.html5.util.sliderPanel.SliderWJquery; import org.geogebra.web.web.gui.GuiManagerW; import org.geogebra.web.web.gui.layout.panels.AlgebraStyleBarW; import org.geogebra.web.web.main.AppWFull; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.Touch; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.DoubleClickEvent; import com.google.gwt.event.dom.client.DoubleClickHandler; 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.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.user.client.Window; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Widget; /** * Controller class of a AV item. * * @author laszlo * */ @SuppressWarnings("javadoc") public class RadioTreeItemController implements ClickHandler, DoubleClickHandler, MouseDownHandler, MouseUpHandler, MouseOverHandler, MouseMoveHandler, MouseOutHandler, TouchStartHandler, TouchMoveHandler, TouchEndHandler, LongTouchHandler { private static final int VERTICAL_PADDING = 20; protected AppWFull app; LatexTreeItem item; private LongTouchManager longTouchManager; protected AVSelectionController selectionCtrl; protected boolean editing = false; private boolean markForEdit = false; public long latestTouchEndTime = 0; private int editHeigth; private boolean inputAsText = false; public RadioTreeItemController(LatexTreeItem item) { this.item = item; this.app = item.app; selectionCtrl = getAV().getSelectionCtrl(); addDomHandlers(item.main); } protected boolean isMarbleHit(MouseEvent<?> evt) { PointerEvent wrappedEvent = PointerEvent.wrapEventAbsolute(evt, ZeroOffset.instance); if (item.marblePanel != null && item.marblePanel.isHit(evt.getClientX(), evt.getClientY())) { if (app.has(Feature.AV_CONTEXT_MENU) && app.isRightClick(wrappedEvent)) { onRightClick(evt.getClientX(), evt.getClientY()); return false; } return true; } return false; } protected static boolean isWidgetHit(Widget w, MouseEvent<?> evt) { return isWidgetHit(w, evt.getClientX(), evt.getClientY()); } static boolean isWidgetHit(Widget w, PointerEvent evt) { return isWidgetHit(w, evt.getX(), evt.getY()); } private static boolean isWidgetHit(Widget w, int x, int y) { if (w == null) { return false; } int left = w.getAbsoluteLeft(); int top = w.getAbsoluteTop(); int right = left + w.getOffsetWidth(); int bottom = top + w.getOffsetHeight(); return (x > left && x < right && y > top && y < bottom); } @Override public void onDoubleClick(DoubleClickEvent evt) { evt.stopPropagation(); if (app.has(Feature.AV_SINGLE_TAP_EDIT)) { return; } if (isMarbleHit(evt)) { return; } if (CancelEventTimer.cancelMouseEvent()) { return; } if (checkEditing()) { return; } startEdit(evt.isControlKeyDown()); } private boolean checkEditing() { return item.commonEditingCheck(); } public boolean isEditing() { return editing; } protected void setEditing(boolean value) { editing = value; } @Override public void onMouseOver(MouseOverEvent event) { if (item.geo == null) { return; } ToolTipManagerW.sharedInstance() .showToolTip(item.geo.getLongDescriptionHTML(true, true)); } @Override public void onMouseOut(MouseOutEvent event) { ToolTipManagerW.sharedInstance().showToolTip(null); } @Override public void onMouseDown(MouseDownEvent event) { event.stopPropagation(); PointerEvent wrappedEvent = PointerEvent.wrapEventAbsolute(event, ZeroOffset.instance); onPointerDown(wrappedEvent); CancelEventTimer.avRestoreWidth(); if (CancelEventTimer.cancelMouseEvent() || isMarbleHit(event) || app.isRightClick(wrappedEvent)) { return; } Log.debug("[xx] mouseDown"); if (app.has(Feature.SHOW_ONE_KEYBOARD_BUTTON_IN_FRAME)) { app.getGuiManager().getLayout().getDockManager() .setFocusedPanel(App.VIEW_ALGEBRA); } if (checkEditing()) { // keep focus in editor event.preventDefault(); if (isEditing()) { item.adjustCaret(event.getClientX(), event.getClientY()); } if (isEditing() && !item.isInputTreeItem()) { return; } } if (!isEditing()) { app.closePopups(); } if (markForEdit() && !item.isInputTreeItem()) { return; } handleAVItem(event); item.updateButtonPanelPosition(); } @Override public void onMouseUp(MouseUpEvent event) { if (CancelEventTimer.cancelMouseEvent()) { return; } Log.debug("[xx] mouseUp"); SliderWJquery.stopSliders(); event.stopPropagation(); if (isEditing()) { return; } if (app.has(Feature.AV_SINGLE_TAP_EDIT) && canEditStart(event)) { editOnTap(isEditing(), event); // MOW-85 move to the very left item.adjustCaret(event.getClientX(), event.getClientY()); } } /** * Determines if the item can be edited at that point that event has * happened. For example editing is not allowed clicking on marbles. * * @param event * The mouse event * @return if editing can start or not. */ protected boolean canEditStart(MouseEvent<?> event) { return !isMarbleHit(event); } @Override public void onMouseMove(MouseMoveEvent event) { if (CancelEventTimer.cancelMouseEvent()) { return; } if (app.has(Feature.AV_SINGLE_TAP_EDIT) && Browser.isTabletBrowser()) { // scroll cancels edit request. markForEdit = false; } event.preventDefault(); } @Override public void onTouchEnd(TouchEndEvent event) { event.stopPropagation(); Log.debug("[xx] touchEnd"); if (item.isInputTreeItem()) { showKeyboard(); setFocusDeferred(); event.preventDefault(); } JsArray<Touch> touches = event.getTargetTouches().length() == 0 ? event.getChangedTouches() : event.getTargetTouches(); boolean active = isEditing(); PointerEvent wrappedEvent = PointerEvent.wrapEvent(touches.get(0), ZeroOffset.instance); if (isLongTouchHappened()) { return; } if (editOnTap(active, wrappedEvent)) { onPointerUp(wrappedEvent); CancelEventTimer.touchEventOccured(); return; } long time = System.currentTimeMillis(); if (time - latestTouchEndTime < 500) { // ctrl key, shift key for TouchEndEvent? interesting... latestTouchEndTime = time; if (!checkEditing()) { startEdit(false // event.isControlKeyDown(), // event.isShiftKeyDown() ); } } else { latestTouchEndTime = time; } getLongTouchManager().cancelTimer(); onPointerUp(wrappedEvent); CancelEventTimer.touchEventOccured(); } private void setFocusDeferred() { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { setFocus(true); } }); } @Override public void onTouchMove(TouchMoveEvent event) { event.stopPropagation(); Log.debug("[xx] touchMove"); if (app.has(Feature.AV_SINGLE_TAP_EDIT)) { markForEdit = false; } if (item.isInputTreeItem() || item.isSliderItem()) { event.preventDefault(); } int x = EventUtil.getTouchOrClickClientX(event); int y = EventUtil.getTouchOrClickClientY(event); getLongTouchManager().rescheduleTimerIfRunning(this, x, y); JsArray<Touch> targets = event.getTargetTouches(); AbstractEvent wrappedEvent = PointerEvent.wrapEvent(targets.get(0), ZeroOffset.instance); onPointerMove(wrappedEvent); CancelEventTimer.touchEventOccured(); } @Override public void onTouchStart(TouchStartEvent event) { event.stopPropagation(); Log.debug("[xx] touchStart"); if (item.isInputTreeItem()) { event.preventDefault(); getAV().resetItems(false); } else { if (getAV().getInputTreeItem() != null) { getAV().getInputTreeItem().getController().stopEdit(); } } int x = EventUtil.getTouchOrClickClientX(event); int y = EventUtil.getTouchOrClickClientY(event); if (isEditing()) { item.adjustCaret(x, y); return; } getLongTouchManager().scheduleTimer(this, x, y); if (markForEdit() && !item.isInputTreeItem()) { return; } // Do NOT prevent default, kills scrolling on touch // event.preventDefault(); handleAVItem(event); AbstractEvent wrappedEvent = PointerEvent.wrapEvent(event, ZeroOffset.instance); onPointerDown(wrappedEvent); CancelEventTimer.touchEventOccured(); } protected void onPointerDown(AbstractEvent event) { if (event.isRightClick()) { onRightClick(event.getX(), event.getY()); return; } if (checkEditing()) { if (!getAV().isEditItem()) { // e.g. Web.html might not be in editing mode // initially (temporary fix) item.ensureEditing(); } item.showKeyboard(); item.removeDummy(); ((PointerEvent) event).getWrappedEvent().stopPropagation(); if (item.isInputTreeItem()) { // put earlier, maybe it freezes afterwards? setFocus(true); } } updateSelection(event.isControlDown(), event.isShiftDown()); // if (app.getActiveEuclidianView() // .getMode() == EuclidianConstants.MODE_MOVE // || app.getActiveEuclidianView() // .getMode() == EuclidianConstants.MODE_SELECTION_LISTENER) { // updateSelection(event.isControlDown(), event.isShiftDown()); // } } /** * * @param event * mouse move event */ protected void onPointerMove(AbstractEvent event) { // used to tell EuclidianView to handle mouse over } protected void onPointerUp(AbstractEvent event) { selectionCtrl.setSelectHandled(false); GeoElement geo = item.geo; if (checkEditing()) { if (item.isInputTreeItem()) { AlgebraStyleBarW styleBar = getAV().getStyleBar(false); if (styleBar != null) { styleBar.update(null); } } return; } // Alt click: copy definition to input field if (geo != null && event.isAltDown() && app.showAlgebraInput()) { // F3 key: copy definition to input bar if (!checkEditing()) { startEdit(event.isControlDown()); return; } } EuclidianViewInterfaceCommon ev = app.getActiveEuclidianView(); int mode = ev.getMode(); if (mode != EuclidianConstants.MODE_MOVE && mode != EuclidianConstants.MODE_SELECTION_LISTENER) { // let euclidianView know about the click ev.clickedGeo(geo, app.isControlDown(event)); } ev.mouseMovedOver(null); // previously av.setFocus, but that scrolls AV and seems not to be // necessary item.getElement().focus(); AlgebraStyleBarW styleBar = getAV().getStyleBar(false); if (styleBar != null) { styleBar.update(geo); } } /** * Adds the needed event handlers to FlowPanel * * @param panel * add events to. */ protected void addDomHandlers(FlowPanel panel) { panel.addDomHandler(this, DoubleClickEvent.getType()); panel.addDomHandler(this, ClickEvent.getType()); panel.addDomHandler(this, MouseOverEvent.getType()); panel.addDomHandler(this, MouseOutEvent.getType()); panel.addDomHandler(this, MouseMoveEvent.getType()); panel.addDomHandler(this, MouseDownEvent.getType()); panel.addDomHandler(this, MouseUpEvent.getType()); panel.addDomHandler(this, TouchStartEvent.getType()); panel.addDomHandler(this, TouchMoveEvent.getType()); panel.addDomHandler(this, TouchEndEvent.getType()); } /** * @param ctrl */ protected void startEdit(boolean ctrl) { EuclidianViewInterfaceCommon ev = app.getActiveEuclidianView(); selectionCtrl.clear(); ev.resetMode(); if (!item.hasGeo() || ctrl) { return; } GeoElement geo = item.geo; if (!isEditing()) { geo.setAnimating(false); setEditHeigth(item.getOffsetHeight()); getAV().startEditItem(geo); if (app.has(Feature.AV_INPUT_BUTTON_COVER) && !app.has(Feature.AV_SINGLE_TAP_EDIT)) { item.hideControls(); } Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() { @Override public void execute() { item.adjustStyleBar(); } }); showKeyboard(); } } public void stopEdit() { if (!editing) { return; } item.stopEditing(item.getText(), null); } protected void showKeyboard() { item.showKeyboard(); } private void editOnTap(boolean active, MouseEvent<?> event) { editOnTap(active, PointerEvent.wrapEventAbsolute(event, ZeroOffset.instance)); } protected boolean editOnTap(boolean active, PointerEvent wrappedEvent) { if (!(app.has(Feature.AV_SINGLE_TAP_EDIT) && markForEdit)) { return false; } markForEdit = false; boolean enable = true; if ((item.isSliderItem() && !isWidgetHit(item.getPlainTextItem(), wrappedEvent))) { enable = false; if (active) { stopEdit(); } } if (enable && (!active || item.isInputTreeItem())) { boolean shift = wrappedEvent.isShiftDown(); boolean ctrl = wrappedEvent.isControlDown(); longTouchManager.cancelTimer(); if (!shift && !ctrl) { Log.debug("[AVTAP] single tap edit begins"); startEdit(false); } updateSelection(ctrl, shift); } return true; } public LongTouchManager getLongTouchManager() { return longTouchManager; } public void setLongTouchManager(LongTouchManager longTouchManager) { this.longTouchManager = longTouchManager; } private boolean markForEdit() { if (app.has(Feature.AV_SINGLE_TAP_EDIT)) { if (markForEdit) { return true; } markForEdit = true; return true; } return false; } @Override public void handleLongTouch(int x, int y) { if (app.has(Feature.AV_SINGLE_TAP_EDIT)) { getAV().resetItems(false); } if (app.has(Feature.AV_CONTEXT_MENU)) { onRightClick(x, y); } } private void onRightClick(int x, int y) { if (!app.isRightClickEnabledForAV()) { return; } if (checkEditing()) { return; } GeoElement geo = item.geo; SelectionManager selection = app.getSelectionManager(); GPoint point = new GPoint(x + Window.getScrollLeft(), y + Window.getScrollTop()); if (geo != null) { if (selection.containsSelectedGeo(geo)) {// popup // menu for // current // selection // (including // selected // object) ((GuiManagerW) app.getGuiManager()) .showPopupMenu(selection.getSelectedGeos(), item.av, point); } else {// select only this object and popup menu selection.clearSelectedGeos(false); selection.addSelectedGeo(geo, true, true); ArrayList<GeoElement> temp = new ArrayList<GeoElement>(); temp.add(geo); ((GuiManagerW) app.getGuiManager()).showPopupMenu(temp, item.av, point); } } } @Override public void onClick(ClickEvent evt) { evt.stopPropagation(); if (CancelEventTimer.cancelMouseEvent()) { return; } PointerEvent wrappedEvent = PointerEvent.wrapEvent(evt, ZeroOffset.instance); onPointerUp(wrappedEvent); } boolean handleAVItem(MouseEvent<?> evt) { return handleAVItem(evt.getClientX(), evt.getClientY(), evt.getNativeButton() == NativeEvent.BUTTON_RIGHT); } protected void handleAVItem(TouchStartEvent evt) { if (evt.getTouches().length() == 0) { return; } Touch t = evt.getTouches().get(0); if (handleAVItem(t.getClientX(), t.getClientY(), false)) { evt.preventDefault(); } } /** * @param x * x-coord * @param y * y-coord * @param rightClick * wheher rght click was used */ protected boolean handleAVItem(int x, int y, boolean rightClick) { if (!selectionCtrl.isSelectHandled() && !app.has(Feature.AV_SINGLE_TAP_EDIT)) { item.selectItem(true); } return false; } public void updateSelection(boolean separated, boolean continous) { GeoElement geo = item.geo; if (geo == null) { selectionCtrl.clear(); getAV().updateSelection(); } else { selectionCtrl.select(geo, separated, continous); if (separated && !selectionCtrl.contains(geo)) { selectionCtrl.setSelectHandled(true); getAV().selectRow(geo, false); } else if (continous) { getAV().updateSelection(); } } } private AlgebraViewW getAV() { return item.getAV(); } public void setFocus(boolean b) { item.setFocus(b, false); } public AppW getApp() { return app; } public void removeGeo() { item.geo.remove(); item.setText(""); // make sure the text is not resubmitted on focus lost getAV().setActiveTreeItem(null); } public int getEditHeigth() { return editHeigth; } public void setEditHeigth(int editHeigth) { this.editHeigth = editHeigth - VERTICAL_PADDING; } public boolean hasMultiGeosSelected() { return selectionCtrl.hasMultGeos(); } public boolean isLongTouchHappened() { return app.has(Feature.AV_CONTEXT_MENU) && getLongTouchManager().isLongTouchHappened(); } /** * When setting to true, all input typed treated as text, * so the newly created item will be GeoText. * * used in LatexTreeItemController * @param value to set. */ protected void setInputAsText(boolean value) { inputAsText = value; item.setInputAsText(value); } public void forceInputAsText() { setInputAsText(true); } /** * @return if input should be treated as text item. */ public boolean isInputAsText() { return inputAsText; } }