package org.geogebra.web.html5.main;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.common.main.App;
import org.geogebra.common.main.GWTKeycodes;
import org.geogebra.common.main.KeyCodes;
import org.geogebra.common.util.debug.Log;
import org.geogebra.common.util.lang.Unicode;
import org.geogebra.web.html5.euclidian.EuclidianViewW;
import org.geogebra.web.html5.gui.GeoGebraFrameW;
import org.geogebra.web.html5.gui.GuiManagerInterfaceW;
import org.geogebra.web.html5.util.ArticleElement;
import org.geogebra.web.html5.util.Dom;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyEvent;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
/**
* Handles keyboard events.
*/
public class GlobalKeyDispatcherW extends
org.geogebra.common.main.GlobalKeyDispatcher implements KeyUpHandler,
KeyDownHandler, KeyPressHandler {
private static boolean controlDown = false;
private static boolean shiftDown = false;
private boolean keydownPreventsDefaultKeypressTAB = false;
/**
* @return whether ctrll is pressed
*/
public static boolean getControlDown() {
return controlDown;
}
/**
* @return whether shift is pressed
*/
public static boolean getShiftDown() {
return shiftDown;
}
/**
* Update ctrl, shift flags
*
* @param ev
* key event
*/
public static void setDownKeys(KeyEvent<? extends EventHandler> ev) {
controlDown = ev.isControlKeyDown();
shiftDown = ev.isShiftKeyDown();
}
/**
* Used if we need tab working properly
*/
private boolean inFocus = false;
private static boolean isHandlingTab;
/**
* @param tab
* whether tab event was registered by preview handler
*/
static void setHandlingTab(boolean tab) {
isHandlingTab = tab;
}
/**
* @param app
* application
*/
public GlobalKeyDispatcherW(App app) {
super(app);
initNativeKeyHandlers();
}
private void initNativeKeyHandlers() {
Event.addNativePreviewHandler(new NativePreviewHandler() {
@Override
public void onPreviewNativeEvent(NativePreviewEvent event) {
Element targetElement = Element.as(event.getNativeEvent()
.getEventTarget());
ArticleElement targetArticle = getGGBArticle(targetElement);
if (targetArticle == null) {
return;
}
HashMap<String, AppW> articleMap = GeoGebraFrameW
.getArticleMap();
if (articleMap.get(targetArticle.getId()) == null) {
return;
}
boolean appfocused = articleMap.get(targetArticle.getId())
.getGlobalKeyDispatcher().isFocused();
switch (event.getTypeInt()) {
default:
// do nothing
break;
case Event.ONKEYDOWN:
if (event.getNativeEvent().getKeyCode() == 9) { // TAB
// pressed
if (!appfocused) {
event.cancel();
setHandlingTab(true);
// TODO - set border in an other place...
GeoGebraFrameW.useDataParamBorder(targetArticle,
getChildElementByStyleName(targetArticle,
"GeoGebraFrame"));
ArticleElement nextArticle = getNextArticle(targetArticle);
focusArticle(nextArticle);
}
}
preventIfNotTabOrEnter(event, appfocused);
break;
case Event.ONKEYPRESS:
case Event.ONKEYUP:
// not TAB and not ENTER
preventIfNotTabOrEnter(event, appfocused);
}
}
});
}
/**
* Focus article or dummy (if article is null)
*
* @param nextArticle
* article to be focused
*/
protected void focusArticle(ArticleElement nextArticle) {
if (nextArticle == null) {
// TODO: go to a dummy after last article
NodeList<Element> dummies = Dom
.getElementsByClassName("geogebraweb-dummy-invisible");
if (dummies.getLength() > 0) {
dummies.getItem(0).focus();
} else {
Log.warn("No dummy found.");
}
} else {
nextArticle.focus();
}
}
/**
* @param event
* native event
* @param appfocused
* whether app is focused
*/
protected void preventIfNotTabOrEnter(NativePreviewEvent event,
boolean appfocused) {
if (event.getNativeEvent().getKeyCode() != 9
&& event.getNativeEvent().getKeyCode() != 13) {
if (!appfocused) {
event.cancel();
}
}
}
/**
* @return whether tab is handled
*/
public static boolean getIsHandlingTab() {
return isHandlingTab;
}
/**
* @param parent
* parent
* @param childName
* class name of child
* @return children with given class name
*/
public Element getChildElementByStyleName(Element parent,
String childName){
NodeList<Element> elements = Dom.getElementsByClassName(childName);
for (int i = 0; i < elements.getLength(); i++) {
if (elements.getItem(i).getParentElement() == parent) {
return elements.getItem(i);
}
}
return null;
}
/**
* @param ggbapp
* current article
* @return next article
*/
ArticleElement getNextArticle(ArticleElement ggbapp) {
ArrayList<ArticleElement> mobileTags = ArticleElement
.getGeoGebraMobileTags();
for (int i = 0; i < mobileTags.size() - 1; i++) {
if (mobileTags.get(i).equals(ggbapp)) {
return mobileTags.get(i + 1);
}
}
NodeList<Element> appletscalers = Dom
.getElementsByClassName("applet_scaler");
for (int i = 0; i < appletscalers.getLength() - 1; i++) {
Element actualArticle = appletscalers.getItem(i)
.getElementsByTagName("Article").getItem(0);
if (ggbapp.equals(actualArticle)) {
return ArticleElement.as(appletscalers.getItem(i + 1)
.getFirstChildElement());
}
}
return null;
}
/**
* @param el
* child
* @return parent article element corresponding to applet
*/
ArticleElement getGGBArticle(Element el) {
// if SVG clicked, getClassName returns non-string
if ((el.getClassName() + "")
.indexOf("geogebraweb-dummy-invisible") >= 0) {
return null;
}
// TODO: sure ArticleElement?
Element ggwparent = getParentWithClassName(el, "geogebraweb");
// debug("ggwparent tagname: " + ggwparent.getTagName());
if (ggwparent != null && ggwparent.getTagName().equals("ARTICLE")) {
return ArticleElement.as(ggwparent);
}
ggwparent = getParentWithClassName(el, "applet_scaler");
if (ggwparent != null) {
NodeList<Element> articles = ggwparent
.getElementsByTagName("article");
if (articles.getLength() > 0) {
return ArticleElement.as(articles.getItem(0));
}
}
return null;
}
private static Element getParentWithClassName(Element child,
String className) {
Element el = child;
do {
List<String> classnames = Arrays.asList(el.getClassName()
.split(" "));
if (classnames.contains(className)) {
return el;
}
if (el.hasParentElement()) {
el = el.getParentElement();
}
} while (el.hasParentElement());
return null;
}
/**
* @param focus
* whether this applet has focus
*/
public void setFocused(boolean focus) {
inFocus = focus;
}
/**
* Set to focus unless we are handlin tab key
*/
public void setFocusedIfNotTab(){
if (isHandlingTab) {
setHandlingTab(false);
} else {
setFocused(true);
}
}
/**
* @return whether this applet has focus
*/
public boolean isFocused() {
return inFocus;
}
@Override
public void onKeyPress(KeyPressEvent event) {
setDownKeys(event);
event.stopPropagation();
if (inFocus) {
// in theory, default action of TAB is not triggered here
// but it seems Firefox triggers the default action of TAB
// here (or some place other than onKeyDown), so we only
// have to call preventdefault if it is not a TAB key!
// TAB only fires in Firefox here, and it only has a keyCode!
KeyCodes kc = KeyCodes.translateGWTcode(event.getNativeEvent()
.getKeyCode());
if (kc != KeyCodes.TAB) {
event.preventDefault();
} else if (keydownPreventsDefaultKeypressTAB) {
// we only have to allow default action for TAB
// if the onKeyDown handler allowed it, so we
// have to check this boolean here, which is double
// useful for some other reason as well,
// in EuclidianViewW, for checking action on focus
event.preventDefault();
}
}
// this needs to be done in onKeyPress -- keyUp is not case sensitive
if (!event.isAltKeyDown() && !event.isControlKeyDown()) {
this.renameStarted(event.getCharCode());
}
}
@Override
public void onKeyUp(KeyUpEvent event) {
setDownKeys(event);
if (inFocus) {
// KeyCodes kc =
// KeyCodes.translateGWTcode(event.getNativeKeyCode());
// if (kc != KeyCodes.TAB) {
// maybe need to check TAB in Firefox, or in onKeyPress
// but probably not, so this check is commented out
event.preventDefault();
// }
}
event.stopPropagation();
// now it is private, but can be public, also it is void, but can return
// boolean as in desktop, if needed
dispatchEvent(event);
}
private void dispatchEvent(KeyUpEvent event) {
// we Must find out something here to identify the component that fired
// this, like class names for example,
// id-s or data-param-attributes
// we have keypress here only
// do this only, if we really have focus
if (inFocus) {
handleKeyPressed(event);
} else if (event.getNativeKeyCode() == com.google.gwt.event.dom.client.KeyCodes.KEY_ENTER) {
setFocused(true);
}
}
private boolean handleKeyPressed(KeyUpEvent event) {
// GENERAL KEYS:
// handle ESC, function keys, zooming with Ctrl +, Ctrl -, etc.
if (handleGeneralKeys(event)) {
return true;
}
// SELECTED GEOS:
// handle function keys, arrow keys, +/- keys for selected geos, etc.
// if (handleSelectedGeosKeys(event,
// app.getSelectionManager().getSelectedGeos())) {
// return true;
// }
return false;
}
/**
* Handles key event by disassembling it into primitive types and handling
* it using the mothod from common
*
* @param event
* event
* @return whether event was consumed
*/
public boolean handleGeneralKeys(KeyUpEvent event) {
KeyCodes kc = KeyCodes.translateGWTcode(event.getNativeKeyCode());
if (kc == KeyCodes.TAB || kc == KeyCodes.ESCAPE) {
// the problem is that we want to prevent the default action
// of the TAB key event... but this is too late to do
// in KeyUpEvent, so instead, we're going to handle TAB
// as early as KeyDownEvent, and do nothing here to make
// sure things are not executed twice (assuming return true)
// maybe in Chrome this is needed here as well...
event.preventDefault();
// in theory, this is already called, but maybe not in case of
// AlgebraInputW.onKeyUp, AutoCompleteTextFieldW.onKeyUp
event.stopPropagation();
return true;
}
boolean handled = handleGeneralKeys(kc,
event.isShiftKeyDown(), event.isControlKeyDown(),
event.isAltKeyDown(), false, true);
if (handled) {
event.preventDefault();
}
return handled;
}
/**
*
* @param event
* native event
* @return whether it was handled
*/
public boolean handleSelectedGeosKeysNative(NativeEvent event) {
return handleSelectedGeosKeys(
KeyCodes.translateGWTcode(event
.getKeyCode()), selection.getSelectedGeos(),
event.getShiftKey(), event.getCtrlKey(), event.getAltKey(),
false);
}
@Override
public void onKeyDown(KeyDownEvent event) {
Log.debug("KEY pressed::"
+ KeyCodes.translateGWTcode(event.getNativeKeyCode()) + " in "
+ getActive());
setDownKeys(event);
// AbstractApplication.debug("onkeydown");
EuclidianViewW.resetTab();
event.stopPropagation();
// this is quite complex, call at the end of the method
keydownPreventsDefaultKeypressTAB = false;
// SELECTED GEOS:
// handle function keys, arrow keys, +/- keys for selected geos, etc.
boolean handled = handleSelectedGeosKeys(
KeyCodes.translateGWTcode(event.getNativeKeyCode()), app
.getSelectionManager().getSelectedGeos(),
event.isShiftKeyDown(), event.isControlKeyDown(),
event.isAltKeyDown(), false);
// if not handled, do not consume so that keyPressed works
if (inFocus && handled) {
keydownPreventsDefaultKeypressTAB = true;
}
// Now comes what were in KeyUpEvent for the TAB key,
// necessary to move it to here because preventDefault only
// works here for the TAB key, otherwise both the default
// browser action (for tabindex) and custom code would run
KeyCodes kc = KeyCodes.translateGWTcode(event.getNativeKeyCode());
if (kc == KeyCodes.TAB) {
// event.stopPropagation() is already called!
boolean success = handleTab(event.isControlKeyDown(),
event.isShiftKeyDown(), true);
keydownPreventsDefaultKeypressTAB = EuclidianViewW
.checkTabPress(success);
} else if (kc == KeyCodes.ESCAPE) {
keydownPreventsDefaultKeypressTAB = true;
// EuclidianViewW.tabPressed = false;
// if (app.isApplet()) {
// app.loseFocus();
// }
app.setMoveMode();
// here we shall focus on a dummy element that is
// after all graphics views by one:
// if (GeoGebraFrameW.lastDummy != null) {
// GeoGebraFrameW.lastDummy.focus();
// }
setFocused(false);
// printActiveElement();
} else if (inFocus && preventBrowserCtrl(kc)
&& event.isControlKeyDown()) {
event.preventDefault();
}
if (keydownPreventsDefaultKeypressTAB) {
event.preventDefault();
}
}
private native String getActive() /*-{
return $doc.activeElement ? $doc.activeElement.tagName + "."
+ $doc.activeElement.className : "?";
}-*/;
private static boolean preventBrowserCtrl(KeyCodes kc) {
return kc == KeyCodes.S || kc == KeyCodes.O;
}
// public static native void printActiveElement() /*-{
// $wnd.console.log($wnd.document.activeElement);
// }-*/;
/**
* This method is almost the same as GlobalKeyDispatcher.handleTab, just is
* also return a value whether the operation was successful in case of no
* cycle
*/
@Override
public boolean handleTab(boolean isControlDown, boolean isShiftDown, boolean cycle) {
app.getActiveEuclidianView().closeDropdowns();
if (isShiftDown) {
selection.selectLastGeo(app.getActiveEuclidianView());
return true;
}
boolean forceRet = false;
if (selection.getSelectedGeos().size() == 0) {
forceRet = true;
}
return selection.selectNextGeo(app.getActiveEuclidianView(), cycle)
|| forceRet;
}
@Override
protected boolean handleCtrlShiftN(boolean isAltDown) {
Log.debug("unimplemented");
return false;
}
@Override
protected boolean handleEnter() {
if (super.handleEnter()) {
return true;
}
if (((AppW) app).isUsingFullGui()
&& ((GuiManagerInterfaceW) app.getGuiManager()).noMenusOpen()) {
if (app.showAlgebraInput()) {
// && !((GuiManagerW) app.getGuiManager()).getAlgebraInput()
// .hasFocus()) {
if (((GuiManagerInterfaceW) app.getGuiManager())
.getAlgebraInput() != null) {
((GuiManagerInterfaceW) app.getGuiManager())
.getAlgebraInput().requestFocus();
return true;
}
}
}
return false;
}
@Override
protected void copyDefinitionsToInputBarAsList(ArrayList<GeoElement> geos) {
Log.debug("unimplemented");
}
@Override
protected void createNewWindow() {
Log.debug("unimplemented");
}
@Override
protected void showPrintPreview(App app2) {
Log.debug("unimplemented");
}
/**
* @param keyCode
* GWT / JavaScript keycode
* @return ug superscript 2 for Alt-2
*/
public static String processAltCode(int keyCode) {
switch (keyCode) {
case GWTKeycodes.KEY_O:
return Unicode.DEGREE;
case GWTKeycodes.KEY_P:
if (shiftDown) {
return Unicode.Pi + "";
}
return Unicode.pi + "";
case GWTKeycodes.KEY_I:
return Unicode.IMAGINARY;
case GWTKeycodes.KEY_A:
if (shiftDown) {
return Unicode.Alpha + "";
}
return Unicode.alpha + "";
case GWTKeycodes.KEY_B:
if (shiftDown) {
return Unicode.Beta + "";
}
return Unicode.beta + "";
case GWTKeycodes.KEY_G:
if (shiftDown) {
return Unicode.Gamma + "";
}
return Unicode.gamma + "";
case GWTKeycodes.KEY_T:
if (shiftDown) {
return Unicode.Theta + "";
}
return Unicode.theta + "";
case GWTKeycodes.KEY_U:
// U, euro sign is shown on HU
return Unicode.INFINITY + "";
case GWTKeycodes.KEY_L:
// L, \u0141 sign is shown on HU
if (shiftDown) {
return Unicode.Lambda + "";
}
return Unicode.lambda + "";
case GWTKeycodes.KEY_M:
if (shiftDown) {
return Unicode.Mu + "";
}
return Unicode.mu + "";
case GWTKeycodes.KEY_W:
// Alt-W is | needed for abs()
if (shiftDown) {
return Unicode.Omega + "";
}
return Unicode.omega + "";
case GWTKeycodes.KEY_R:
return Unicode.SQUARE_ROOT + "";
case GWTKeycodes.KEY_ONE:
return Unicode.Superscript_1 + "";
case GWTKeycodes.KEY_TWO:
return Unicode.Superscript_2 + "";
case GWTKeycodes.KEY_THREE:
return Unicode.Superscript_3 + "";
case GWTKeycodes.KEY_FOUR:
return Unicode.Superscript_4 + "";
case GWTKeycodes.KEY_FIVE:
return Unicode.Superscript_5 + "";
case GWTKeycodes.KEY_SIX:
return Unicode.Superscript_6 + "";
case GWTKeycodes.KEY_SEVEN:
return Unicode.Superscript_7 + "";
case GWTKeycodes.KEY_EIGHT:
return Unicode.Superscript_8 + "";
case GWTKeycodes.KEY_NINE:
return Unicode.Superscript_9 + "";
case GWTKeycodes.KEY_ZERO:
return Unicode.Superscript_0 + "";
case GWTKeycodes.KEY_MINUS:
return Unicode.Superscript_Minus + "";
case GWTKeycodes.KEY_X:
return "^x";
case GWTKeycodes.KEY_Y:
return "^y";
default:
return null;
}
}
/**
*
* @param e
* The KeyEvent
* @return true if unwanted key combination has pressed.
*/
public static boolean isBadKeyEvent(KeyEvent<? extends EventHandler> e) {
return e.isAltKeyDown() && !e.isControlKeyDown()
&& e.getNativeEvent().getCharCode() > 128;
}
}