/**
* This file is part of the ReTeX library - https://github.com/himamis/ReTeX
*
* Copyright (C) 2015 Balazs Bencze
*
* 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; either version 2 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* A copy of the GNU General Public License can be found in the file
* LICENSE.txt provided with the source distribution of this program (see
* the META-INF directory in the source jar). This license can also be
* found on the GNU website at http://www.gnu.org/licenses/gpl.html.
*
* If you did not receive a copy of the GNU General Public License along
* with this program, contact the lead developer, or write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
package com.himamis.retex.editor.web;
import java.util.ArrayList;
import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.Context2d;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.Style.VerticalAlign;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
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.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.Widget;
import com.himamis.retex.editor.share.controller.CursorController;
import com.himamis.retex.editor.share.editor.MathField;
import com.himamis.retex.editor.share.editor.MathFieldInternal;
import com.himamis.retex.editor.share.event.ClickListener;
import com.himamis.retex.editor.share.event.FocusListener;
import com.himamis.retex.editor.share.event.KeyEvent;
import com.himamis.retex.editor.share.event.KeyListener;
import com.himamis.retex.editor.share.event.MathFieldListener;
import com.himamis.retex.editor.share.input.KeyboardInputAdapter;
import com.himamis.retex.editor.share.meta.MetaModel;
import com.himamis.retex.editor.share.model.MathFormula;
import com.himamis.retex.renderer.share.CursorBox;
import com.himamis.retex.renderer.share.SelectionBox;
import com.himamis.retex.renderer.share.TeXFormula;
import com.himamis.retex.renderer.share.TeXIcon;
import com.himamis.retex.renderer.web.JlmLib;
import com.himamis.retex.renderer.web.graphics.JLMContext2d;
public class MathFieldW implements MathField, IsWidget {
protected static MetaModel sMetaModel = new MetaModel();
private MathFieldInternal mathFieldInternal;
private Canvas html;
private Context2d ctx;
private Panel parent;
private boolean focused = false;
private TeXIcon lastIcon;
private double ratio = 1;
private KeyListener keyListener;
private boolean rightAltDown = false;
private boolean leftAltDown = false;
private boolean enabled = true;
private static Timer tick;
private BlurHandler onTextfieldBlur;
private Timer focuser;
private boolean pasteInstalled = false;
private int bottomOffset;
static ArrayList<MathFieldW> instances = new ArrayList<MathFieldW>();
// can't be merged with instances.size because we sometimes remove an
// instance
private static int counter = 0;
/**
*
* @param parent
* parent element
* @param canvas
* drawing context
* @param listener
* listener for special events
*/
public MathFieldW(Panel parent, Canvas canvas,
MathFieldListener listener, boolean directFormulaBuilder) {
html = canvas;
bottomOffset = 10;
this.parent = parent;
mathFieldInternal = new MathFieldInternal(this, directFormulaBuilder);
getHiddenTextArea();
// el.getElement().setTabIndex(1);
this.ctx = canvas.getContext2d();
SelectionBox.touchSelection = false;
mathFieldInternal.setSelectionMode(true);
mathFieldInternal.setFieldListener(listener);
mathFieldInternal.setType(TeXFormula.SANSSERIF);
mathFieldInternal.setFormula(MathFormula.newFormula(sMetaModel));
initTimer();
instances.add(this);
canvas.addDomHandler(new MouseDownHandler() {
@Override
public void onMouseDown(MouseDownEvent event) {
if (!isEnabled()) {
return;
}
event.stopPropagation();
// prevent default to keep focus; also avoid dragging the whole
// editor
event.preventDefault();
setFocus(true);
rightAltDown = false;
leftAltDown = false;
}
}, MouseDownEvent.getType());
setKeyListener(wrap, keyListener);
}
private static void initTimer() {
if (tick == null) {
tick = new Timer() {
@Override
public void run() {
CursorBox.blink = !CursorBox.blink;
for (MathFieldW field : instances) {
field.repaintWeb();
}
}
};
tick.scheduleRepeating(500);
}
}
/**
* @return whether the field can repaint and accept events
*/
protected boolean isEnabled() {
return enabled;
}
/**
* @param flag
* whether the field can repaint and accept events
*/
public void setEnabled(boolean flag) {
this.enabled = flag;
if (parent != null && clip != null) {
parent.add(clip);
}
if (!flag) {
setFocus(false);
}
}
@Override
public void setTeXIcon(TeXIcon icon) {
this.lastIcon = icon;
double height = roundUp(icon.getIconHeight() + bottomOffset);
ctx.getCanvas().getStyle().setHeight(height,
Unit.PX);
ctx.getCanvas().getStyle()
.setWidth(roundUp(icon.getIconWidth() + 30), Unit.PX);
parent.setHeight(height + "px");
parent.getElement().getStyle().setVerticalAlign(VerticalAlign.TOP);
repaintWeb();
}
@Override
public void setFocusListener(FocusListener focusListener) {
// addFocusListener(new FocusListenerAdapterW(focusListener));
}
@Override
public void setClickListener(ClickListener clickListener) {
ClickAdapterW adapter = new ClickAdapterW(clickListener, this);
adapter.listenTo(html);
}
public void setPixelRatio(double ratio) {
this.ratio = ratio;
}
@Override
public void setKeyListener(final KeyListener keyListener) {
this.keyListener = keyListener;
}
private void setKeyListener(final Widget html2,
final KeyListener keyListener) {
html2.addDomHandler(new KeyPressHandler() {
@Override
public void onKeyPress(KeyPressEvent event) {
// don't kill Ctrl+V or write V
if (event.isControlKeyDown() && (event.getCharCode() == 'v'
|| event.getCharCode() == 'V') || leftAltDown) {
event.stopPropagation();
} else {
keyListener.onKeyTyped(
new KeyEvent(event.getNativeEvent().getKeyCode(), 0,
getChar(event.getNativeEvent())));
event.stopPropagation();
event.preventDefault();
}
}
}, KeyPressEvent.getType());
html2.addDomHandler(new KeyUpHandler() {
@Override
public void onKeyUp(KeyUpEvent event) {
if (checkPowerKeyInput(html2.getElement())) {
keyListener.onKeyTyped(new KeyEvent(0, 0, '^'));
onFocusTimer(); // refocus to remove the half-written letter
updateAltForKeyUp(event);
event.preventDefault();
return;
}
int code = fixCode(event.getNativeEvent());
keyListener.onKeyReleased(
new KeyEvent(code, getModifiers(event),
getChar(event.getNativeEvent())));
updateAltForKeyUp(event);
if (code == KeyEvent.VK_DELETE || code == KeyEvent.VK_ESCAPE) {
event.preventDefault();
}
}
}, KeyUpEvent.getType());
html2.addDomHandler(new KeyDownHandler() {
@Override
public void onKeyDown(KeyDownEvent event) {
if (isRightAlt(event.getNativeEvent())) {
rightAltDown = true;
}
if (isLeftAlt(event.getNativeEvent())) {
leftAltDown = true;
}
int code = fixCode(event.getNativeEvent());
boolean handled = keyListener.onKeyPressed(
new KeyEvent(code, getModifiers(event),
getChar(event.getNativeEvent())));
// need to prevent sdefault for arrows to kill keypress
// (otherwise strange chars appear in Firefox). Backspace/delete
// also need killing.
// also kill events while left alt down: alt+e, alt+d working in
// browser
if (code == KeyEvent.VK_DELETE || code == KeyEvent.VK_ESCAPE
|| handled
|| leftAltDown) {
event.preventDefault();
}
event.stopPropagation();
}
}, KeyDownEvent.getType());
}
/**
* Update alt flags after key released
*
* @param event
* keyUp event
*/
protected void updateAltForKeyUp(KeyUpEvent event) {
if (isRightAlt(event.getNativeEvent())) {
rightAltDown = false;
}
if (isLeftAlt(event.getNativeEvent())) {
leftAltDown = false;
}
event.stopPropagation();
}
native boolean checkPowerKeyInput(Element element) /*-{
if (element.value.match(/\^$/)) {
element.value = '';
return true;
}
return false;
}-*/;
/**
* @param nativeEvent
* native event
* @return whether this is right alt up/down event
*/
public static boolean isRightAlt(NativeEvent nativeEvent) {
return checkCode(nativeEvent, "AltRight");
}
public static native boolean checkCode(NativeEvent evt,
String check) /*-{
return evt.code == check;
}-*/;
private static native boolean checkNativeKey(NativeEvent evt,
String check) /*-{
return evt.key == check;
}-*/;
/**
* @param nativeEvent
* native event
* @return whether this is left alt up/down event
*/
public static boolean isLeftAlt(NativeEvent nativeEvent) {
return checkCode(nativeEvent, "AltLeft");
}
protected int fixCode(NativeEvent evt) {
if (evt.getKeyCode() == 46) {
return KeyEvent.VK_DELETE;
}
if (evt.getKeyCode() == 44) {
return KeyEvent.VK_DELETE;
}
if (checkNativeKey(evt, "[")) {
return KeyEvent.VK_OPEN_BRACKET;
}
if (checkNativeKey(evt, "{")) {
return KeyEvent.VK_OPEN_BRACKET;
}
if (checkNativeKey(evt, "(")) {
return KeyEvent.VK_OPEN_PAREN;
}
return evt.getKeyCode();
}
protected int getModifiers(
com.google.gwt.event.dom.client.KeyEvent<?> event) {
return (event.isShiftKeyDown() ? KeyEvent.SHIFT_MASK : 0)
+ (event.isControlKeyDown() || rightAltDown ? KeyEvent.CTRL_MASK
: 0)
+ (event.isAltKeyDown() ? KeyEvent.ALT_MASK : 0);
}
protected char getChar(NativeEvent nativeEvent) {
if (MathFieldW.checkCode(nativeEvent, "NumpadDecimal")) {
return '.';
}
return (char) nativeEvent.getCharCode();
}
@Override
public boolean hasParent() {
return false;
}
@Override
public void requestViewFocus() {
setEnabled(true);
setFocus(true);
}
@Override
public void requestLayout() {
// for desktop only
}
public KeyListener getKeyListener() {
return mathFieldInternal;
}
@Override
public MetaModel getMetaModel() {
return sMetaModel;
}
@Override
public void repaint() {
// called to often, use repaintWeb for actual repaint
}
public void repaintWeb() {
if (lastIcon == null) {
return;
}
if (!active(wrap.getElement()) && this.enabled) {
wrap.getElement().focus();
}
final double height = roundUp(lastIcon.getIconHeight() + bottomOffset);
final double width = roundUp(lastIcon.getIconWidth() + 30);
ctx.getCanvas().setHeight(((int) Math.ceil(height * ratio)));
ctx.getCanvas().setWidth((int) Math.ceil(width * ratio));
ctx.setFillStyle("rgb(255,255,255)");
((JLMContext2d) ctx).scale2(ratio, ratio);
ctx.fillRect(0, 0, ctx.getCanvas().getWidth(), height);
JlmLib.draw(lastIcon, ctx, 0, 0, "#000000", "#FFFFFF", null);
}
private native boolean active(Element element) /*-{
return $doc.activeElement == element;
}-*/;
/**
*
* for ratio 1.5 and w=5 CSS width we would get 7.5 coord space width; round
* up to 8
*/
private double roundUp(int w) {
return Math.ceil(w * ratio) / ratio;
}
@Override
public boolean hasFocus() {
return focused;
}
@Override
public void hideCopyPasteButtons() {
// TODO Auto-generated method stub
}
@Override
public boolean showKeyboard() {
// TODO Auto-generated method stub
return false;
}
@Override
public void showCopyPasteButtons() {
// TODO Auto-generated method stub
}
@Override
public void scroll(int dx, int dy) {
// TODO Auto-generated method stub
}
@Override
public void fireInputChangedEvent() {
// TODO Auto-generated method stub
}
@Override
public Widget asWidget() {
return html;
}
public void setFormula(MathFormula formula) {
this.mathFieldInternal.setFormula(formula);
}
public MathFormula getFormula() {
return this.mathFieldInternal.getFormula();
}
public void setFocus(boolean focus) {
if (focus) {
startBlink();
focuser = new Timer() {
@Override
public void run() {
onFocusTimer();
}
};
focuser.schedule(200);
startEditing();
wrap.getElement().focus();
if (!pasteInstalled) {
pasteInstalled = true;
installPaste(this.getHiddenTextArea());
}
} else {
if (focuser != null) {
focuser.cancel();
}
instances.remove(this);
// last repaint with no cursor
CursorBox.blink = false;
repaintWeb();
this.lastIcon = null;
}
this.focused = focus;
}
/**
* Make sure the HTML element has focus and update to render cursor
*/
protected void onFocusTimer() {
BlurHandler oldBlur = this.onTextfieldBlur;
onTextfieldBlur = null;
mathFieldInternal.update();
// first focus canvas to get the scrolling right
html.getElement().focus();
// after set focus to the keyboard listening element
wrap.getElement().focus();
onTextfieldBlur = oldBlur;
}
private native void installPaste(Element target) /*-{
var that = this;
target.addEventListener('paste',
function(a){
if(a.clipboardData){
that.@com.himamis.retex.editor.web.MathFieldW::insertString(Ljava/lang/String;)(a.clipboardData.getData("text/plain"));
}else if($wnd.clipboardData){
that.@com.himamis.retex.editor.web.MathFieldW::insertString(Ljava/lang/String;)($wnd.clipboardData.getData("Text"));
}
}
);
}-*/;
public void startEditing() {
if (mathFieldInternal.getEditorState().getCurrentField() == null) {
mathFieldInternal.getCursorController();
CursorController
.lastField(mathFieldInternal.getEditorState());
}
// update even when cursor didn't change here
mathFieldInternal.update();
}
public String deleteCurrentWord() {
return this.mathFieldInternal.deleteCurrentWord();
}
public String getCurrentWord() {
return this.mathFieldInternal.getCurrentWord();
}
public void selectNextArgument() {
this.mathFieldInternal.selectNextArgument();
}
public void startBlink() {
if (!instances.contains(this)) {
instances.add(this);
}
}
@Override
public void paste() {
// insertString(getSystemClipboardChromeWebapp(html.getElement()));
}
/**
* @param text
* input text; similar to simple keyPress events but do not
* create fractions/exponents
*/
public void insertString(String text) {
KeyboardInputAdapter.insertString(mathFieldInternal, text);
mathFieldInternal.selectNextArgument();
mathFieldInternal.update();
}
private TextArea wrap;
private SimplePanel clip;
private Element getHiddenTextArea() {
if (clip == null) {
clip = new SimplePanel();
Element el = getHiddenTextAreaNative(counter++,
clip.getElement());
wrap = TextArea.wrap(el);
wrap.addFocusHandler(new FocusHandler() {
@Override
public void onFocus(FocusEvent event) {
event.stopPropagation();
}
});
wrap.addBlurHandler(new BlurHandler() {
@Override
public void onBlur(BlurEvent event) {
event.stopPropagation();
if (onTextfieldBlur != null) {
onTextfieldBlur.onBlur(event);
}
}
});
clip.setWidget(wrap);
}
if (parent != null) {
parent.add(clip);
}
return wrap.getElement();
}
public void setOnBlur(BlurHandler run) {
this.onTextfieldBlur = run;
}
private static native Element getHiddenTextAreaNative(int counter,
Element clipDiv) /*-{
var hiddenTextArea = $doc.getElementById('hiddenCopyPasteLatexArea'
+ counter);
if (!hiddenTextArea) {
hiddenTextArea = $doc.createElement("textarea");
hiddenTextArea.id = 'hiddenCopyPasteLatexArea' + counter;
hiddenTextArea.style.opacity = 0;
clipDiv.style.zIndex = -32000;
//* although clip is for absolute position, necessary!
//* as it is deprecated, may cause CSS challenges later
clipDiv.style.clip = "rect(1em 1em 1em 1em)";
//* top/left will be specified dynamically, depending on scrollbar
clipDiv.style.display = "inline";
clipDiv.style.width = "1px";
clipDiv.style.height = "1px";
clipDiv.style.position = "relative";
clipDiv.style.top = "-15px";
clipDiv.className = "textAreaClip";
hiddenTextArea.style.width = "1px";
hiddenTextArea.style.padding = 0;
hiddenTextArea.style.border = 0;
hiddenTextArea.style.minHeight = 0;
hiddenTextArea.style.height = "1px";//prevent messed up scrolling in FF/IE
$doc.body.appendChild(hiddenTextArea);
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
.test(window.navigator.userAgent)) {
hiddenTextArea.setAttribute("disabled", "true");
}
}
//hiddenTextArea.value = '';
return hiddenTextArea;
}-*/;
@Override
public void copy() {
nativeCopy(mathFieldInternal.copy());
}
private native void nativeCopy(String value) /*-{
var copyFrom = this.@com.himamis.retex.editor.web.MathFieldW::getHiddenTextArea()();
copyFrom.value = value;
copyFrom.select();
$doc.execCommand('copy');
}-*/;
@Override
public native boolean useCustomPaste() /*-{
return false;
}-*/;
public void setFontSize(double size) {
this.mathFieldInternal.setSize(size);
this.mathFieldInternal.update();
}
public void adjustCaret(int absX, int absY) {
if (SelectionBox.touchSelection) {
return;
}
int x = absX - asWidget().getAbsoluteLeft();
int y = absY - asWidget().getAbsoluteTop();
if (x > asWidget().getOffsetWidth()) {
CursorController.lastField(mathFieldInternal.getEditorState());
mathFieldInternal.update();
} else if (x < 0) {
CursorController.firstField(mathFieldInternal.getEditorState());
mathFieldInternal.update();
}else {
mathFieldInternal.onPointerUp(x, y);
}
}
public void insertFunction(String text) {
mathFieldInternal.insertFunction(text);
}
public void checkEnterReleased(Runnable r) {
mathFieldInternal.checkEnterReleased(r);
}
public void setPlainTextMode(boolean plainText) {
this.mathFieldInternal.setPlainTextMode(plainText);
}
public void blur() {
this.wrap.setFocus(false);
if (this.onTextfieldBlur != null) {
this.onTextfieldBlur.onBlur(null);
}
}
}