package com.hipipal.texteditor.ui.view;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.hipipal.texteditor.common.Constants;
import com.hipipal.texteditor.common.Settings;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.os.Handler;
import android.text.Editable;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextWatcher;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnKeyListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.Scroller;
import com.hipipal.texteditor.R;
/**
* TODO create a syntax highlighter
*/
public class AdvancedEditText extends EditText implements Constants,
OnKeyListener, OnGestureListener {
String fileType = null;
Boolean isWatch = true;
/**
* @param context
* the current context
* @param attrs
* some attributes
* @category ObjectLifecycle
*/
public AdvancedEditText(Context context, AttributeSet attrs) {
super(context, attrs);
mPaintNumbers = new Paint();
mPaintNumbers.setTypeface(Typeface.MONOSPACE);
mPaintNumbers.setAntiAlias(true);
mPaintHighlight = new Paint();
mScale = context.getResources().getDisplayMetrics().density;
mPadding = (int) (mPaddingDP * mScale);
mHighlightedLine = mHighlightStart = -1;
mDrawingRect = new Rect();
mLineBounds = new Rect();
mGestureDetector = new GestureDetector(getContext(), this);
updateFromSettings("");
}
public void setFileType(String ft) {
fileType = ft;
}
/**
* @see android.widget.TextView#computeScroll()
* @category View
*/
@Override
public void computeScroll() {
if (mTedScroller != null) {
if (mTedScroller.computeScrollOffset()) {
scrollTo(mTedScroller.getCurrX(), mTedScroller.getCurrY());
}
} else {
super.computeScroll();
}
}
/**
* @see EditText#onDraw(Canvas)
* @category View
*/
@Override
public void onDraw(Canvas canvas) {
int count, padding, lineX, baseline;
// padding
padding = mPadding;
count = getLineCount();
if (Settings.SHOW_LINE_NUMBERS) {
padding = (int) (Math.floor(Math.log10(count)) + 1);
padding = (int) ((padding * mPaintNumbers.getTextSize()) + mPadding + (Settings.TEXT_SIZE
* mScale * 0.5));
setPadding(padding, mPadding, mPadding, mPadding);
} else {
setPadding(mPadding, mPadding, mPadding, mPadding);
}
// get the drawing boundaries
getDrawingRect(mDrawingRect);
// display current line
computeLineHighlight();
// draw line numbers
count = getLineCount();
lineX = (int) (mDrawingRect.left + padding - (Settings.TEXT_SIZE
* mScale * 0.5));
for (int i = 0; i < count; i++) {
baseline = getLineBounds(i, mLineBounds);
if ((mMaxSize != null) && (mMaxSize.x < mLineBounds.right)) {
mMaxSize.x = mLineBounds.right;
}
if ((mLineBounds.bottom < mDrawingRect.top)
|| (mLineBounds.top > mDrawingRect.bottom)) {
continue;
}
if ((i == mHighlightedLine) && (!Settings.WORDWRAP)) {
canvas.drawRect(mLineBounds, mPaintHighlight);
}
if (Settings.SHOW_LINE_NUMBERS) {
canvas.drawText("" + (i + 1), mDrawingRect.left + mPadding,
baseline, mPaintNumbers);
}
if (Settings.SHOW_LINE_NUMBERS) {
canvas.drawLine(lineX, mDrawingRect.top, lineX,
mDrawingRect.bottom, mPaintNumbers);
}
}
if (mMaxSize != null) {
mMaxSize.y = mLineBounds.bottom;
mMaxSize.x = Math.max(mMaxSize.x + mPadding - mDrawingRect.width(),
0);
mMaxSize.y = Math.max(
mMaxSize.y + mPadding - mDrawingRect.height(), 0);
}
super.onDraw(canvas);
}
/**
* @see android.view.View.OnKeyListener#onKey(android.view.View, int,
* android.view.KeyEvent)
*/
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
return false;
}
/**
* @see android.widget.TextView#onTouchEvent(android.view.MotionEvent)
* @category GestureDetection
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
if (mGestureDetector != null) {
return mGestureDetector.onTouchEvent(event);
}
return true;
}
/**
* @see android.view.GestureDetector.OnGestureListener#onDown(android.view.MotionEvent)
* @category GestureDetection
*/
@Override
public boolean onDown(MotionEvent e) {
return true;
}
/**
* @see android.view.GestureDetector.OnGestureListener#onSingleTapUp(android.view.MotionEvent)
* @category GestureDetection
*/
@Override
public boolean onSingleTapUp(MotionEvent e) {
if (isEnabled()) {
((InputMethodManager) getContext().getSystemService(
Context.INPUT_METHOD_SERVICE)).showSoftInput(this,
InputMethodManager.SHOW_IMPLICIT);
}
return true;
}
/**
* @see android.view.GestureDetector.OnGestureListener#onShowPress(android.view.MotionEvent)
* @category GestureDetection
*/
@Override
public void onShowPress(MotionEvent e) {
}
/**
* @see android.view.GestureDetector.OnGestureListener#onLongPress(android.view.MotionEvent)
*/
@Override
public void onLongPress(MotionEvent e) {
}
/**
* @see android.view.GestureDetector.OnGestureListener#onScroll(android.view.MotionEvent,
* android.view.MotionEvent, float, float)
*/
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
// mTedScroller.setFriction(0);
return true;
}
/**
* @see android.view.GestureDetector.OnGestureListener#onFling(android.view.MotionEvent,
* android.view.MotionEvent, float, float)
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
if (!Settings.FLING_TO_SCROLL) {
return true;
}
if (mTedScroller != null) {
mTedScroller.fling(getScrollX(), getScrollY(), -(int) velocityX,
-(int) velocityY, 0, mMaxSize.x, 0, mMaxSize.y);
}
return true;
}
/**
* Update view settings from the app preferences
*
* @category Custom
*/
public void updateFromSettings(String fileType) {
Log.d(TAG, "updateFromSettings:"+fileType);
if (isInEditMode()) {
return;
}
setTypeface(Settings.getTypeface(getContext()));
// wordwrap
setHorizontallyScrolling(!Settings.WORDWRAP);
// color Theme
switch (Settings.COLOR) {
case COLOR_NEGATIVE:
setBackgroundResource(R.drawable.textfield_black);
setTextColor(Color.WHITE);
mPaintHighlight.setColor(Color.WHITE);
mPaintNumbers.setColor(Color.GRAY);
break;
case COLOR_MATRIX:
setBackgroundResource(R.drawable.textfield_matrix);
setTextColor(Color.GREEN);
mPaintHighlight.setColor(Color.GREEN);
mPaintNumbers.setColor(Color.rgb(0, 128, 0));
break;
case COLOR_SKY:
setBackgroundResource(R.drawable.textfield_sky);
setTextColor(Color.rgb(0, 0, 64));
mPaintHighlight.setColor(Color.rgb(0, 0, 64));
mPaintNumbers.setColor(Color.rgb(0, 128, 255));
break;
case COLOR_DRACULA:
setBackgroundResource(R.drawable.textfield_dracula);
setTextColor(Color.RED);
mPaintHighlight.setColor(Color.RED);
mPaintNumbers.setColor(Color.rgb(192, 0, 0));
break;
case COLOR_CLASSIC:
default:
setBackgroundResource(R.drawable.textfield_white);
setTextColor(Color.BLACK);
mPaintHighlight.setColor(Color.BLACK);
mPaintNumbers.setColor(Color.GRAY);
break;
}
mPaintHighlight.setAlpha(48);
// text size
setTextSize(Settings.TEXT_SIZE);
mPaintNumbers.setTextSize(Settings.TEXT_SIZE * mScale * 0.85f);
// refresh view
postInvalidate();
refreshDrawableState();
// use Fling when scrolling settings ?
if (Settings.FLING_TO_SCROLL) {
mTedScroller = new Scroller(getContext());
mMaxSize = new Point();
} else {
mTedScroller = null;
mMaxSize = null;
}
/*if (this.fileType!=null) {
if (this.fileType.equals("py")) {
init();
}
} else if (fileType!=null && !fileType.equals("")) {*/
//this.fileType = fileType;
//Log.d(TAG, "fileType:"+fileType);
Log.d(TAG, "init");
if (fileType.equals("py")) {
//Log.d(TAG, "init OK");
isWatch = true;
init();
refresh();
} else {
isWatch = false;
cancelUpdate();
unHightlight();
}
//}
}
/**
* Compute the line to highlight based on selection
*/
protected void computeLineHighlight() {
int i, line, selStart;
String text;
if (!isEnabled()) {
mHighlightedLine = -1;
return;
}
selStart = getSelectionStart();
if (mHighlightStart != selStart) {
text = getText().toString();
line = i = 0;
while (i < selStart) {
i = text.indexOf("\n", i);
if (i < 0) {
break;
}
if (i < selStart) {
++line;
}
++i;
}
mHighlightedLine = line;
}
}
/** The line numbers paint */
protected Paint mPaintNumbers;
/** The line numbers paint */
protected Paint mPaintHighlight;
/** the offset value in dp */
protected int mPaddingDP = 6;
/** the padding scaled */
protected int mPadding;
/** the scale for desnity pixels */
protected float mScale;
/** the scroller instance */
protected Scroller mTedScroller;
/** the velocity tracker */
protected GestureDetector mGestureDetector;
/** the Max size of the view */
protected Point mMaxSize;
/** the highlighted line index */
protected int mHighlightedLine;
protected int mHighlightStart;
protected Rect mDrawingRect, mLineBounds;
/////////
public interface OnTextChangedListener
{
public void onTextChanged( String text );
}
public OnTextChangedListener onTextChangedListener = null;
public int updateDelay = 1000;
public int errorLine = 0;
public boolean dirty = false;
private static final int COLOR_ERROR = 0x80ff0000;
//private static final int COLOR_NUMBER = 0xff7ba212;
private static final int COLOR_KEYWORD = 0xff7ba212;
private static final int COLOR_BUILTIN = 0xffd79e39;
private static final int COLOR_COMMENT = 0xff808080;
private static final int COLOR_QUOTE = 0xff399ed7;
private static final Pattern line = Pattern.compile(
".*\\n" );
private static final Pattern numbers = Pattern.compile(
"\\b(\\d*[.]?\\d+)\\b" );
private static final Pattern keywords = Pattern.compile(
"\\b(break|continue|del|"+
"except|exec|finally|"+
"pass|print|raise|"+
"return|try|with|"+
"global|assert|"+
"lambda|yield|"+
"def|class|self|"+
"for|while|"+
"if|elif|else|"+
"and|in|is|not|or|"+
"import|from|as)\\b" );
private static final Pattern builtins = Pattern.compile(
"\\b(True|False|bool|enumerate|set|frozenset|help|"+
"reversed|sorted|sum|"+
"Ellipsis|None|NotImplemented|__import__|abs|"+
"apply|buffer|callable|chr|classmethod|cmp|"+
"coerce|compile|complex|delattr|dict|dir|divmod|"+
"eval|execfile|file|filter|float|getattr|globals|"+
"hasattr|hash|hex|id|input|int|intern|isinstance|"+
"issubclass|iter|len|list|locals|long|map|max|"+
"min|object|oct|open|ord|pow|property|range|"+
"raw_input|reduce|reload|repr|round|setattr|"+
"slice|staticmethod|str|super|tuple|type|unichr|"+
"unicode|vars|xrange|zip|"+
"ArithmeticError|AssertionError|AttributeError|"+
"DeprecationWarning|EOFError|EnvironmentError|"+
"Exception|FloatingPointError|IOError|"+
"ImportError|IndentationError|IndexError|"+
"KeyError|KeyboardInterrupt|LookupError|"+
"MemoryError|NameError|NotImplementedError|"+
"OSError|OverflowError|OverflowWarning|"+
"ReferenceError|RuntimeError|RuntimeWarning|"+
"StandardError|StopIteration|SyntaxError|"+
"SyntaxWarning|SystemError|SystemExit|TabError|"+
"TypeError|UnboundLocalError|UnicodeError|"+
"UnicodeEncodeError|UnicodeDecodeError|"+
"UnicodeTranslateError|"+
"UserWarning|ValueError|Warning|WindowsError|"+
"ZeroDivisionError)\\b" );
private static final Pattern comments = Pattern.compile(
"/\\*(?:.|[\\n\\r])*?\\*/|"+
"#.*\n|"+
"\"\"\"(?:.|[\\n\\r])*?\"\"\"|"+
"\'\'\'(?:.|[\\n\\r])*?\'\'\'");
private static final Pattern trailingWhiteSpace = Pattern.compile(
"[\\t ]+$",
Pattern.MULTILINE );
private static final Pattern quotes = Pattern.compile(
"\"([^[\"\\n]])+\"|"+
"\'([^[\'\\n]])+\'"
);
private final Handler updateHandler = new Handler();
private final Runnable updateRunnable =
new Runnable()
{
@Override
public void run()
{
Editable e = getText();
if( onTextChangedListener != null )
onTextChangedListener.onTextChanged( e.toString() );
highlightWithoutChange( e );
}
};
private boolean modified = true;
public void setTextHighlighted( CharSequence text )
{
cancelUpdate();
errorLine = 0;
dirty = false;
modified = false;
setText( highlight( new SpannableStringBuilder( text ) ) );
modified = true;
if( onTextChangedListener != null )
onTextChangedListener.onTextChanged( text.toString() );
}
public String getCleanText()
{
return trailingWhiteSpace
.matcher( getText() )
.replaceAll( "" );
}
public void refresh()
{
highlightWithoutChange( getText() );
}
public void unHightlight() {
unHightWithoutChange( getText() );
}
private void init()
{
setHorizontallyScrolling( true );
setFilters( new InputFilter[]{
new InputFilter()
{
@Override
public CharSequence filter(
CharSequence source,
int start,
int end,
Spanned dest,
int dstart,
int dend )
{
if( modified &&
end-start == 1 &&
start < source.length() &&
dstart < dest.length() )
{
char c = source.charAt( start );
if( c == '\n' )
return autoIndent(
source,
start,
end,
dest,
dstart,
dend );
}
return source;
}
} } );
addTextChangedListener(
new TextWatcher()
{
@Override
public void onTextChanged(
CharSequence s,
int start,
int before,
int count )
{
}
@Override
public void beforeTextChanged(
CharSequence s,
int start,
int count,
int after )
{
}
@Override
public void afterTextChanged( Editable e )
{
if (isWatch) {
cancelUpdate();
if( !modified )
return;
dirty = true;
updateHandler.postDelayed(
updateRunnable,
updateDelay );
}
}
} );
}
private void cancelUpdate()
{
updateHandler.removeCallbacks( updateRunnable );
}
private void highlightWithoutChange( Editable e )
{
modified = false;
highlight( e );
modified = true;
}
public void unHightWithoutChange(Editable e) {
Log.d(TAG, "unHightWithoutChange");
modified = false;
clearSpans(e);
modified = true;
}
private Editable highlight( Editable e )
{
try
{
// don't use e.clearSpans() because it will remove
// too much
clearSpans( e );
if( e.length() == 0 )
return e;
if( errorLine > 0 )
{
Matcher m = line.matcher( e );
for( int n = errorLine;
n-- > 0 && m.find(); );
e.setSpan(
new BackgroundColorSpan( COLOR_ERROR ),
m.start(),
m.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
}
/*for( Matcher m = numbers.matcher( e );
m.find(); )
e.setSpan(
new ForegroundColorSpan( COLOR_NUMBER ),
m.start(),
m.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );*/
for( Matcher m = keywords.matcher( e );
m.find(); )
e.setSpan(
new ForegroundColorSpan( COLOR_KEYWORD ),
m.start(),
m.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
for( Matcher m = builtins.matcher( e );
m.find(); )
e.setSpan(
new ForegroundColorSpan( COLOR_BUILTIN ),
m.start(),
m.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
for( Matcher m = comments.matcher( e );
m.find(); )
e.setSpan(
new ForegroundColorSpan( COLOR_COMMENT ),
m.start(),
m.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
for( Matcher m = quotes.matcher( e );
m.find(); )
e.setSpan(
new ForegroundColorSpan( COLOR_QUOTE ),
m.start(),
m.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
}
catch( Exception ex )
{
}
return e;
}
private void clearSpans( Editable e )
{
// remove foreground color spans
{
ForegroundColorSpan spans[] = e.getSpans(
0,
e.length(),
ForegroundColorSpan.class );
for( int n = spans.length; n-- > 0; )
e.removeSpan( spans[n] );
}
// remove background color spans
{
BackgroundColorSpan spans[] = e.getSpans(
0,
e.length(),
BackgroundColorSpan.class );
for( int n = spans.length; n-- > 0; )
e.removeSpan( spans[n] );
}
}
private CharSequence autoIndent(
CharSequence source,
int start,
int end,
Spanned dest,
int dstart,
int dend )
{
String indent = "";
int istart = dstart-1;
int iend = -1;
// find start of this line
boolean dataBefore = false;
int pt = 0;
for( ; istart > -1; --istart )
{
char c = dest.charAt( istart );
if( c == '\n' )
break;
if( c != ' ' &&
c != '\t' )
{
if( !dataBefore )
{
// indent always after those characters
if( c == '{' ||
c == '+' ||
c == '-' ||
c == '*' ||
c == '/' ||
c == '%' ||
c == '^' ||
c == '=' )
--pt;
dataBefore = true;
}
// parenthesis counter
if( c == '(' )
--pt;
else if( c == ')' )
++pt;
}
}
// copy indent of this line into the next
if( istart > -1 )
{
char charAtCursor = dest.charAt( dstart );
for( iend = ++istart;
iend < dend;
++iend )
{
char c = dest.charAt( iend );
// auto expand comments
if( charAtCursor != '\n' &&
c == '/' &&
iend+1 < dend &&
dest.charAt( iend ) == c )
{
iend += 2;
break;
}
if( c != ' ' &&
c != '\t' )
break;
}
indent += dest.subSequence( istart, iend );
}
// add new indent
if( pt < 0 )
indent += "\t";
// append white space of previous line and new indent
return source+indent;
}
}