/* JMathField.java * ========================================================================= * This file is part of the Mirai Math TN - http://mirai.sourceforge.net * * Copyright (C) 2008-2009 Bea Petrovicova * * 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.share.editor; import java.util.ArrayList; import java.util.Collections; import com.google.j2objc.annotations.Weak; import com.himamis.retex.editor.share.controller.CursorController; import com.himamis.retex.editor.share.controller.EditorState; import com.himamis.retex.editor.share.controller.InputController; import com.himamis.retex.editor.share.controller.KeyListenerImpl; import com.himamis.retex.editor.share.controller.MathFieldController; 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.model.MathArray; import com.himamis.retex.editor.share.model.MathCharacter; import com.himamis.retex.editor.share.model.MathComponent; import com.himamis.retex.editor.share.model.MathContainer; import com.himamis.retex.editor.share.model.MathFormula; import com.himamis.retex.editor.share.model.MathSequence; import com.himamis.retex.renderer.share.CursorBox; import com.himamis.retex.renderer.share.SelectionBox; /** * This class is a Math Field. Displays and allows to edit single formula. * * @author Bea Petrovicova, Bencze Balazs */ public class MathFieldInternal implements KeyListener, FocusListener, ClickListener { @Weak private MathField mathField; private CursorController cursorController; private InputController inputController; private MathFieldController mathFieldController; private KeyListenerImpl keyListener; private EditorState editorState; private MathFormula mathFormula; private int[] mouseDownPos; private boolean selectionDrag; private MathFieldListener listener; private boolean enterPressed; private Runnable enterCallback; private boolean directFormulaBuilder; public MathFieldInternal(MathField mathField) { this(mathField, false); } public MathFieldInternal(MathField mathField, boolean directFormulaBuilder) { this.mathField = mathField; this.directFormulaBuilder = directFormulaBuilder; cursorController = new CursorController(); inputController = new InputController(mathField.getMetaModel()); keyListener = new KeyListenerImpl(cursorController, inputController); mathFormula = MathFormula.newFormula(mathField.getMetaModel()); mathFieldController = new MathFieldController(mathField, directFormulaBuilder); inputController.setMathField(mathField); setupMathField(); } private void setupMathField() { mathField.setFocusListener(this); mathField.setClickListener(this); mathField.setKeyListener(this); } public void setSize(double size) { mathFieldController.setSize(size); } public void setType(int type) { mathFieldController.setType(type); } public MathFormula getFormula() { return mathFormula; } public void setFormula(MathFormula formula) { mathFormula = formula; editorState = new EditorState(mathField.getMetaModel()); editorState.setRootComponent(formula.getRootComponent()); editorState.setCurrentField(formula.getRootComponent()); editorState.setCurrentOffset(editorState.getCurrentField().size()); keyListener.setEditorState(editorState); mathFieldController.update(formula, editorState, false); } public void setFormula(MathFormula formula, ArrayList<Integer> path) { mathFormula = formula; editorState = new EditorState(mathField.getMetaModel()); editorState.setRootComponent(formula.getRootComponent()); CursorController.setPath(path, getEditorState()); keyListener.setEditorState(editorState); mathFieldController.update(formula, editorState, false); } public InputController getInputController() { return inputController; } public CursorController getCursorController() { return cursorController; } public MathFieldController getMathFieldController() { return mathFieldController; } public EditorState getEditorState() { return editorState; } public KeyListenerImpl getKeyListener() { return keyListener; } public void update() { update(false); } private void update(boolean focusEvent) { mathFieldController.update(mathFormula, editorState, focusEvent); } @Override public void onFocusGained() { update(true); } @Override public void onFocusLost() { update(true); } @Override public boolean onKeyPressed(KeyEvent keyEvent) { if (keyEvent.getKeyCode() == 13 || keyEvent.getKeyCode() == 10) { if (listener != null) { this.enterPressed = true; listener.onEnter(); return true; } } boolean arrow = false; if (keyEvent.getKeyCode() >= 37 && keyEvent.getKeyCode() <= 40) { // move cursor arrow = true; if (listener != null) { listener.onCursorMove(); if (keyEvent.getKeyCode() == KeyEvent.VK_UP) { listener.onUpKeyPressed(); } else if (keyEvent.getKeyCode() == KeyEvent.VK_DOWN) { listener.onDownKeyPressed(); } } } if (keyEvent.getKeyCode() == KeyEvent.VK_CONTROL) { return false; } if (keyEvent.getKeyCode() == KeyEvent.VK_ESCAPE) { if (listener != null && listener.onEscape()) { return true; } } boolean handled = keyListener.onKeyPressed(keyEvent); if (handled) { update(); if (!arrow && listener != null) { listener.onKeyTyped(); } } return handled; } @Override public boolean onKeyReleased(KeyEvent keyEvent) { enterPressed = false; if (keyEvent.getKeyCode() == 13 || keyEvent.getKeyCode() == 10) { if (enterCallback != null) { enterCallback.run(); enterCallback = null; return true; } } boolean alt = (keyEvent.getKeyModifiers() & KeyEvent.ALT_MASK) > 0 && (keyEvent.getKeyModifiers() & KeyEvent.CTRL_MASK) == 0; if (alt) { String str = listener.alt(keyEvent.getKeyCode(), (keyEvent.getKeyModifiers() & KeyEvent.SHIFT_MASK) > 0); for (int i = 0; str != null && i < str.length(); i++) { keyListener.onKeyTyped(str.charAt(i)); } notifyAndUpdate(); } return false; } @Override public boolean onKeyTyped(KeyEvent keyEvent) { return onKeyTyped(keyEvent, true); } @Override public boolean onKeyTyped(KeyEvent keyEvent, boolean fire) { boolean alt = (keyEvent.getKeyModifiers() & KeyEvent.ALT_MASK) > 0; boolean enter = keyEvent.getUnicodeKeyChar() == (char) 13 || keyEvent.getUnicodeKeyChar() == (char) 10; boolean handled = alt || enter || ((keyEvent.getKeyModifiers() & KeyEvent.CTRL_MASK) > 0) || keyListener.onKeyTyped(keyEvent.getUnicodeKeyChar()); if (handled && fire) { notifyAndUpdate(); } return handled; } public void setPlainTextMode(boolean plainText) { this.inputController.setCreateFrac(!plainText); } private void notifyAndUpdate() { if (listener != null) { listener.onKeyTyped(); } update(); } @Override public void onPointerDown(int x, int y) { if (selectionMode) { ArrayList<Integer> list = new ArrayList<Integer>(); if (SelectionBox.touchSelection) { if (length(SelectionBox.startX - x, SelectionBox.startY - y) < 10) { editorState.cursorToSelectionEnd(); selectionDrag = true; return; } if (length(SelectionBox.endX - x, SelectionBox.endY - y) < 10) { // editorState.anchor(true); selectionDrag = true; editorState.cursorToSelectionStart(); return; } } mathFieldController.getPath(mathFormula, x, y, list); editorState.resetSelection(); this.mouseDownPos = new int[]{x, y}; moveToSelection(x, y); mathFieldController.update(mathFormula, editorState, false); } } private static double length(double d, double e) { return Math.sqrt(d * d + e * e); } @Override public void onPointerUp(int x, int y) { if (scrollOccured) { scrollOccured = false; } else if (longPressOccured) { longPressOccured = false; } else { if (this.selectionDrag) { selectionDrag = false; return; } ArrayList<Integer> list = new ArrayList<Integer>(); mathFieldController.getPath(mathFormula, x, y, list); MathComponent cursor = editorState.getCursorField( editorState.getSelectionEnd() != null && selectionLeft(x)); moveToSelection(x, y); editorState.resetSelection(); if (selectionMode && mousePositionChanged(x, y)) { editorState.extendSelection(selectionLeft(x)); editorState.extendSelection(cursor); } // TODO only hide copy button only when no selection // (see commented below, MOB-567 and MOB-568) mathField.hideCopyPasteButtons(); /* if (!editorState.hasSelection()){ mathField.hideCopyButton(); } */ mathFieldController.update(mathFormula, editorState, false); mathField.showKeyboard(); mathField.requestViewFocus(); } mouseDownPos = null; } private boolean selectionLeft(int x) { return x > SelectionBox.startX; } @Override public void onLongPress(int x, int y) { longPressOccured = true; if (!mathFormula.isEmpty()) { editorState.selectAll(); } mathFieldController.update(mathFormula, editorState, false); mathField.showCopyPasteButtons(); mathField.showKeyboard(); mathField.requestViewFocus(); } @Override public void onScroll(int dx, int dy) { if (!selectionMode) { mathField.scroll(dx, dy); scrollOccured = true; } mathField.requestViewFocus(); } private boolean scrollOccured = false; private boolean longPressOccured = false; private boolean selectionMode = false; /** * says if dragging over should select or swype * * @param flag * flag */ public void setSelectionMode(boolean flag) { selectionMode = flag; } private boolean mousePositionChanged(int x, int y) { return mouseDownPos != null && (Math.abs(x - mouseDownPos[0]) > 10 || Math.abs(y - mouseDownPos[1]) > 10); } private void moveToSelectionDirect(int x, int y) { ArrayList<Integer> list2 = new ArrayList<Integer>(); EditorState mc = mathFieldController.getPath(mathFormula, x, y, list2); if (mc != null && mc.getCurrentField() != null) { editorState.setCurrentField(mc.getCurrentField()); editorState.setCurrentOffset(mc.getCurrentOffset()); } } private void moveToSelection(int x, int y) { if (this.directFormulaBuilder) { this.moveToSelectionDirect(x, y); } else { this.moveToSelectionIterative(x, y); } } private void moveToSelectionIterative(int x, int y) { // System.out.println("SELECTION" + list); CursorController.firstField(editorState); double dist = Integer.MAX_VALUE; MathSequence closestComponent = null; int closestOffset = -1; do { ArrayList<Integer> list2 = new ArrayList<Integer>(); mathFieldController.getSelectedPath(mathFormula, list2, editorState.getCurrentField(), editorState.getCurrentOffset()); reverse(list2); double currentDist = Math.abs(x - CursorBox.startX) + Math.abs(y - CursorBox.startY); if (currentDist < dist) { dist = currentDist; closestComponent = editorState.getCurrentField(); closestOffset = editorState.getCurrentOffset(); } } while (CursorController.nextCharacter(editorState)); if (closestComponent != null) { editorState.setCurrentField(closestComponent); editorState.setCurrentOffset(closestOffset); ArrayList<Integer> list2 = new ArrayList<Integer>(); mathFieldController.getSelectedPath(mathFormula, list2, editorState.getCurrentField(), editorState.getCurrentOffset()); } } private static void reverse(ArrayList<Integer> list2) { Collections.reverse(list2); } @Override public void onPointerMove(int x, int y) { if (!mousePositionChanged(x, y) && !selectionDrag) { editorState.resetSelection(); mathFieldController.update(mathFormula, editorState, false); return; } ArrayList<Integer> list = new ArrayList<Integer>(); mathFieldController.getPath(mathFormula, x, y, list); MathComponent cursor = editorState.getCursorField( editorState.getSelectionEnd() == null || selectionLeft(x)); MathSequence current = editorState.getCurrentField(); int offset = editorState.getCurrentOffset(); moveToSelection(x, y); editorState.resetSelection(); editorState.extendSelection(selectionLeft(x)); editorState.setCurrentField(current); editorState.setCurrentOffset(offset); editorState.extendSelection(cursor); mathFieldController.update(mathFormula, editorState, false); } public boolean isEmpty() { return mathFormula.isEmpty(); } /** * @param listener * listener */ public void setFieldListener(MathFieldListener listener) { this.listener = listener; } public String deleteCurrentWord() { StringBuilder str = new StringBuilder(" "); MathSequence sel = editorState.getCurrentField(); if (sel != null) { for (int i = Math.min(editorState.getCurrentOffset() - 1, sel.size() - 1); i >= 0; i--) { if (sel.getArgument(i) instanceof MathCharacter) { if (!((MathCharacter) sel.getArgument(i)).isCharacter()) { return str.reverse().toString().trim() + ";" + (sel.getArgument(i)); } str.append( ((MathCharacter) sel.getArgument(i)).getUnicode()); sel.removeArgument(i); editorState.decCurrentOffset(); } } } return str.reverse().toString().trim(); } public String getCurrentWord() { StringBuilder str = new StringBuilder(" "); MathSequence sel = editorState.getCurrentField(); if (sel != null) { int wordEnd = editorState.getCurrentOffset() - 1; if (editorState.getSelectionEnd() != null) { wordEnd = editorState.getSelectionEnd().getParentIndex(); } for (int i = Math.min(wordEnd, sel.size() - 1); i >= 0; i--) { if (sel.getArgument(i) instanceof MathCharacter) { if (!((MathCharacter) sel.getArgument(i)).isCharacter()) { break; } str.append( ((MathCharacter) sel.getArgument(i)).getUnicode()); // FactoryProvider.getInstance().debug(str); } else { break; } } } return str.reverse().toString().trim(); } public void selectNextArgument() { EditorState state = getEditorState(); MathSequence seq = state.getCurrentField(); if (seq != null && seq.size() > 0) { MathComponent last = seq.getArgument(state.getCurrentOffset() - 1); if (last instanceof MathArray && ((MathArray) last).size() > 0) { MathSequence args = ((MathArray) last).getArgument(0); if (InputController.doSelectNext(args, state, 0)) { update(); } } } } public String copy() { if(listener!=null){ getInputController(); return (listener.serialize( InputController.getSelectionText(getEditorState()))); } return ""; } public void onInsertString() { ArrayList<Integer> path = new ArrayList<Integer>(); path.add(getEditorState().getCurrentOffset() - getEditorState().getCurrentField().size()); MathContainer field = getEditorState().getCurrentField(); while (field != null) { if (field.getParent() != null) { path.add(field.getParentIndex() - field.getParent().size()); } field = field.getParent(); } reverse(path); // for (int i : path) { // FactoryProvider.getInstance().debug("" + i); // } if (listener != null) { listener.onInsertString(); } getMathFieldController().setSelectedPath(getFormula(), path, getEditorState()); mathField.requestViewFocus(); // do this as late as possible if (listener != null) { listener.onKeyTyped(); } } public void insertFunction(String text) { inputController.newFunction(editorState, text, "frac".equals(text) ? 1 : 0); if (listener != null) { listener.onKeyTyped(); } } public void checkEnterReleased(Runnable r) { if (this.enterPressed) { this.enterCallback = r; } else { r.run(); } } }