/*
* Catroid: An on-device visual programming system for Android devices
* Copyright (C) 2010-2016 The Catrobat Team
* (<http://developer.catrobat.org/credits>)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* An additional term exception under section 7 of the GNU Affero
* General Public License, version 3, is available at
* http://developer.catrobat.org/license_additional_term
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.catrobat.catroid.formulaeditor;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Layout;
import android.text.Spannable;
import android.text.style.BackgroundColorSpan;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.EditText;
import org.catrobat.catroid.formulaeditor.InternFormula.TokenSelectionType;
import org.catrobat.catroid.ui.fragment.FormulaEditorFragment;
public class FormulaEditorEditText extends EditText implements OnTouchListener {
private static final BackgroundColorSpan COLOR_ERROR = new BackgroundColorSpan(0xFFF00000);
private static final BackgroundColorSpan COLOR_HIGHLIGHT = new BackgroundColorSpan(0xFF33B5E5);
private static FormulaEditorHistory history = null;
FormulaEditorFragment formulaEditorFragment = null;
private int absoluteCursorPosition = 0;
private InternFormula internFormula;
private Context context;
private Paint paint = new Paint();
final GestureDetector gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent event) {
internFormula.setCursorAndSelection(absoluteCursorPosition, true);
history.updateCurrentSelection(internFormula.getSelection());
highlightSelection();
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent motion) {
Layout layout = getLayout();
if (layout != null) {
float lineHeight = getLineHeight();
int yCoordinate = (int) motion.getY();
int cursorY = 0;
int paddingLeft = getPaddingLeft();
int cursorXOffset = (int) motion.getX() - paddingLeft;
int initialScrollY = getScrollY();
int firstLineSize = (int) (initialScrollY % lineHeight);
int numberOfVisibleLines = (int) (getHeight() / lineHeight);
if (yCoordinate <= lineHeight - firstLineSize) {
scrollBy(0, (int) (initialScrollY > lineHeight ? -1 * (firstLineSize + lineHeight / 2) : -1
* firstLineSize));
cursorY = 0;
} else if (yCoordinate >= numberOfVisibleLines * lineHeight - lineHeight / 2) {
if (!(yCoordinate > layout.getLineCount() * lineHeight - getScrollY() - getPaddingTop())) {
scrollBy(0, (int) (lineHeight - firstLineSize + lineHeight / 2));
}
cursorY = numberOfVisibleLines;
} else {
for (int i = 1; i <= numberOfVisibleLines; i++) {
if (yCoordinate <= ((lineHeight - firstLineSize) + getPaddingTop() + i * lineHeight)) {
cursorY = i;
break;
}
}
}
int linesDown = (int) (initialScrollY / lineHeight);
while (cursorY + linesDown >= layout.getLineCount()) {
linesDown--;
}
int tempCursorPosition = layout.getOffsetForHorizontal(cursorY + linesDown, cursorXOffset);
if (tempCursorPosition > length()) {
tempCursorPosition = length();
}
if (!isDoNotMoveCursorOnTab()) {
absoluteCursorPosition = tempCursorPosition;
}
absoluteCursorPosition = absoluteCursorPosition > length() ? length() : absoluteCursorPosition;
setSelection(absoluteCursorPosition);
postInvalidate();
internFormula.setCursorAndSelection(absoluteCursorPosition, false);
highlightSelection();
history.updateCurrentSelection(internFormula.getSelection());
history.updateCurrentCursor(absoluteCursorPosition);
formulaEditorFragment.refreshFormulaPreviewString();
formulaEditorFragment.updateButtonsOnKeyboardAndInvalidateOptionsMenu();
}
return true;
}
});
private boolean doNotMoveCursorOnTab = false;
public FormulaEditorEditText(Context context) {
super(context);
this.context = context;
}
public FormulaEditorEditText(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
}
public void init(FormulaEditorFragment formulaEditorFragment) {
this.formulaEditorFragment = formulaEditorFragment;
this.setOnTouchListener(this);
this.setLongClickable(false);
this.setSelectAllOnFocus(false);
this.setCursorVisible(false);
cursorAnimation.run();
}
public void enterNewFormula(InternFormulaState internFormulaState) {
internFormula = internFormulaState.createInternFormulaFromState();
internFormula.generateExternFormulaStringAndInternExternMapping(context);
updateTextAndCursorFromInternFormula();
internFormula.selectWholeFormula();
highlightSelection();
if (history == null) {
history = new FormulaEditorHistory(internFormula.getInternFormulaState());
} else {
history.init(internFormula.getInternFormulaState());
}
}
public void overwriteCurrentFormula(InternFormulaState internFormulaState) {
internFormula = internFormulaState.createInternFormulaFromState();
internFormula.generateExternFormulaStringAndInternExternMapping(context);
updateTextAndCursorFromInternFormula();
internFormula.selectWholeFormula();
highlightSelection();
history.push(internFormula.getInternFormulaState());
String resultingText = updateTextAndCursorFromInternFormula();
setSelection(absoluteCursorPosition);
formulaEditorFragment.refreshFormulaPreviewString(resultingText);
}
private Runnable cursorAnimation = new Runnable() {
@Override
public void run() {
paint.setColor((paint.getColor() == 0x00000000) ? 0xff000000 : 0x00000000);
invalidate();
postDelayed(cursorAnimation, 500);
}
};
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
absoluteCursorPosition = absoluteCursorPosition > length() ? length() : absoluteCursorPosition;
paint.setStrokeWidth(3);
Layout layout = getLayout();
if (layout != null) {
int line = layout.getLineForOffset(absoluteCursorPosition);
float xCoordinate = layout.getPrimaryHorizontal(absoluteCursorPosition) + getPaddingLeft();
float startYCoordinate = layout.getLineBaseline(line) + layout.getLineAscent(line);
float endYCoordinate = layout.getLineBaseline(line) + layout.getLineAscent(line) + getTextSize();
endYCoordinate += line == 0 ? 5 : 0; // First line in FE is a little bit higher so we need a bigger cursor too.
canvas.drawLine(xCoordinate, startYCoordinate, xCoordinate, endYCoordinate, paint);
}
}
public void highlightSelection() {
Spannable highlightSpan = this.getText();
highlightSpan.removeSpan(COLOR_HIGHLIGHT);
highlightSpan.removeSpan(COLOR_ERROR);
int selectionStartIndex = internFormula.getExternSelectionStartIndex();
int selectionEndIndex = internFormula.getExternSelectionEndIndex();
TokenSelectionType selectionType = internFormula.getExternSelectionType();
if (selectionStartIndex == -1 || selectionEndIndex == -1 || selectionEndIndex == selectionStartIndex
|| selectionEndIndex > highlightSpan.length()) {
return;
}
if (selectionType == TokenSelectionType.USER_SELECTION) {
highlightSpan.setSpan(COLOR_HIGHLIGHT, selectionStartIndex, selectionEndIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} else {
highlightSpan.setSpan(COLOR_ERROR, selectionStartIndex, selectionEndIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public void setParseErrorCursorAndSelection() {
internFormula.selectParseErrorTokenAndSetCursor();
highlightSelection();
setSelection(absoluteCursorPosition);
}
public void handleKeyEvent(int resource, String name) {
internFormula.handleKeyInput(resource, context, name);
history.push(internFormula.getInternFormulaState());
String resultingText = updateTextAndCursorFromInternFormula();
setSelection(absoluteCursorPosition);
formulaEditorFragment.refreshFormulaPreviewString(resultingText);
}
public String getStringFromInternFormula() {
return internFormula.getExternFormulaString();
}
public String getSelectedTextFromInternFormula() {
return internFormula.getSelectedText();
}
public void overrideSelectedText(String string) {
internFormula.overrideSelectedText(string, context);
history.push(internFormula.getInternFormulaState());
String resultingText = updateTextAndCursorFromInternFormula();
setSelection(absoluteCursorPosition);
formulaEditorFragment.refreshFormulaPreviewString(resultingText);
}
public boolean hasChanges() {
return history != null && history.hasUnsavedChanges();
}
public void formulaSaved() {
history.changesSaved();
}
public void endEdit() {
history.clear();
}
public void quickSelect() {
internFormula.selectWholeFormula();
highlightSelection();
}
public boolean undo() {
if (!history.undoIsPossible()) {
return false;
}
InternFormulaState lastStep = history.backward();
if (lastStep != null) {
internFormula = lastStep.createInternFormulaFromState();
internFormula.generateExternFormulaStringAndInternExternMapping(context);
internFormula.updateInternCursorPosition();
updateTextAndCursorFromInternFormula();
}
formulaEditorFragment.refreshFormulaPreviewString();
return true;
}
public boolean redo() {
if (!history.redoIsPossible()) {
return false;
}
InternFormulaState nextStep = history.forward();
if (nextStep != null) {
internFormula = nextStep.createInternFormulaFromState();
internFormula.generateExternFormulaStringAndInternExternMapping(context);
internFormula.updateInternCursorPosition();
updateTextAndCursorFromInternFormula();
}
formulaEditorFragment.refreshFormulaPreviewString();
return true;
}
private String updateTextAndCursorFromInternFormula() {
String newExternFormulaString = internFormula.getExternFormulaString();
setText(newExternFormulaString);
absoluteCursorPosition = internFormula.getExternCursorPosition();
if (absoluteCursorPosition > length()) {
absoluteCursorPosition = length();
}
highlightSelection();
return newExternFormulaString;
}
@Override
public boolean onTouch(View view, MotionEvent motion) {
return gestureDetector.onTouchEvent(motion);
}
@Override
public boolean onCheckIsTextEditor() {
return false;
}
public InternFormulaParser getFormulaParser() {
return internFormula.getInternFormulaParser();
}
public boolean isDoNotMoveCursorOnTab() {
return doNotMoveCursorOnTab;
}
public void setDoNotMoveCursorOnTab(boolean doNotMoveCursorOnTab) {
this.doNotMoveCursorOnTab = doNotMoveCursorOnTab;
}
public FormulaEditorHistory getHistory() {
return history;
}
public boolean isThereSomethingToDelete() {
return internFormula.isThereSomethingToDelete();
}
}