<|start_filename|>src/com/myopicmobile/textwarrior/android/ClipboardPanel.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.android;

import org.miscwidgets.widget.Panel;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageButton;

public class ClipboardPanel extends Panel{
	protected ImageButton _cut;
	protected ImageButton _copy;
	protected ImageButton _paste;
	
	private final float MIN_FLICK_VELOCITY = 150;
	private boolean mFlushedHandle; //whether the handle should be wrapped to the contents
	GestureDetector _gestureDetector;
	
	public ClipboardPanel(Context context, AttributeSet attrs) {
		super(context, attrs);
		TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ClipboardPanel);
		mFlushedHandle = a.getBoolean(R.styleable.ClipboardPanel_flushedHandle, false);

		_gestureDetector = new GestureDetector(context, _contentGestureListener);
		_gestureDetector.setIsLongpressEnabled(false);
	}

	@Override
	protected void onFinishInflate() {
		super.onFinishInflate();
		
    	_cut = (ImageButton) findViewById(R.id.cut);
    	_copy = (ImageButton) findViewById(R.id.copy);
    	_paste = (ImageButton) findViewById(R.id.paste);
	}
	
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		if(mFlushedHandle){
			// Calculate the dimensions of the wrapped panel contents.
			// If this is a vertical panel, restrict the handle width to the content width;
			// If this is a horizontal panel, restrict the handle height to the content height;
			View content = getContent();
			content.measure(widthMeasureSpec, heightMeasureSpec);
			if (getPosition() == Panel.TOP || getPosition() == Panel.BOTTOM) {
				widthMeasureSpec = MeasureSpec.makeMeasureSpec(
						content.getMeasuredWidth(), MeasureSpec.EXACTLY);
			} else {
				heightMeasureSpec = MeasureSpec.makeMeasureSpec(
						content.getMeasuredHeight(), MeasureSpec.EXACTLY);
			}
		}
		
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent event){
		_gestureDetector.onTouchEvent(event);
		return super.dispatchTouchEvent(event);
	}

	private final GestureDetector.SimpleOnGestureListener _contentGestureListener
		= new GestureDetector.SimpleOnGestureListener() {
		@Override
		public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
			int orientation = getPosition();
			if (isOpen() && ((orientation == Panel.TOP && velocityY < -MIN_FLICK_VELOCITY) ||
						(orientation == Panel.BOTTOM && velocityY > MIN_FLICK_VELOCITY) ||
						(orientation == Panel.LEFT && velocityX < -MIN_FLICK_VELOCITY) ||
						(orientation == Panel.RIGHT && velocityX > MIN_FLICK_VELOCITY))){
				setOpen(false, true);
			}
			return true;
		}
	};

	/**
	 * Enables/disables the individual clipboard buttons
	 * 
	 * @param cut
	 * @param copy
	 * @param paste
	 */
	public void setClipboardButtonState(boolean cut, boolean copy, boolean paste){
    	_cut.setEnabled(cut);
    	_copy.setEnabled(copy);
    	_paste.setEnabled(paste);
	}
	
	public void setCutListener(OnClickListener listener){
		_cut.setOnClickListener(listener);
	}
	
	public void setCopyListener(OnClickListener listener){
		_copy.setOnClickListener(listener);
	}
	
	public void setPasteListener(OnClickListener listener){
		_paste.setOnClickListener(listener);
	}
}


<|start_filename|>src/com/myopicmobile/textwarrior/common/ColorScheme.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */

package com.myopicmobile.textwarrior.common;

public class ColorScheme {
	/* In ARGB format: 0xAARRGGBB */
	public static final int foregroundColor = 0xFF000000; //BLACK
	public static final int backgroundColor = 0xFFFFFFFF; //WHITE
	
	public static final int selForegroundColor = 0xFFFFFFFF; //WHITE
	public static final int selBackgroundColor = 0xFF800000; //MAROON
	
	public static final int caretForegroundColor = 0xFFFFFFFF; //WHITE
	public static final int caretBackgroundColor = 0xFF0000FF; //BLUE
	public static final int caretDisabledColor = 0xFF808080; //GREY
	
	public static final int commentColor = 0xFF3F7F5F; // Eclipse default color
	public static final int keywordColor = 0xFF7F0055; // Eclipse default color
	public static final int literalColor = 0xFF2A00FF; // Eclipse default color
	public static final int secondaryColor = 0xFF8B0000; //DARKRED;
	
	// Currently, color scheme is tightly coupled with semantics of the token types
	public static int getTokenColor(int tokenType){
		int color;
		switch(tokenType){
		 case Lexer.NORMAL:
		     color = ColorScheme.foregroundColor;
			 break;
		 case Lexer.KEYWORD:
		     color = ColorScheme.keywordColor;
			 break;
		 case Lexer.DOUBLE_SYMBOL_LINE: //fall-through
		 case Lexer.DOUBLE_SYMBOL_DELIMITED_MULTILINE:
		 case Lexer.SINGLE_SYMBOL_LINE_B:
		     color = ColorScheme.commentColor;
			 break;
		 case Lexer.SINGLE_SYMBOL_DELIMITED_A: //fall-through
		 case Lexer.SINGLE_SYMBOL_DELIMITED_B:
		     color = ColorScheme.literalColor;
			 break;
		 case Lexer.SINGLE_SYMBOL_LINE_A: //fall-through
		 case Lexer.SINGLE_SYMBOL_WORD:
		     color = ColorScheme.secondaryColor;
			 break;
		 default:
			 TextWarriorException.assertVerbose(false,
			 	"Invalid token type");
		     color = ColorScheme.foregroundColor;
			 break;
		}
		return color;
	}
}


<|start_filename|>src/com/myopicmobile/textwarrior/common/LanguagePython.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.common;

/**
 * Singleton class containing the symbols and operators of the Python language
 */
public class LanguagePython extends LanguageCFamily{
	private static LanguageCFamily _theOne = null;
	
	private final static String[] keywords = {
		"and", "assert", "break", "class", "continue", "def", "del",
		"elif", "else", "except", "exec", "finally", "for", "from",
		"global", "if", "import", "in", "is", "lambda", "not", "or",
		"pass", "print", "raise", "return", "try", "while", "with",
		"yield", "True", "False", "None"
		};
	
	private final static char[] operators = {
		'(', ')', '{', '}', '.', ',', ';', '=', '+', '-',
		'/', '*', '&', '!', '|', ':', '[', ']', '<', '>',
		'~', '%', '^'
	}; // no ternary operator ? :


	@Override
	public boolean isWordStart(char c){
		return (c == '@');
	}
	
	@Override
	public boolean isLineAStart(char c){
		return false;
	}
	
	@Override
	public boolean isLineBStart(char c){
		return (c == '#');
	}

	@Override
	public boolean isLineStart(char c0, char c1){
		return false;
	}

	@Override
	public boolean isMultilineStartDelimiter(char c0, char c1){
		return false;
	}
	
	public static LanguageCFamily getCharacterEncodings(){
		if(_theOne == null){
			_theOne = new LanguagePython();
		}
		return _theOne;
	}
	
	private LanguagePython(){
		super.registerKeywords(keywords);
		super.replaceOperators(operators);
	}
}


<|start_filename|>src/com/myopicmobile/textwarrior/common/LanguageRuby.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.common;

/**
 * Singleton class containing the symbols and operators of the Ruby language
 */
public class LanguageRuby extends LanguageCFamily {
	private static LanguageCFamily _theOne = null;
	
	private final static String[] keywords = {
		"alias", "and", "BEGIN", "begin", "break", "case", "catch", "class", "def",
		"defined?", "do", "else", "elsif", "END", "end", "ensure", "false",
		"for", "if", "in", "module", "next", "nil", "not", "or", "public",
		"private", "protected", "raise", "redo", "rescue", "retry", "return", "self",
		"super", "then", "throw", "true", "undef", "unless", "until", "when", "while",
		"yield", "self", "nil", "true", "false", "TRUE", "FALSE", "NIL"
		};


	@Override
	public boolean isWordStart(char c){
		return (c == '$');
	}
	
	@Override
	public boolean isLineAStart(char c){
		return false;
	}
	
	@Override
	public boolean isLineBStart(char c){
		return (c == '#');
	}

	@Override
	public boolean isLineStart(char c0, char c1){
		return false;
	}

	@Override
	public boolean isMultilineStartDelimiter(char c0, char c1){
		return false;
	}
	
	public static LanguageCFamily getCharacterEncodings(){
		if(_theOne == null){
			_theOne = new LanguageRuby();
		}
		return _theOne;
	}
	
	private LanguageRuby(){
		super.registerKeywords(keywords);
	}
}


<|start_filename|>src/com/myopicmobile/textwarrior/common/LanguageCpp.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.common;

/**
 * Singleton class containing the symbols and operators of the C++ language
 */
public class LanguageCpp extends LanguageCFamily{
	private static LanguageCFamily _theOne = null;
	
	private final static String[] keywords = {
		"bool", "char", "double", "float", "int", "long", "short", "void", "wchar_t",
		"auto", "const", "extern", "mutable", "register", "static", "volatile",
		"signed", "unsigned", "true", "false",
		"new", "delete", "sizeof", "typedef", "typeid", "typename",
		"const_cast", "dynamic_cast", "reinterpret_cast", "static_cast",
		"class", "enum", "explicit", "operator", "struct", "template", "union", "virtual",
		"private", "protected", "public", "friend", "this",
		"break", "case", "catch", "continue", "default", "do", "else", "for",
		"goto", "if", "return", "switch", "throw", "try", "while",
		"export", "namespace", "using", "asm", "inline",
		"and", "and_eq", "bitand", "bitor", "compl", "not", "not_eq",
		"or", "or_eq", "xor", "xor_eq"
		};

	public static LanguageCFamily getCharacterEncodings(){
		if(_theOne == null){
			_theOne = new LanguageCpp();
		}
		return _theOne;
	}
	
	private LanguageCpp(){
		super.registerKeywords(keywords);
	}
}


<|start_filename|>src/com/myopicmobile/textwarrior/android/LiquidNavigationMethod.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.android;


import com.myopicmobile.textwarrior.common.TextWarriorException;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.util.Log;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.WindowManager;

public class LiquidNavigationMethod extends TouchNavigationMethod{
	// FLAME_IMG is used as a button background. When pressed, a large
	// GLOW_IMG is overlaid.
	private final Bitmap FLAME_IMG = BitmapFactory.decodeResource(
			_textField.getContext().getResources(), R.drawable.fire);
	private final Bitmap GLOW_IMG = BitmapFactory.decodeResource(
			_textField.getContext().getResources(), R.drawable.fire_glow);
	
	private boolean _isButtonPressed = false;
	private Paint _brush = new Paint();
	private final static int BUTTON_ALPHA = 128;
	
	public LiquidNavigationMethod(FreeScrollingTextField textField){
		super(textField);
		SensorManager sm = (SensorManager) textField.getContext()
				.getSystemService(Context.SENSOR_SERVICE);
		
		if(sm.getDefaultSensor(Sensor.TYPE_ORIENTATION) == null){
			Log.w(TextWarriorApplication.LOG_TAG,
			"Liquid navigation selected but device does not have an orientation sensor");
		}
		
		TextWarriorException.assertVerbose(GLOW_IMG.getWidth() >= FLAME_IMG.getWidth()
				 && GLOW_IMG.getHeight() >= FLAME_IMG.getHeight(),
		 	"Flame glow cannot be smaller than flame image");
		
		_brush.setAlpha(BUTTON_ALPHA);
	}

	@Override
	public boolean onDown(MotionEvent e) {
		int x = (int) e.getX() + _textField.getScrollX();
		int y = (int) e.getY() + _textField.getScrollY();
		_isButtonPressed = isInButton(x, y);
		
		if(_isButtonPressed){
			startSensing();
			
			invalidateGlow();
			return true;
		}
		else{
			return super.onDown(e);
		}
	}

	@Override
	public boolean onUp(MotionEvent e) {
		if(_isButtonPressed){
			stopSensing();
			_textField.stopAutoScrollCaret();
			
			_isButtonPressed = false;
			_prevRoll = INIT_ROLL;
			_prevPitch = INIT_PITCH;
			invalidateGlow();
		}
		return super.onUp(e);
	}

	@Override
	public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
			float distanceY) {
		if(_isButtonPressed){
			//TODO find out if ACTION_UP events are actually passed to onScroll
			if ((e2.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP){
				onUp(e2);
			}

			//do nothing
			return true;
		}
		else{
			return super.onScroll(e1, e2, distanceX, distanceY);
		}
	}
	
	@Override
	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
			float velocityY) {
		if(_isButtonPressed){
			onUp(e2);
			return true;
		}
		else{
			return super.onFling(e1, e2, velocityX, velocityY);
		}
	}

	@Override
	public boolean onSingleTapConfirmed(MotionEvent e) {
		int x = (int) e.getX() + _textField.getScrollX();
		int y = (int) e.getY() + _textField.getScrollY();

		//ignore taps on button
		if(isInButton(x, y)){
			return true;
		}
		else{
			return super.onSingleTapConfirmed(e);
		}
	}

	@Override
	public boolean onDoubleTap(MotionEvent e) {
		int x = (int) e.getX() + _textField.getScrollX();
		int y = (int) e.getY() + _textField.getScrollY();

		//ignore taps on button
		if(isInButton(x, y)){
			return true;
		}
		else{
			return super.onDoubleTap(e);
		}
	}
	
	private boolean isInButton(int x, int y) {
		Rect bounds = getBoundingBox(FLAME_IMG);
		return (x >= bounds.left && x < bounds.right
				&& y >= bounds.top && y < bounds.bottom
				);
	}
	

	/**
	 * Returns the bounding box of image when it is positioned at the 
	 * bottom-left/right corner of _textField. The corner used depends on 
	 * whether the navigation mode is right- or left-handed.
	 */
	private Rect getBoundingBox(Bitmap image){
		int bottom = _textField.getScrollY() + _textField.getHeight();
		int top = bottom - image.getHeight();
		
		int left, right;
		if(isRightHanded()){
			right = _textField.getScrollX() + _textField.getWidth();
			left = right - image.getWidth();
		}
		else{
			left = _textField.getScrollX();
			right = left + image.getWidth();
		}
		
		return new Rect(left, top, right, bottom);
	}

	
	private void startSensing() {
		SensorManager sm = (SensorManager) _textField.getContext()
			.getSystemService(Context.SENSOR_SERVICE);
		Sensor orientationSensor = sm.getDefaultSensor(Sensor.TYPE_ORIENTATION);
		sm.registerListener(orientationListener,
				orientationSensor, SensorManager.SENSOR_DELAY_NORMAL);
		_textField.post(_updateSensorResults);
	}
	
	private void stopSensing() {
		_textField.removeCallbacks(_updateSensorResults);
		SensorManager sm = (SensorManager) _textField.getContext()
			.getSystemService(Context.SENSOR_SERVICE);
		Sensor orientationSensor = sm.getDefaultSensor(Sensor.TYPE_ORIENTATION);
		sm.unregisterListener(orientationListener, orientationSensor);
	}
	
	@Override
	void onPause() {
		//TODO investigate on different Android devices whether a
		//MotionEvent.ACTION_UP is generated while a finger is still on the
		//screen and TextWarrior goes to the background because the device enters
		//sleep mode or an incoming call occurs, etc. If yes, there is no need
		//to call onUp() again here.
		onUp(null);
		super.onPause();
	}


	private final SensorEventListener orientationListener = new SensorEventListener() {
		public void onSensorChanged(SensorEvent sensorEvent) {
			_currPitch = sensorEvent.values[1];
			_currRoll = sensorEvent.values[2];
		}
		
		public void onAccuracyChanged(Sensor sensor, int accuracy) {
			//do nothing
		}
	};



	private final static int UP = 0x10;
	private final static int DOWN = 0x20;
	private final static int LEFT = 1;
	private final static int RIGHT = 2;
	private final static int ORIENTATION_X_MASK = 0x0F;
	private final static int ORIENTATION_Y_MASK = 0xF0;
	
	private final boolean isLeft(int orientation){
		return (orientation & ORIENTATION_X_MASK) == LEFT;
	}
	
	private final boolean isRight(int orientation){
		return (orientation & ORIENTATION_X_MASK) == RIGHT;
	}
	
	private final boolean isUp(int orientation){
		return (orientation & ORIENTATION_Y_MASK) == UP;
	}
	
	private final boolean isDown(int orientation){
		return (orientation & ORIENTATION_Y_MASK) == DOWN;
	}

	private final static float INIT_ROLL = 0.0f;
	private final static float INIT_PITCH = -10.0f;
	private float _prevRoll = INIT_ROLL;
	private float _prevPitch = INIT_PITCH;
	private float _currRoll = INIT_ROLL;
	private float _currPitch = INIT_PITCH;
	private final static int UPDATE_PERIOD = 200;
	
	//Thresholds where a rotation is recognized to be in a certain direction
	private final static float UP_THRESHOLD = -10;
	private final static float DOWN_THRESHOLD = -60;
	private final static float LEFT_THRESHOLD = 20;
	private final static float RIGHT_THRESHOLD = -20;
	
	private int calcOrientation(float pitch, float roll){
		WindowManager sm = (WindowManager) _textField.getContext()
			.getSystemService(Context.WINDOW_SERVICE);
		int screenOrientation = sm.getDefaultDisplay().getOrientation();
		
		float correctedPitch, correctedRoll;
		switch(screenOrientation){
		case Surface.ROTATION_0:
			correctedPitch = pitch;
			correctedRoll = roll;
			break;
		case Surface.ROTATION_90:
			correctedPitch = -roll;
			correctedRoll = pitch;
			break;
		case Surface.ROTATION_180:
			correctedPitch = -pitch;
			correctedRoll = -roll;
			break;
		case Surface.ROTATION_270:
			correctedPitch = roll;
			correctedRoll = -pitch;
			break;
		default:
			Log.w(TextWarriorApplication.LOG_TAG, "Unsupported screen orientation");
			correctedPitch = pitch;
			correctedRoll = roll;
			break;
		}

		int correctedOrientation = 0;
		if(correctedRoll >= LEFT_THRESHOLD){
			correctedOrientation |= LEFT;
		}
		else if(correctedRoll <= RIGHT_THRESHOLD){
			correctedOrientation |= RIGHT;
		}

		if(correctedPitch >= UP_THRESHOLD){
			correctedOrientation |= UP;
		}
		else if(correctedPitch <= DOWN_THRESHOLD){
			correctedOrientation |= DOWN;
		}
		
		return correctedOrientation;
	}
	
	private final Runnable _updateSensorResults = new Runnable(){
		public void run(){
			int orientation = calcOrientation(_currPitch, _currRoll);
			int prevOrientation = calcOrientation(_prevPitch, _prevRoll);
			
			if(isLeft(orientation)){
				if(isRight(prevOrientation)){
					_textField.stopAutoScrollCaret(FreeScrollingTextField.SCROLL_RIGHT);
				}
				if(!isLeft(prevOrientation)){
					_textField.autoScrollCaret(FreeScrollingTextField.SCROLL_LEFT);
				}
			}
			else if(isRight(orientation)){
				if(isLeft(prevOrientation)){
					_textField.stopAutoScrollCaret(FreeScrollingTextField.SCROLL_LEFT);
				}
				if(!isRight(prevOrientation)){
					_textField.autoScrollCaret(FreeScrollingTextField.SCROLL_RIGHT);
				}
			}
			else{
				//neutral orientation in x-axis; stop existing x-scrolls
				if(isLeft(prevOrientation)){
					_textField.stopAutoScrollCaret(FreeScrollingTextField.SCROLL_LEFT);
				}
				else if(isRight(prevOrientation)){
					_textField.stopAutoScrollCaret(FreeScrollingTextField.SCROLL_RIGHT);
				}
			}
			
			if(isUp(orientation)){
				if(isDown(prevOrientation)){
					_textField.stopAutoScrollCaret(FreeScrollingTextField.SCROLL_DOWN);
				}
				if(!isUp(prevOrientation)){
					_textField.autoScrollCaret(FreeScrollingTextField.SCROLL_UP);
				}
			}
			else if(isDown(orientation)){
				if(isUp(prevOrientation)){
					_textField.stopAutoScrollCaret(FreeScrollingTextField.SCROLL_UP);
				}
				if(!isDown(prevOrientation)){
					_textField.autoScrollCaret(FreeScrollingTextField.SCROLL_DOWN);
				}
			}
			else{
				//neutral orientation in y-axis; stop existing y-scrolls
				if(isUp(prevOrientation)){
					_textField.stopAutoScrollCaret(FreeScrollingTextField.SCROLL_UP);
				}
				else if(isDown(prevOrientation)){
					_textField.stopAutoScrollCaret(FreeScrollingTextField.SCROLL_DOWN);
				}
			}

			_prevPitch = _currPitch;
			_prevRoll = _currRoll;
			
			_textField.postDelayed(_updateSensorResults, UPDATE_PERIOD);
		}
	};


	private void invalidateGlow() {
		_textField.invalidate(getBoundingBox(GLOW_IMG));
	}
	
	@Override
	public void onTextDrawComplete(Canvas canvas) {
		Rect buttonBounds = getBoundingBox(FLAME_IMG);

		canvas.drawBitmap(FLAME_IMG, buttonBounds.left, buttonBounds.top, _brush);
		
		if(_isButtonPressed){
			_brush.setAlpha(255);

			if(isRightHanded()){
				int left = buttonBounds.right - GLOW_IMG.getWidth();
				int top = buttonBounds.bottom - GLOW_IMG.getHeight();
				canvas.drawBitmap(GLOW_IMG, left, top, _brush);
			}
			else{
				Matrix m = new Matrix();
				m.preScale(-1, 1); //flip the glow image
				m.postTranslate(GLOW_IMG.getWidth() + buttonBounds.left,
					buttonBounds.bottom - GLOW_IMG.getHeight());
				canvas.drawBitmap(GLOW_IMG, m, _brush);
			}
			
			_brush.setAlpha(BUTTON_ALPHA);
		}
	}

}


<|start_filename|>src/com/myopicmobile/textwarrior/common/DocumentProvider.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.common;

import java.util.List;

/**
 * Iterator class to access characters of the underlying text buffer.
 * 
 * The usage procedure is as follows:
 * 1. Call seekLine(lineNumber) to mark the position to start iterating
 * 2. Call hasNext() to see if there are any more char
 * 3. Call next() to get the next char
 *
 * If there is more than 1 DocumentProvider pointing to the same Document,
 * changes made by one DocumentProvider will not cause other DocumentProviders
 * to be notified. Implement a publish/subscribe interface if required.
 */
public class DocumentProvider {
	/** Current position in the text. Range [ 0, _theText.getTextLength() ) */
	private int _currIndex;
	private TextBuffer _theText;
	
	public DocumentProvider(){
		_currIndex = 0;
		_theText = new TextBuffer();
	}
	
	public DocumentProvider(TextBuffer buf){
		_currIndex = 0;
		_theText = buf;
	}

	public DocumentProvider(DocumentProvider rhs){
		_currIndex = 0;
		_theText = rhs._theText;
	}
	
	/**
	 * Get a substring of up to maxChars length, starting from charOffset
	 */
	public char[] subSequence(int charOffset, int maxChars){
		return _theText.subSequence(charOffset, maxChars);
	}
	
	public char charAt(int charOffset){
		if(_theText.isValid(charOffset)){
			return _theText.charAt(charOffset);
		}
		else{
			return LanguageCFamily.NULL_CHAR;
		}
	}
	
	/**
	 * Get the row number that charOffset is on
	 */
	public int getRowIndex(int charOffset){
		return _theText.getLineIndex(charOffset);
	}
	
	public int getStartCharOfRow(int rowIndex){
		return _theText.getCharOffset(rowIndex);
	}
	
	/**
	 * Sets the iterator to point at the first character of startingLine.
	 * 
	 * If the line does not exist, hasNext() will return false, and _currIndex 
	 * will be set to -1.
	 * 
	 * @return The index of the first character on startingLine, or -1 
	 * 			if startingLine does not exist
	 */
	public int seekLine(int startingLine){
		_currIndex = _theText.getCharOffset(startingLine);
		return _currIndex;
	}
	
	/**
	 * Sets the iterator to point at startingChar.
	 * 
	 * If startingChar is invalid, hasNext() will return false, and _currIndex 
	 * will be set to -1.
	 */
	public int seekChar(int startingChar){
		if(_theText.isValid(startingChar)){
			_currIndex = startingChar;
		}
		else{
			_currIndex = -1;
		}
		return _currIndex;
	}
	
	public boolean hasNext(){
		return (_currIndex >= 0 &&
				_currIndex < _theText.getTextLength());
	}
	
	/**
	 * Returns the next character and moves the iterator forward.
	 * 
	 * Does not do bounds-checking. It is the responsibility of the caller
	 * to check hasNext() first.
	 * 
	 * @return Next character
	 */
	public char next(){
		char nextChar = _theText.charAt(_currIndex);
		++_currIndex;
		return nextChar;
	}
	
	/**
	 * Inserts c into the document, shifting existing characters from 
	 * insertionPoint (inclusive) to the right
	 */
	public void insertBefore(char c, int insertionPoint, long timestamp){
		char[] a = new char[1];
		a[0] = c;
		_theText.insert(a, insertionPoint, timestamp);
	}

	/**
	 * Inserts characters of cArray into the document, shifting existing
	 * characters from insertionPoint (inclusive) to the right
	 */
	public void insertBefore(char[] cArray, int insertionPoint, long timestamp){
		_theText.insert(cArray, insertionPoint, timestamp);
	}

	/**
	 * Deletes the character at deletionPoint index.
	 */
	public void deleteAt(int deletionPoint, long timestamp){
		_theText.delete(deletionPoint, 1, timestamp);
	}
	

	/**
	 * Deletes up to maxChars number of characters starting from deletionPoint
	 */
	public void deleteAt(int deletionPoint, int maxChars, long time){
		_theText.delete(deletionPoint, maxChars, time);
	}

	/**
	 * Returns true if the underlying text buffer is in batch edit mode
	 */
	public boolean isBatchEdit(){
		return _theText.isBatchEdit();
	}
	
	/**
	 * Signals the beginning of a series of insert/delete operations that can be 
	 * undone/redone as a single unit
	 */
	public void beginBatchEdit(){
		_theText.beginBatchEdit();
	}
	
	/**
	 * Signals the end of a series of insert/delete operations that can be 
	 * undone/redone as a single unit
	 */
	public void endBatchEdit(){
		_theText.endBatchEdit();
	}
	
	/**
	 * Returns the number of characters in the row specified by rowNumber
	 */
	public int rowLength(int rowNumber){
		return _theText.getLineLength(rowNumber);
	}
	
	/**
	 * Returns the number of characters in the document, including the terminal
	 * End-Of-File character
	 */
	public int docLength(){
		return _theText.getTextLength();
	}
	
	/**
	 * Returns the number of rows in the document
	 */
	public int rowCount(){
		return _theText.getLineCount();
	}

	/**
	 * Returns the character encoding scheme used by the document
	 */
	public String getEncodingScheme() {
		return _theText.getEncodingScheme();
	}
	
	/**
	 * Returns the line terminator style used by the document
	 */
	public String getEOLType() {
		return _theText.getEOLType();
	}
	
	//TODO make thread-safe
	/**
	 * Removes spans from the document.
	 * Beware: Not thread-safe! Another thread may be modifying the same spans
	 * returned from getSpans()
	 */
	public void clearSpans(){
		_theText.clearSpans();
	}
	
	/**
	 * Beware: Not thread-safe!
	 */
	public List<Pair> getSpans(){
		return _theText.getSpans();
	}

	/**
	 * Sets the spans to use in the document.
	 * Spans are continuous sequences of characters that have the same format 
	 * like color, font, etc.
	 * 
	 * @param spans A collection of Pairs, where Pair.first is the start 
	 * 		position of the token, and Pair.second is the type of the token.
	 */
	public void setSpans(List<Pair> spans){
		_theText.setSpans(spans);
	}
	
	public boolean canUndo() {
		return _theText.canUndo();
	}
	
	public boolean canRedo() {
		return _theText.canRedo();
	}
	
	public int undo() {
		return _theText.undo();
	}
	
	public int redo() {
		return _theText.redo();
	}
}


<|start_filename|>assets/help_de.html<|end_filename|>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>TextWarrior Hilfe</title>
</head>
<body>
<h3>Im Bau...</h3>

<p>Der Hilftext auf Deutsch ist leider nicht verfügbar zurzeit.</p>

<p><a href="help.html">Zeigen mir den Hilftext auf Englisch.</a></p>

<p>Wollen Sie uns helfen mit der Übersetzung? Dann schicken Sie uns einen email 
an <a href="mailto:<EMAIL>"><EMAIL></a></p>
</body>
</html>

<|start_filename|>src/com/myopicmobile/textwarrior/common/LanguageNonProg.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.common;

/**
 * Singleton class that represents a non-programming language without keywords, 
 * operators etc.
 */
public class LanguageNonProg extends LanguageCFamily{
	private static LanguageCFamily _theOne = null;
	
	private final static String[] keywords = {};
	
	private final static char[] operators = {};


	public static LanguageCFamily getCharacterEncodings(){
		if(_theOne == null){
			_theOne = new LanguageNonProg();
		}
		return _theOne;
	}
	
	private LanguageNonProg(){
		super.registerKeywords(keywords);
		super.replaceOperators(operators);
	}

	@Override
	public boolean isProgLang(){
		return false;
	}

	@Override
	public boolean isEscapeChar(char c){
		return false;
	}

	@Override
	public boolean isDelimiterA(char c){
		return false;
	}

	@Override
	public boolean isDelimiterB(char c){
		return false;
	}
	
	@Override
	public boolean isLineAStart(char c){
		return false;
	}

	@Override
	public boolean isLineStart(char c0, char c1){
		return false;
	}

	@Override
	public boolean isMultilineStartDelimiter(char c0, char c1){
		return false;
	}

	@Override
	public boolean isMultilineEndDelimiter(char c0, char c1){
		return false;
	}
}


<|start_filename|>src/com/myopicmobile/textwarrior/android/FilePicker.java<|end_filename|>
/* 
 * Copyright (C) 2008 OpenIntents.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * Based on AndDev.org's file browser V 2.0.
 */

/*
 * @author <NAME>
 * @date 30 Dec 2010
 * 
 * - Back button exits activity instead of going up the directory tree
 * 
 * - Removed file management functions, dialogs and menu items for renaming, copying, etc.
 * 
 * - Removed option to show the directory line as input box instead of button row
 * 
 * - Removed EulaActivity, ThumbnailLoader, context menu, mime type handling, 
 * 		STATE_BROWSE, STATE_PICK_DIRECTORY, handling changed icons, jumpTo(File)
 * 
 * - Added ACTION_PICK_FILENAME_FOR_SAVE: Browse to a directory and select an
 * 		existing filename or enter a new filename
 * 
 * - In STATE_PICK_FILE, selecting a file exits FilePicker and returns the
 * 		filepath to the calling activity, instead of populating mEditFilename
 * 
 * - In STATE_ENTER_FILENAME, selecting a file populates mEditFilename.
 * 
 * - Selected files will never launch an intent to open them.
 * 
 * - Grouped mEditFilename and mButtonPick into a LinearLayout
 * 
 * - Changed icons and made them bigger. See res/
 */

package com.myopicmobile.textwarrior.android;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import org.openintents.filemanager.DirectoryContents;
import org.openintents.filemanager.IconifiedText;
import org.openintents.filemanager.IconifiedTextListAdapter;
import org.openintents.filemanager.util.FileUtils;

import android.app.ListActivity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

public class FilePicker extends ListActivity { 
    private static final String ROOT_DIR = "/";
    private static final String PATH_SEPARATOR = "/";

	private int mState;
	
    /** Browse a dir tree and select a file by clicking on it */
	private static final int STATE_PICK_FILE = 1;
	/** 
	 * Browse to the dir where the file is to be saved in.
	 * An EditText box is available for the user to type in the filename */
	private static final int STATE_ENTER_FILENAME = 2;
	
	private static final String BUNDLE_CURRENT_DIRECTORY = "current_directory";
	private static final String BUNDLE_TEXTBOX_CONTENTS = "context_file";
	
	/** Contains directories and files together */
     private ArrayList<IconifiedText> directoryEntries = new ArrayList<IconifiedText>();

     /** Dir separate for sorting */
     List<IconifiedText> mListDir = new ArrayList<IconifiedText>();
     
     /** Files separate for sorting */
     List<IconifiedText> mListFile = new ArrayList<IconifiedText>();
     
     /** SD card separate for sorting */
     List<IconifiedText> mListSdCard = new ArrayList<IconifiedText>();
     
     private File currentDirectory = new File(""); 
     
     private String mSdCardPath = "";

     private LinearLayout mFilenameBar;
     private EditText mEditFilename;
     private Button mButtonPick;
     private LinearLayout mDirectoryButtons;

     private TextView mEmptyText;
     private ProgressBar mProgressBar;
     
     private DirectoryScanner mDirectoryScanner;
     private File mPreviousDirectory;
     
     private Handler currentHandler;

 	 static final public int MESSAGE_SHOW_DIRECTORY_CONTENTS = 500;	// List of contents is ready, obj = DirectoryContents
     static final public int MESSAGE_SET_PROGRESS = 501; // Set progress bar, arg1 = current value, arg2 = max value
     
     /** Called when the activity is first created. */ 
     @Override 
     public void onCreate(Bundle icicle) { 
          super.onCreate(icicle); 

          currentHandler = new Handler() {
			public void handleMessage(Message msg) {
				FilePicker.this.handleMessage(msg);
			}
		};

		  requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
          setContentView(R.layout.filelist);
          
          mEmptyText = (TextView) findViewById(R.id.empty_text);
          mProgressBar = (ProgressBar) findViewById(R.id.scan_progress);

		  getListView().setEmptyView(findViewById(R.id.empty));
	      getListView().setTextFilterEnabled(true);
	      getListView().requestFocus();
	      getListView().requestFocusFromTouch();
	      
          mDirectoryButtons = (LinearLayout) findViewById(R.id.directory_buttons);

          mEditFilename = (EditText) findViewById(R.id.filename);
          mButtonPick = (Button) findViewById(R.id.button_pick);
          mFilenameBar = (LinearLayout) findViewById(R.id.enter_filename_bar);
		  mFilenameBar.setVisibility(View.GONE); // display only when dir contents are loaded
          
          mButtonPick.setOnClickListener(new View.OnClickListener() {
				public void onClick(View view) {
					pickEnteredFilename();
				}
          });
          
          getSdCardPath();
          
          Intent intent = getIntent();
          String action = intent.getAction();
          
          File browseto = new File(ROOT_DIR);
          
          if (!TextUtils.isEmpty(mSdCardPath)) {
        	  browseto = new File(mSdCardPath);
          }
          
          // Default state
          mState = STATE_ENTER_FILENAME;
          
    	  if (action.equals(TextWarriorIntents.ACTION_PICK_FILE)) {
    		  mState = STATE_PICK_FILE;  

    	  } else if (action.equals(TextWarriorIntents.ACTION_PICK_FILENAME_FOR_SAVE)) {
    		  mState = STATE_ENTER_FILENAME;
    	  }
    	  else{
    		  Log.w(this.toString(), "No action specified. Default to name file mode");
    	  }

          // Set current directory based on intent data.
    	  File file = FileUtils.getFile(intent.getData());
    	  if (file != null) {
    		  File displayedDir = FileUtils.getPathWithoutFilename(file);
    		  if (displayedDir.isDirectory()) {
    			  browseto = displayedDir;
    		  }
    		  if (!file.isDirectory()) {
    			  mEditFilename.setText(file.getName());
    		  }
    	  }
    	  
    	  String title = intent.getStringExtra(TextWarriorIntents.EXTRA_TITLE);
    	  if (title != null) {
    		  setTitle(title);
    	  }

    	  String buttontext = intent.getStringExtra(TextWarriorIntents.EXTRA_BUTTON_TEXT);
    	  if (buttontext != null) {
    		  mButtonPick.setText(buttontext);
    	  }

          if (icicle != null) {
        	  browseto = new File(icicle.getString(BUNDLE_CURRENT_DIRECTORY));
        	  mEditFilename.setText(icicle.getString(BUNDLE_TEXTBOX_CONTENTS));
        	  refreshDirectoryPanel();
          }
          
          browseTo(browseto);
     }
     
     public void onDestroy() {
    	 super.onDestroy();
    	 
    	 // Stop the scanner.
    	 DirectoryScanner scanner = mDirectoryScanner;
    	 
    	 if (scanner != null) {
    		 scanner.cancel = true;
    	 }
    	 
    	 mDirectoryScanner = null;
     }
     
     private void handleMessage(Message message) {
    	 switch (message.what) {
    	 case MESSAGE_SHOW_DIRECTORY_CONTENTS:
    		 showDirectoryContents((DirectoryContents) message.obj);
    		 break;
    		 
    	 case MESSAGE_SET_PROGRESS:
    		 setProgress(message.arg1, message.arg2);
    		 break;
    	 }
     }

     private void setProgress(int progress, int maxProgress) {
    	 mProgressBar.setMax(maxProgress);
    	 mProgressBar.setProgress(progress);
    	 mProgressBar.setVisibility(View.VISIBLE);
     }
     
     private void showDirectoryContents(DirectoryContents contents) {
    	 mDirectoryScanner = null;
    	 
    	 mListSdCard = contents.getListSdCard();
    	 mListDir = contents.getListDir();
    	 mListFile = contents.getListFile();
    	 
    	 directoryEntries.ensureCapacity(mListSdCard.size() + mListDir.size() + mListFile.size());
    	 
         addAllElements(directoryEntries, mListSdCard);
         addAllElements(directoryEntries, mListDir);
         addAllElements(directoryEntries, mListFile);
          
         IconifiedTextListAdapter itla = new IconifiedTextListAdapter(this); 
         itla.setListItems(directoryEntries, getListView().hasTextFilter());          
         setListAdapter(itla); 
	     getListView().setTextFilterEnabled(true);

         selectInList(mPreviousDirectory);
         refreshDirectoryPanel();
         setProgressBarIndeterminateVisibility(false);
         
         if(mState == STATE_ENTER_FILENAME){
        	 mFilenameBar.setVisibility(View.VISIBLE);
         }
    	 mProgressBar.setVisibility(View.GONE);
    	 mEmptyText.setVisibility(View.VISIBLE);
     }

 	/**
 	 * 
 	 */
 	private void refreshDirectoryPanel() {
 		setDirectoryButtons();
 	} 
     

 	@Override
 	protected void onSaveInstanceState(Bundle outState) {
 		super.onSaveInstanceState(outState);
 		outState.putString(BUNDLE_CURRENT_DIRECTORY, currentDirectory.getAbsolutePath());
 		outState.putString(BUNDLE_TEXTBOX_CONTENTS, mEditFilename.getText().toString());
 	}

	private void pickEnteredFilename() {
		String filename = mEditFilename.getText().toString();
		if (filename.length() == 0){
			Toast.makeText(this, R.string.dialog_error_no_file_name, Toast.LENGTH_LONG).show();
			return;
		}
		// No easy way to check filename for illegal characters like |?<>*+ because
		// this is dependent on the file system type - FAT32, ext3, NTFS...
		// The main application will catch such errors when it tries
		// to create a file with the illegal name.

		File file = FileUtils.getFile(currentDirectory.getAbsolutePath(), filename);
		if(file.isDirectory()){
			Toast.makeText(this, R.string.dialog_error_not_a_file, Toast.LENGTH_LONG).show();
			return;
		}
		boolean exists = file.exists();
		File parent = file.getParentFile();
		if(!exists && !parent.canWrite()){
			// misc errors
			Toast.makeText(this, R.string.dialog_error_file_write_denied, Toast.LENGTH_LONG).show();
			return;
		}
		
    	pickFile(file);
     }

	private void pickFile(File file) {
    	Intent intent = getIntent();
    	intent.setData(FileUtils.getUri(file));
    	setResult(RESULT_OK, intent);
    	finish();
     }
      
     /** 
      * This function browses up one level 
      * according to the field: currentDirectory 
      */ 
     private void upOneLevel(){
         if(currentDirectory.getParent() != null) 
               browseTo(currentDirectory.getParentFile()); 
     }
     
     /**
      * Browse to some location by clicking on a list item.
      * @param aDirectory
      */
     private void browseTo(final File aDirectory){ 
          if (aDirectory.isDirectory()){
    		   mPreviousDirectory = currentDirectory;
               currentDirectory = aDirectory;
               refreshList();
          }else{ 
        	  if (mState == STATE_PICK_FILE) {
	        	  pickFile(aDirectory); 
        	  } else if (mState == STATE_ENTER_FILENAME) {
        		  // Fill in the text box for the filename
        		  mEditFilename.setText(aDirectory.getName());
        	  }
          } 
     }

     private void refreshList() {
    	  // Cancel an existing scanner, if applicable.
    	  DirectoryScanner scanner = mDirectoryScanner;
    	  if (scanner != null) {
    		  scanner.cancel = true;
    	  }

    	  directoryEntries.clear(); 
          mListDir.clear();
          mListFile.clear();
          mListSdCard.clear();
          
          setProgressBarIndeterminateVisibility(true);
          
          // Don't show the "folder empty" text since we're scanning.
          mEmptyText.setVisibility(View.GONE);
          
          // Also DON'T show the progress bar - it's kind of lame to show that
          // for less than a second.
          mProgressBar.setVisibility(View.GONE);
          setListAdapter(null); 
          
		  mDirectoryScanner = new DirectoryScanner(currentDirectory, this, currentHandler, mSdCardPath);
		  mDirectoryScanner.start();
     } 
     
     private void selectInList(File selectFile) {
    	 String filename = selectFile.getName();
    	 IconifiedTextListAdapter la = (IconifiedTextListAdapter) getListAdapter();
    	 int count = la.getCount();
    	 for (int i = 0; i < count; i++) {
    		 IconifiedText it = (IconifiedText) la.getItem(i);
    		 if (it.getText().equals(filename)) {
    			 getListView().setSelection(i);
    			 break;
    		 }
    	 }
     }
     
     private void addAllElements(List<IconifiedText> addTo, List<IconifiedText> addFrom) {
    	 int size = addFrom.size();
    	 for (int i = 0; i < size; i++) {
    		 addTo.add(addFrom.get(i));
    	 }
     }
     
     private void setDirectoryButtons() {
    	 String[] parts = currentDirectory.getAbsolutePath().split("/");
    	 
    	 mDirectoryButtons.removeAllViews();
    	 
    	 int WRAP_CONTENT = LinearLayout.LayoutParams.WRAP_CONTENT;
    	 
    	 // Add home button separately
    	 ImageButton ib = new ImageButton(this);
    	 ib.setImageResource(R.drawable.ic_launcher_home);
		 ib.setLayoutParams(new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
		 ib.setOnClickListener(new View.OnClickListener() {
			public void onClick(View view) {
				browseTo(new File(ROOT_DIR));
			}
		 });
		 mDirectoryButtons.addView(ib);
		 
    	 // Add other buttons
    	 
    	 String dir = "";
    	 
    	 for (int i = 1; i < parts.length; i++) {
    		 dir += PATH_SEPARATOR + parts[i];
    		 if (dir.equals(mSdCardPath)) {
    			 // Add SD card button
    			 ib = new ImageButton(this);
    	    	 ib.setImageResource(R.drawable.icon_sdcard);
    			 ib.setLayoutParams(new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
    			 ib.setOnClickListener(new View.OnClickListener() {
    					public void onClick(View view) {
    						browseTo(new File(mSdCardPath));
    					}
    			 });
    			 mDirectoryButtons.addView(ib);
    		 } else {
	    		 Button b = new Button(this);
	    		 b.setLayoutParams(new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
	    		 b.setText(parts[i]);
	    		 b.setTag(dir);
	    		 b.setOnClickListener(new View.OnClickListener() {
	 				public void onClick(View view) {
	 					String dir = (String) view.getTag();
	 					browseTo(new File(dir));
	 				}
	    		 });
    			 mDirectoryButtons.addView(b);
    		 }
    	 }
    	 
    	 checkButtonLayout();
     }

     private void checkButtonLayout() {
    	 // Let's measure how much space we need:
    	 int spec = View.MeasureSpec.UNSPECIFIED;
    	 mDirectoryButtons.measure(spec, spec);
    	 
    	 int requiredwidth = mDirectoryButtons.getMeasuredWidth();
    	 int width = getWindowManager().getDefaultDisplay().getWidth();
    	 
    	 if (requiredwidth > width) {
        	 int WRAP_CONTENT = LinearLayout.LayoutParams.WRAP_CONTENT;
        	 
        	 // Create a new button that shows that there is more to the left:
        	 ImageButton ib = new ImageButton(this);
        	 ib.setImageResource(R.drawable.ic_menu_back_small);
    		 ib.setLayoutParams(new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));

    		 ib.setOnClickListener(new View.OnClickListener() {
    				public void onClick(View view) {
    					// Up one directory.
    					upOneLevel();
    				}
    		 });
    		 mDirectoryButtons.addView(ib, 0);
    		 
    		 // New button needs even more space
    		 ib.measure(spec, spec);
    		 requiredwidth += ib.getMeasuredWidth();

    		 // Need to take away some buttons
    		 // but leave at least "back" button and one directory button.
    		 while (requiredwidth > width && mDirectoryButtons.getChildCount() > 2) {
    			 View view = mDirectoryButtons.getChildAt(1);
    			 requiredwidth -= view.getMeasuredWidth();
    			 
	    		 mDirectoryButtons.removeViewAt(1);
    		 }
    	 }
     }
     
     @Override 
     protected void onListItemClick(ListView l, View v, int position, long id) { 
          super.onListItemClick(l, v, position, id); 
          
          IconifiedTextListAdapter adapter = (IconifiedTextListAdapter) getListAdapter();
          
          if (adapter == null) {
        	  return;
          }
          
          IconifiedText text = (IconifiedText) adapter.getItem(position);
          String file = text.getText();

    	  String curdir = currentDirectory.getAbsolutePath();
    	  File clickedFile = FileUtils.getFile(curdir, file);
           if (clickedFile != null) {
                browseTo(clickedFile);
           }
     }

    private void getSdCardPath() {
    	 mSdCardPath = android.os.Environment
			.getExternalStorageDirectory().getAbsolutePath();
    }
}

<|start_filename|>src/com/myopicmobile/textwarrior/android/FreeScrollingTextField.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */

/*
 *****************************************************************************
 *
 * --------------------------------- row length
 * Hello World(\n)                 | 12
 * This is a test of the caret(\n) | 28
 * func|t|ions(\n)                 | 10
 * of this program(EOF)            | 16
 * ---------------------------------
 * 
 * The figure illustrates the convention for counting characters.
 * Rows 36 to 39 of a hypothetical text file are shown.
 * The 0th char of the file is off-screen.
 * Assume the first char on screen is the 257th char.
 * The caret is before the char 't' of the word "functions". The caret is drawn
 * as a filled blue rectangle enclosing the 't'.
 * 
 * _caretPosition == 257 + 12 + 28 + 4 == 301
 * 
 * Note 1: EOF (End Of File) is a real char with a length of 1
 * Note 2: Characters enclosed in parentheses are non-printable
 * 
 *****************************************************************************
 *
 * There is a difference between rows and lines in TextWarrior.
 * Rows are displayed while lines are a pure logical construct.
 * When there is no word-wrap, a line of text is displayed as a row on screen.
 * With word-wrap, a very long line of text may be split across several rows 
 * on screen.
 * 
 */
package com.myopicmobile.textwarrior.android;

import java.util.List;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.text.ClipboardManager;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.Scroller;

import com.myopicmobile.textwarrior.common.ColorScheme;
import com.myopicmobile.textwarrior.common.DocumentProvider;
import com.myopicmobile.textwarrior.common.LanguageCFamily;
import com.myopicmobile.textwarrior.common.Lexer;
import com.myopicmobile.textwarrior.common.Pair;
import com.myopicmobile.textwarrior.common.RowListener;
import com.myopicmobile.textwarrior.common.TextWarriorException;

/**
 * A custom text view that uses a solid shaded caret (aka cursor) instead of a 
 * blinking caret and allows a variety of navigation methods to be easily
 * integrated.
 * 
 * Responsibilities
 * 1. Display text
 * 2. Display padding
 * 3. Scrolling
 * 4. Store and display caret position and selection range
 * 5. Store font type, font size, and tab length
 * 6. Interpret non-touch input events and shortcut keystrokes, triggering
 *    the appropriate inner class controller actions
 * 7. Reset view, set cursor position and selection range
 *
 * Inner class controller responsibilities
 * 1. Caret movement
 * 2. Activate/deactivate selection mode
 * 3. Cut, copy, paste, delete, insert
 * 4. Schedule areas to repaint and analyze for spans in response to edits
 * 5. Directs scrolling if caret movements or edits causes the caret to be off-screen
 * 6. Notify rowListeners when caret row changes
 * 7. Provide helper methods for InputConnection to setComposingText from the IME
 *
 * This class is aware that the underlying text buffer uses an extra char (EOF) 
 * to mark the end of the text. The text size reported by the text buffer includes
 * this extra char. Some bounds manipulation is done so that this implementation
 * detail is hidden from client classes.
 */
public class FreeScrollingTextField extends View{

	protected DocumentProvider _hDoc; // the model in MVC
	TextFieldController _fieldController; // the controller in MVC
	TextFieldInputConnection _inputConnection;
	private Scroller _scroller;
	protected TouchNavigationMethod _navMethod;
	protected RowListener _rowLis;
	protected SelectionModeListener _selModeLis;
	protected boolean _isDirty = false;

	protected int _caretPosition = 0;
	private int _caretRow = 0; // can be calculated, but stored for efficiency purposes
	protected int _selectionAnchor = -1; // inclusive
	protected int _selectionEdge = -1; // exclusive

	private Paint _brush;
	/** Max amount that can be scrolled horizontally for the current frame */
	private int _xExtent = 0;
	protected int _tabLength = DEFAULT_TAB_LENGTH_SPACES;


	/** Scale factor for the width of a caret when on a \n or EOF char.
	 *  A factor of 1.0 is equals to the width of a space character */
	protected static float EMPTY_CARET_WIDTH_SCALE = 0.75f;
	/** When in selection mode, the caret height is scaled by this factor */
	protected static float SEL_CARET_HEIGHT_SCALE = 0.5f;
	protected static int DEFAULT_TAB_LENGTH_SPACES = 4;
	protected static int BASE_TEXT_SIZE_PIXELS = 16;


	public FreeScrollingTextField(Context context, AttributeSet attrs){
		super(context, attrs);
		_hDoc = new DocumentProvider();
		_navMethod = new TouchNavigationMethod(this);
		_scroller = new Scroller(context);
		initView();
	}
	
	public FreeScrollingTextField(Context context, AttributeSet attrs, int defStyle){
		super(context, attrs, defStyle);
		_hDoc = new DocumentProvider();
		_navMethod = new TouchNavigationMethod(this);
		_scroller = new Scroller(context);
		initView();
	}

	private void initView(){
		_fieldController = this.new TextFieldController();

		_brush = new Paint();
		_brush.setAntiAlias(true);
		_brush.setTextSize(BASE_TEXT_SIZE_PIXELS);
		
		setBackgroundColor(ColorScheme.backgroundColor);
		setFocusableInTouchMode(true);
		setHapticFeedbackEnabled(true);
		
		_rowLis = new RowListener() {
			@Override
			public void onRowChange(int newRowIndex) {
				// Do nothing
			}
		};
		
		_selModeLis = new SelectionModeListener() {
			@Override
			public void onSelectionModeChanged(boolean active) {
				// Do nothing
			}
		};
		resetView();

		//TODO find out if this function works
		//setScrollContainer(true);
	}

	private void resetView(){
		_caretPosition = 0;
		_caretRow = 0;
		_xExtent = 0;
		_fieldController.setSelectText(false);
		_fieldController.stopTextComposing();
		_hDoc.clearSpans();
		_rowLis.onRowChange(0);
		scrollTo(0, 0);
	}

	public void changeDocumentProvider(DocumentProvider hDoc){
		_hDoc = hDoc;
		resetView();
		_fieldController.cancelSpanning(); //stop existing lex threads
		_fieldController.determineSpans();
		invalidate();
	}

	DocumentProvider createDocumentProvider(){
		return new DocumentProvider(_hDoc);
	}

	public void setRowListener(RowListener rLis){
		_rowLis = rLis;
	}

	public void setSelModeListener(SelectionModeListener sLis){
		_selModeLis = sLis;
	}

	/**
	 * Sets the caret navigation method used by this text field
	 */
	public void setNavigationMethod(TouchNavigationMethod navMethod) {
		_navMethod = navMethod;
	}

	public void setDirty(boolean set){
		_isDirty = set;
	}

	public boolean isDirty(){
		return _isDirty;
	}

	@Override
	public InputConnection onCreateInputConnection (EditorInfo outAttrs){
		outAttrs.inputType = InputType.TYPE_CLASS_TEXT
			| InputType.TYPE_TEXT_FLAG_MULTI_LINE;
		outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_ENTER_ACTION
			| EditorInfo.IME_ACTION_DONE
			| EditorInfo.IME_FLAG_NO_EXTRACT_UI;
		if (_inputConnection == null){
			_inputConnection = this.new TextFieldInputConnection(this);
		}
		else{
			_inputConnection.resetComposingState();
		}
		return _inputConnection;
	}

	@Override
	public boolean onCheckIsTextEditor(){
		return true;
	}

	@Override
	public boolean isSaveEnabled() {
		return true;
	}

  //---------------------------------------------------------------------
  //------------------------- Layout methods ----------------------------
//TODO test with height less than 1 complete row
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(useAllDimensions(widthMeasureSpec),
        		useAllDimensions(heightMeasureSpec));
    }

    @Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		makeCharVisible(_caretPosition);
	}

	private int useAllDimensions(int measureSpec) {
        int specMode = MeasureSpec.getMode(measureSpec);
        int result = MeasureSpec.getSize(measureSpec);

        if (specMode != MeasureSpec.EXACTLY && specMode != MeasureSpec.AT_MOST) {
            result = Integer.MAX_VALUE;
			TextWarriorException.assertVerbose(false,
			 	"MeasureSpec cannot be UNSPECIFIED. Setting dimensions to max.");
        }

        return result;
    }

	final protected int getNumVisibleRows(){
		return (int) Math.ceil((double) getContentHeight() / rowHeight());
	}

	final protected int rowHeight(){
		Paint.FontMetricsInt metrics = _brush.getFontMetricsInt();
		//TODO confirm ascent and leading are negative in Android
		return (-metrics.ascent - metrics.leading + metrics.descent);
	}
	
	/*	 
     The only methods that have to worry about padding are invalidate, draw 
	 and computeVerticalScrollRange() methods. Other methods can assume that
	 the text completely fills a rectangular viewport given by getContentWidth()
	 and getContentHeight()
	 */
	final public int getContentHeight(){
		return getHeight() - getPaddingTop() - getPaddingBottom();
	}

	final public int getContentWidth(){
		return getWidth() - getPaddingLeft() - getPaddingRight();
	}
	
	
	//---------------------------------------------------------------------
	//-------------------------- Paint methods ----------------------------
	/**
	 * The first row of text to paint, which may be partially visible.
	 * Deduced from the clipping rectangle given to onDraw()
	 */
	private int getBeginPaintRow(Canvas canvas){
		Rect bounds = canvas.getClipBounds();
		return bounds.top / rowHeight(); 
	}

	/**
	 * The last row of text to paint, which may be partially visible.
	 * Deduced from the clipping rectangle given to onDraw()
	 */
	private int getEndPaintRow(Canvas canvas){
		//clip top and left are inclusive; bottom and right are exclusive
		Rect bounds = canvas.getClipBounds();
		return (bounds.bottom - 1) / rowHeight(); 
	}
	
	/**
	 * @return The x-value of the baseline for drawing text on the given row
	 */
	private int getPaintBaseline(int row){
		Paint.FontMetricsInt metrics = _brush.getFontMetricsInt();
		return (row + 1) * rowHeight() - metrics.descent - 1;
		//TODO confirm if origin used in Canvas.drawText is on ascent section
		// or descent section. If in descent section, don't need to subtract 1
	}
	
	@Override
	protected void onDraw(Canvas canvas) {
		canvas.save();
		
		//translate clipping region to create padding around edges
		canvas.clipRect(getScrollX() + getPaddingLeft(),
				getScrollY() + getPaddingTop(),
				getScrollX() + getWidth() - getPaddingRight(),
				getScrollY() + getHeight() - getPaddingBottom());
		canvas.translate(getPaddingLeft(), getPaddingTop());
		realDraw(canvas);
		
		canvas.restore();
 		
 		_navMethod.onTextDrawComplete(canvas);
	}

	private void realDraw(Canvas canvas){
 		int beginPaintRow = getBeginPaintRow(canvas);
	    int currentIndex = _hDoc.getStartCharOfRow(beginPaintRow);
 		if(currentIndex < 0){
 			return;
 		}

		//----------------------------------------------
		// set up span coloring settings
		//----------------------------------------------
		int spanIndex = 0;
		List<Pair> spans = _hDoc.getSpans();

	    // There must be at least one span to paint, even for an empty file,
	    // where the span contains only the EOF character
		TextWarriorException.assertVerbose(!spans.isEmpty(),
		 	"No spans to paint in TextWarrior.paint()");

		Pair nextSpan = (Pair) spans.get(spanIndex++);
		Pair currSpan;
		do{
			currSpan = nextSpan;
			if(spanIndex < spans.size()){
				nextSpan = (Pair) spans.get(spanIndex++);
			}
			else{
				nextSpan = null;
			}
		}
		while(nextSpan != null &&
				nextSpan.getFirst() <= currentIndex);
		
		int spanColor = ColorScheme.getTokenColor(currSpan.getSecond());
		_brush.setColor(spanColor);

		//----------------------------------------------
		// set up graphics settings
		//----------------------------------------------
	    int paintX = 0;
	    int paintY = getPaintBaseline(beginPaintRow);
	    int endY = getPaintBaseline(getEndPaintRow(canvas));

	    //----------------------------------------------
	    // start painting!
	    //----------------------------------------------
	    _hDoc.seekChar(currentIndex);
	    while (paintY <= endY && _hDoc.hasNext()){
	     	// check if formatting changes are needed
	     	if (reachedNextSpan(currentIndex, nextSpan)){
	 			currSpan = nextSpan;
	 			spanColor = ColorScheme.getTokenColor(currSpan.getSecond());
	 			_brush.setColor(spanColor);
	
	 			if(spanIndex < spans.size()){
	 				nextSpan = (Pair) spans.get(spanIndex++);
	 			}
	 			else{
	 				nextSpan = null;
	 			}
	     	}
	
	     	char c = _hDoc.next();
//TODO investigate performance gain	 if (paintX < getScrollX() + getWidth()){

	    	if (currentIndex == _caretPosition){
	    		paintX += drawCaret(canvas, c, paintX, paintY);
	    	}
	    	else if (_fieldController.inSelectionRange(currentIndex)){
	    		paintX += drawSelectedText(canvas, c, paintX, paintY);
	    	}
	    	else{
	    		paintX += drawChar(canvas, c, paintX, paintY);
	    	}

	     	++currentIndex;
	     	if (c == LanguageCFamily.NEWLINE){
	 	 		paintY += rowHeight();
	     		if (paintX > _xExtent){
	     			_xExtent = paintX;
	     		}
	 	 		paintX = 0;
	     	}
		} // end while
	    
 		if (paintX > _xExtent){
 			// record widest line seen so far
 			_xExtent = paintX;
 		}
	}
	
	private int drawChar(Canvas canvas, char c, int paintX, int paintY){
		if(c != LanguageCFamily.NEWLINE &&
				c != LanguageCFamily.EOF &&
				c != LanguageCFamily.TAB){
 			char[] ca = {c};
	 		canvas.drawText(ca, 0, 1, paintX, paintY, _brush);
		}

		return getAdvance(c);
	}

	// paintY is the baseline for text, NOT the top extent
	private void drawTextBackground(Canvas canvas, int paintX, int paintY,
			int advance){
		Paint.FontMetricsInt metrics = _brush.getFontMetricsInt();
 		canvas.drawRect(paintX,
 				//TODO confirm if origin is in ascent or descent section
 				paintY + metrics.ascent + metrics.leading + 1,
 				paintX + advance,
 				paintY + metrics.descent + 1, 
 				_brush);
	}

	private int drawSelectedText(Canvas canvas, char c, int paintX, int paintY){
		int oldColor = _brush.getColor();
		int advance = getAdvance(c);

		_brush.setColor(ColorScheme.selBackgroundColor);
		drawTextBackground(canvas, paintX, paintY, advance);

		_brush.setColor(ColorScheme.selForegroundColor);
		drawChar(canvas, c, paintX, paintY);

		_brush.setColor(oldColor);
		return advance;
	}


	private int drawCaret(Canvas canvas, char c, int paintX, int paintY){
		int originalColor = _brush.getColor();
		int textColor = originalColor;
		int advance = getAdvance(c);

	  	if(_caretPosition == _selectionAnchor &&
	  			_caretPosition != _selectionEdge){
	  		// draw selection background
			_brush.setColor(ColorScheme.selBackgroundColor);
			drawTextBackground(canvas, paintX, paintY, advance);
			textColor = ColorScheme.caretForegroundColor;
	  	}

	  	int caretColor = (isFocused()) ? ColorScheme.caretBackgroundColor:
	  		ColorScheme.caretDisabledColor;
		_brush.setColor(caretColor);
	  	if (_caretPosition == _selectionEdge ||
	  			_caretPosition == _selectionAnchor){
	  		// draw half caret
	  		Paint.FontMetricsInt metrics = _brush.getFontMetricsInt();
	 		canvas.drawRect(paintX,
	 				paintY + (metrics.ascent * SEL_CARET_HEIGHT_SCALE),
	 				paintX + advance,
	 				paintY + metrics.descent + 1, 
	 				_brush);
	  	}
	  	else{
	  		// draw full caret
			drawTextBackground(canvas, paintX, paintY, advance);
			textColor = ColorScheme.caretForegroundColor;
	  	}

		_brush.setColor(textColor);
  		// draw text
		drawChar(canvas, c, paintX, paintY);
		_brush.setColor(originalColor);
		return advance;
	}
	
	/**
	 * Returns printed width of c.
	 * 
	 * Takes into account user-specified tab width and also handles
	 * application-defined widths for NEWLINE and EOF
	 * 
	 * @param c Character to measure
	 * @return Advance of character
	 */
	protected int getAdvance(char c){
		int advance;
		
		switch (c){
 		case LanguageCFamily.NEWLINE: // fall-through
 		case LanguageCFamily.EOF:
 			advance = getEmptyAdvance();
 			break;
 		case LanguageCFamily.TAB:
 			advance = getTabAdvance();
 			break;
 		default:
 			char[] ca = {c};
 			advance = (int) _brush.measureText(ca, 0, 1);
 			break;	
		}
		
		return advance;
	}
	
	final protected int getEmptyAdvance(){
		return (int) (EMPTY_CARET_WIDTH_SCALE * _brush.measureText(" ", 0, 1));
	}
	
	final protected int getTabAdvance(){
		return _tabLength * (int) _brush.measureText(" ", 0, 1);
	}
	
	/**
	 * Invalidate rows from startRow (inclusive) to endRow (exclusive)
	 */
	private void invalidateRows(int startRow, int endRow) {
		TextWarriorException.assertVerbose(startRow <= endRow && startRow >= 0,
	 		"Invalid startRow and/or endRow");

        //TODO The descent of (startRow-1) and the ascent of (startRow+1)
		//may jut inside startRow, so parts of these rows have to be invalidated
		//as well. This is a problem for Thai, Vietnamese and Indic scripts
		int top = startRow * rowHeight() + getPaddingTop();
        if (startRow > 0) {
	  		Paint.FontMetricsInt metrics = _brush.getFontMetricsInt();
            top -= metrics.descent;
        }
        Rect caretSpill = _navMethod.getCaretBloat();
        top = Math.max(0, top - caretSpill.top);
        
		super.invalidate(0,
			top,
			getScrollX() + getWidth(),
			endRow * rowHeight() + getPaddingTop() + caretSpill.bottom);
	}

	/**
	 * Invalidate rows from startRow (inclusive) to the end of the field
	 */
	private void invalidateFromRow(int startRow) {
		TextWarriorException.assertVerbose(startRow >= 0,
	 		"Invalid startRow");
		
        //TODO The descent of (startRow-1) and the ascent of (startRow+1)
		//may jut inside startRow, so parts of these rows have to be invalidated
		//as well. This is a problem for Thai, Vietnamese and Indic scripts
		int top = startRow * rowHeight() + getPaddingTop();
        if (startRow > 0) {
	  		Paint.FontMetricsInt metrics = _brush.getFontMetricsInt();
            top -= metrics.descent;
        }
        Rect caretSpill = _navMethod.getCaretBloat();
        top = Math.max(0, top - caretSpill.top);
        
		super.invalidate(0,
			top,
			getScrollX() + getWidth(),
			getScrollY() + getHeight());
	}

	private void invalidateCaretRow(){
		invalidateRows(_caretRow, _caretRow+1);
	}

	public void invalidateSelectionRows(){
		int startRow = _hDoc.getRowIndex(_selectionAnchor);
		int endRow = _hDoc.getRowIndex(_selectionEdge);

		invalidateRows(startRow, endRow+1);
	}

	/**
	 * Scrolls the text horizontally and/or vertically if the character 
	 * specified by charOffset is not in the visible text region.
	 * The view is invalidated if it is scrolled.
	 * 
	 * @param charOffset The index of the character to make visible
	 * @return True if the drawing area was scrolled horizontally
	 * 			and/or vertically
	 */
	public boolean makeCharVisible(int charOffset){
		TextWarriorException.assertVerbose(
				charOffset >= 0 && charOffset < _hDoc.docLength(),
				"Invalid charOffset given");
		int scrollVerticalBy = makeCharRowVisible(charOffset);
		int scrollHorizontalBy = makeCharColumnVisible(charOffset);
		
		if (scrollVerticalBy == 0 && scrollHorizontalBy == 0){
			return false;
		}
		else{
			scrollBy(scrollHorizontalBy, scrollVerticalBy);
			return true;
		}
	}
 
	/**
	 * Calculates the amount to scroll vertically if the char is not
	 * in the visible region.
	 * 
	 * @param charOffset The index of the character to make visible
	 * @return The amount to scroll vertically
	 */
	private int makeCharRowVisible(int charOffset){
		int scrollBy = 0;
		int charTop = _hDoc.getRowIndex(charOffset) * rowHeight();
		int charBottom = charTop + rowHeight();

		if (charTop < getScrollY()){
			scrollBy = charTop - getScrollY();
		}
		else if (charBottom > (getScrollY() + getContentHeight())){
			scrollBy = charBottom - getScrollY() - getContentHeight();
		}

		return scrollBy;
	}

	/**
	 * Calculates the amount to scroll horizontally if the char is not
	 * in the visible region.
	 * 
	 * @param charOffset The index of the character to make visible
	 * @return The amount to scroll horizontally
	 */
	private int makeCharColumnVisible(int charOffset){
		int scrollBy = 0;
		Pair visibleRange = getCharExtent(charOffset);

	    int charLeft = visibleRange.getFirst();
	    int charRight = visibleRange.getSecond();

	    if (charRight > (getScrollX() + getContentWidth())){
	    	scrollBy = charRight - getScrollX() - getContentWidth();
	    }

	    if (charLeft < getScrollX()){
	    	scrollBy = charLeft - getScrollX();
	    }
	    
	    return scrollBy;
	}
	
	/**
	 * Calculates the x-coordinate extent of charOffset.
	 * 
	 * @return The x-values of left and right edges of charOffset. Pair.first 
	 * 		contains the left edge and Pair.second contains the right edge
	 */
	protected Pair getCharExtent(int charOffset){
		int rowIndex = _hDoc.getRowIndex(charOffset);
		int charCount = _hDoc.seekLine(rowIndex);
		int left = 0;
		int right = 0;

		while(charCount <= charOffset && _hDoc.hasNext()){
			left = right;
			char c = _hDoc.next();
			switch (c){
			case LanguageCFamily.NEWLINE:
			case LanguageCFamily.EOF:
				right += getEmptyAdvance();
				break;
			case LanguageCFamily.TAB:
				right += getTabAdvance();
				break;
			default:
				char[] ca = {c};
				right += (int) _brush.measureText(ca, 0, 1);
				break;
			}
			++charCount;
		}
 
		return new Pair(left, right);
	}
	
	/**
	 * Returns the bounding box of a character in the text field.
	 * The coordinate system used is one where (0, 0) is the top left corner
	 * of the text, before padding is added.
	 * 
	 * @param charOffset The character offset of the character of interest
	 * 
	 * @return Rect(left, top, right, bottom) of the character bounds,
	 * 		or Rect(-1, -1, -1, -1) if there is no character at that coordinate.
	 */
	public Rect getBoundingBox(int charOffset){
		if(charOffset < 0 || charOffset >= _hDoc.docLength()){
			return new Rect(-1, -1, -1, -1);
		}

		int row = _hDoc.getRowIndex(charOffset);
		int top = row * rowHeight();
		int bottom = top + rowHeight();
		
		Pair xExtent = getCharExtent(charOffset);
		int left = xExtent.getFirst();
		int right = xExtent.getSecond();
		
		return new Rect(left, top, right, bottom);
	}

	
	//---------------------------------------------------------------------
	//------------------- Scrolling and touch -----------------------------
	/**
	 * Maps a coordinate to the character that it is on. If the coordinate is
	 * on empty space, the nearest character on the corresponding row is returned.
	 * If there is no character on the row, -1 is returned.
	 * 
	 * The coordinates passed in should not have padding applied to them.
	 * 
	 * @param x x-coordinate
	 * @param y y-coordinate
	 * 
	 * @return The index of the closest character, or -1 if there is
	 * 			no character or nearest character at that coordinate
	 */
	public int coordToCharIndex(int x, int y){
		int row = y / rowHeight();
		int charIndex = _hDoc.seekLine(row);
		if(charIndex >= 0){
			if(x < 0){
				return charIndex; // coordinate is outside, to the left of view
			}
			
			int extent = 0;
			while(extent < x && _hDoc.hasNext()){
				char c = _hDoc.next();
				if (c == LanguageCFamily.NEWLINE || c == LanguageCFamily.EOF){
					break;
				}
				else if (c == LanguageCFamily.TAB){
					extent += getTabAdvance();
				}
				else{
					char[] ca = {c};
					extent += (int) _brush.measureText(ca, 0, 1);
				}
				++charIndex;
			}
			if(extent > x){
				//went one past the mapped char
				--charIndex;
			}
			return charIndex;
		}
		else{
			//non-existent row
			return -1;
		}
	}
	
	/**
	 * Maps a coordinate to the character that it is on.
	 * Returns -1 if there is no character on the coordinate.
	 * 
	 * The coordinates passed in should not have padding applied to them.
	 * 
	 * @param x x-coordinate
	 * @param y y-coordinate
	 * 
	 * @return The index of the character that is on the coordinate,
	 * 			or -1 if there is no character at that coordinate.
	 */
	public int coordToCharIndexStrict(int x, int y){
		int row = y / rowHeight();
		int charIndex = _hDoc.seekLine(row);
		
		if(charIndex >= 0 && x >= 0){
			int extent = 0;
			while(extent < x && _hDoc.hasNext()){
				char c = _hDoc.next();
				if (c == LanguageCFamily.NEWLINE || c == LanguageCFamily.EOF){
					break;
				}
				else if (c == LanguageCFamily.TAB){
					extent += getTabAdvance();
				}
				else{
					char[] ca = {c};
					extent += (int) _brush.measureText(ca, 0, 1);
				}
				++charIndex;
			}
			
			if(extent < x){
				charIndex = -1; //no char on x
			}
			else{
				//went one past the mapped char
				--charIndex;
			}
		}
		else{
			//non-existent row
			charIndex = -1;
		}
		
		return charIndex;
	}
	
	/**
	 * Not private to allow access by TouchNavigationMethod
	 * 
	 * @return The maximum x-value that can be scrolled to for the current rows
	 * of text in the viewport.
	 */
	int getMaxScrollX(){
		return Math.max(0,
				_xExtent - getContentWidth() + _navMethod.getCaretBloat().right);
	}
	
	/**
	 * Not private to allow access by TouchNavigationMethod
	 * 
	 * @return The maximum y-value that can be scrolled to.
	 */
	int getMaxScrollY(){
		return Math.max(0,
			_hDoc.rowCount()*rowHeight() - getContentHeight() + _navMethod.getCaretBloat().bottom);
	}
	
	@Override
	protected int computeVerticalScrollOffset() {
		return getScrollY();
	}

	@Override
	protected int computeVerticalScrollRange() {
		return _hDoc.rowCount() * rowHeight() + getPaddingTop() + getPaddingBottom();
	}

	@Override
	public void computeScroll() {
		if (_scroller.computeScrollOffset()){
			scrollTo(_scroller.getCurrX(), _scroller.getCurrY());
		}
	}

	/**
	 * Start fling scrolling
	 */
	public void flingScroll(int velocityX, int velocityY) {
		_scroller.fling(getScrollX(), getScrollY(), velocityX, velocityY,
				0, getMaxScrollX(), 0, getMaxScrollY());
		// Keep on drawing until the animation has finished.
		postInvalidate();
	}

	public boolean isFlingScrolling() {
		return !_scroller.isFinished();
	}

	public void stopFlingScrolling() {
		_scroller.forceFinished(true);
	}


	//---------------------------------------------------------------------
	//--------------------------  Caret Scroll  ---------------------------
	public final static int SCROLL_UP = 0;
	public final static int SCROLL_DOWN = 1;
	public final static int SCROLL_LEFT = 2;
	public final static int SCROLL_RIGHT = 3;
	private final static long SCROLL_PERIOD = 250;
	
	/**
	 * Starting scrolling continuously in scrollDir.
	 * Not private to allow access by TouchNavigationMethod.
	 * 
	 * @return True if auto-scrolling started
	 */	
	boolean autoScrollCaret(int scrollDir) {
		boolean scrolled = false;
		switch(scrollDir){
		case SCROLL_UP:
			removeCallbacks(_scrollCaretUpTask);
			if((!caretOnFirstRowOfFile())){
				post(_scrollCaretUpTask);
				scrolled = true;
			}
			break;
		case SCROLL_DOWN:
			removeCallbacks(_scrollCaretDownTask);
			if(!caretOnLastRowOfFile()){
				post(_scrollCaretDownTask);
				scrolled = true;
			}
			break;
		case SCROLL_LEFT:
			removeCallbacks(_scrollCaretLeftTask);
			if (_caretPosition > 0 &&
    				_caretRow == _hDoc.getRowIndex(_caretPosition - 1)){
				post(_scrollCaretLeftTask);
				scrolled = true;
			}
			break;
		case SCROLL_RIGHT:
			removeCallbacks(_scrollCaretRightTask);
    		if (!caretOnEOF() &&
    				_caretRow == _hDoc.getRowIndex(_caretPosition + 1)){
    			post(_scrollCaretRightTask);
				scrolled = true;
    		}
			break;
		default:
			 TextWarriorException.assertVerbose(false,
			 	"Invalid scroll direction");
			break;
		}
		return scrolled;
	}
	
	/**
	 * Stops automatic scrolling initiated by autoScrollCaret(int).
	 * Not private to allow access by TouchNavigationMethod
	 */
	void stopAutoScrollCaret() {
		removeCallbacks(_scrollCaretDownTask);
		removeCallbacks(_scrollCaretUpTask);
		removeCallbacks(_scrollCaretLeftTask);
		removeCallbacks(_scrollCaretRightTask);
	}
	
	/**
	 * Stops automatic scrolling in scrollDir direction.
	 * Not private to allow access by TouchNavigationMethod
	 */
	void stopAutoScrollCaret(int scrollDir) {
		switch(scrollDir){
		case SCROLL_UP:
			removeCallbacks(_scrollCaretUpTask);
			break;
		case SCROLL_DOWN:
			removeCallbacks(_scrollCaretDownTask);
			break;
		case SCROLL_LEFT:
			removeCallbacks(_scrollCaretLeftTask);
			break;
		case SCROLL_RIGHT:
			removeCallbacks(_scrollCaretRightTask);
			break;
		default:
			 TextWarriorException.assertVerbose(false,
			 	"Invalid scroll direction");
			break;
		}
	}
	
	private final Runnable _scrollCaretDownTask = new Runnable(){
		public void run(){
			_fieldController.moveCaretDown();
			if(!caretOnLastRowOfFile()){
				postDelayed(_scrollCaretDownTask, SCROLL_PERIOD);
			}
		}
	};

	private final Runnable _scrollCaretUpTask = new Runnable(){
		public void run(){
			_fieldController.moveCaretUp();
			if(!caretOnFirstRowOfFile()){
				postDelayed(_scrollCaretUpTask, SCROLL_PERIOD);
			}
		}
	};

	private final Runnable _scrollCaretLeftTask = new Runnable(){
		public void run(){
			_fieldController.moveCaretLeft();
    		if (_caretPosition > 0 &&
    				_caretRow == _hDoc.getRowIndex(_caretPosition - 1)){
    			postDelayed(_scrollCaretLeftTask, SCROLL_PERIOD);
    		}
		}
	};

	private final Runnable _scrollCaretRightTask = new Runnable(){
		public void run(){
			_fieldController.moveCaretRight();
    		if (!caretOnEOF() &&
    				_caretRow == _hDoc.getRowIndex(_caretPosition + 1)){
    			postDelayed(_scrollCaretRightTask, SCROLL_PERIOD);
    		}
		}
	};
	
	
	//---------------------------------------------------------------------
	//------------------------- Caret methods -----------------------------
	
	public int getCaretRow(){
		return _caretRow;
	}
  
	public int getCaretPosition(){
		return _caretPosition;
	}
	
	/**
	 * This helper method should only be used by internal methods after setting
	 * _caretPosition, in order to to recalculate the new row the caret is on.
	 * Typically, the return value is then used to set _caretRow. 
	 */
	private int determineCaretRow(){
		return _hDoc.getRowIndex(_caretPosition);
	}
	
	/**
	 * Sets the caret to position i, scrolls it to view and invalidates 
	 * the necessary areas for redrawing
	 * 
	 * @param i The character index that the caret should be set to
	 */
	public void moveCaret(int i) {
		_fieldController.moveCaret(i);
	}
	
	/**
	 * Sets the caret one position back, scrolls it on screen, and invalidates
	 * the necessary areas for redrawing.
	 * 
	 * If the caret is already on the first character, nothing will happen.
	 */
	public void moveCaretLeft() {
		_fieldController.moveCaretLeft();
	}
	
	/**
	 * Sets the caret one position forward, scrolls it on screen, and 
	 * invalidates the necessary areas for redrawing.
	 * 
	 * If the caret is already on the last character, nothing will happen.
	 */
	public void moveCaretRight() {
		_fieldController.moveCaretRight();
	}
	
	/**
	 * Sets the caret one row down, scrolls it on screen, and invalidates the
	 * necessary areas for redrawing.
	 * 
	 * If the caret is already on the last row, nothing will happen.
	 */
	public void moveCaretDown() {
		_fieldController.moveCaretDown();
	}
	
	/**
	 * Sets the caret one row up, scrolls it on screen, and invalidates the
	 * necessary areas for redrawing.
	 * 
	 * If the caret is already on the first row, nothing will happen.
	 */
	public void moveCaretUp() {
		_fieldController.moveCaretUp();
	}

	/**
	 * Scrolls the caret into view if it is not on screen
	 */
	public void focusCaret() {
		makeCharVisible(_caretPosition);
	}

	/**
	 * @return The column number where charOffset appears on
	 */
	protected int getColumn(int charOffset){
		int row = _hDoc.getRowIndex(charOffset);
		TextWarriorException.assertVerbose(row >= 0,
 			"Invalid char offset given to getColumn");
		int firstCharOfRow = _hDoc.getStartCharOfRow(row);
		return charOffset - firstCharOfRow;
	}

	final protected boolean caretOnFirstRowOfFile(){
		return (_caretRow == 0);
	}

	final protected boolean caretOnLastRowOfFile(){
		return (_caretRow == (_hDoc.rowCount()-1));
	}
  
	final protected boolean caretOnEOF(){
		return (_caretPosition == (_hDoc.docLength()-1));
	}


	//---------------------------------------------------------------------
	//------------------------- Text Selection ----------------------------

	public final boolean isSelectText(){
		return _fieldController.isSelectText();
	}
	
	/**
	 * Enter or exit select mode.
	 * Invalidates necessary areas for repainting.
	 * 
	 * @param mode If true, enter select mode; else exit select mode
	 */
	public void selectText(boolean mode){
		if(_fieldController.isSelectText() && !mode){
			invalidateSelectionRows();
			_fieldController.setSelectText(false);
		}
		else if(!_fieldController.isSelectText() && mode){
			invalidateCaretRow();
			_fieldController.setSelectText(true);
		}
	}
	
	public void selectAll(){
		_fieldController.setSelectionRange(0, _hDoc.docLength()-1, false);
	}

	public void setSelectionRange(int beginPosition, int numChars){
		_fieldController.setSelectionRange(beginPosition, numChars, true);
	}
	
	public boolean inSelectionRange(int charOffset){
		return _fieldController.inSelectionRange(charOffset);
	}
	
	public int getSelectionStart(){
		return _selectionAnchor;
	}

	public int getSelectionEnd(){
		return _selectionEdge;
	}
	
	public void focusSelectionStart(){
		_fieldController.focusSelection(true);
	}

	public void focusSelectionEnd(){
		_fieldController.focusSelection(false);
	}
	
	public void cut(ClipboardManager cb) {
		_fieldController.cut(cb);
	}

	public void copy(ClipboardManager cb) {
		_fieldController.copy(cb);
	}
	
	public void paste(String text) {
		_fieldController.paste(text);
	}
	
	//---------------------------------------------------------------------
	//------------------------- Formatting methods ------------------------
	
	private boolean reachedNextSpan(int charIndex, Pair span){
		return (span == null) ? false : (charIndex == span.getFirst());
	}

	public void respan() {
		_fieldController.determineSpans();
	}
	
	public void cancelSpanning() {
		_fieldController.cancelSpanning();
	}

	/**
	 * Sets the text to use the new typeface, scrolls the view to display the 
	 * caret if needed, and invalidates the entire view
	 */
	public void setTypeface(Typeface typeface) {
		_brush.setTypeface(typeface);
		if(!makeCharVisible(_caretPosition)){
			invalidate();
		}
	}
	
	/**
	 * Sets the text size to be factor of the base text size, scrolls the view 
	 * to display the caret if needed, and invalidates the entire view
	 */
	public void setZoom(float factor){
		if(factor <= 0){
			return;
		}
		
		int newSize = (int) (factor * BASE_TEXT_SIZE_PIXELS);
		_brush.setTextSize(newSize);
		if(!makeCharVisible(_caretPosition)){
			invalidate();
		}
	}
	
	/**
	 * Sets the length of a tab character, scrolls the view to display the 
	 * caret if needed, and invalidates the entire view
	 * 
	 * @param spaceCount The number of spaces a tab represents
	 */
	public void setTabSpaces(int spaceCount){
		if(spaceCount < 0){
			return;
		}
		
		_tabLength = spaceCount;
		if(!makeCharVisible(_caretPosition)){
			invalidate();
		}
	}
	
	//---------------------------------------------------------------------
	//------------------------- Event handlers ----------------------------
	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event){
		// Let the touch navigation method intercept the key event first
		if(_navMethod.onKeyDown(keyCode, event)){
			return true;
		}
		
    	if (KeysInterpreter.isNavigationKey(event)){
    		if(event.isShiftPressed() && !isSelectText()){
				invalidateCaretRow();
    			_fieldController.setSelectText(true);
    		}
    		else if(!event.isShiftPressed() && isSelectText()){
				invalidateSelectionRows();
    			_fieldController.setSelectText(false);
    		}

    		switch(keyCode){
    		case KeyEvent.KEYCODE_DPAD_RIGHT:
    			_fieldController.moveCaretRight();
    			break;
    		case KeyEvent.KEYCODE_DPAD_LEFT:
    			_fieldController.moveCaretLeft();
    			break;
    		case KeyEvent.KEYCODE_DPAD_DOWN:
    			_fieldController.moveCaretDown();
    			break;
    		case KeyEvent.KEYCODE_DPAD_UP:
    			_fieldController.moveCaretUp();
    			break;
    		default:
    			break;
    		}
			//event.startTracking(); // let IME receive long press events
    		return true;
        }
    	
    	char c = KeysInterpreter.keyEventToPrintableChar(event);
		if(c != LanguageCFamily.NULL_CHAR){
			_fieldController.onPrintableChar(c, event.getEventTime());
			//event.startTracking(); // let IME receive long press events
			return true;
		}

    	return super.onKeyDown(keyCode, event);
	}

	@Override
	public boolean onKeyUp(int keyCode, KeyEvent event) {
		if(_navMethod.onKeyUp(keyCode, event)){
			return true;
		}
		
		return super.onKeyUp(keyCode, event);
	}

	@Override
	public boolean onTrackballEvent(MotionEvent event) {
		// TODO Test on real device
		int deltaX = Math.round(event.getX());
		int deltaY = Math.round(event.getY());
		while(deltaX > 0){
			_fieldController.moveCaretRight();
			--deltaX;
		}
		while(deltaX < 0){
			_fieldController.moveCaretLeft();
			++deltaX;
		}
		while(deltaY > 0){
			_fieldController.moveCaretDown();
			--deltaY;
		}
		while(deltaY < 0){
			_fieldController.moveCaretUp();
			++deltaY;
		}
		return true;
	}

	@Override
	public boolean onTouchEvent(MotionEvent event){
		if(isFocused()){
			_navMethod.onTouchEvent(event);
		}
		else{
			if((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP
					&& isInView((int) event.getX(), (int) event.getY())){
			// somehow, the framework does not automatically change the focus
			// to this view when it is touched
				requestFocus();
			}
		}
		return true;
	}

	final private boolean isInView(int x, int y) {
		return (x >= 0 && x < getWidth() &&
				y >= 0 && y < getHeight());
	}

	@Override
	protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
		super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
		invalidateCaretRow();
	}


	public void showIME(boolean show){
		InputMethodManager im = (InputMethodManager) getContext()
				.getSystemService(Context.INPUT_METHOD_SERVICE);
		if(show){
			im.showSoftInput(this, 0);
		}
		else{
			im.hideSoftInputFromWindow(this.getWindowToken(), 0); 
		}
	}


	/**
	 * Some navigation methods use sensors or have states for their widgets.
	 * They should be notified of application lifecycle events so they can
	 * start/stop sensing and load/store their GUI state.
	 * 
	 * Not public. Should only be called by {@link TextWarriorApplication}
	 */
	void onPause() {
		_navMethod.onPause();
	}

	void onResume() {
		_navMethod.onResume();
	}
	
	void onDestroy() {
		_fieldController.cancelSpanning();
	}
	
	//*********************************************************************
    //************************ Controller logic ***************************
    //*********************************************************************

	private class TextFieldController
	implements Lexer.LexCallback{
		private boolean _isInSelectionMode = false;
		private Lexer _lexer = new Lexer(this);

		/**
		 * Analyze the text for programming language keywords and redraws the
		 * text view when done. The global programming language used is set with
		 * the static method Lexer.setLanguage(Language)
		 * 
		 * Does nothing if the Lexer language is not a programming language
		 */
		public void determineSpans() {
			_lexer.tokenize(_hDoc);
		}
		
		public void cancelSpanning() {
			_lexer.cancelTokenize();
		}
		
		@Override
		//This is usually called from a non-UI thread
		public void lexDone(final List<Pair> results) {
			post(new Runnable(){
				public void run(){
					_hDoc.setSpans(results);
					invalidate();
				}
			});
		}

	    //- TextFieldController -----------------------------------------------
	    //---------------------------- Key presses ----------------------------
		
		//TODO minimise invalidate calls from moveCaretXX() and deletion
		public void onPrintableChar(char c, long eventTime) {
			// delete currently selected text, if any
			boolean selectionDeleted = false;
			if(_isInSelectionMode){
				selectionDelete();
				selectionDeleted = true;
			}

			switch(c){
			case LanguageCFamily.BACKSPACE:
				if(selectionDeleted){
					break;
				}
				
				if (_caretPosition > 0){
					moveCaretLeft();
					char deleted = _hDoc.charAt(_caretPosition);
					_hDoc.deleteAt(_caretPosition, System.nanoTime());

					if (deleted == LanguageCFamily.NEWLINE){
						// mark rest of screen from caret for repainting
						invalidateFromRow(_caretRow);
					}
				}
				break;
				
			case LanguageCFamily.NEWLINE:
				// mark rest of screen from caret for repainting
				invalidateFromRow(_caretRow);
			//fall-through
				
			default:
				_hDoc.insertBefore(c, _caretPosition, System.nanoTime());
				moveCaretRight();
				break;
			}

			setDirty(true);
			determineSpans();
		}

		public void moveCaretDown(){
	    	if (!caretOnLastRowOfFile()){
	    		int currCaret = _caretPosition;
	    		int currRow = _caretRow;
	    		int newRow = currRow + 1;
	    		int currColumn = getColumn(currCaret);
	    		int currRowLength = _hDoc.rowLength(currRow);
	    		int newRowLength = _hDoc.rowLength(newRow);
	    		
	    		if (currColumn < newRowLength){
		    		// Position at the same column as old row.
		    		_caretPosition += currRowLength;
		    	}
		    	else{
		    		// Column does not exist in the new row (new row is too short).
		    		// Position at end of new row instead.
		    		_caretPosition +=
		    			currRowLength - currColumn + newRowLength - 1;
		    	}
	    		++_caretRow;

	    		updateSelectionRange(currCaret, _caretPosition);
	    		if (!makeCharVisible(_caretPosition)){
	    			invalidateRows(currRow, newRow + 1);
	    		}
	    		_rowLis.onRowChange(newRow);
	    		stopTextComposing();
	    	}
	    }

		public void moveCaretUp(){
	    	if (!caretOnFirstRowOfFile()){
	    		int currCaret = _caretPosition;
	    		int currRow = _caretRow;
	    		int newRow = currRow - 1;
	    		int currColumn = getColumn(currCaret);
	    		int newRowLength = _hDoc.rowLength(newRow);

		    	if (currColumn < newRowLength){
		    		// Position at the same column as old row.
		    		_caretPosition -= newRowLength;
		    	}
		    	else{
		    		// Column does not exist in the new row (new row is too short).
		    		// Position at end of new row instead.
		    		_caretPosition -= (currColumn + 1);
		    	}
	    		--_caretRow;

	    		updateSelectionRange(currCaret, _caretPosition);
	    		if (!makeCharVisible(_caretPosition)){
	    			invalidateRows(newRow, currRow + 1);
	    		}
	    		_rowLis.onRowChange(newRow);
	    		stopTextComposing();
	    	}
	    }

		public void moveCaretRight(){
	    	if(!caretOnEOF()){
	    		int currRow = _caretRow;
	    		++_caretPosition;
	    		int newRow = determineCaretRow();
	    		if(currRow != newRow){
	    			_caretRow = newRow;
	    			_rowLis.onRowChange(newRow);
	    		}
	    		updateSelectionRange(_caretPosition-1, _caretPosition);
	    		if (!makeCharVisible(_caretPosition)){
	    			invalidateRows(currRow, newRow + 1);
	    		}
	    		stopTextComposing();
	    	}
	    }

		public void moveCaretLeft(){
	    	if(_caretPosition > 0){
	    		int currRow = _caretRow;
	    		--_caretPosition;
	    		int newRow = determineCaretRow();
	    		if(currRow != newRow){
	    			_caretRow = newRow;
	    			_rowLis.onRowChange(newRow);
	    		}
	    		updateSelectionRange(_caretPosition+1, _caretPosition);
	    		if (!makeCharVisible(_caretPosition)){
	    			invalidateRows(newRow, currRow + 1);
	    		}
	    		stopTextComposing();
	    	}
	    }

		public void moveCaret(int i) {
			if(i < 0 || i >= _hDoc.docLength()){
				TextWarriorException.assertVerbose(false,
					"Invalid caret position");
				return;
			}

    		updateSelectionRange(_caretPosition, i);
			_caretPosition = i;
			updateAfterCaretJump();
		}

		private void updateAfterCaretJump() {
			int oldRow = _caretRow;
			_caretRow = determineCaretRow();

			if (oldRow != _caretRow){
				_rowLis.onRowChange(_caretRow);
			}
			if(!makeCharVisible(_caretPosition)){
				invalidateRows(oldRow, oldRow+1);
				invalidateCaretRow();
			}
			stopTextComposing();
		}

		private void stopTextComposing(){
			InputMethodManager im = (InputMethodManager) getContext()
				.getSystemService(Context.INPUT_METHOD_SERVICE);
			// This is an overkill way to inform the InputMethod that the caret
			// might have changed position and it should re-evaluate the
			// caps mode to use.
			im.restartInput(FreeScrollingTextField.this);
			
			if(_inputConnection != null && _inputConnection.isComposingStarted()){
				_inputConnection.resetComposingState();
			}
		}
		
	    //- TextFieldController -----------------------------------------------
	    //-------------------------- Selection mode ---------------------------
		public final boolean isSelectText(){
			return _isInSelectionMode;
		}
		
		/**
		 * Enter or exit select mode.
		 * Does not invalidate view.
		 * 
		 * @param mode If true, enter select mode; else exit select mode
		 */
		public void setSelectText(boolean mode){
			if(!(mode ^ _isInSelectionMode)){
				return;
			}

			if(mode){
				_selectionAnchor = _caretPosition;
				_selectionEdge = _caretPosition;
			}
			else{
				_selectionAnchor = -1;
				_selectionEdge = -1;
			}
			_isInSelectionMode = mode;
			_selModeLis.onSelectionModeChanged(mode);
		}

		public boolean inSelectionRange(int charOffset){
			if(_selectionAnchor < 0){
				return false;
			}
		
			return (_selectionAnchor <= charOffset &&
					charOffset < _selectionEdge);
		}
	    
		/**
		 * Selects numChars count of characters starting from beginPosition.
		 * Invalidates necessary areas.
		 * 
		 * @param beginPosition
		 * @param numChars
		 * @param scrollToStart If true, the start of the selection will be scrolled
		 * into view. Otherwise, the end of the selection will be scrolled.
		 */
		public void setSelectionRange(int beginPosition, int numChars,
				boolean scrollToStart){
			TextWarriorException.assertVerbose(
				(beginPosition >= 0) && numChars <= (_hDoc.docLength()-1) && numChars >= 0,
			 	"Invalid range to select");

			if(_isInSelectionMode){
				// unhighlight previous selection
				invalidateSelectionRows();
			}
			else{
				// unhighlight caret
				invalidateCaretRow();
				setSelectText(true);
			}

			_selectionAnchor = beginPosition;
			_selectionEdge = _selectionAnchor + numChars;
			
			_caretPosition = _selectionEdge;
			stopTextComposing();
			int newRow = determineCaretRow();
			if(newRow != _caretRow){
				_caretRow = newRow;
				_rowLis.onRowChange(newRow);
			}
			
			boolean scrolled = makeCharVisible(_selectionEdge);

			if(scrollToStart){
				//TODO reduce unnecessary scrolling and write a method to scroll
				// the beginning of multi-line selections as far left as possible
				scrolled = makeCharVisible(_selectionAnchor);
			}


			if (!scrolled){
				invalidateSelectionRows();
			}
		}

		/**
		 * Moves the caret to an edge of selected text and scrolls it to view.
		 * 
		 * @param beginning If true, moves the caret to the beginning of
		 * the selection. Otherwise, moves the caret to the end of the selection.
		 * In all cases, the caret is scrolled to view if it is not visible. 
		 */
		public void focusSelection(boolean start){
			if(_isInSelectionMode){
				if(start && _caretPosition != _selectionAnchor){
					_caretPosition = _selectionAnchor;
					updateAfterCaretJump();
				}
				else if(!start &&_caretPosition != _selectionEdge){
					_caretPosition = _selectionEdge;
					updateAfterCaretJump();
				}
			}
		}

		
	    /**
	     * Used by internal methods to update selection boundaries when a new 
	     * caret position is set.
	     * Does nothing if not in selection mode.
	     */
	    private void updateSelectionRange(int oldCaretPosition, int newCaretPosition){
	    	if (!_isInSelectionMode){
	    		return;
	    	}

	    	if (oldCaretPosition < _selectionEdge){
	    		if(newCaretPosition > _selectionEdge){
	    			_selectionAnchor = _selectionEdge;
	    			_selectionEdge = newCaretPosition;
	    		}
	    		else{
	    			_selectionAnchor = newCaretPosition;
	    		}
	    		
	    	}
	    	else{
	    		if(newCaretPosition < _selectionAnchor){
	    			_selectionEdge = _selectionAnchor;
	    			_selectionAnchor = newCaretPosition;
	    		}
	    		else{
	    			_selectionEdge = newCaretPosition;
	    		}
	    	}
		}
	    
		
	    //- TextFieldController -----------------------------------------------
	    //------------------------ Cut, copy, paste ---------------------------

		/**
		 * Convenience method for consecutive copy and paste calls
		 */
		public void cut(ClipboardManager cb) {
			copy(cb);
			selectionDelete();
		}
		
		/**
		 * Copies the selected text to the clipboard.
		 * 
		 * Does nothing if not in select mode.
		 */
		public void copy(ClipboardManager cb) {
			//TODO catch OutOfMemoryError
			if(_isInSelectionMode &&
					_selectionAnchor < _selectionEdge){
				char[] contents = _hDoc.subSequence(_selectionAnchor,
					_selectionEdge - _selectionAnchor);
				cb.setText(new String(contents));
			}
		}
		
		/**
		 * Inserts text at the caret position.
		 * Existing selected text will be deleted and select mode will end.
		 * The deleted area will be invalidated.
		 * 
		 * After insertion, the inserted area will be invalidated.
		 */
		public void paste(String text){
			if(text == null){
				return;
			}
			
			_hDoc.beginBatchEdit();
			selectionDelete();
			int originalRow = _caretRow;
			_hDoc.insertBefore(text.toCharArray(), _caretPosition, System.nanoTime());
			_hDoc.endBatchEdit();
			
			_caretPosition += text.length();
			int newRow = determineCaretRow();
			if(newRow != originalRow){
				_caretRow = newRow;
				_rowLis.onRowChange(newRow);
			}

			setDirty(true);
			determineSpans();
			stopTextComposing();

			if(!makeCharVisible(_caretPosition)){
				if(originalRow == newRow){
					//pasted text only affects current row
					invalidateRows(originalRow, newRow+1);
				}
				else{
					invalidateFromRow(originalRow);
				}
			}
		}
		
		/**
		 * Deletes selected text, exits select mode and invalidates deleted area.
		 * If the selected range is empty, this method exits select mode and
		 * invalidates the caret.
		 * 
		 * Does nothing if not in select mode.
		 */
		public void selectionDelete(){
			if(!_isInSelectionMode){
				return;
			}
			
			int totalChars = _selectionEdge - _selectionAnchor;

			if(totalChars > 0){
				int newRow = _hDoc.getRowIndex(_selectionAnchor);
				boolean isSingleRowSel = _hDoc.getRowIndex(_selectionEdge) == newRow;
				_hDoc.deleteAt(_selectionAnchor, totalChars, System.nanoTime());

				_caretPosition = _selectionAnchor;
				if(newRow != _caretRow){
					_caretRow = newRow;
					_rowLis.onRowChange(newRow);
				}
				setDirty(true);
				determineSpans();
				setSelectText(false);
				stopTextComposing();

				if(!makeCharVisible(_caretPosition)){
					if(isSingleRowSel){
						invalidateRows(newRow, newRow+1);
					}
					else{
						invalidateFromRow(newRow);
					}
				}
			}
			else{
				setSelectText(false);
				invalidateCaretRow();
			}
		}
		

	    //- TextFieldController -----------------------------------------------
	    //----------------- Helper methods for InputConnection ----------------
		 
		/**
		 * Deletes existing selected text, then deletes charCount number of 
		 * characters starting at from, and inserts text in its place.
		 * 
		 * Unlike paste or selectionDelete, does not signal the end of
		 * text composing to the IME.
		 */
		void replaceComposingText(int from, int charCount, String text){
			int startInvalidateRow = _caretRow;
			boolean invalidateSingleRow = true;
			boolean dirty = false;

			//delete selection
			if(_isInSelectionMode){
				int totalChars = _selectionEdge - _selectionAnchor;

				if(totalChars > 0){
					_hDoc.deleteAt(_selectionAnchor, totalChars, System.nanoTime());
					_caretPosition = _selectionAnchor;
					int newRow = determineCaretRow();
					if(newRow < startInvalidateRow){
						startInvalidateRow = newRow;
						invalidateSingleRow = false;
					}
					dirty = true;
				}

				setSelectText(false);
			}

			//delete requested chars
			if(charCount > 0){
				_hDoc.deleteAt(from, charCount, System.nanoTime());
				_caretPosition = from;
				int newRow = determineCaretRow();
				if(newRow < startInvalidateRow){
					startInvalidateRow = newRow;
					invalidateSingleRow = false;
				}
				dirty = true;
			}

			//insert
			if(text != null && text.length() > 0){
				_hDoc.insertBefore(text.toCharArray(), _caretPosition, System.nanoTime());
				_caretPosition += text.length();
				dirty = true;
			}
			
			if(dirty){
				setDirty(true);
				determineSpans();
			}

			int newRow = determineCaretRow();
			if(newRow != _caretRow){
				_caretRow = newRow;
				_rowLis.onRowChange(newRow);
				invalidateSingleRow = false;
			}

			if(!makeCharVisible(_caretPosition)){
				if(invalidateSingleRow){
					invalidateRows(newRow, newRow+1);
				}
				else{
					invalidateFromRow(startInvalidateRow);
				}
			}
		}
		
		/**
		 * Delete leftLength characters of text before the current caret
		 * position, and delete rightLength characters of text after the current 
		 * cursor position.
		 * 
		 * Unlike paste or selectionDelete, does not signal the end of
		 * text composing to the IME.
		 */
		void deleteAroundComposingText(int left, int right){
			int start = _caretPosition - left;
			if(start < 0){
				start = 0;
			}
			int end = _caretPosition + right;
			if(end > (_hDoc.docLength()-1)){ //don't include terminal EOF
				end = _hDoc.docLength()-1;
			}
			replaceComposingText(start, end-start, "");
		}

		String getTextAfterCursor(int maxLen){
			return new String(_hDoc.subSequence(_caretPosition, maxLen));
		}

		String getTextBeforeCursor(int maxLen){
			int start = _caretPosition - maxLen;
			if(start < 0){
				start = 0;
			}
			return new String(_hDoc.subSequence(start, _caretPosition-start));
		}
	}//end inner controller class





	//*********************************************************************
    //************************** InputConnection **************************
    //*********************************************************************
	/*
	 * Does not provide ExtractedText related methods
	 */
	private class TextFieldInputConnection extends BaseInputConnection{
		private boolean _isComposing = false;
		private int _composingCharCount = 0;

		public TextFieldInputConnection(FreeScrollingTextField v){
			super(v, true);
		}
		
		public void resetComposingState(){
			_composingCharCount = 0;
			_isComposing = false;
			_hDoc.endBatchEdit();
		}

		/**
		 * Only true when the InputConnection has not been used by the IME yet.
		 * Can be programatically cleared by resetComposingState()
		 */
		public boolean isComposingStarted() {
			return _isComposing;
		}

		@Override
		public boolean setComposingText(CharSequence text, int newCursorPosition){
			_isComposing = true;
			if(!_hDoc.isBatchEdit()){
				_hDoc.beginBatchEdit();
			}
			
			_fieldController.replaceComposingText(
					getCaretPosition() - _composingCharCount,
					_composingCharCount,
					text.toString());
			_composingCharCount = text.length();
			
			//TODO reduce invalidate calls
			if(newCursorPosition > 1){
				_fieldController.moveCaret(_caretPosition + newCursorPosition - 1);
			}
			else if (newCursorPosition <= 0){
				_fieldController.moveCaret(_caretPosition - text.length() - newCursorPosition);
			}
			return true;
		}
		
		@Override
		public boolean commitText(CharSequence text, int newCursorPosition) {
			_isComposing = true;
			_fieldController.replaceComposingText(
					getCaretPosition() - _composingCharCount,
					_composingCharCount,
					text.toString());
			_composingCharCount = 0;
			_hDoc.endBatchEdit();

			//TODO reduce invalidate calls
			if(newCursorPosition > 1){
				_fieldController.moveCaret(_caretPosition + newCursorPosition - 1);
			}
			else if (newCursorPosition <= 0){
				_fieldController.moveCaret(_caretPosition - text.length() - newCursorPosition);
			}
			return true;
		}


		@Override
		public boolean deleteSurroundingText(int leftLength, int rightLength){
			if(_composingCharCount != 0){
				Log.d(TextWarriorApplication.LOG_TAG,
						"Warning: Implmentation of InputConnection.deleteSurroundingText" +
						" will not skip composing text");
			}
			
			_fieldController.deleteAroundComposingText(leftLength, rightLength);
			return true;
		}

		@Override
		public boolean finishComposingText() {
			resetComposingState();
			return true;
		}

		@Override
		public int getCursorCapsMode(int reqModes) {
			int capsMode = 0;

			// Ignore InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; not used in TextWarrior
			
			if((reqModes & InputType.TYPE_TEXT_FLAG_CAP_WORDS)
					== InputType.TYPE_TEXT_FLAG_CAP_WORDS){
				int prevChar = _caretPosition - 1;
				if(prevChar < 0 || Lexer.getLanguage().isWhitespace(_hDoc.charAt(prevChar)) ){
					capsMode |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
					
					//set CAP_SENTENCES if client is interested in it
					if((reqModes & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
							== InputType.TYPE_TEXT_FLAG_CAP_SENTENCES){
						capsMode |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
					}
				}
			}
			
			// Strangely, Android soft keyboard does not set TYPE_TEXT_FLAG_CAP_SENTENCES
			// in reqModes even if it is interested in doing auto-capitalization.
			// Android bug? Therefore, we assume TYPE_TEXT_FLAG_CAP_SENTENCES
			// is always set to be on the safe side.
			else {
				LanguageCFamily lang = Lexer.getLanguage();

				int prevChar = _caretPosition - 1;
				int whitespaceCount = 0;
				boolean capsOn = true;

				// Turn on caps mode only for the first char of a sentence.
				// A fresh line is also considered to start a new sentence.
				// The position immediately after a period is considered lower-case.
				// Examples: "abc.com" but "abc. Com"
				while(prevChar >= 0){
					char c = _hDoc.charAt(prevChar);
					if(c == LanguageCFamily.NEWLINE){
						break;
					}
					
					if(!lang.isWhitespace(c)){
						if(whitespaceCount == 0 || !lang.isSentenceTerminator(c) ){
							capsOn = false;
						}
						break;
					}
					
					++whitespaceCount;
					--prevChar;
				}
				
				if(capsOn){
					capsMode |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
				}
			}
			
			return capsMode;
		}

		@Override
		public CharSequence getTextAfterCursor(int maxLen, int flags) {
			return _fieldController.getTextAfterCursor(maxLen); //ignore flags
		}

		@Override
		public CharSequence getTextBeforeCursor(int maxLen, int flags) {
			return _fieldController.getTextBeforeCursor(maxLen); //ignore flags
		}

		@Override
		public boolean setSelection(int start, int end) {
			if(start == end){
				_fieldController.moveCaret(start);
			}
			else{
				_fieldController.setSelectionRange(start, end-start, false);
			}
			return true;
		}
		
	}// end inner class


}


<|start_filename|>src/com/myopicmobile/textwarrior/common/ReadThread.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.common;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;


public class ReadThread extends FileIOThread{
	final protected TextBuffer _buf;
	
	//TODO use DocumentProvider instead of TextBuffer
	public ReadThread(File file, TextBuffer buf,
			String encoding, String EOLchar){
		super(file, encoding, EOLchar);
		_buf = buf;
	}
	
	public void run(){
		_isDone = false;
		_abortFlag.clear();

		try{
			realRead();
		}
		catch(OutOfMemoryError e){
			broadcastError(ProgressSource.READ,
					ERROR_OUT_OF_MEMORY, _outOfMemoryMsg); //TODO localise message
			/* useful memory statistics
			long freeMem = Runtime.getRuntime().freeMemory();
			long max = 1000000 * ((ActivityManager) getSystemService(ACTIVITY_SERVICE)).getMemoryClass();
			long totalMem = Runtime.getRuntime().totalMemory();
			long maxMem = Runtime.getRuntime().maxMemory();
			*/
		}
		catch (IOException ex) {
			broadcastError(ProgressSource.READ,
					ERROR_UNKNOWN, ex.getLocalizedMessage());
	    }
	}

	private void realRead() throws IOException{
		FileInputStream fs = new FileInputStream(_file);
		
		try{
			detectEncodingAndEOL();
			char[] newBuffer = allocateBuffer();

			Pair statistics = _converter.readAndConvert(fs, newBuffer,
					_encoding, _EOLchar, _abortFlag);

            if(!_abortFlag.isSet()){
            	_buf.setBuffer(newBuffer,
            			_encoding,
            			_EOLchar,
            			statistics.getFirst(),
            			statistics.getSecond());
				_isDone = true;
            	broadcastComplete(ProgressSource.READ);
            }
            else{
            	broadcastCancel(ProgressSource.READ);
            }
		}
		finally{
			fs.close();
		}
	}

	// throws OutOfMemoryError if there is not enough memory or
	// total characters > Integer.MAX_VALUE
	private char[] allocateBuffer(){
		TextWarriorException.assertVerbose(
				!_encoding.equals(EncodingScheme.TEXT_ENCODING_AUTO),
				"AUTO encoding not yet resolved");
		TextWarriorException.assertVerbose(
				!_EOLchar.equals(EncodingScheme.LINE_BREAK_AUTO),
				"AUTO line break terminator not yet resolved");

		long textLength = _file.length();
		if (_encoding.equals(EncodingScheme.TEXT_ENCODING_UTF16BE) ||
				_encoding.equals(EncodingScheme.TEXT_ENCODING_UTF16LE)){
			textLength >>>= 1; // 2 bytes in the file == 1 char
		}
		if(textLength > Integer.MAX_VALUE){
			throw new OutOfMemoryError();
		}

		int implSize = TextBuffer.memoryNeeded((int) textLength);
		if(implSize == -1){
			throw new OutOfMemoryError();
		}

		_totalChar = (int) textLength;
		return new char[implSize];
	}

	private void detectEncodingAndEOL() throws IOException{
		if (_encoding.equals(EncodingScheme.TEXT_ENCODING_AUTO)){
			_encoding = _converter.getEncodingScheme(_file);
		}
		if(_EOLchar.equals(EncodingScheme.LINE_BREAK_AUTO)){
			_EOLchar = _converter.getEOLType(_file, _encoding);
		}
	}

	@Override
	public int getMax(){
		return MAX_PROGRESS;
	}

	@Override
	public int getCurrent(){
		double progressProportion = (_totalChar == 0) ? 0 :
			(double) _converter.getProgress() / (double) _totalChar;
		return (int) (progressProportion * MAX_PROGRESS);
	}
	
	
	private int _totalChar = 0;
	/** reported progress will be scaled from 0 to MAX_PROGRESS */
	private final static int MAX_PROGRESS = 100;
	private static final String _outOfMemoryMsg = "Not enough memory";
}

<|start_filename|>src/com/myopicmobile/textwarrior/common/LanguageJava.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.common;

/**
 * Singleton class containing the symbols and operators of the Java language
 */
public class LanguageJava extends LanguageCFamily{
	private static LanguageCFamily _theOne = null;
	
	private final static String[] keywords = {
		"void", "boolean", "byte", "char", "short", "int", "long", "float", "double", "strictfp",
		"import", "package", "new", "class", "interface", "extends", "implements", "enum",
		"public", "private", "protected", "static", "abstract", "final", "native", "volatile",
		"assert", "try", "throw", "throws", "catch", "finally", "instanceof", "super", "this",
		"if", "else", "for", "do", "while", "switch", "case", "default",
		"continue", "break", "return", "synchronized", "transient",
		"true", "false", "null"
		};


	public static LanguageCFamily getCharacterEncodings(){
		if(_theOne == null){
			_theOne = new LanguageJava();
		}
		return _theOne;
	}
	
	private LanguageJava(){
		super.registerKeywords(keywords);
	}
	
	/**
	 * Java has no preprocessors. Override base class implementation
	 */
	public boolean isLineAStart(char c){
		return false;
	}
}


<|start_filename|>src/com/myopicmobile/textwarrior/common/FileIOThread.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.common;

import java.io.File;
import java.util.Vector;


public abstract class FileIOThread extends Thread implements ProgressSource{
	final protected File _file;
	protected String _encoding;
	protected String _EOLchar;
	protected Flag _abortFlag;
	protected boolean _isDone = false;
	protected Vector<ProgressObserver> _progressObservers;
	protected CharEncodingUtils _converter = new CharEncodingUtils();

	public FileIOThread(File file, String encoding, String EOLchar){
		_file = file;
		_encoding = encoding;
		_EOLchar = EOLchar;
        _abortFlag = new Flag();
        _progressObservers = new Vector<ProgressObserver>();
	}

	@Override
	public final void forceStop(){
		if(!_abortFlag.isSet()){
			_abortFlag.set();
		}
	}

	@Override
	synchronized public final void registerObserver(ProgressObserver po){
		_progressObservers.addElement(po);
	}

	@Override
	synchronized public final void removeObservers(){
		_progressObservers.clear();
	}

	synchronized protected void broadcastComplete(int requestCode){
		for(ProgressObserver po : _progressObservers){
			po.onComplete(requestCode, null);
		}
	}

	synchronized protected void broadcastError(int requestCode,
			int errorCode, final String msg){
		for(ProgressObserver po : _progressObservers){
			po.onError(requestCode, errorCode, msg);
		}
	}
	
	synchronized protected void broadcastCancel(int requestCode){
		for(ProgressObserver po : _progressObservers){
			po.onCancel(requestCode);
		}
	}

	@Override
	public final int getMin(){
		return 0;
	}

	@Override
	public final boolean isDone(){
		return _isDone;
	}
}


<|start_filename|>assets/help_es.html<|end_filename|>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Ayuda de TextWarrior</title>
</head>
<body>
<h3>En construccíon...</h3>

<p>Desgraciadamente, el texto de ayuda en español no está disponible actualmente.</p>

<p><a href="help.html">Muéstrame el texto en inglés.</a></p>

<p>¿Quiere usted ayudarnos con la traducción? Envie un email a 
<a href="mailto:<EMAIL>"><EMAIL></a></p>

</body>
</html>

<|start_filename|>src/com/myopicmobile/textwarrior/android/VolumeKeysNavigationMethod.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */

package com.myopicmobile.textwarrior.android;


import android.view.KeyEvent;

public class VolumeKeysNavigationMethod extends TouchNavigationMethod{
	public VolumeKeysNavigationMethod(FreeScrollingTextField textField){
		super(textField);
	}
	
	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		if(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN){
			_textField.moveCaretLeft();
			return true;
		}
		else if(keyCode == KeyEvent.KEYCODE_VOLUME_UP){
			_textField.moveCaretRight();
			return true;
		}
		
		return false;
	}

	@Override
	public boolean onKeyUp(int keyCode, KeyEvent event) {
		if(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
				keyCode == KeyEvent.KEYCODE_VOLUME_UP){
			return true;
		}

		return false;
	}
}


<|start_filename|>src/com/myopicmobile/textwarrior/android/TextWarriorHelp.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.android;

import android.app.Activity;
import android.content.Intent;
import android.net.MailTo;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class TextWarriorHelp extends Activity{
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
        setContentView(R.layout.help);
        
		WebView contents = (WebView) findViewById(R.id.help_content);
		contents.getSettings().setBuiltInZoomControls(true);
		contents.setWebViewClient(new EmailLinksClient(this));

		contents.loadUrl(determineHelpFile());
	}

	private String determineHelpFile(){
		String lang = getResources().getConfiguration().locale.getLanguage();
        String helpFile;
        
        //I hate hard-coding
        if(lang.equals("fr")){
        	helpFile = "file:///android_asset/help_fr.html";
        }
        else if(lang.equals("es")){
        	helpFile = "file:///android_asset/help_es.html";
        }
        else if(lang.equals("de")){
        	helpFile = "file:///android_asset/help_de.html";
        }
        else if(lang.equals("zh")){
        	String country =  getResources().getConfiguration().locale.getCountry();
        	if(country.equals("TW") || country.equals("HK")){
        		helpFile = "file:///android_asset/help_zh_tw.html";
        	}
        	else{
            	helpFile = "file:///android_asset/help_zh_cn.html";
        	}
        }
        else{
        	helpFile = "file:///android_asset/help.html";
        }
		return helpFile;
	}
	
	
	// mailto links don't work in WebView. This class fixes that.
	private class EmailLinksClient extends WebViewClient {
		private Activity _activity;
		
		public EmailLinksClient(Activity context){
		    _activity = context;
		}
		
		@Override
		public boolean shouldOverrideUrlLoading(WebView view, String url) {     
		    if(url.startsWith(MailTo.MAILTO_SCHEME)){
		        MailTo mt = MailTo.parse(url);
		        Intent i = new Intent(Intent.ACTION_SEND);
		        i.setType("plain/text");
		        i.putExtra(Intent.EXTRA_EMAIL, new String[]{mt.getTo()});
		        i.putExtra(Intent.EXTRA_SUBJECT, mt.getSubject());
		        i.putExtra(Intent.EXTRA_CC, mt.getCc());
		        i.putExtra(Intent.EXTRA_TEXT, mt.getBody());
		        _activity.startActivity(Intent.createChooser(i, null));
		        
		        // Apparently on some devices, the clicked link will fail to
		        // invalidate after being pressed, so refresh the page here
		        view.reload();
		        return true;
		    }
		    view.loadUrl(url);
		    return true;
		}
	}
}


<|start_filename|>assets/help.html<|end_filename|>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>TextWarrior Help</title>
</head>
<body>
<h3>Contents</h3>
<ul>
<li><a href="#navigation">Moving the cursor</a>
<li><a href="#selection">Text Selection</a>
<li><a href="#clipboard">Clipboard</a>
<li><a href="#find">Find / Replace</a>
<li><a href="#keyboard">Keyboard Guide</a>
<li><a href="#settings">Settings</a>
<li><a href="#roadmap">What's New</a>
<li><a href="#issues">Known Issues and Limitations</a>
</ul>

<br>

<h3><a id = "navigation">Moving the cursor</a></h3>
<p>Making text editing easier with a touch screen is a priority of TextWarrior. 
To help with that, there are five methods to choose from for positioning the cursor.</p>
<p>These can be accessed from <i>Settings → Cursor navigation</i>.</p>

<br>

<h4>Basic</h4>
<p>Tap anywhere on the text.</p>
<p>You can also touch the cursor and drag it to the desired position. 
This only works when <i>Settings → Drag select</i> is disabled.</p>
<p>The basic method is always active, even if another navigation method is chosen.</p>

<br>

<h4>Trackpad</h4>
<p>Tap the 4-arrow button at the bottom of the screen to display the trackpad.</p>
<p>To move the cursor, move in the desired direction on the trackpad.</p>
<p>Tap anywhere on the trackpad to hide it again.</p>

<br>

<h4>Yo-yo</h4>
<p>Grab the round handle dangling below the cursor and drag it in the desired direction.</p>

<br>

<h4>Liquid</h4>
<i>Available only on devices with accelerometers.</i>
<p>Press and hold the fire button at the bottom of the screen to activate liquid mode.</p>
<p>In liquid mode, the cursor can be moved by tilting the device in the desired direction.</p>
<ul>
<li>Up: Screen faces the sky
<li>Down: Screen is vertical and faces you. Bottom of the screen points towards the ground.
<li>Left: Screen is tilted to the left
<li>Right: Screen is tilted to the right
<li>Rest (no movement): Screen faces you at a 45 degree angle with the ground
</ul>
<p>Release the fire button to freeze cursor movement and exit liquid mode.</p>
<p>For best results, disable automatic switching of portrait and landscape modes 
on your device. Also, avoid moving the cursor in 2 directions at the same time.</p>
<p><b>Warning:</b> This mode is power-intensive. Watch those battery levels!</p>

<br>

<h4>Volume keys</h4>
<p>Press the volume up/down key to move the cursor to the left/right.</p>
<p>In this mode, the volume keys will no longer control the device volume. However, 
the keys will work normally again when switching to another application.</p>

<br>

<h3><a id = "selection">Text Selection</a></h3>
<h4>Making a selection</h4>
<p>Double-tap anywhere on the text to enter selection mode.</p>
<p>Alternatively, if <i>Settings → Drag select</i> is enabled, simply touch and drag the cursor.</p>
<p>With a hardware keyboard, you can also hold shift and press any direction key.</p>
<p>In selection mode, the cursor will change into a shorter rectangle.</p>
<p><b>Tip:</b> After double-tapping, you can leave your finger on the screen and start changing the selection range immediately.</p>

<h4>Changing the selection range</h4>
<p>Touch and drag either end of the selection to the desired position.</p>
<p>The cursor navigation methods described above can also be used. Tap on the 
beginning or end of the selection to move the cursor there, to make it the target 
of the current navigation method.</p>
<p>With a hardware keyboard, you can also hold shift and press a direction key.</p>
</body>

<h4>Cancel a selection</h4>
<p>To exit selection mode, tap any unselected area.</p>
<p>You can also double-tap on the selected text.</p>
<p>With a hardware keyboard, you can also press any direction key.</p>
</body>

<h4>Selection properties</h4>
<p>The <i>Properties</i> option available from the main menu shows statistics related to 
the selection like word and character count. If there is no selection, statistics for the 
entire file will be shown instead.</p>
</body>

<br>

<h3><a id = "clipboard">Clipboard</a></h3>
<p>The arrow tab at the top right corner of the screen opens the clipboard.</p>
<p>Tap or drag it down to display the clipboard.</p>
<p>Tap the arrow again or drag it up to close the clipboard. 
The clipboard can also be closed by flicking it upwards.</p>

<br>

<h3><a id = "find">Find / Replace</a></h3>
<h4>Display the find and replace bars</h4>
<p>Toggle the display of the find bar by selecting "Find" on the menu.</p>
<p>Alternatively, press the "Search" button on your device if it has one.</p>
<p>The replace bar is normally hidden from view. 
To display it, click the double arrows on the left of the search text box. 
Click on it again to hide it.</p>

<h4>Button guide</h4>
<p>Magnifying glass pointing right: Search forwards</p>
<p>Magnifying glass pointing left: Search backwards</p>
<p>Looped arrow: Replace selected text</p>
<p>Multiple looped arrows: Replace all</p>
<p>Gear: Find options</p>

<br>

<h3><a id = "keyboard">Keyboard Guide</a></h3>
The following shortcuts apply to most hardware keyboards.
<p>Shift+Enter: Switch between the find/replace bar and the main text area</p>
<p>Shift+Space: Insert a tab</p>

<br>

<h3><a id = "settings">Settings</a></h3>
<h4>Opening and saving</h4>
<p>If you don't understand the settings in this category, select "Auto".</p>

<br>

<h3><a id = "roadmap">What's New</a></h3>
<h4>v0.71</h4>
<ul>
<li>Added C#, Javascript, Objective-C, PHP, Python and Ruby support
<li>Auto-capitalization of sentences. Accessed through the Android keyboard settings
<li>Bigger trackpad with opening/closing animation
<li>Improved display on very high density screens
<li>Improved handling of force close due to low memory
<li>New icons for copy, replace and trackpad
<li>Bugfix: Undo of inserts causes occasional crash
</ul>

<h4>v0.7</h4>
<ul>
<li>Undo/redo
<li>Added fonts: Bitstream Vera Mono (good for viewing code) and Proggy Clean (good for small text)
<li>New icon
</ul>

<h4>v0.61</h4>
<ul>
<li>Fixed crashes with certain software keyboards on Android 2.3 and above
<li>For volume keys navigation method, removed beep sound after releasing a volume key
</ul>

<h4>v0.6</h4>
<ul>
<li>Added trackpad, yo-yo, liquid and volume keys navigation methods
<li>Added drag select
</ul>

<h4>v0.52</h4>
<ul>
<li>Recent file history
<li>Character and word count
<li>Bugfix: Uneven scrolling at the end of the document
</ul>

<h4>v0.51</h4>
<ul>
<li>Fling scrolling and scrollbars
<li>Localisation for Chinese (Simplified and Traditional)
</ul>

<h4>Roadmap</h4>
<p>Sep/Oct 2011: Second beta release</p>

<br>

<h3><a id = "issues">Known Issues and Limitations</a></h3>
<ul>
<li>No word-wrap
<li>In landscape mode, the virtual keyboard can obscure the cursor
<li>In landscape mode, the text suggestions given by the virtual keyboard is not visible
<li>If there is not enough memory when inserting or copying text, TextWarrior will terminate abruptly without saving
<li>The cut/copy/paste buttons only apply to the main text, not the find and replace text boxes
<li>Syntax highlighting takes a long time when the file is very large
<li>Multi-line string literals are not supported
<li>C#: Verbatim strings are not supported
<li>PHP: Only keywords in all lowercase are recognised. Parameters passed by 
	reference are not colored. Heredoc and Nowdoc are not supported
<li>Files containing a mix of line terminator styles may not open correctly
</ul>

</body>
</html>

<|start_filename|>src/com/myopicmobile/textwarrior/common/WriteThread.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.common;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;


public class WriteThread extends FileIOThread{
	private DocumentProvider _hDoc;
	// reported progress will be scaled from 0 to MAX_PROGRESS
	private final static int MAX_PROGRESS = 100;
	
	public WriteThread(File file, DocumentProvider hDoc,
	String encoding, String EOLchar){
		super(file, encoding, EOLchar);
		_hDoc = hDoc;
		_totalChar = _hDoc.docLength();
		TextWarriorException.assertVerbose(_totalChar > 0,
				 "File to save must have at least 1 char");
	}

	public void run(){
		_isDone = false;
		_abortFlag.clear();
		
		try{
			realWrite();
		}
		catch (IOException ex) {
			broadcastError(ProgressSource.WRITE,
				ERROR_UNKNOWN, ex.getLocalizedMessage());
	    }
	}

	private void realWrite() throws IOException{
		FileOutputStream fs = new FileOutputStream(_file);

		try{
			resolveAutoEncodingAndEOL();
	        _converter.writeAndConvert(fs, _hDoc, _encoding, _EOLchar, _abortFlag);

	        if(!_abortFlag.isSet()){
				_isDone = true;
            	broadcastComplete(ProgressSource.WRITE);
	        }
            else{
            	broadcastCancel(ProgressSource.WRITE);
            }
		}
		finally{
			fs.close();
		}
	}

	private void resolveAutoEncodingAndEOL() {
		if (_encoding.equals(EncodingScheme.TEXT_ENCODING_AUTO)){
			_encoding = _hDoc.getEncodingScheme();
		}
		if(_EOLchar.equals(EncodingScheme.LINE_BREAK_AUTO)){
			_EOLchar = _hDoc.getEOLType();
		}
	}

	@Override
	public int getMax(){
		return MAX_PROGRESS;
	}

	@Override
	public int getCurrent(){
		double progressProportion = (double) _converter.getProgress()
				/ (double) _totalChar;
		return (int) (progressProportion * MAX_PROGRESS);
	}
	
	private int _totalChar = 0;
}


<|start_filename|>src/com/myopicmobile/textwarrior/android/TextWarriorSettings.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.android;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.PreferenceActivity;

import com.myopicmobile.textwarrior.common.EncodingScheme;

public class TextWarriorSettings extends PreferenceActivity {
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		addPreferencesFromResource(R.xml.preference_screen_layout);

		ListPreference inputEncodingPref =
			(ListPreference) findPreference(getString(R.string.settings_key_file_input_format));
		ListPreference outputEncodingPref =
			(ListPreference) findPreference(getString(R.string.settings_key_file_output_format));
		ListPreference lineTerminatorPref =
			(ListPreference) findPreference(getString(R.string.settings_key_line_terminator_style));

		inputEncodingPref.setEntryValues(EncodingScheme.encodingSchemes);
		inputEncodingPref.setEntries(EncodingScheme.encodingSchemesAliases);
		outputEncodingPref.setEntryValues(EncodingScheme.encodingSchemes);
		outputEncodingPref.setEntries(EncodingScheme.encodingSchemesAliases);
		lineTerminatorPref.setEntryValues(EncodingScheme.lineTerminators);
		lineTerminatorPref.setEntries(EncodingScheme.lineTerminatorsAliases);
		
		if(!hasOrientationSensor()){
			removeLiquidNavigationMenuOption();
		}
	}

	private boolean hasOrientationSensor() {
		SensorManager sm = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
		return (sm.getDefaultSensor(Sensor.TYPE_ORIENTATION) != null);
	}

	private void removeLiquidNavigationMenuOption() {
		ListPreference navigationMethodPref =
			(ListPreference) findPreference(getString(R.string.settings_key_navigation_method));
		CharSequence[] navMethods = navigationMethodPref.getEntryValues();
		CharSequence[] navMethodsNames = navigationMethodPref.getEntries();
		
		String liquidNavKey = getString(R.string.settings_navigation_method_liquid);
		CharSequence[] trimmedEntries = new CharSequence[navMethods.length - 1];
		CharSequence[] trimmedEntryValues = new CharSequence[navMethods.length - 1];
		for(int i = 0, j = 0; i < navMethods.length; ++i){
			if(navMethods[i].equals(liquidNavKey)){
				continue;
			}
			trimmedEntryValues[j] = navMethods[i];
			trimmedEntries[j] = navMethodsNames[i];
			++j;
		}
		
		navigationMethodPref.setEntryValues(trimmedEntryValues);
		navigationMethodPref.setEntries(trimmedEntries);
	}
}


<|start_filename|>src/com/myopicmobile/textwarrior/android/YoyoNavigationMethod.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */

package com.myopicmobile.textwarrior.android;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.MotionEvent;

import com.myopicmobile.textwarrior.common.Pair;

public class YoyoNavigationMethod extends TouchNavigationMethod{
	private boolean _isHandleTouched = false;
	private final Yoyo _yoyo = new Yoyo();

	public YoyoNavigationMethod(FreeScrollingTextField textField){
		super(textField);
	}
	
	@Override
	public boolean onDown(MotionEvent e) {
		super.onDown(e);
		if(!_isCaretTouched){
			int x = (int) e.getX() + _textField.getScrollX();
			int y = (int) e.getY() + _textField.getScrollY();
			_isHandleTouched = _yoyo.isInHandle(x, y);
			
			if(_isHandleTouched){
				_yoyo.setInitialTouch(x, y);
				_yoyo.invalidateHandle();
			}
		}
		
		return true;
	}

	@Override
	public boolean onUp(MotionEvent e) {
		if(_isHandleTouched){
			_isHandleTouched = false;
			_yoyo.clearInitialTouch();
			
			//TODO animate the handle to fly back to the caret position
			Rect caret = _textField.getBoundingBox(_textField.getCaretPosition());
			int x = caret.left + _textField.getPaddingLeft();
			int y = caret.bottom + _textField.getPaddingTop();
			_yoyo.attachYoyo(x, y);
		}
		super.onUp(e);
		return true;
	}
	
	@Override
	public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
			float distanceY) {

		if(_isHandleTouched){
			//TODO find out if ACTION_UP events are actually passed to onScroll
			if ((e2.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP){
				onUp(e2);
			}
			else{
				moveHandle(e2);
			}
			
			return true;
		}
		else{
			return super.onScroll(e1, e2, distanceX, distanceY);
		}
	}

	private void moveHandle(MotionEvent e) {
		int handleX = (int) e.getX() + _textField.getScrollX();
		int handleY = (int) e.getY() + _textField.getScrollY();
		
		Pair foundIndex = _yoyo.findNearestChar((int) e.getX(), (int) e.getY());
		int newCaretIndex = foundIndex.getFirst();

		if(newCaretIndex >= 0){
			_textField.moveCaret(newCaretIndex);
			//snap the handle to the caret
			Rect newCaretBounds = _textField.getBoundingBox(newCaretIndex);
			int newX = newCaretBounds.left + _textField.getPaddingLeft();
			int newY = newCaretBounds.bottom + _textField.getPaddingTop();

			boolean isExact = (foundIndex.getSecond() != -1);
			if(isExact){
				_yoyo.attachYoyo(newX, newY);
			}
			else{
				_yoyo.stretchYoyoFromTo(newX, newY, handleX, handleY);
			}
		}
		else{
			//not under a character; freely position the handle
			_yoyo.stretchYoyoTo(handleX, handleY);
		}
	}

	
	@Override
	public boolean onSingleTapConfirmed(MotionEvent e) {
		int x = (int) e.getX() + _textField.getScrollX();
		int y = (int) e.getY() + _textField.getScrollY();
		
		//ignore taps on handle
		if(_yoyo.isInHandle(x, y)){
			return true;
		}
		else{
			return super.onSingleTapConfirmed(e);
		}
	}
	
	@Override
	public boolean onDoubleTap(MotionEvent e) {
		int x = (int) e.getX() + _textField.getScrollX();
		int y = (int) e.getY() + _textField.getScrollY();
		
		//ignore taps on handle
		if(_yoyo.isInHandle(x, y)){
			return true;
		}
		else{
			return super.onDoubleTap(e);
		}
	}

	@Override
	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
			float velocityY) {

		if(_isHandleTouched){
			onUp(e2);
			return true;
		}
		else{
			return super.onFling(e1, e2, velocityX, velocityY);
		}
	}

	@Override
	public void onTextDrawComplete(Canvas canvas) {		
		if(!_isHandleTouched){
			Rect caret = _textField.getBoundingBox(_textField.getCaretPosition());
			int x = caret.left + _textField.getPaddingLeft();
			int y = caret.bottom + _textField.getPaddingTop();

			_yoyo.setRestingCoord(x, y);
		}

		_yoyo.draw(canvas, _isHandleTouched);
	}

	@Override
	public Rect getCaretBloat() {
		return _yoyo.getRestingSize();
	}
	

	private class Yoyo{
		private final static int YOYO_STRING_RESTING_HEIGHT = 28;
		//For those who want to change HANDLE_IMG, ensure that the image is a square,
		//or modify draw() so that the proper vertical center is calculated
		private final Bitmap HANDLE_IMG = BitmapFactory.decodeResource(
				_textField.getContext().getResources(), R.drawable.yoyo);
		private final Bitmap HANDLE_SELECTED_IMG = BitmapFactory.decodeResource(
				_textField.getContext().getResources(), R.drawable.yoyo_selected);
		private final Rect HANDLE_SIZE;

		//coordinates where the top of the yoyo string is attached
		private int _anchorX = 0;
		private int _anchorY = 0;
		
		//coordinates of the top-left corner of the yoyo handle
		private int _handleX = 0;
		private int _handleY = 0;
		
		//the offset where the handle is first touched,
		//(0,0) being the top-left of the handle
		private int _xOffset = 0;
		private int _yOffset = 0;

		private final static int YOYO_HANDLE_ALPHA = 180;
		private final static int YOYO_STRING_COLOR = 0x808080;
		private final Paint _brush;

		public Yoyo(){
			int radius = getRadius();
			HANDLE_SIZE = new Rect(
					radius,
					0,
					HANDLE_IMG.getWidth() - radius,
					HANDLE_IMG.getHeight() + YOYO_STRING_RESTING_HEIGHT);
			
			_brush = new Paint();
			_brush.setColor(YOYO_STRING_COLOR);
			_brush.setAlpha(YOYO_HANDLE_ALPHA);	
		}

		/**
		 * Draws the yoyo handle and string. The Yoyo handle can extend into 
		 * the padding region.
		 * 
		 * @param canvas
		 * @param activated True if the yoyo is activated. This causes a 
		 * 		different image to be loaded.
		 */
		public void draw(Canvas canvas, boolean activated){
			int radius = getRadius();
			Bitmap handle;
			if(activated){
				_brush.setAlpha(238); //make handle more solid
				canvas.drawLine(_anchorX, _anchorY,
						_handleX + radius, _handleY + radius, _brush);
				handle = HANDLE_SELECTED_IMG;
			}
			else{
				_brush.setAlpha(YOYO_HANDLE_ALPHA);
				canvas.drawLine(_anchorX, _anchorY,
						_handleX + radius, _handleY + 2, _brush);
				// +2 to make the string jut in a little to the handle image
				handle = HANDLE_IMG;
			}
			canvas.drawBitmap(handle, _handleX, _handleY, _brush);
		}
		
		final public int getRadius() {
			return HANDLE_IMG.getWidth() / 2;
		}
		
		public Rect getRestingSize(){
			return HANDLE_SIZE;
		}
		
		/**
		 * Clear the yoyo at the current position and attaches it to (x, y),
		 * with the handle hanging directly below.
		 */
		public void attachYoyo(int x, int y) {
			invalidateYoyo(); //clear old position
			setRestingCoord(x, y);
			invalidateYoyo(); //update new position
		}
		
		/**
		 * Stretch the yoyo with the attachment point at (anchorX, anchorY),
		 * and the handle top-left corner at (handleX, handleY).
		 */
		public void stretchYoyoFromTo(int anchorX, int anchorY, int handleX, int handleY) {
			invalidateYoyo(); //clear old position
			_anchorX = anchorX;
			_anchorY = anchorY;
			_handleX = handleX - _xOffset;
			_handleY = handleY - _yOffset;
			invalidateYoyo(); //update new position
		}
		
		/**
		 * Keep the yoyo attached to the current position but move the handle
		 * to (x, y).
		 */
		public void stretchYoyoTo(int x, int y) {
			stretchYoyoFromTo(_anchorX, _anchorY, x, y);
		}
		
		/**
		 * Sets the yoyo string to be attached at (x, y), with the handle 
		 * hanging directly below, but does not trigger any redrawing
		 */
		public void setRestingCoord(int x, int y){
			_anchorX = x;
			_anchorY = y;
			_handleX = x - getRadius();
			_handleY = y + YOYO_STRING_RESTING_HEIGHT;
		}
		
		private void invalidateYoyo(){
			int handleCenter = _handleX + getRadius();
			int x0, x1, y0, y1;
			if(handleCenter >= _anchorX){
				x0 = _anchorX;
				x1 = handleCenter + 1;
			}
			else{
				x0 = handleCenter;
				x1 = _anchorX + 1;
			}
			
			if(_handleY >= _anchorY){
				y0 = _anchorY;
				y1 = _handleY;
			}
			else{
				y0 = _handleY;
				y1 = _anchorY;
			}

			//invalidate the string area
			_textField.invalidate(x0, y0, x1, y1);
			invalidateHandle();
		}

		public void invalidateHandle() {
			Rect handleExtent = new Rect(_handleX, _handleY,
					_handleX + HANDLE_IMG.getWidth(), _handleY + HANDLE_IMG.getHeight());
			_textField.invalidate(handleExtent);
		}
		
		/**
		 * This method projects a yoyo string directly above the handle and
		 * determines which character it should be attached to, or -1 if no
		 * suitable character can be found.
		 * 
		 * (handleX, handleY) is the handle origin in screen coordinates,
		 * where (0, 0) is the top left corner of the textField, regardless of
		 * its internal scroll values.
		 * 
		 * @return Pair.first contains the nearest character while Pair.second
		 * 			is the exact character found by a strict search 
		 * 
		 */
		public Pair findNearestChar(int handleX, int handleY) {
			int attachedLeft = screenToViewX(handleX) - _xOffset + getRadius();
			int attachedBottom = screenToViewY(handleY) - _yOffset - YOYO_STRING_RESTING_HEIGHT - 1;

			return new Pair(_textField.coordToCharIndex(attachedLeft, attachedBottom),
					_textField.coordToCharIndexStrict(attachedLeft, attachedBottom));
		}

		/**
		 * Records the coordinates of the initial down event on the
		 * handle so that subsequent movement events will result in the
		 * handle being offset correctly.
		 * 
		 * Does not check if isInside(x, y). Calling methods have
		 * to ensure that (x, y) is within the handle area.
		 */
		public void setInitialTouch(int x, int y) {
			_xOffset = x - _handleX;
			_yOffset = y - _handleY;
		}
		
		public void clearInitialTouch(){
			_xOffset = 0;
			_yOffset = 0;
		}
		
		public boolean isInHandle(int x, int y){
			return (x >= _handleX
					&& x < (_handleX + HANDLE_IMG.getWidth())
					&& y >= _handleY
					&& y < (_handleY + HANDLE_IMG.getHeight())
			);
		}
	}//end inner class
}


<|start_filename|>src/com/myopicmobile/textwarrior/android/FindPanel.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.android;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TableLayout;
import android.widget.TableRow;

public class FindPanel extends TableLayout{
	private TextWarriorApplication _callback;
	private EditText _findText;
	private ImageButton _find;
	private ImageButton _findBackwards;

	private ImageButton _toggleReplaceBar;
	private TableRow _replaceBar;
	private EditText _replaceText;
	private ImageButton _replace;
	private ImageButton _replaceAll;

	private ImageButton _displayOptions;
	private AlertDialog _optionsDialog;
	private CheckBox _caseSensitive;
	private CheckBox _matchWholeWord;

	public FindPanel(Context context, AttributeSet attrs) {
		super(context, attrs);
		LayoutInflater inflater = (LayoutInflater) context.getSystemService(
	      Context.LAYOUT_INFLATER_SERVICE);
		inflater.inflate(R.layout.find_panel, this);
		inflater = null;
		
		_findText = (EditText) findViewById(R.id.find_panel_search_text);
		_find = (ImageButton) findViewById(R.id.find_panel_find_next);
		_findBackwards = (ImageButton) findViewById(R.id.find_panel_find_prev);
		_toggleReplaceBar = (ImageButton) findViewById(R.id.find_panel_replace_bar_toggle);
		_replaceBar = (TableRow) findViewById(R.id.find_panel_replace_bar);
		_replaceText = (EditText) findViewById(R.id.find_panel_replace_text);
		_displayOptions = (ImageButton) findViewById(R.id.find_panel_settings);
		_replace = (ImageButton) findViewById(R.id.find_panel_replace);
		_replaceAll = (ImageButton) findViewById(R.id.find_panel_replace_all);

		createOptionsDialog(context);

		_toggleReplaceBar.setImageResource(R.drawable.arrow_down);
		_replaceBar.setVisibility(GONE);
		_toggleReplaceBar.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
            	if(_replaceBar.getVisibility() == VISIBLE){
            		_replaceBar.setVisibility(GONE);
            		_toggleReplaceBar.setImageResource(R.drawable.arrow_down);
            	}
            	else{
            		_replaceBar.setVisibility(VISIBLE);
            		_toggleReplaceBar.setImageResource(R.drawable.arrow_up);
            	}
            }
        });

		_displayOptions.setOnClickListener(new View.OnClickListener() {
             public void onClick(View v) {
     			displaySettings();
             }
         });
		
		_findText.setOnKeyListener(new View.OnKeyListener(){
			public boolean onKey (View v, int keyCode, KeyEvent event){
				if(keyCode == KeyEvent.KEYCODE_ENTER){
					if(event.getAction() == KeyEvent.ACTION_DOWN){
						_callback.find(_findText.getText().toString(), 
								_caseSensitive.isChecked(),
								_matchWholeWord.isChecked());
					}
					return true;
				}
				return false;
			}
		});
		
		_replaceText.setOnKeyListener(new View.OnKeyListener(){
			public boolean onKey (View v, int keyCode, KeyEvent event){
				if(keyCode == KeyEvent.KEYCODE_ENTER){
					if(event.getAction() == KeyEvent.ACTION_DOWN){
						_callback.replaceSelection(_replaceText.getText().toString());
					}
					return true;
				}
				return false;
			}
		});
		
		_find.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
				_callback.find(_findText.getText().toString(), 
						_caseSensitive.isChecked(),
						_matchWholeWord.isChecked());
            }
        });
		
		_findBackwards.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
				_callback.findBackwards(_findText.getText().toString(), 
						_caseSensitive.isChecked(),
						_matchWholeWord.isChecked());
            }
        });
		
		_replace.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
				_callback.replaceSelection(_replaceText.getText().toString());
            }
        });

		_replaceAll.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
				_callback.replaceAll(_findText.getText().toString(), 
						_replaceText.getText().toString(), 
						_caseSensitive.isChecked(),
						_matchWholeWord.isChecked());
            }
        });
		
		// EditText views pad the entire row
		setColumnShrinkable(1, true);
		setColumnStretchable(1, true);
	}

	private void createOptionsDialog(Context context) {
		AlertDialog.Builder builder = new AlertDialog.Builder(context);

		LayoutInflater inflater = (LayoutInflater) context.getSystemService(
	      Context.LAYOUT_INFLATER_SERVICE);
		View settingsLayout = inflater.inflate(R.layout.find_options, null);

		builder.setView(settingsLayout);
		builder.setTitle(context.getString(R.string.find_panel_options));
    	builder.setPositiveButton(android.R.string.ok,
    			new DialogInterface.OnClickListener() {
           public void onClick(DialogInterface dialog, int id) {
                dialog.dismiss();
           }
        });
		_optionsDialog = builder.create();

		_caseSensitive = (CheckBox) settingsLayout.findViewById(R.id.find_panel_case_sensitive);
		_matchWholeWord = (CheckBox) settingsLayout.findViewById(R.id.find_panel_match_whole_word);
	}

	public void displaySettings() {
		_optionsDialog.show();
	}
	
	public void setCallback(TextWarriorApplication c){
		_callback = c;
	}

	@Override
	protected boolean onRequestFocusInDescendants(int direction,
			Rect previouslyFocusedRect) {
		_findText.requestFocus();
		return true;
	}

	@Override
	public Parcelable onSaveInstanceState() {
		Parcelable superState = super.onSaveInstanceState();
		FindPanelSavedState ss = new FindPanelSavedState(superState);
		ss.replaceBarVisibility = _replaceBar.getVisibility();
		ss.optionsCaseSensitive = _caseSensitive.isChecked();
		ss.optionsWholeWord = _matchWholeWord.isChecked();
		ss.optionsDialogShown = _optionsDialog.isShowing();
		return ss;
	}

	@Override
	protected void onRestoreInstanceState(Parcelable state) {
	    if(!(state instanceof FindPanelSavedState)) {
	      super.onRestoreInstanceState(state);
	      return;
	    }

	    FindPanelSavedState ss = (FindPanelSavedState) state;
	    super.onRestoreInstanceState(ss.getSuperState());

    	//default visibility of _replaceBar is GONE
	    if(ss.replaceBarVisibility == VISIBLE){
        	_replaceBar.setVisibility(VISIBLE);
        	_toggleReplaceBar.setImageResource(R.drawable.arrow_up);
	    }
	    _caseSensitive.setChecked(ss.optionsCaseSensitive);
	    _matchWholeWord.setChecked(ss.optionsWholeWord);
	    if(ss.optionsDialogShown){
	    	_optionsDialog.show();
	    }
	}
	
	static class FindPanelSavedState extends View.BaseSavedState {
		int replaceBarVisibility;
		boolean optionsCaseSensitive;
		boolean optionsWholeWord;
		boolean optionsDialogShown;

		/**
		 * Constructor called from FindPanel.onSaveInstanceState()
		 */
		FindPanelSavedState(Parcelable superState) {
			super(superState);
		}
		
		/**
		 * Constructor called from CREATOR
		 */
		private FindPanelSavedState(Parcel in) {
			super(in);
			this.replaceBarVisibility = in.readInt();
			this.optionsCaseSensitive = in.readInt() != 0;
			this.optionsWholeWord = in.readInt() != 0;
			this.optionsDialogShown = in.readInt() != 0;
		}

		@Override
		public void writeToParcel(Parcel out, int flags) {
			super.writeToParcel(out, flags);
			out.writeInt(this.replaceBarVisibility);
			out.writeInt(this.optionsCaseSensitive ? 1 : 0);
			out.writeInt(this.optionsWholeWord ? 1 : 0);
			out.writeInt(this.optionsDialogShown ? 1 : 0);
		}

		public static final Parcelable.Creator<FindPanelSavedState> CREATOR =
			new Parcelable.Creator<FindPanelSavedState>() {
				public FindPanelSavedState createFromParcel(Parcel in) {
					return new FindPanelSavedState(in);
				}
				
				public FindPanelSavedState[] newArray(int size) {
					return new FindPanelSavedState[size];
				}
			};
		}

}


<|start_filename|>assets/help_fr.html<|end_filename|>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Aide de TextWarrior</title>
</head>
<body>
<h3>En construction...</h3>

<p>Malheureusement, le texte d'aide en français n'est pas actuellement disponible.</p>

<p><a href="help.html">Faites voir le texte en anglais.</a></p>

<p>Voulez-vous nous aider à faire des traductions? Alors, envoyez un email à 
<a href="mailto:<EMAIL>"><EMAIL></a><p>

</body>
</html>

<|start_filename|>src/com/myopicmobile/textwarrior/common/CharEncodingUtils.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.common;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;


/**
 * 
 * Helper class to convert between different line terminator and encoding formats
 * and analyze a piece of text.
 * 
 * It is strongly recommended to use the native charset converters of the 
 * platform, if available, since the converters here do not handle errors well.
 * 
 * XXtoUTF16BE methods normalise all line terminator types to '\n' as a side-effect.
 * 
 * UTF16BEtoXX methods assume the line terminator type is '\n', add a BOM to the 
 * output stream as a side-effect and do not check the validity of the input UTF16 chars.
 * Malformed surrogate pairs and illegal Unicode values will be copied over as is.
 * 
 * UTF16toUTF16BE does not check the validity of the input chars.
 * Malformed surrogate pairs and illegal Unicode values will be copied over as is.
 * 
 * This class is not thread-safe. Only one thread should be accessing an object 
 * at a time.
 *
 */
// To implement multi-threaded access, please examine how _unitsDone is used
public class CharEncodingUtils {
	private int _unitsDone = 0;
	
	/**
	 * Returns the progress of the current operation. The units used depends on
	 * the type of operation.
	 * 
	 * Not synchronized; other threads calling this may get outdated values
	 * but it should be all right if only an approximate value is needed 
	 */
	public int getProgress(){
		return _unitsDone;
	}
	
	/**
	 * Returns the encoding scheme used in file, according to the byte-order mark.
	 * If there is no byte-order mark, TEXT_ENCODING_UTF8 is returned.
	 * 
	 * Therefore, encoding schemes such as ASCII and Latin-1 will be erroneously
	 * recognized as UTF-8.
	 * 
	 * @return One of TEXT_ENCODING_UTF8, TEXT_ENCODING_UTF16BE or 
	 * 			TEXT_ENCODING_UTF16LE
	 */
	public String getEncodingScheme(File file)
	throws IOException{
		byte[] byteOrderMark = {0, 0, 0};
		FileInputStream fs = new FileInputStream(file);
		
		try{
			for(int i = 0; i < 3; ++i){
				byteOrderMark[i] = (byte) fs.read();
			}
		}
		finally{
			fs.close();
			fs = null;
		}
		
		if (byteOrderMark[0] == (byte) 0xFE &&
			byteOrderMark[1] == (byte) 0xFF){
			return EncodingScheme.TEXT_ENCODING_UTF16BE;
		}
		else if (byteOrderMark[0] == (byte) 0xFF &&
				byteOrderMark[1] == (byte) 0xFE){
			return EncodingScheme.TEXT_ENCODING_UTF16LE;
		}
		/* Uncomment if UTF-8 should not be the default encoding
		else if (byteOrderMark[0] == (byte) 0xEF &&
				byteOrderMark[1] == (byte) 0xBB &&
				byteOrderMark[2] == (byte) 0xBF){
			return EncodingScheme.TEXT_ENCODING_UTF8;
		}
		*/
		else{
			//TODO more robust algorithm for differentiating between encoding schemes without BOM
			return EncodingScheme.TEXT_ENCODING_UTF8;
		}
	}
	
	/**
	 * Returns the line terminator style used in file.
	 * 
	 * @return One of LINE_BREAK_LF, LINE_BREAK_CR or LINE_BREAK_CRLF
	 */
	public String getEOLType(File file, String encoding)
	throws IOException{
		FileInputStream fs = new FileInputStream(file);
		String EOLType = null;
		int c;
		int prev = 0;
		
		try{
			if(encoding.equals(EncodingScheme.TEXT_ENCODING_UTF16BE)){
				fs.read(); // discard upper byte
			}
			
			while (EOLType == null &&
			(c = fs.read()) != -1){
				if (c == '\n' && prev != '\r'){
					EOLType = EncodingScheme.LINE_BREAK_LF;
				}
				if (prev == '\r'){
					if(c == '\n'){
						EOLType = EncodingScheme.LINE_BREAK_CRLF;
					}
					else{
						EOLType = EncodingScheme.LINE_BREAK_CR;
					}
				}
	
				prev = c;
				if(encoding.equals(EncodingScheme.TEXT_ENCODING_UTF16BE) ||
				encoding.equals(EncodingScheme.TEXT_ENCODING_UTF16LE)){
					fs.read(); // discard upper byte
				}
			}
		}
		finally{
			fs.close();
			fs = null;
		}

		if (EOLType == null){
			return EncodingScheme.LINE_BREAK_LF;
		}
		else{
			return EOLType;
		}
	}

	/**
	 * Skips the byte-order mark, if any, and returns the first byte after that.
	 * If there are no valid bytes, -1 is returned
	 */
	private int stripByteOrderMark(InputStream byteStream)
	throws IOException{
		int firstByte = byteStream.read();
		if (firstByte == 0xFE || firstByte == 0xFF){
			// UTF-16
			byteStream.read(); // discard 2nd BOM byte
			firstByte = byteStream.read();
			_unitsDone += 2;
		}
		else if (firstByte == 0xEF){
			// UTF-8
			byteStream.read(); // discard 2nd BOM byte
			byteStream.read(); // discard 3rd BOM byte
			firstByte = byteStream.read();
			_unitsDone += 3;
		}
		return firstByte;
	}
	
	public void writeByteOrderMark(OutputStream byteStream, String encoding)
	throws IOException{
		if(encoding.equals(EncodingScheme.TEXT_ENCODING_UTF16BE)){
	    	byteStream.write(0xFE);
	    	byteStream.write(0xFF);
		}
		else if(encoding.equals(EncodingScheme.TEXT_ENCODING_UTF16LE)){
	    	byteStream.write(0xFF);
	    	byteStream.write(0xFE);
		}
		else if(encoding.equals(EncodingScheme.TEXT_ENCODING_UTF8)){
	    	byteStream.write(0xEF);
	    	byteStream.write(0xBB);
	    	byteStream.write(0xBF);
		}
	}
	
	/**
	 * Reads bytes from byteStream into buffer, converting to UTF-16BE encoding
	 * and normalising all line terminators to UNIX style '\n'
	 * 
	 * @param byteStream
	 * @param buffer
	 * @param encoding Encoding scheme of byteStream. Cannot be Auto! Call 
	 * 			getEncodingScheme() to find the exact type first
	 * @param EOLchar Line terminator style of byteStream
	 * @param abort Other threads can set this to abort the read operation
	 * @return Pair(size of converted text, number of lines)
	 */
	public Pair readAndConvert(InputStream byteStream,
	char[] buffer, String encoding, String EOLchar, Flag abort)
	throws IOException{
		_unitsDone = 0;

		if(encoding.equals(EncodingScheme.TEXT_ENCODING_LATIN1)){
			return Latin1toUTF16BE(byteStream, buffer, EOLchar, abort);
		}
		else if(encoding.equals(EncodingScheme.TEXT_ENCODING_UTF16BE)){
			return UTF16toUTF16BE(byteStream, buffer, true, EOLchar, abort);
		}
		else if(encoding.equals(EncodingScheme.TEXT_ENCODING_UTF16LE)){
			return UTF16toUTF16BE(byteStream, buffer, false, EOLchar, abort);
		}
		else if(encoding.equals(EncodingScheme.TEXT_ENCODING_UTF8)){
			return UTF8toUTF16BE(byteStream, buffer, EOLchar, abort);
		}
		else{
			TextWarriorException.assertVerbose(false,
					"Unsupported encoding option" + encoding);
			return new Pair(0, 0);
		}
	}
	
	private Pair Latin1toUTF16BE(InputStream byteStream,
	char[] buffer, String EOLchar, Flag abort)
	throws IOException{
		int currCharRead;
		int lineCount = 1;
		int totalChar = 0;
	
		while((currCharRead = byteStream.read()) != -1 && !abort.isSet()){
			++_unitsDone;

			if (currCharRead == '\r'){
				if(EOLchar.equals(EncodingScheme.LINE_BREAK_CRLF)){
					//TODO assert there is a valid '/n' after this '/r'
					byteStream.read(); // discard the next char '\n'
					++_unitsDone;
				}
				currCharRead = '\n';
			}
			
			if (currCharRead == '\n'){
				++lineCount;
			}
			buffer[totalChar++] = (char) currCharRead;
		}

		return new Pair(totalChar, lineCount);
	}

	private Pair UTF16toUTF16BE(InputStream byteStream,
	char[] buffer, boolean isBigEndian, String EOLchar, Flag abort)
	throws IOException{
		int byte0, byte1;
		int lineCount = 1;
		int totalChar = 0;
		char currCharRead;

		byte0 = stripByteOrderMark(byteStream);
		while(byte0 != -1
		&& (byte1 = byteStream.read()) != -1
		&& !abort.isSet()){
			_unitsDone += 2;
			//TODO place conditional outside loop
			if(isBigEndian){
				currCharRead = (char) (byte1 | (byte0 << 8));
			}
			else{
				currCharRead = (char) (byte0 | (byte1 << 8));
			}
			
			if (currCharRead == '\r'){
				if(EOLchar.equals(EncodingScheme.LINE_BREAK_CRLF)){
					//TODO assert there is a valid '/n' after this '/r'
					byteStream.read(); // discard the next 2 bytes representing '\n'
					byteStream.read();
					++_unitsDone;
				}
				currCharRead = '\n';
			}
			
			if (currCharRead == '\n'){
				++lineCount;
			}
			
			buffer[totalChar++] = currCharRead;
			byte0 = byteStream.read();
		}

		return new Pair(totalChar, lineCount);
	}
	
	public void writeAndConvert(OutputStream byteStream,
	DocumentProvider hDoc, String encoding, String EOLchar, Flag abort)
	throws IOException{
		_unitsDone = 0;
		hDoc.seekChar(0);

		if(encoding.equals(EncodingScheme.TEXT_ENCODING_LATIN1)){
			UTF16BEtoLatin1(byteStream, hDoc, EOLchar, abort);
		}
		else if(encoding.equals(EncodingScheme.TEXT_ENCODING_UTF16BE)){
			UTF16BEtoUTF16(byteStream, hDoc, true, EOLchar, abort);
		}
		else if(encoding.equals(EncodingScheme.TEXT_ENCODING_UTF16LE)){
			UTF16BEtoUTF16(byteStream, hDoc, false, EOLchar, abort);
		}
		else if(encoding.equals(EncodingScheme.TEXT_ENCODING_UTF8)){
			UTF16BEtoUTF8(byteStream, hDoc, EOLchar, abort);
		}
		else{
			TextWarriorException.assertVerbose(false,
					"Unsupported encoding option" + encoding);
		}
	}
	
	private void UTF16BEtoLatin1(OutputStream byteStream,
	DocumentProvider hDoc, String EOLchar, Flag abort)
	throws IOException{
		while(hDoc.hasNext() && !abort.isSet()){
			char curr = hDoc.next();
	    	++_unitsDone;
	    	
	    	if(curr == LanguageCFamily.EOF){
	    		break;
	    	}

			// convert '\n' to desired line terminator symbol
	    	if (curr == '\n' &&
	    	EOLchar.equals(EncodingScheme.LINE_BREAK_CRLF)){
		    	byteStream.write('\r');
	    	}
	    	else if (curr == '\n' &&
	    	EOLchar.equals(EncodingScheme.LINE_BREAK_CR)){
	    		curr = '\r';
	    	}
	    	byteStream.write(curr);
		}
	}
	
	private void UTF16BEtoUTF16(OutputStream byteStream,
	DocumentProvider hDoc, boolean isBigEndian, String EOLchar, Flag abort)
	throws IOException{
		String bom = isBigEndian ? EncodingScheme.TEXT_ENCODING_UTF16BE
				: EncodingScheme.TEXT_ENCODING_UTF16LE;
		writeByteOrderMark(byteStream, bom);

		while(hDoc.hasNext() && !abort.isSet()){
			char curr = hDoc.next();
	    	++_unitsDone;

	    	if(curr == LanguageCFamily.EOF){
	    		break;
	    	}

			// convert '\n' to desired line terminator symbol
	    	if (curr == '\n' &&
	    	EOLchar.equals(EncodingScheme.LINE_BREAK_CRLF)){
	    		if(isBigEndian){
		    		byteStream.write(0); byteStream.write('\r');
		    	}
	    		else{
		    		byteStream.write('\r'); byteStream.write(0);
	    		}
	    	}
	    	else if (curr == '\n' &&
	    	EOLchar.equals(EncodingScheme.LINE_BREAK_CR)){
	    		curr = '\r';
	    	}
			//TODO place conditional outside loop
	    	if(isBigEndian){
	    		byteStream.write(curr >>> 8);
	    		byteStream.write(curr & 0xFF);
	    	}
	    	else{
	    		byteStream.write(curr & 0xFF);
	    		byteStream.write(curr >>> 8);
	    	}
		}
	}
	
	
	/*
	 * The following UTF encoding form conversion methods were modified from
	 * an algorithm by <NAME>, pp. 543, Unicode Demystified, 2003
	 */
	
	// Lookup table to keep track of how many more bytes left to process to get 
	// a character. First index is the number of bytes of a UTF-8 character that
	// has been processed. Second index is the top 5 bits of the current byte.
	// Result of the lookup is the number of bytes left to process, or
	// -1 == illegal lead byte; -2 == illegal trailing byte
	private static final byte[][] states = {
		// 00 08 10 18 20 28 30 38 40 48 50 58 60 68 70 78
		// 80 88 90 98 A0 A8 B0 B8 C0 C8 D0 D8 E0 E8 F0 F8
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		-1, -1, -1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 2, 2, 3, -1},
		{-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
		 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -2, -2, -2, -2, -2, -2},
		{-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
		 1, 1, 1, 1, 1, 1, 1, 1, -2, -2, -2, -2, -2, -2, -2, -2},
		{-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
		 2, 2, 2, 2, 2, 2, 2, 2, -2, -2, -2, -2, -2, -2, -2, -2},
		};
	
	private static final byte[] masks = { 0x7F, 0x1F, 0x0F, 0x07 };

	
	private Pair UTF8toUTF16BE(InputStream byteStream,
	char[] buffer, String EOLchar, Flag abort)
	throws IOException{
		int currByte;
		int utf32Char = 0;
		int lineCount = 1;
		int totalChar = 0;
		int state = 0;
		byte mask = 0;

		currByte = stripByteOrderMark(byteStream);
		while(currByte != -1 && !abort.isSet()){
			++_unitsDone;
			state = states[state][currByte >>> 3];
			
			switch(state){
			case 0:
				utf32Char += currByte & 0x7F;
				
				if (utf32Char == '\r'){
					if(EOLchar.equals(EncodingScheme.LINE_BREAK_CRLF)){
						//TODO assert there is a valid '/n' after this '/r'
						byteStream.read(); // discard the next char '\n'
						++_unitsDone;
					}
					utf32Char = '\n';
				}

				if(utf32Char <= 0xFFFF){
					if (utf32Char == '\n'){
						++lineCount;
					}
					buffer[totalChar++] = (char) utf32Char;
				}
				else{
					// not in the BMP; split into surrogate pair
					buffer[totalChar++] = (char) ((utf32Char >> 10) + 0xD7C0);
					buffer[totalChar++] = (char) ((utf32Char & 0x03FF) + 0xDC00);
				}
				utf32Char = 0;
				mask = 0;
				break;
				
			case 1: // fall-through
			case 2: // fall-through
			case 3:
				if (mask == 0){
					mask = masks[state];
				}
				utf32Char += currByte & mask;
				utf32Char <<= 6;
				mask = (byte) 0x3F;
				break;
				
			case -2: // fall-through
			case -1:
				//TODO replace malformed sequence with the Unicode replacement char 0xFFFD
				/*
				buffer[totalChar++] = 0xDBBF;
				buffer[totalChar++] = 0xDFFD;
				*/
				// Since FFFD in UTF-16 requires a surrogate pair, and
				// TextWarrior cannot handle surrogate pairs yet, use '?' instead
				buffer[totalChar++] = '?';
				state = 0;
				utf32Char = 0;
				mask = 0;
				if(state == -2){
					// keep this byte for the next while iteration
					continue;
				}
				break;
			}
			currByte = byteStream.read();
		}

		return new Pair(totalChar, lineCount);
	}
	
	private void UTF16BEtoUTF8(OutputStream byteStream,
	DocumentProvider hDoc, String EOLchar, Flag abort)
	throws IOException{
		writeByteOrderMark(byteStream, EncodingScheme.TEXT_ENCODING_UTF8);
		while(hDoc.hasNext() && !abort.isSet()){
			int utf32Char = 0;
			char curr = hDoc.next();
	    	++_unitsDone;

	    	if(curr == LanguageCFamily.EOF){
	    		break;
	    	}
	    	
			if(curr < 0xD800 || curr > 0xDFFF){
				utf32Char = curr;
			}
			else{
				// combine surrogate pair to UTF-32 value
				utf32Char = (curr-0xD7C0) << 10;
				utf32Char += hDoc.next() & 0x03FF;
			}
	    	
			// Encode variable number of UTF-8 bytes depending on the UTF-32 value
		    if (utf32Char < 0x80){
		    	// convert '\n' to desired line terminator symbol
		    	if (utf32Char == '\n' &&
		    	EOLchar.equals(EncodingScheme.LINE_BREAK_CRLF)){
					byteStream.write('\r');
		    	}
		    	else if (utf32Char == '\n' &&
		    	EOLchar.equals(EncodingScheme.LINE_BREAK_CR)){
		    		utf32Char = '\r';
		    	}
		    	byteStream.write(utf32Char);
		    }
		    else if (utf32Char < 0x800){
		    	byteStream.write((utf32Char >> 6) + 0xC0);
		    	byteStream.write((utf32Char & 0x3F) + 0x80);
		    }
		    else if (utf32Char < 0x10000){
		    	byteStream.write((utf32Char >> 12) + 0xE0);
		    	byteStream.write(((utf32Char >> 6) & 0x3F) + 0x80);
		    	byteStream.write((utf32Char & 0x3F) + 0x80);
		    }
		    else{
		    	byteStream.write((utf32Char >> 18) + 0xF0);
		    	byteStream.write(((utf32Char >> 12) & 0x3F) + 0x80);
		    	byteStream.write(((utf32Char >> 6) & 0x3F) + 0x80);
		    	byteStream.write((utf32Char & 0x3F) + 0x80);
		    }
		}
	}

	/**
	 * Analyze word and character count from start to end-1 position in src
	 */
	public Statistics analyze(DocumentProvider src, int start, int end, Flag abort) {
		if(start < 0 || start >= src.docLength()){
			TextWarriorException.assertVerbose(false,
				"Invalid start index");
			return new Statistics(0, 0, 0, 0);
		}
		if(start > end){
			TextWarriorException.assertVerbose(false,
				"Start index cannot be greater than end index");
			return new Statistics(0, 0, 0, 0);
		}
		if(start == end){
			return new Statistics(0, 0, 0, 0);
		}
		
		int wordCount = 0;
		_unitsDone = 0;
		int whiteSpaceCount = 0;
		int lines = 1;
		LanguageCFamily charSet = Lexer.getLanguage();
		
		char firstChar = src.charAt(start);
		//whether the current char and possibly the ones before are whitespace
		boolean whiteSpaceRun = charSet.isWhitespace(firstChar);
		
		src.seekChar(start);
		while(src.hasNext() && _unitsDone < (end-start)
				&& !abort.isSet()){
			char c = src.next();
			++_unitsDone;

			if(c == '\n'){
				++lines;
			}
			
			if(charSet.isWhitespace(c)){
				++whiteSpaceCount;
				
				if(!whiteSpaceRun){
					whiteSpaceRun = true;
					++wordCount;
				}
			}
			else{
				whiteSpaceRun = false;
			}
		}
		
		if(!whiteSpaceRun){
			// the final word didn't end with whitespace
			++wordCount;
		}
		if(!src.hasNext() && _unitsDone > 0){
			// exclude the terminal EOF character
			--_unitsDone;
			--whiteSpaceCount;
		}
		
		return new Statistics(wordCount, _unitsDone, whiteSpaceCount, lines);
	}

	public static class Statistics{
		public int wordCount = 0;
		public int charCount = 0;
		public int whitespaceCount = 0;
		public int lineCount = 0;
		
		public Statistics(){
		}
		
		public Statistics(int words, int chars, int whitespaces, int lines){
			wordCount = words;
			charCount = chars;
			whitespaceCount = whitespaces;
			lineCount = lines;
		}
	}
}

<|start_filename|>src/org/openintents/filemanager/DirectoryContents.java<|end_filename|>
package org.openintents.filemanager;

import java.util.List;
/* 
 * @author <NAME>
 * @date 30 Dec 2010
 *
 * - Made class public
 * - Added getters and setters
 * - Removed unused imports
 * - Removed noMedia boolean member
 */

public class DirectoryContents {
    List<IconifiedText> listDir;
    List<IconifiedText> listFile;
    List<IconifiedText> listSdCard;

    public final List<IconifiedText> getListDir() {
		return listDir;
	}

	public final void setListDir(List<IconifiedText> listDir) {
		this.listDir = listDir;
	}

	public final List<IconifiedText> getListFile() {
		return listFile;
	}

	public final void setListFile(List<IconifiedText> listFile) {
		this.listFile = listFile;
	}

	public final List<IconifiedText> getListSdCard() {
		return listSdCard;
	}

	public final void setListSdCard(List<IconifiedText> listSdCard) {
		this.listSdCard = listSdCard;
	}
}


<|start_filename|>src/com/myopicmobile/textwarrior/common/AnalyzeStatisticsThread.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.common;

import java.util.Vector;

public class AnalyzeStatisticsThread extends Thread implements ProgressSource{
	protected Flag _abortFlag;
	protected boolean _isDone = false;
	protected Vector<ProgressObserver> _progressObservers;
	private DocumentProvider _hDoc;
	private int _start, _end;
	protected CharEncodingUtils.Statistics _results;
	protected CharEncodingUtils _analyzer = new CharEncodingUtils();
	
	public AnalyzeStatisticsThread(DocumentProvider hDoc,
			int start, int end){
		_hDoc = hDoc;
		_start = start;
		_end = end;
        _abortFlag = new Flag();
        _progressObservers = new Vector<ProgressObserver>();
	}
	
	public void run(){
		_isDone = false;
		_abortFlag.clear();
		_results = null;
		
		_results = _analyzer.analyze(_hDoc, _start, _end, _abortFlag);

		if(!_abortFlag.isSet()){
			_isDone = true;
			broadcastComplete(_results);
		}
		else{
			broadcastCancel();
		}
	}

	@Override
	public final void forceStop(){
		if(!_abortFlag.isSet()){
			_abortFlag.set();
		}
	}

	@Override
	synchronized public final void registerObserver(ProgressObserver po){
		_progressObservers.addElement(po);
	}

	@Override
	synchronized public final void removeObservers(){
		_progressObservers.clear();
	}

	synchronized protected void broadcastComplete(
			CharEncodingUtils.Statistics results){
		for(ProgressObserver po : _progressObservers){
			po.onComplete(ProgressSource.ANALYZE_TEXT, results);
		}
	}
	
	synchronized protected void broadcastCancel(){
		for(ProgressObserver po : _progressObservers){
			po.onCancel(ProgressSource.ANALYZE_TEXT);
		}
	}

	@Override
	public final int getMin(){
		return 0;
	}

	@Override
	public int getMax(){
		return Math.max(1, _end - _start);
	}

	@Override
	public int getCurrent(){
		return _analyzer.getProgress();
	}

	@Override
	public final boolean isDone(){
		return _isDone;
	}
	
	public final CharEncodingUtils.Statistics getResults(){
		return _results;
	}
}


<|start_filename|>src/com/myopicmobile/textwarrior/android/DirectoryScanner.java<|end_filename|>
/* 
 * Copyright (C) 2008 OpenIntents.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * @author <NAME>
 * @date 30 Dec 2010
 * 
 * - Removed Cupcake-specific methods, mWriteableOnly, mDirectoriesOnly, 
 * 		file/dir count, .nomedia and mime type handling.
 * - Display last modified date in each entry instead of size
 */

package com.myopicmobile.textwarrior.android;

import java.io.File;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.openintents.filemanager.DirectoryContents;
import org.openintents.filemanager.IconifiedText;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;

import com.myopicmobile.textwarrior.android.R;

public class DirectoryScanner extends Thread {

	private static final String TAG = "OIFM_DirScanner";
	
	private File currentDirectory;
	boolean cancel;

	private String mSdCardPath;
	private Context context;
	private Handler handler;
	private long operationStartTime;
	
	// Update progress bar every n files
	static final private int PROGRESS_STEPS = 50;
    

	DirectoryScanner(File directory, Context context, Handler handler, String sdCardPath) {
		super("Directory Scanner");
		currentDirectory = directory;
		this.context = context;
		this.handler = handler;
		this.mSdCardPath = sdCardPath;
	}
	
	private void clearData() {
		// Remove all references so we don't delay the garbage collection.
		context = null;
		handler = null;
	}

	public void run() {
		Log.v(TAG, "Scanning directory " + currentDirectory);
		
		File[] files = currentDirectory.listFiles();

		int totalCount = 0;
		
		if (cancel) {
			Log.v(TAG, "Scan aborted");
			clearData();
			return;
		}
		
		if (files == null) {
			Log.v(TAG, "Returned null - inaccessible directory?");
			totalCount = 0;
		} else {
			totalCount = files.length;
		}
		
		operationStartTime = SystemClock.uptimeMillis();
		
		Log.v(TAG, "Counting files... (total count=" + totalCount + ")");

		int progress = 0;
		
		/** Dir separate for sorting */
		List<IconifiedText> listDir = new ArrayList<IconifiedText>(totalCount);

		/** Files separate for sorting */
		List<IconifiedText> listFile = new ArrayList<IconifiedText>(totalCount);

		/** SD card separate for sorting */
		List<IconifiedText> listSdCard = new ArrayList<IconifiedText>(3);

		// Cache some commonly used icons.
		Drawable sdIcon = context.getResources().getDrawable(R.drawable.icon_sdcard);
		Drawable folderIcon = context.getResources().getDrawable(R.drawable.ic_launcher_folder);
		Drawable genericFileIcon = context.getResources().getDrawable(R.drawable.icon_file);

		
		if (files != null) {
			for (File currentFile : files){ 
				if (cancel) {
					// Abort!
					Log.v(TAG, "Scan aborted while checking files");
					clearData();
					return;
				}

				progress++;
				updateProgress(progress, totalCount);		
				
				String modifiedDate = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
										.format(currentFile.lastModified());
				if (currentFile.isDirectory()) { 
					if (currentFile.getAbsolutePath().equals(mSdCardPath)) {
						listSdCard.add(new IconifiedText( 
								currentFile.getName(), "", sdIcon)); 
					} else {
						listDir.add(new IconifiedText( 
									currentFile.getName(), modifiedDate, folderIcon));
					}
				}else{
					//is file
					listFile.add(new IconifiedText( 
						currentFile.getName(), modifiedDate, genericFileIcon));
				} 
			}
		}
		
		Log.v(TAG, "Sorting results...");
		
		Collections.sort(listDir); 
		Collections.sort(listFile); 

		if (!cancel) {
			Log.v(TAG, "Sending data back to main thread");
			
			DirectoryContents contents = new DirectoryContents();

			contents.setListDir(listDir);
			contents.setListFile(listFile);
			contents.setListSdCard(listSdCard);

			Message msg = handler.obtainMessage(FilePicker.MESSAGE_SHOW_DIRECTORY_CONTENTS);
			msg.obj = contents;
			msg.sendToTarget();
		}

		clearData();
	}
	
	private void updateProgress(int progress, int maxProgress) {
		// Only update the progress bar every n steps...
		if ((progress % PROGRESS_STEPS) == 0) {
			// Also don't update for the first second.
			long curTime = SystemClock.uptimeMillis();
			
			if (curTime - operationStartTime < 1000L) {
				return;
			}
			
			// Okay, send an update.
			Message msg = handler.obtainMessage(FilePicker.MESSAGE_SET_PROGRESS);
			msg.arg1 = progress;
			msg.arg2 = maxProgress;
			msg.sendToTarget();
		}
	}
}


<|start_filename|>src/com/myopicmobile/textwarrior/android/PollingProgressDialog.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.android;

import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.os.Handler;

import com.myopicmobile.textwarrior.common.ProgressObserver;
import com.myopicmobile.textwarrior.common.ProgressSource;
import com.myopicmobile.textwarrior.common.TextWarriorException;


public class PollingProgressDialog extends ProgressDialog
implements ProgressObserver, OnCancelListener{
	private Context _context;
	private Handler _handler;
	private ProgressSource _src;
	private int _min;
	private boolean pollActive;
	private final static int POLL_PERIOD = 250; // in milliseconds

	public PollingProgressDialog(Context context, ProgressSource src,
			boolean showProgress, boolean cancellable){
		super(context);
		_src = src;
		_context = context;

		//TODO assert on UI thread
		_handler = new Handler();
		setIndeterminate(!showProgress);
		if(showProgress){
			setTitle(_context.getString(R.string.progress_dialog_message));
			setIcon(android.R.drawable.ic_dialog_info);
			setProgress(_min);
			setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
			pollActive = true;
		}
		else{
			setMessage(_context.getString(R.string.progress_dialog_message));
			setProgressStyle(ProgressDialog.STYLE_SPINNER);
			pollActive = false;
		}
		
		setCancelable(cancellable);
		if(cancellable){
			setButton(BUTTON_NEGATIVE, _context.getString(android.R.string.cancel),
				new DialogInterface.OnClickListener(){
				    public void onClick(DialogInterface dialog, int which) {
				    	dialog.cancel();
				    }
			});
			setOnCancelListener(this);
    	}

		_min = _src.getMin();
		TextWarriorException.assertVerbose(_min < _src.getMax(),
			"Maximum progress value <= min value");
	}
	
	public void startDelayedPollingDialog(){
		_src.registerObserver(this);
		setMax(_src.getMax());
		//wait for a while before showing a dialog
		_handler.postDelayed(_startProgressTask, POLL_PERIOD);
	}
	
	public void startPollingDialog(){
		_src.registerObserver(this);
		setMax(_src.getMax());
		_handler.post(_startProgressTask);
	}
	
	private final Runnable _startProgressTask = new Runnable(){
		public void run(){
			if(pollActive){
				updateProgress(_src.getCurrent());
				_handler.postDelayed(_updateProgressTask, POLL_PERIOD);
			}
			show();
		}
	};
	
	private final Runnable _updateProgressTask = new Runnable(){
		public void run(){
			updateProgress(_src.getCurrent());
		    _handler.postDelayed(_updateProgressTask, POLL_PERIOD);
		}
	};

	private void updateProgress(int value){
		setProgress(value - _min);
	}

	@Override
	public void onComplete(int requestCode, Object result){
		_handler.removeCallbacks(_startProgressTask);
		_handler.removeCallbacks(_updateProgressTask);
		dismiss();
	}
	
	@Override
	public void onError(int requestCode, int errorCode, String message){
		//TODO test this branch
    	//no need to call _src.forceStop() since worker thread would have stopped on error
		_handler.removeCallbacks(_startProgressTask);
		_handler.removeCallbacks(_updateProgressTask);
		dismiss();
	}
	
	@Override
	public void onCancel(DialogInterface dialog){
		_handler.removeCallbacks(_startProgressTask);
		_handler.removeCallbacks(_updateProgressTask);
		_src.forceStop(); //this causes _src to broadcast onCancel(int), which we ignore below
	}

	@Override
	public void onCancel(int requestCode) {
		//do nothing
	}
}


<|start_filename|>src/com/myopicmobile/textwarrior/android/TextWarriorApplication.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */

package com.myopicmobile.textwarrior.android;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

import org.openintents.filemanager.IconifiedText;
import org.openintents.filemanager.IconifiedTextListAdapter;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.DialogInterface.OnCancelListener;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.text.ClipboardManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.view.animation.LinearInterpolator;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.OnItemClickListener;

import com.myopicmobile.textwarrior.common.AnalyzeStatisticsThread;
import com.myopicmobile.textwarrior.common.CharEncodingUtils;
import com.myopicmobile.textwarrior.common.DocumentProvider;
import com.myopicmobile.textwarrior.common.EncodingScheme;
import com.myopicmobile.textwarrior.common.FindThread;
import com.myopicmobile.textwarrior.common.LanguageC;
import com.myopicmobile.textwarrior.common.LanguageCpp;
import com.myopicmobile.textwarrior.common.LanguageCsharp;
import com.myopicmobile.textwarrior.common.LanguageJava;
import com.myopicmobile.textwarrior.common.LanguageJavascript;
import com.myopicmobile.textwarrior.common.LanguageNonProg;
import com.myopicmobile.textwarrior.common.LanguageObjectiveC;
import com.myopicmobile.textwarrior.common.LanguagePHP;
import com.myopicmobile.textwarrior.common.LanguagePython;
import com.myopicmobile.textwarrior.common.LanguageRuby;
import com.myopicmobile.textwarrior.common.Lexer;
import com.myopicmobile.textwarrior.common.ProgressObserver;
import com.myopicmobile.textwarrior.common.ProgressSource;
import com.myopicmobile.textwarrior.common.ReadThread;
import com.myopicmobile.textwarrior.common.RowListener;
import com.myopicmobile.textwarrior.common.TextBuffer;
import com.myopicmobile.textwarrior.common.TextWarriorException;
import com.myopicmobile.textwarrior.common.WriteThread;
import com.myopicmobile.textwarrior.common.FindThread.FindResults;

public class TextWarriorApplication extends Activity implements
		ProgressObserver, RowListener, SelectionModeListener,
		SharedPreferences.OnSharedPreferenceChangeListener {

	protected FreeScrollingTextField _editField;
	protected FindPanel _findPanel;
	protected ClipboardPanel _clipboardPanel;
	protected String _filename = null;
	private String _lastSelectedFile = null; // latest result from FilePicker;
												// may not refer to a valid file
	private TextBuffer _newDoc; // used as a holder when reading in a file
	protected RecentFiles _recentFiles;

	// -----------------------------------------------------------------------
	// ------------------- Creational and init methods -----------------------

	@Override
	/*
	 * 3 different scenarios are distinguished here. 1. Fresh start of an
	 * activity. 2. Activity re-created immediately after a configuration change
	 * 3. Activity re-created after the system force-killed its process, for
	 * example, to reclaim resources
	 */
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		createTextField();
		createFindPanel();
		createClipboardPanel();
		restorePersistentOptions();

		_recentFiles = new RecentFiles(this);

		NonConfigurationState ncs = (NonConfigurationState) getLastNonConfigurationInstance();
		if (savedInstanceState == null) {
			/* Scenario 1 */
			setModel(new TextBuffer());

			Intent i = getIntent();
			String action = i.getAction();
			if (action.equals(Intent.ACTION_VIEW)
					|| action.equals(Intent.ACTION_EDIT)) {
				open(i.getData().getPath());
			}
		} else if (ncs != null) {
			/* Scenario 2 */
			restoreNonConfigurationState(ncs);
			restoreUiState((UiState) savedInstanceState.getParcelable(STATE_UI));
		} else {
			/* Scenario 3 */
			setModel(new TextBuffer());

			// Workaround to dismiss system-managed dialogs that were at the
			// foreground when the process was force-killed
			Handler h = new Handler();
			h.post(new Runnable() {
				public void run() {
					dismissAllDialogs();
					showDialog(DIALOG_PREV_INSTANCE_CRASHED_ID);
				}
			});

			/*
			 * _recoveredUiState = (UiState)
			 * savedInstanceState.getParcelable(STATE_UI); //XXX if worker
			 * threads were interrupted, display info box
			 * 
			 * int recoveryFileType = getRecoveryFileType(); if(recoveryFileType
			 * == RECOVERY_FILE_TEMP){ //XXX handle accessing of temp file by
			 * multiple instances of the activity open(TEMP_FILE_PATH); } else
			 * if(recoveryFileType == RECOVERY_FILE_SOURCE){ SharedPreferences
			 * recoveryFilePrefs = getSharedPreferences(PREFS_RECOVERY_FILE, 0);
			 * String sourceFile =
			 * recoveryFilePrefs.getString(RECOVERY_STATE_SOURCE_FILE, "");
			 * open(sourceFile); } else if(recoveryFileType ==
			 * RECOVERY_FILE_CREATE_ERROR){ //XXX inform user that edits could
			 * not be recovered resetRecoveryFilePrefs(); //clear recovery error
			 * state }
			 */
		}

		updateTitle();
		updateClipboardButtons();
	}

	private void createClipboardPanel() {
		_clipboardPanel = (ClipboardPanel) findViewById(R.id.clipboard_drawer);
		_clipboardPanel.setInterpolator(new LinearInterpolator());

		_clipboardPanel.setCutListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				cut();
			}
		});

		_clipboardPanel.setCopyListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				copy();
			}
		});

		_clipboardPanel.setPasteListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				paste();
			}
		});
	}

	/**
	 * Enable/disable cut/copy/paste buttons based on text selection state
	 */
	private void updateClipboardButtons() {
		boolean isSelecting = _editField.isSelectText();
		ClipboardManager cb = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
		_clipboardPanel.setClipboardButtonState(isSelecting, isSelecting,
				cb.hasText());
	}

	private void createFindPanel() {
		_findPanel = (FindPanel) findViewById(R.id.find_panel);
		_findPanel.setCallback(this);
	}

	private void createTextField() {
		_editField = (FreeScrollingTextField) findViewById(R.id.work_area);
		_editField.setRowListener(this);
		_editField.setSelModeListener(this);
	}

	private void restorePersistentOptions() {
		PreferenceManager.getDefaultSharedPreferences(this)
				.registerOnSharedPreferenceChangeListener(this);
		SharedPreferences pref = PreferenceManager
				.getDefaultSharedPreferences(this);

		setFont(pref);
		setTabSpaces(pref);
		setSyntaxColor(pref);
		setZoom(pref);
		setNavigationMethod(pref);
	}

	private void setModel(TextBuffer buf) {
		_editField.changeDocumentProvider(new DocumentProvider(buf));
		_editField.setDirty(false);
	}

	private void updateTitle() {
		SharedPreferences pref = PreferenceManager
				.getDefaultSharedPreferences(this);
		boolean showLineNumbers = pref.getBoolean(
				getString(R.string.settings_key_show_row_number), false);

		String title;
		if (showLineNumbers) {
			title = createTitle(_editField.getCaretRow() + 1);
		} else {
			title = createTitle();
		}

		setTitle(title);
	}

	private String createTitle() {
		String title;
		if (_filename != null) {
			title = (new File(_filename)).getName();
		} else {
			title = getString(R.string.title);
		}
		return title;
	}

	private String createTitle(int rowIndex) {
		String title = "(" + rowIndex + ") " + createTitle();
		return title;
	}

	public boolean onCreateOptionsMenu(Menu menu) {
		MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.main_menu, menu);

		onPrepareOptionsMenu(menu);
		return true;
	}

	public boolean onPrepareOptionsMenu(Menu menu) {
		// cut and paste options
		menu.setGroupVisible(R.id.menu_group_selection_actions,
				_editField.isSelectText());

		// save option
		MenuItem saveMenuItem = menu.findItem(R.id.save);
		saveMenuItem.setEnabled(_editField.isDirty());

		// paste option
		MenuItem pasteMenuItem = menu.findItem(R.id.paste);
		ClipboardManager cb = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
		pasteMenuItem.setVisible(cb.hasText());

		// undo and redo options
		DocumentProvider doc = _editField.createDocumentProvider();
		MenuItem undoMenuItem = menu.findItem(R.id.undo);
		undoMenuItem.setEnabled(doc.canUndo());
		MenuItem redoMenuItem = menu.findItem(R.id.redo);
		redoMenuItem.setVisible(doc.canRedo());

		return true;
	}

	public boolean onOptionsItemSelected(MenuItem item) {
		int itemId = item.getItemId();
		if (itemId == R.id.new_file) {
			if (_editField.isDirty()) {
				_saveFinishedCallback = SAVE_CALLBACK_NEW;
				showDialog(DIALOG_PROMPT_SAVE_ID);
			} else {
				onNew();
			}
		} else if (itemId == R.id.recent_files) {
			if (_editField.isDirty()) {
				_saveFinishedCallback = SAVE_CALLBACK_OPEN_RECENT;
				showDialog(DIALOG_PROMPT_SAVE_ID);
			} else {
				onOpenRecent();
			}
		} else if (itemId == R.id.open) {
			if (_editField.isDirty()) {
				_saveFinishedCallback = SAVE_CALLBACK_OPEN;
				showDialog(DIALOG_PROMPT_SAVE_ID);
			} else {
				onOpen();
			}
		} else if (itemId == R.id.save) {
			onSave();
		} else if (itemId == R.id.save_as) {
			onSaveAs();
		} else if (itemId == R.id.undo) {
			onUndo(true);
		} else if (itemId == R.id.redo) {
			onUndo(false);
		} else if (itemId == R.id.find_panel_toggle) {
			toggleFindPanel();
		} else if (itemId == R.id.go_to_line) {
			showDialog(DIALOG_GOTO_LINE_ID);
		} else if (itemId == R.id.cut) {
			cut();
		} else if (itemId == R.id.copy) {
			copy();
		} else if (itemId == R.id.paste) {
			paste();
		} else if (itemId == R.id.select_all) {
			_editField.selectAll();
		} else if (itemId == R.id.statistics) {
			analyzeTextProperties();
		} else if (itemId == R.id.change_input_method) {
			InputMethodManager im = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
			im.showInputMethodPicker();
		} else if (itemId == R.id.settings) {
			Intent i = new Intent(this, TextWarriorSettings.class);
			startActivity(i);
		} else if (itemId == R.id.help) {
			Intent h = new Intent(this, TextWarriorHelp.class);
			startActivity(h);
		} else if (itemId == R.id.about) {
			showDialog(DIALOG_ABOUT_ID);
		} else {
			return false;
		}
		return true;
	}

	// -----------------------------------------------------------------------
	// ------------------------- Menu item callbacks -------------------------

	private void onNew() {
		setModel(new TextBuffer());
		_filename = null;
		updateTitle();
	}

	// API 7 Eclair does not support a Bundle argument for showDialog(), which
	// makes
	// maintaining session state across chained dialogs more tedious.
	// When the user selects New or Open from a menu and is prompted to save the
	// current dirty file, a callback will be set to the appropriate action
	// after
	// the save is completed.
	// Several alternative flow of events might occur: file is read-only, file
	// does
	// not have a name, ran out of space for saving... These will display yet
	// another
	// dialog to the user on how to proceed, which might trigger even more
	// dialogs.
	// We have to make sure the callback is cancelled if the user aborts from
	// any of
	// these cases.
	protected int _saveFinishedCallback = CALLBACK_NONE;
	protected String _dialogErrorMsg = "";
	private final OnCancelListener cancelSaveCallback = new OnCancelListener() {
		@Override
		public void onCancel(DialogInterface dialog) {
			// cancel pending callback
			_saveFinishedCallback = CALLBACK_NONE;
		}
	};

	private void onOpen() {
		Intent i = new Intent(this,
				com.myopicmobile.textwarrior.android.FilePicker.class);
		i.setAction(TextWarriorIntents.ACTION_PICK_FILE);
		i.putExtra(TextWarriorIntents.EXTRA_TITLE,
				getString(R.string.file_picker_title_pick_file));
		i.setData(getLastSelectedUri());
		startActivityForResult(i, TextWarriorIntents.REQUEST_PICK_FILE);
	}

	private void onOpenRecent() {
		showDialog(DIALOG_OPEN_RECENT_ID);
	}

	private void onSave() {
		if (_filename == null) {
			onSaveAs(); // ask for a name for the untitled file
		} else {
			save(_filename, true);
		}
	}

	private void onSaveAs() {
		Intent i = new Intent(this,
				com.myopicmobile.textwarrior.android.FilePicker.class);
		i.setAction(TextWarriorIntents.ACTION_PICK_FILENAME_FOR_SAVE);
		i.putExtra(TextWarriorIntents.EXTRA_TITLE,
				getString(R.string.file_picker_title_enter_filename));
		i.putExtra(TextWarriorIntents.EXTRA_BUTTON_TEXT,
				getString(R.string.file_picker_label_save));
		i.setData(getLastSelectedUri());
		startActivityForResult(i,
				TextWarriorIntents.REQUEST_PICK_FILENAME_FOR_SAVE);
	}

	public void cut() {
		_editField.cut((ClipboardManager) getSystemService(CLIPBOARD_SERVICE));
		updateClipboardButtons();
	}

	public void copy() {
		_editField.copy((ClipboardManager) getSystemService(CLIPBOARD_SERVICE));
		updateClipboardButtons();
	}

	public void paste() {
		ClipboardManager cb = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
		_editField.paste(cb.getText().toString());
	}

	// if undo is false, a redo is done instead
	private void onUndo(boolean undo) {
		DocumentProvider doc = _editField.createDocumentProvider();
		int newPosition = undo ? doc.undo() : doc.redo();

		if (newPosition >= 0) {
			if (undo && !doc.canUndo()) {
				_editField.setDirty(false);
			} else {
				_editField.setDirty(true);
			}

			_editField.respan();
			_editField.selectText(false);
			_editField.moveCaret(newPosition);
			_editField.invalidate();
		}
	}

	public void goToRow(int rowIndex) {
		DocumentProvider src = _editField.createDocumentProvider();

		if (rowIndex < 0) {
			rowIndex = 0;
		}

		int charOffset = src.getStartCharOfRow(rowIndex);
		if (charOffset == -1) {
			// set to last row
			rowIndex = src.rowCount() - 1;
			charOffset = src.getStartCharOfRow(rowIndex);
		}
		_editField.moveCaret(charOffset);
	}

	private void analyzeTextProperties() {
		DocumentProvider doc = _editField.createDocumentProvider();
		int start, end;
		if (_editField.isSelectText()) {
			start = _editField.getSelectionStart();
			end = _editField.getSelectionEnd();
		} else {
			start = 0;
			end = doc.docLength();
		}
		_taskAnalyze = new AnalyzeStatisticsThread(doc, start, end);
		_taskAnalyze.registerObserver(this);

		PollingProgressDialog dialog = new PollingProgressDialog(this,
				_taskAnalyze, true, true);
		dialog.startDelayedPollingDialog();
		_taskAnalyze.start();
	}

	private void toggleFindPanel() {
		if (_findPanel.getVisibility() == View.VISIBLE) {
			_findPanel.setVisibility(View.GONE);
		} else {
			_findPanel.setVisibility(View.VISIBLE);
			_findPanel.requestFocus();
		}
	}

	// -----------------------------------------------------------------------
	// --------------------------- Dialog methods ----------------------------

	protected Dialog onCreateDialog(int id) {
		Dialog dialog = null;
		AlertDialog.Builder builder = new AlertDialog.Builder(this);
		switch (id) {
		case DIALOG_OPEN_RECENT_ID:
			dialog = createOpenRecentFileDialog(builder);
			break;

		case DIALOG_PROMPT_SAVE_ID:
			dialog = createPromptSaveDialog(builder);
			break;

		case DIALOG_CONFIRM_OVERWRITE_ID:
			dialog = createConfirmOverwriteDialog(builder);
			break;

		case DIALOG_GOTO_LINE_ID:
			dialog = createGotoLineDialog(builder);
			break;

		case DIALOG_STATISTICS_ID:
			dialog = createStatisticsDialog(builder);
			break;

		case DIALOG_OPEN_AGAIN_ID:
			dialog = createOpenAgainDialog(builder);
			break;

		case DIALOG_SAVE_AGAIN_ID:
			dialog = createSaveAgainDialog(builder);
			break;

		case DIALOG_PREV_INSTANCE_CRASHED_ID:
			dialog = createPrevInstanceCrashedDialog(builder);
			break;

		case DIALOG_ABOUT_ID:
			dialog = createAboutDialog(builder);
			break;

		default:
			TextWarriorException.assertVerbose(false, "Invalid dialog ID");
			break;
		}
		return dialog;
	}

	private Dialog createOpenAgainDialog(AlertDialog.Builder builder) {
		builder.setTitle(_dialogErrorMsg);
		builder.setIcon(android.R.drawable.ic_dialog_alert);
		builder.setNeutralButton(R.string.dialog_button_go_back,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.dismiss();
						TextWarriorApplication.this.onOpen();
					}
				});
		builder.setNegativeButton(android.R.string.cancel,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.cancel();
					}
				});
		return builder.create();
	}

	private Dialog createSaveAgainDialog(AlertDialog.Builder builder) {
		builder.setTitle(_dialogErrorMsg);
		builder.setIcon(android.R.drawable.ic_dialog_alert);
		builder.setNeutralButton(R.string.dialog_button_save_different_name,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.dismiss();
						TextWarriorApplication.this.onSaveAs();
					}
				});
		builder.setNegativeButton(android.R.string.cancel,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.cancel();
					}
				});
		Dialog dialog = builder.create();
		dialog.setOnCancelListener(cancelSaveCallback);
		return dialog;
	}

	private Dialog createConfirmOverwriteDialog(AlertDialog.Builder builder) {
		builder.setMessage(R.string.dialog_confirm_overwrite);
		builder.setPositiveButton(R.string.dialog_button_overwrite,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.dismiss();
						TextWarriorApplication.this.save(_lastSelectedFile,
								true);
					}
				});
		builder.setNeutralButton(R.string.dialog_button_go_back,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.dismiss();
						TextWarriorApplication.this.onSaveAs();
					}
				});
		builder.setNegativeButton(android.R.string.cancel,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.cancel();
					}
				});
		Dialog dialog = builder.create();
		dialog.setOnCancelListener(cancelSaveCallback);
		return dialog;
	}

	private Dialog createPromptSaveDialog(AlertDialog.Builder builder) {
		builder.setTitle(R.string.dialog_prompt_save);
		builder.setIcon(android.R.drawable.ic_dialog_alert);
		builder.setPositiveButton(R.string.dialog_button_save,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.dismiss();
						TextWarriorApplication.this.onSave();
					}
				});
		builder.setNeutralButton(R.string.dialog_button_discard,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.dismiss();
						TextWarriorApplication.this.saveFinishedCallback();
					}
				});
		builder.setNegativeButton(android.R.string.cancel,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.cancel();
					}
				});
		Dialog dialog = builder.create();
		dialog.setOnCancelListener(cancelSaveCallback);
		return dialog;
	}

	private Dialog createGotoLineDialog(AlertDialog.Builder builder) {
		builder.setTitle(R.string.dialog_go_to_line);
		LinearLayout content = (LinearLayout) getLayoutInflater().inflate(
				R.layout.goto_line_dialog_content, null);
		final EditText lineNumberField = (EditText) content
				.findViewById(R.id.dialog_go_to_line_text);
		/*
		 * TODO implement imeAction
		 * lineNumberField.setOnEditorActionListener(new
		 * OnEditorActionListener(){
		 * 
		 * @Override public boolean onEditorAction(TextView v, int actionId,
		 * KeyEvent event) { if(actionId == EditorInfo.IME_ACTION_GO || actionId
		 * == EditorInfo.IME_NULL){ String intStr =
		 * lineNumberField.getText().toString(); if(intStr.length() > 0){
		 * TextWarriorApplication.this.goToRow( Integer.parseInt(intStr) - 1); }
		 * dialog.dismiss(); return true; } return false; } });
		 */

		builder.setView(content);
		builder.setPositiveButton(android.R.string.ok,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						String intStr = lineNumberField.getText().toString();
						if (intStr.length() > 0) {
							TextWarriorApplication.this.goToRow(Integer
									.parseInt(intStr) - 1);
						}
						dialog.dismiss();
					}
				});
		builder.setNeutralButton(android.R.string.cancel,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.cancel();
					}
				});

		return builder.create();
	}

	// Too bad there is no way to retrieve a Dialog's content view, so it has
	// to be stored as a member variable
	private ViewGroup _recentFilesLayout;
	private ViewGroup _statisticsLayout;

	private Dialog createOpenRecentFileDialog(AlertDialog.Builder builder) {
		builder.setTitle(R.string.dialog_recent_files);
		_recentFilesLayout = (FrameLayout) getLayoutInflater().inflate(
				R.layout.recent_filelist, null);

		builder.setView(_recentFilesLayout);
		builder.setNegativeButton(android.R.string.cancel,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.cancel();
					}
				});
		final Dialog dialog = builder.create();

		ListView fileListView = (ListView) _recentFilesLayout
				.findViewById(R.id.recent_files_list);
		IconifiedTextListAdapter itla = new IconifiedTextListAdapter(this);
		fileListView.setAdapter(itla);
		populateRecentFilesDialog(dialog);

		fileListView.setOnItemClickListener(new OnItemClickListener() {

			@Override
			public void onItemClick(AdapterView<?> parent, View view,
					int position, long id) {
				IconifiedTextListAdapter adapter = (IconifiedTextListAdapter) parent
						.getAdapter();

				if (adapter == null) {
					return;
				}
				IconifiedText selEntry = (IconifiedText) adapter
						.getItem(position);
				String selFilename = selEntry.getInfo();
				open(selFilename);
				dialog.dismiss();
			}
		});
		return dialog;
	}

	private void populateRecentFilesDialog(Dialog dialog) {
		List<String> fileList = _recentFiles.getRecentFiles();

		ListView fileListView = (ListView) _recentFilesLayout
				.findViewById(R.id.recent_files_list);
		TextView emptyText = (TextView) _recentFilesLayout
				.findViewById(R.id.recent_files_empty);

		if (fileList.size() > 0) {
			fileListView.setVisibility(View.VISIBLE);
			emptyText.setVisibility(View.GONE);

			ArrayList<IconifiedText> iconifiedEntries = new ArrayList<IconifiedText>();
			ListIterator<String> filesIter = fileList.listIterator();

			Drawable iconFile = getResources()
					.getDrawable(R.drawable.icon_file);
			Drawable iconMissing = getResources().getDrawable(
					R.drawable.icon_file_missing);
			while (filesIter.hasNext()) {
				String fullFilename = filesIter.next();
				File file = new File(fullFilename);
				String filename = file.getName();
				Drawable icon = iconFile;

				IconifiedText entry = new IconifiedText(filename, fullFilename,
						icon);

				if (!file.exists()) {
					entry.setSelectable(false);
					entry.setText(filename + " ("
							+ getString(R.string.file_picker_file_not_found)
							+ ")");
					entry.setIcon(iconMissing);
				}
				iconifiedEntries.add(entry);
			}
			IconifiedTextListAdapter adapter = (IconifiedTextListAdapter) fileListView
					.getAdapter();
			adapter.setListItems(iconifiedEntries, false);
			adapter.notifyDataSetChanged();
		} else {
			emptyText.setVisibility(View.VISIBLE);
			fileListView.setVisibility(View.GONE);
		}
	}

	private Dialog createStatisticsDialog(AlertDialog.Builder builder) {
		_statisticsLayout = (ScrollView) getLayoutInflater().inflate(
				R.layout.statistics, null);

		builder.setView(_statisticsLayout);
		builder.setIcon(android.R.drawable.ic_dialog_info);
		builder.setTitle(R.string.dialog_statistics);
		builder.setNeutralButton(android.R.string.ok,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.dismiss();
					}
				});
		updateStatisticsDialog();
		return builder.create();
	}

	// API 7 Eclair does not support a Bundle argument for showDialog(), so
	// the contents of the statistics dialog has to be saved in a member
	// variable
	// before showDialog() is called, and then accessed here when the dialog
	// is being prepared to be shown.
	/** word count, line count etc. Calculated on demand */
	private CharEncodingUtils.Statistics _statistics = new CharEncodingUtils.Statistics();

	private void updateStatisticsDialog() {
		TextView t = (TextView) _statisticsLayout
				.findViewById(R.id.statistics_word_count);
		t.setText(Integer.toString(_statistics.wordCount));

		t = (TextView) _statisticsLayout
				.findViewById(R.id.statistics_char_count);
		t.setText(Integer.toString(_statistics.charCount));

		t = (TextView) _statisticsLayout
				.findViewById(R.id.statistics_char_no_whitespace_count);
		t.setText(Integer.toString(_statistics.charCount
				- _statistics.whitespaceCount));

		t = (TextView) _statisticsLayout
				.findViewById(R.id.statistics_row_count);
		t.setText(Integer.toString(_statistics.lineCount));

		DocumentProvider doc = _editField.createDocumentProvider();

		t = (TextView) _statisticsLayout.findViewById(R.id.statistics_format);
		t.setText(doc.getEncodingScheme());

		t = (TextView) _statisticsLayout
				.findViewById(R.id.statistics_line_terminator_style);
		t.setText(doc.getEOLType());
	}

	private Dialog createPrevInstanceCrashedDialog(AlertDialog.Builder builder) {
		builder.setTitle(R.string.dialog_sorry);
		builder.setIcon(android.R.drawable.ic_dialog_alert);
		builder.setMessage(R.string.dialog_sorry_for_the_crash);
		builder.setNeutralButton(R.string.dialog_argh,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.dismiss();
					}
				});
		Dialog dialog = builder.create();
		return dialog;
	}

	private Dialog createAboutDialog(AlertDialog.Builder builder) {
		builder.setTitle(R.string.dialog_about);
		builder.setIcon(android.R.drawable.ic_dialog_info);
		ScrollView aboutText = (ScrollView) getLayoutInflater().inflate(
				R.layout.about_dialog_content, null);
		builder.setView(aboutText);
		builder.setNeutralButton(android.R.string.ok,
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.dismiss();
					}
				});
		return builder.create();
	}

	protected void onPrepareDialog(int id, Dialog dialog) {
		switch (id) {
		case DIALOG_OPEN_RECENT_ID:
			populateRecentFilesDialog(dialog);
			break;

		case DIALOG_OPEN_AGAIN_ID: // fall-through
		case DIALOG_SAVE_AGAIN_ID:
			((AlertDialog) dialog).setTitle(_dialogErrorMsg);
			break;

		case DIALOG_STATISTICS_ID:
			updateStatisticsDialog();
			break;

		default:
			// do nothing
			break;
		}
	}

	protected void dismissAllDialogs() {
		removeDialog(DIALOG_PREV_INSTANCE_CRASHED_ID);
		removeDialog(DIALOG_OPEN_AGAIN_ID);
		removeDialog(DIALOG_SAVE_AGAIN_ID);
		removeDialog(DIALOG_CONFIRM_OVERWRITE_ID);
		removeDialog(DIALOG_PROMPT_SAVE_ID);
		removeDialog(DIALOG_OPEN_RECENT_ID);
		removeDialog(DIALOG_GOTO_LINE_ID);
		removeDialog(DIALOG_STATISTICS_ID);
		removeDialog(DIALOG_ABOUT_ID);
	}

	// -----------------------------------------------------------------------
	// --------------------- Find and replace methods ------------------------

	/**
	 * Switch focus between the find panel and the main editing area
	 * 
	 * @return If the focus was switched successfully between the find panel and
	 *         main editing area
	 */
	private boolean togglePanelFocus() {
		if (_findPanel.getVisibility() == View.VISIBLE) {
			if (_editField.isFocused()) {
				_findPanel.requestFocus();
			} else {
				_editField.requestFocus();
			}

			return true;
		}
		return false;
	}

	public void find(String what, boolean isCaseSensitive, boolean isWholeWord) {
		if (what.length() > 0) {
			int startingPosition = _editField.isSelectText() ? _editField
					.getSelectionStart() + 1
					: _editField.getCaretPosition() + 1;

			_taskFind = FindThread.createFindThread(
					_editField.createDocumentProvider(), what,
					startingPosition, true, isCaseSensitive, isWholeWord);
			_taskFind.registerObserver(this);

			PollingProgressDialog dialog = new PollingProgressDialog(this,
					_taskFind, false, false);
			dialog.startDelayedPollingDialog();
			_taskFind.start();
		}
	}

	public void findBackwards(String what, boolean isCaseSensitive,
			boolean isWholeWord) {
		if (what.length() > 0) {
			int startingPosition = _editField.isSelectText() ? _editField
					.getSelectionStart() - 1
					: _editField.getCaretPosition() - 1;

			_taskFind = FindThread.createFindThread(
					_editField.createDocumentProvider(), what,
					startingPosition, false, isCaseSensitive, isWholeWord);
			_taskFind.registerObserver(this);

			PollingProgressDialog dialog = new PollingProgressDialog(this,
					_taskFind, false, false);
			dialog.startDelayedPollingDialog();
			_taskFind.start();
		}
	}

	public void replaceSelection(String replacementText) {
		if (_editField.isSelectText()) {
			_editField.paste(replacementText);
		}
	}

	public void replaceAll(String what, String replacementText,
			boolean isCaseSensitive, boolean isWholeWord) {
		if (what.length() > 0) {
			int startingPosition = _editField.getCaretPosition();
			_taskFind = FindThread.createReplaceAllThread(
					_editField.createDocumentProvider(), what, replacementText,
					startingPosition, isCaseSensitive, isWholeWord);
			_taskFind.registerObserver(this);

			PollingProgressDialog dialog = new PollingProgressDialog(this,
					_taskFind, true, false);
			dialog.startDelayedPollingDialog();
			_taskFind.start();
		}
	}

	// -----------------------------------------------------------------------
	// ----------------------- Open and save methods -------------------------

	private Uri getLastSelectedUri() {
		if (_lastSelectedFile == null) {
			return null;
		}

		Uri.Builder ub = new Uri.Builder();
		ub.scheme("file://");
		ub.appendPath(_lastSelectedFile);
		return ub.build();
	}

	public void open(String filename) {
		_lastSelectedFile = filename;

		SharedPreferences prefs = PreferenceManager
				.getDefaultSharedPreferences(this);
		String encoding = prefs.getString(
				getString(R.string.settings_key_file_input_format),
				EncodingScheme.TEXT_ENCODING_AUTO);
		String eolChar = prefs.getString(
				getString(R.string.settings_key_line_terminator_style),
				EncodingScheme.LINE_BREAK_AUTO);

		File inputFile = new File(filename);
		_newDoc = new TextBuffer();
		_taskRead = new ReadThread(inputFile, _newDoc, encoding, eolChar);
		_taskRead.registerObserver(this); // so that readTask can notify
											// TextWarriorApplication when done

		PollingProgressDialog dialog = new PollingProgressDialog(this,
				_taskRead, true, true);
		dialog.startDelayedPollingDialog();
		_taskRead.start();
	}

	/**
	 * Preconditions: 1. filename is not a directory 2. filename does not
	 * contain illegal symbols used by the file system (For example, in FAT
	 * systems, <>!* are not allowed in filenames) 3. if filename refers to a
	 * file that has not been created yet, the user has write access to the
	 * containing directory
	 * 
	 */
	public void save(String filename, boolean overwrite) {
		_lastSelectedFile = filename;
		File outputFile = new File(filename);

		if (outputFile.exists()) {
			if (!outputFile.canWrite()) {
				_dialogErrorMsg = getString(R.string.dialog_error_attempt_overwrite_read_only);
				showDialog(DIALOG_SAVE_AGAIN_ID);
				return;
			}

			if (!overwrite) {
				showDialog(DIALOG_CONFIRM_OVERWRITE_ID);
				return;
			}
		}

		SharedPreferences prefs = PreferenceManager
				.getDefaultSharedPreferences(this);
		String encoding = prefs.getString(
				getString(R.string.settings_key_file_output_format),
				EncodingScheme.TEXT_ENCODING_AUTO);
		String eolChar = prefs.getString(
				getString(R.string.settings_key_line_terminator_style),
				EncodingScheme.LINE_BREAK_AUTO);

		_taskWrite = new WriteThread(outputFile,
				_editField.createDocumentProvider(), encoding, eolChar);
		_taskWrite.registerObserver(this);

		PollingProgressDialog dialog = new PollingProgressDialog(this,
				_taskWrite, true, true);
		dialog.startDelayedPollingDialog();
		_taskWrite.start();
	}

	private void displayOpenError(String msg) {
		Log.e(this.toString(), msg);
		_dialogErrorMsg = getString(R.string.dialog_error_file_open) + msg;
		showDialog(DIALOG_OPEN_AGAIN_ID);
	}

	private void displaySaveError(String msg) {
		Log.e(this.toString(), msg);
		_dialogErrorMsg = getString(R.string.dialog_error_file_save) + msg;
		// TODO add hints on how how to resolve error.
		// see R.string.dialog_msg_save_file_error_recovery_tips
		showDialog(DIALOG_SAVE_AGAIN_ID);
	}

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		switch (requestCode) {
		case TextWarriorIntents.REQUEST_PICK_FILE:
			if (resultCode == RESULT_OK) {
				open(data.getData().getPath());
			}
			break;
		case TextWarriorIntents.REQUEST_PICK_FILENAME_FOR_SAVE:
			if (resultCode == RESULT_OK) {
				save(data.getData().getPath(), false);
			} else {
				// save cancelled by user; remove pending callback
				_saveFinishedCallback = CALLBACK_NONE;
			}
			break;
		default:
			super.onActivityResult(requestCode, resultCode, data);
		}
	}

	@Override
	// This method is called by various worker threads
	public void onComplete(final int requestCode, final Object result) {
		runOnUiThread(new Runnable() {
			public void run() {
				if (requestCode == ProgressSource.READ) {
					setModel(_newDoc);
					_filename = _lastSelectedFile;
					_recentFiles.addRecentFile(_lastSelectedFile);

					/*
					 * int recoveryFileType = getRecoveryFileType();
					 * if(recoveryFileType == RECOVERY_FILE_TEMP){
					 * SharedPreferences recoveryFilePrefs =
					 * getSharedPreferences(PREFS_RECOVERY_FILE, 0); String
					 * sourceFile =
					 * recoveryFilePrefs.getString(RECOVERY_STATE_SOURCE_FILE,
					 * ""); _filename = _lastSelectedFile = sourceFile;
					 * restoreUiState(_recoveredUiState);
					 * resetRecoveryFilePrefs(); } else if(recoveryFileType ==
					 * RECOVERY_FILE_SOURCE){ _filename = _lastSelectedFile;
					 * restoreUiState(_recoveredUiState);
					 * resetRecoveryFilePrefs(); }
					 */

					updateTitle();
					_taskRead = null;
				}

				else if (requestCode == ProgressSource.WRITE) {
					_filename = _lastSelectedFile;
					updateTitle();
					_recentFiles.addRecentFile(_lastSelectedFile);
					_editField.setDirty(false);
					Toast.makeText(TextWarriorApplication.this,
							R.string.dialog_file_save_success,
							Toast.LENGTH_SHORT).show();
					saveFinishedCallback();
					_taskWrite = null;
				}

				else if (requestCode == ProgressSource.FIND
						|| requestCode == ProgressSource.FIND_BACKWARDS) {
					final int foundIndex = ((FindResults) result).foundOffset;
					final int length = ((FindResults) result).searchTextLength;

					if (foundIndex != -1) {
						_editField.setSelectionRange(foundIndex, length);
					} else {
						Toast.makeText(TextWarriorApplication.this,
								R.string.dialog_find_no_results,
								Toast.LENGTH_SHORT).show();
					}
					_taskFind = null;
				}

				else if (requestCode == ProgressSource.REPLACE_ALL) {
					final int replacementCount = ((FindResults) result).replacementCount;
					final int newCaretPosition = ((FindResults) result).newStartPosition;
					if (replacementCount > 0) {
						_editField.setDirty(true);
						_editField.selectText(false);
						_editField.moveCaret(newCaretPosition);
						_editField.respan();
						_editField.invalidate(); // TODO reduce invalidate calls
					}
					Toast.makeText(
							TextWarriorApplication.this,
							getString(R.string.dialog_replace_all_result)
									+ replacementCount, Toast.LENGTH_SHORT)
							.show();
					_taskFind = null;
				} else if (requestCode == ProgressSource.ANALYZE_TEXT) {
					_statistics = (CharEncodingUtils.Statistics) result;
					_taskAnalyze = null;
					showDialog(DIALOG_STATISTICS_ID);
				}
			}
		});
	}

	@Override
	public void onError(final int requestCode, final int errorCode,
			final String message) {
		runOnUiThread(new Runnable() {
			public void run() {
				if (requestCode == ProgressSource.READ) {
					_taskRead = null;
					displayOpenError(message);
					/*
					 * if(getRecoveryFileType() != RECOVERY_FILE_NONE){ //XXX
					 * inform user that edits could not be recovered
					 * resetRecoveryFilePrefs(); }
					 */
				} else if (requestCode == ProgressSource.WRITE) {
					_taskWrite = null;
					displaySaveError(message);
				} else if (requestCode == ProgressSource.FIND
						|| requestCode == ProgressSource.FIND_BACKWARDS
						|| requestCode == ProgressSource.REPLACE_ALL) {
					_taskFind = null;
				} else if (requestCode == ProgressSource.ANALYZE_TEXT) {
					_taskAnalyze = null;
				}
			}
		});
	}

	@Override
	public void onCancel(int requestCode) {
		if (requestCode == ProgressSource.READ) {
			_taskRead = null;
			/*
			 * if(getRecoveryFileType() != RECOVERY_FILE_NONE){
			 * resetRecoveryFilePrefs(); }
			 */
		} else if (requestCode == ProgressSource.WRITE) {
			_taskWrite = null;
		} else if (requestCode == ProgressSource.FIND
				|| requestCode == ProgressSource.FIND_BACKWARDS
				|| requestCode == ProgressSource.REPLACE_ALL) {
			_taskFind = null;
		} else if (requestCode == ProgressSource.ANALYZE_TEXT) {
			_taskAnalyze = null;
		}
	}

	protected void saveFinishedCallback() {
		if (_saveFinishedCallback == SAVE_CALLBACK_NEW) {
			onNew();
		} else if (_saveFinishedCallback == SAVE_CALLBACK_OPEN_RECENT) {
			onOpenRecent();
		} else if (_saveFinishedCallback == SAVE_CALLBACK_OPEN) {
			onOpen();
		} else if (_saveFinishedCallback == SAVE_CALLBACK_EXIT) {
			finish();
		}
		_saveFinishedCallback = CALLBACK_NONE;
	}

	// -----------------------------------------------------------------------
	// ------------------------- UI event handlers ---------------------------

	@Override
	public void onRowChange(int newRowIndex) {
		SharedPreferences pref = PreferenceManager
				.getDefaultSharedPreferences(this);
		boolean showLineNumbers = pref.getBoolean(
				getString(R.string.settings_key_show_row_number), false);
		if (showLineNumbers) {
			// change from 0-based to 1-based indexing
			setTitle(createTitle(newRowIndex + 1));
		}
	}

	@Override
	public void onSelectionModeChanged(boolean active) {
		SharedPreferences pref = PreferenceManager
				.getDefaultSharedPreferences(this);
		if (active && _editField.isFocused() && !_clipboardPanel.isOpen()) {
			boolean autoOpen = pref.getBoolean(
					getString(R.string.settings_key_auto_open_clipboard), true);
			if (autoOpen) {
				_clipboardPanel.setOpen(true, true);
			}
		} else if (!active && _editField.isFocused()
				&& _clipboardPanel.isOpen()) {
			boolean autoClose = pref.getBoolean(
					getString(R.string.settings_key_auto_close_clipboard),
					false);
			if (autoClose) {
				_clipboardPanel.setOpen(false, true);
			}
		}
		updateClipboardButtons();
	}

	@Override
	public boolean dispatchKeyEvent(KeyEvent event) {
		boolean handled = false;

		// Intercept keystroke shortcuts
		if (KeysInterpreter.isSwitchPanel(event)
				&& event.getAction() == KeyEvent.ACTION_DOWN) {
			// the view gaining focus must be able to ignore the corresponding
			// key up event sent to it when the key is released
			handled = togglePanelFocus();
		}

		if (!handled) {
			handled = super.dispatchKeyEvent(event);
		}
		return handled;
	}

	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		if (keyCode == KeyEvent.KEYCODE_SEARCH) {
			// handle accidental touching of virtual hard keys as described in
			// http://android-developers.blogspot.com/2009/12/back-and-other
			// -hard-keys-three-stories.html
			event.startTracking();
			return true;
		}

		return super.onKeyDown(keyCode, event);
	}

	@Override
	public boolean onKeyUp(int keyCode, KeyEvent event) {
		if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isCanceled()
				&& event.isTracking()) {
			toggleFindPanel();
			return true;
		}

		return super.onKeyUp(keyCode, event);
	}

	@Override
	public void onBackPressed() {
		if (_editField.isDirty()) {
			_saveFinishedCallback = SAVE_CALLBACK_EXIT;
			showDialog(DIALOG_PROMPT_SAVE_ID);
		} else {
			finish();
		}
	}

	@Override
	public void onSharedPreferenceChanged(SharedPreferences pref, String key) {

		if (key.equals(getString(R.string.settings_key_zoom_size))) {
			setZoom(pref);
		} else if (key.equals(getString(R.string.settings_key_font))) {
			setFont(pref);
		} else if (key
				.equals(getString(R.string.settings_key_navigation_method))) {
			setNavigationMethod(pref);
		} else if (key.equals(getString(R.string.settings_key_syntax_color))) {
			setSyntaxColor(pref);
		} else if (key.equals(getString(R.string.settings_key_tab_spaces))) {
			setTabSpaces(pref);
		} else if (key.equals(getString(R.string.settings_key_show_row_number))) {
			updateTitle();
		}
	}

	// -----------------------------------------------------------------------
	// ----------------------- Preference settings ---------------------------

	private void setSyntaxColor(SharedPreferences pref) {
		String syntaxKey = getString(R.string.settings_key_syntax_color);
		String lang = pref.getString(syntaxKey,
				getString(R.string.settings_syntax_none));

		if (lang.equals(getString(R.string.settings_syntax_c))) {
			Lexer.setLanguage(LanguageC.getCharacterEncodings());
			_editField.respan();
		} else if (lang.equals(getString(R.string.settings_syntax_cpp))) {
			Lexer.setLanguage(LanguageCpp.getCharacterEncodings());
			_editField.respan();
		} else if (lang.equals(getString(R.string.settings_syntax_csharp))) {
			Lexer.setLanguage(LanguageCsharp.getCharacterEncodings());
			_editField.respan();
		} else if (lang.equals(getString(R.string.settings_syntax_java))) {
			Lexer.setLanguage(LanguageJava.getCharacterEncodings());
			_editField.respan();
		} else if (lang.equals(getString(R.string.settings_syntax_javascript))) {
			Lexer.setLanguage(LanguageJavascript.getCharacterEncodings());
			_editField.respan();
		} else if (lang.equals(getString(R.string.settings_syntax_objc))) {
			Lexer.setLanguage(LanguageObjectiveC.getCharacterEncodings());
			_editField.respan();
		} else if (lang.equals(getString(R.string.settings_syntax_php))) {
			Lexer.setLanguage(LanguagePHP.getCharacterEncodings());
			_editField.respan();
		} else if (lang.equals(getString(R.string.settings_syntax_python))) {
			Lexer.setLanguage(LanguagePython.getCharacterEncodings());
			_editField.respan();
		} else if (lang.equals(getString(R.string.settings_syntax_ruby))) {
			Lexer.setLanguage(LanguageRuby.getCharacterEncodings());
			_editField.respan();
		} else {
			TextWarriorException.assertVerbose(
					lang.equals(getString(R.string.settings_syntax_none)),
					"Unsupported language set for syntax highlighting");
			Lexer.setLanguage(LanguageNonProg.getCharacterEncodings());
			_editField.cancelSpanning();
			_editField.createDocumentProvider().clearSpans();
		}
	}

	private void setNavigationMethod(SharedPreferences pref) {
		String navKey = getString(R.string.settings_key_navigation_method);
		String nav = pref.getString(navKey,
				getString(R.string.settings_navigation_method_trackpad));

		if (nav.equals(getString(R.string.settings_navigation_method_vol_keys))) {
			_editField.setNavigationMethod(new VolumeKeysNavigationMethod(
					_editField));
		} else if (nav
				.equals(getString(R.string.settings_navigation_method_trackpad))) {
			_editField.setNavigationMethod(new TrackpadNavigationMethod(
					_editField));
		} else if (nav
				.equals(getString(R.string.settings_navigation_method_yoyo))) {
			_editField
					.setNavigationMethod(new YoyoNavigationMethod(_editField));
		} else if (nav
				.equals(getString(R.string.settings_navigation_method_liquid))) {
			_editField.setNavigationMethod(new LiquidNavigationMethod(
					_editField));
		} else {
			_editField
					.setNavigationMethod(new TouchNavigationMethod(_editField));
		}
	}

	private final static String FONT_PATH_BITSTREAM_VERA = "typefaces/VeraMono.ttf";
	private final static String FONT_PATH_PROGGY_CLEAN = "typefaces/ProggyCleanSZ.ttf";

	private void setFont(SharedPreferences pref) {
		String fontKey = getString(R.string.settings_key_font);
		String font = pref.getString(fontKey,
				getString(R.string.settings_font_sans_serif));

		if (font.equals(getString(R.string.settings_font_sans_serif))) {
			_editField.setTypeface(Typeface.SANS_SERIF);
		} else if (font.equals(getString(R.string.settings_font_serif))) {
			_editField.setTypeface(Typeface.SERIF);
		} else if (font.equals(getString(R.string.settings_font_monospace))) {
			_editField.setTypeface(Typeface.MONOSPACE);
		} else if (font
				.equals(getString(R.string.settings_font_bitstream_vera))) {
			Typeface t = Typeface.createFromAsset(getAssets(),
					FONT_PATH_BITSTREAM_VERA);
			_editField.setTypeface(t);
		} else if (font.equals(getString(R.string.settings_font_proggy_clean))) {
			Typeface t = Typeface.createFromAsset(getAssets(),
					FONT_PATH_PROGGY_CLEAN);
			_editField.setTypeface(t);
		} else {
			TextWarriorException.assertVerbose(false, "Unsupported font");
			Log.w(LOG_TAG, "Unsupported font");
		}
	}

	private void setZoom(SharedPreferences pref) {
		String zoomKey = getString(R.string.settings_key_zoom_size);
		String zoomStr = pref.getString(zoomKey,
				getString(R.string.settings_zoom_default));
		_editField.setZoom(Integer.parseInt(zoomStr) / 100.0f);
	}

	private void setTabSpaces(SharedPreferences pref) {
		String tabSpacesKey = getString(R.string.settings_key_tab_spaces);
		String tabSpacesStr = pref.getString(tabSpacesKey,
				getString(R.string.settings_tab_spaces_default));
		_editField.setTabSpaces(Integer.parseInt(tabSpacesStr));
	}

	// -----------------------------------------------------------------------
	// ------------------- Android lifecycle methods -------------------------

	@Override
	protected void onPause() {
		super.onPause();
		_recentFiles.save();
		_editField.onPause();
	}

	@Override
	protected void onResume() {
		super.onResume();
		_editField.onResume();
	}

	/*
	 * // The only safe way of saving edited text to a temp (rescue) file is in
	 * // onPause(). However, this is problematic because saving of files can
	 * take // an arbitrarily long time, and onPause() is supposed to be
	 * lightweight. // Temp file saving can be done in onDestroy(), but this
	 * means that if the // system decides to kill the activity's process, all
	 * edits will be lost. protected void onPause(){ ...same as existing code...
	 * 
	 * //save edits in a temp file if process is being force-killed to reclaim
	 * memory if(!isFinishing() && !configurationChange) { //XXX Associate temp
	 * file metadata with a particular activity instance. // Example bug:
	 * Activity has 2 running instances A and B. A is force- // killed and
	 * metadata is saved in PREFS_RECOVERY_FILE. B is then force- // killed and
	 * overwrites the PREFS_RECOVERY_FILE. When A is run again, // it wrongly
	 * uses the metadata saved by B. SharedPreferences settings =
	 * getSharedPreferences(PREFS_RECOVERY_FILE, 0); SharedPreferences.Editor
	 * editor = settings.edit(); editor.putInt(RECOVERY_STATE_INTERRUPTED_TASKS,
	 * stopAllWorkerThreads()); String fileToReload = _filename; boolean isDirty
	 * = _editField.isDirty();
	 * 
	 * if(isDirty && !saveToRescueFile()){ fileToReload = ""; //abandon unsaved
	 * edits }
	 * 
	 * editor.putString(RECOVERY_STATE_SOURCE_FILE, fileToReload);
	 * editor.putBoolean(RECOVERY_STATE_DIRTY, isDirty); editor.commit(); } }
	 * 
	 * private boolean saveToRescueFile(){ boolean saveSuccess = false;
	 * 
	 * String state = Environment.getExternalStorageState(); //TODO 1: test if
	 * system will kill the activity before saving a large file is done //TODO
	 * 2: handle accessing of temp file by multiple instances of the activity if
	 * (Environment.MEDIA_MOUNTED.equals(state)) { CharEncodingUtils converter =
	 * new CharEncodingUtils(); String tempFilePath =
	 * Environment.getExternalStorageDirectory().getPath() + TEMP_FILE_PATH;
	 * 
	 * try{ FileOutputStream fs = new FileOutputStream(new File(tempFilePath));
	 * 
	 * try{ DocumentProvider doc = _editField.createDocumentProvider();
	 * converter.writeAndConvert(fs, doc, doc.getEncodingScheme(),
	 * doc.getEOLType(), new Flag()); saveSuccess = true; } finally{ fs.close();
	 * } } catch (IOException ex) { //do nothing; } }
	 * 
	 * return saveSuccess; }
	 * 
	 * private void resetRecoveryFilePrefs() { SharedPreferences.Editor editor =
	 * getSharedPreferences(PREFS_RECOVERY_FILE, 0).edit();
	 * editor.putString(RECOVERY_STATE_SOURCE_FILE, "");
	 * editor.putBoolean(RECOVERY_STATE_DIRTY, false);
	 * editor.putInt(RECOVERY_STATE_INTERRUPTED_TASKS, 0); editor.commit(); }
	 * 
	 * private int getRecoveryFileType() { SharedPreferences recoveryFilePrefs =
	 * getSharedPreferences(PREFS_RECOVERY_FILE, 0); String recoveryFile =
	 * recoveryFilePrefs.getString(RECOVERY_STATE_SOURCE_FILE, ""); boolean
	 * isDirty = recoveryFilePrefs.getBoolean(RECOVERY_STATE_DIRTY, false);
	 * 
	 * int type; if(recoveryFile.length() > 0){ if(isDirty){ type =
	 * RECOVERY_FILE_TEMP; } else{ type = RECOVERY_FILE_SOURCE; } } else
	 * if(isDirty){ type = RECOVERY_FILE_CREATE_ERROR; } else{ type =
	 * RECOVERY_FILE_NONE; }
	 * 
	 * return type; }
	 * 
	 * private UiState _recoveredUiState;
	 * 
	 * private static final int RECOVERY_FILE_NONE = 0; private static final int
	 * RECOVERY_FILE_SOURCE = 1; private static final int RECOVERY_FILE_TEMP =
	 * 2; private static final int RECOVERY_FILE_CREATE_ERROR = 4; private
	 * static final int RECOVERY_FILE_MISSING = 8;
	 * 
	 * private final static String PREFS_RECOVERY_FILE = "recoveryFilePrefs";
	 * private final static String RECOVERY_STATE_DIRTY = "dirty"; private final
	 * static String RECOVERY_STATE_SOURCE_FILE = "filename"; private final
	 * static String RECOVERY_STATE_INTERRUPTED_TASKS = "interruptedTasks";
	 * 
	 * private static final String TEMP_FILE_PATH =
	 * "/Android/data/com.myopicmobile.textwarrior.android/files/temp";
	 */

	@Override
	protected void onDestroy() {
		_editField.onDestroy();

		if (isFinishing()) {
			// Scenario 1: killed normally by user
			stopAllWorkerThreads();
		} else {
			// Scenario 2: killed by system because device configuration changed

			// leave existing worker threads running, to be reattached to
			// observer UI elements when the activity is re-created
			if (_taskRead != null) {
				_taskRead.removeObservers();
			}
			if (_taskWrite != null) {
				_taskWrite.removeObservers();
			}
			if (_taskFind != null) {
				_taskFind.removeObservers();
			}
			if (_taskAnalyze != null) {
				_taskAnalyze.removeObservers();
			}
		}

		super.onDestroy();
	}

	private void stopAllWorkerThreads() {
		if (_taskRead != null) {
			_taskRead.removeObservers();
			_taskRead.forceStop();
		}
		if (_taskWrite != null) {
			_taskWrite.removeObservers();
			_taskWrite.forceStop();
		}
		if (_taskFind != null) {
			_taskFind.removeObservers();
			_taskFind.forceStop();
		}
		if (_taskAnalyze != null) {
			_taskAnalyze.removeObservers();
			_taskAnalyze.forceStop();
		}
	}

	/**
	 * Contains application state that is expensive to reconstruct after a
	 * configuration change. Also contains non-UI state.
	 */
	private static class NonConfigurationState {
		DocumentProvider mDoc;
		TextBuffer mTmpBuffer;
		ReadThread mReadTask;
		WriteThread mWriteTask;
		FindThread mFindTask;
		AnalyzeStatisticsThread mAnalyzeTask;
		CharEncodingUtils.Statistics mStatistics;

		boolean mDirty;
		int mSaveCallback;
		String mFilename;
		String mPrevFilename;
		String mDialogErrMsg;
	}

	@Override
	public Object onRetainNonConfigurationInstance() {
		NonConfigurationState s = new NonConfigurationState();
		s.mDoc = _editField.createDocumentProvider();
		s.mTmpBuffer = _newDoc;
		s.mReadTask = _taskRead;
		s.mWriteTask = _taskWrite;
		s.mFindTask = _taskFind;
		s.mAnalyzeTask = _taskAnalyze;
		s.mStatistics = _statistics;

		s.mDirty = _editField.isDirty();
		s.mSaveCallback = _saveFinishedCallback;
		s.mFilename = _filename;
		s.mPrevFilename = _lastSelectedFile;
		s.mDialogErrMsg = _dialogErrorMsg;
		return s;
	}

	private void restoreNonConfigurationState(NonConfigurationState ncState) {
		if (ncState != null) {
			_editField.changeDocumentProvider(ncState.mDoc);
			_editField.setDirty(ncState.mDirty);
			_filename = ncState.mFilename;
			_lastSelectedFile = ncState.mPrevFilename;
			_dialogErrorMsg = ncState.mDialogErrMsg;
			_saveFinishedCallback = ncState.mSaveCallback;
			_newDoc = ncState.mTmpBuffer;
			_statistics = ncState.mStatistics;

			restoreDisplayedDialogs(ncState);
		}
	}

	@Override
	protected void onSaveInstanceState(Bundle outState) {
		super.onSaveInstanceState(outState);
		outState.putParcelable(STATE_UI, new UiState(this));
	}

	private void restoreUiState(UiState uiState) {
		final int caretPosition = uiState.mCaretPosition;
		if (uiState.mSelectMode) {
			final int selStart = uiState.mSelectBegin;
			final int selEnd = uiState.mSelectEnd;

			// allow _editField to layout first to get non-zero height and width
			_editField.post(new Runnable() {
				@Override
				public void run() {
					_editField.setSelectionRange(selStart, selEnd - selStart);
					if (caretPosition < selEnd) {
						_editField.focusSelectionStart(); // caret at the end by
															// default
					}
				}
			});
		} else {
			// allow _editField to layout first to get non-zero height and width
			_editField.post(new Runnable() {
				@Override
				public void run() {
					_editField.moveCaret(caretPosition);
				}
			});
		}

		if (uiState.mClipboardOpen) {
			_clipboardPanel.setOpen(true, false); // closed by default
		}

		_findPanel.setVisibility(uiState.mFindPanelVisibility);
	}

	private void restoreDisplayedDialogs(NonConfigurationState ss) {
		_taskRead = ss.mReadTask;
		_taskWrite = ss.mWriteTask;
		_taskFind = ss.mFindTask;
		_taskAnalyze = ss.mAnalyzeTask;

		// Observers previously attached to worker threads are invalid after
		// the activity restarts.
		// Reassign listeners and show progress dialogs if worker threads are
		// active

		// FIXME There is a small possibility that worker tasks might signal an
		// error or cancel event in the interval between onSaveInstanceState()
		// and
		// onRestoreInstanceState()/onCreate(). Hence, the error/cancel message
		// is lost. To solve this, worker tasks have to implement additional
		// error/cancel states that can be queried here.
		if (_taskRead != null) {
			if (_taskRead.isDone()) {
				onComplete(ProgressSource.READ, null);
			} else {
				_taskRead.registerObserver(this);
				PollingProgressDialog dialog = new PollingProgressDialog(this,
						_taskRead, true, true);
				dialog.startPollingDialog();
			}
		}

		else if (_taskWrite != null) {
			if (_taskRead.isDone()) {
				onComplete(ProgressSource.WRITE, null);
			} else {
				_taskWrite.registerObserver(this);
				PollingProgressDialog dialog = new PollingProgressDialog(this,
						_taskWrite, true, true);
				dialog.startPollingDialog();
			}
		}

		else if (_taskFind != null) {
			if (_taskAnalyze.isDone()) {
				onComplete(_taskFind.getRequestCode(), _taskFind.getResults());
			} else {
				_taskFind.registerObserver(this);
				PollingProgressDialog dialog;

				if (_taskFind.getRequestCode() == ProgressSource.REPLACE_ALL) {
					dialog = new PollingProgressDialog(this, _taskFind, true,
							false);
				} else {
					dialog = new PollingProgressDialog(this, _taskFind, false,
							false);
				}
				dialog.startPollingDialog();
			}
		}

		else if (_taskAnalyze != null) {
			if (_taskAnalyze.isDone()) {
				onComplete(ProgressSource.ANALYZE_TEXT,
						_taskAnalyze.getResults());
			} else {
				_taskAnalyze.registerObserver(this);
				PollingProgressDialog dialog = new PollingProgressDialog(this,
						_taskAnalyze, true, true);
				dialog.startPollingDialog();
			}
		}
	}

	static class UiState implements Parcelable {
		int mCaretPosition;
		boolean mSelectMode;
		int mSelectBegin;
		int mSelectEnd;
		int mFindPanelVisibility;
		boolean mClipboardOpen;

		@Override
		public int describeContents() {
			return 0;
		}

		public UiState(TextWarriorApplication app) {
			mCaretPosition = app._editField.getCaretPosition();
			mSelectMode = app._editField.isSelectText();
			mSelectBegin = app._editField.getSelectionStart();
			mSelectEnd = app._editField.getSelectionEnd();

			mFindPanelVisibility = app._findPanel.getVisibility();
			mClipboardOpen = app._clipboardPanel.isOpen();

		}

		private UiState(Parcel in) {
			mCaretPosition = in.readInt();
			mSelectMode = in.readInt() != 0;
			mSelectBegin = in.readInt();
			mSelectEnd = in.readInt();

			mFindPanelVisibility = in.readInt();
			mClipboardOpen = in.readInt() != 0;
		}

		@Override
		public void writeToParcel(Parcel out, int flags) {
			out.writeInt(mCaretPosition);
			out.writeInt(mSelectMode ? 1 : 0);
			out.writeInt(mSelectBegin);
			out.writeInt(mSelectEnd);

			out.writeInt(mFindPanelVisibility);
			out.writeInt(mClipboardOpen ? 1 : 0);
		}

		public static final Parcelable.Creator<UiState> CREATOR = new Parcelable.Creator<UiState>() {
			public UiState createFromParcel(Parcel in) {
				return new UiState(in);
			}

			public UiState[] newArray(int size) {
				return new UiState[size];
			}
		};
	}

	private ReadThread _taskRead = null;
	private WriteThread _taskWrite = null;
	private FindThread _taskFind = null;
	private AnalyzeStatisticsThread _taskAnalyze = null;

	static final String LOG_TAG = "TextWarrior";

	private static final int DIALOG_OPEN_AGAIN_ID = 0;
	private static final int DIALOG_SAVE_AGAIN_ID = 1;
	private static final int DIALOG_CONFIRM_OVERWRITE_ID = 2;
	private static final int DIALOG_PROMPT_SAVE_ID = 3;
	private static final int DIALOG_OPEN_RECENT_ID = 4;
	private static final int DIALOG_GOTO_LINE_ID = 5;
	private static final int DIALOG_STATISTICS_ID = 6;
	private static final int DIALOG_ABOUT_ID = 7;
	private static final int DIALOG_PREV_INSTANCE_CRASHED_ID = 8;

	private static final int SAVE_CALLBACK_NEW = 1;
	private static final int SAVE_CALLBACK_OPEN = 2;
	private static final int SAVE_CALLBACK_OPEN_RECENT = 3;
	private static final int SAVE_CALLBACK_EXIT = 4;
	private static final int CALLBACK_NONE = -1;

	// keys for saving and restoring application state
	private final static String STATE_UI = "ui";

}


<|start_filename|>src/com/myopicmobile/textwarrior/android/TrackpadNavigationMethod.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.android;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;

import com.myopicmobile.textwarrior.common.TextWarriorException;

public class TrackpadNavigationMethod extends TouchNavigationMethod{
	private boolean _isTrackpadPressed = false;
	private final Trackpad _trackpad = new Trackpad();
	
	public TrackpadNavigationMethod(FreeScrollingTextField textField){
		super(textField);
	}

	@Override
	public boolean onDown(MotionEvent e) {
		int x = (int) e.getX() + _textField.getScrollX();
		int y = (int) e.getY() + _textField.getScrollY();
		_isTrackpadPressed = _trackpad.isInTrackpad(x, y);
		
		if(_isTrackpadPressed){
			return true;
		}
		else{
			return super.onDown(e);
		}
	}

	@Override
	public boolean onUp(MotionEvent e) {
		if(_isTrackpadPressed){
			_isTrackpadPressed = false;
			_xAccum = 0.0f;
			_yAccum = 0.0f;
		}
		super.onUp(e);
		return true;
	}

	@Override
	public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
			float distanceY) {
		if(_isTrackpadPressed){
			if(_trackpad.isOpen()){
				moveCaretWithTrackpad(-distanceX, -distanceY);
			}
			//TODO find out if ACTION_UP events are actually passed to onScroll
			if ((e2.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP){
				onUp(e2);
			}
			
			return true;
		}
		else{
			return super.onScroll(e1, e2, distanceX, distanceY);
		}
	}
	
	@Override
	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
			float velocityY) {
		if(_isTrackpadPressed){
			onUp(e2);
			return true;
		}
		else{
			return super.onFling(e1, e2, velocityX, velocityY);
		}
	}

//TODO test on a multitouch device with individual tracking of points
/*
	private final static int INVALID_ID = -1;
	private int _secondPointerId = INVALID_ID;
	private float _lastSecondaryX = INVALID_ID;
	private float _lastSecondaryY = INVALID_ID;

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		int pointerId = getPointerId(event);
		int action = event.getAction() & MotionEvent.ACTION_MASK;
		
		//detect secondary motion events
		if (event.getPointerCount() == 2
				&& _secondPointerId == INVALID_ID
				&& action == MotionEvent.ACTION_POINTER_DOWN){
			_secondPointerId = pointerId;
			int pointerIndex = event.findPointerIndex(pointerId);
			_lastSecondaryX = event.getX(pointerIndex);
			_lastSecondaryY = event.getY(pointerIndex);
		}
		else if(_secondPointerId == pointerId){
			if (action == MotionEvent.ACTION_MOVE){
				int pointerIndex = event.findPointerIndex(pointerId);
				float distanceX = event.getX(pointerIndex);
				float distanceY = event.getY(pointerIndex);
				
				moveCaretWithTrackpad(distanceX - _lastSecondaryX,
						distanceY - _lastSecondaryY);
				_lastSecondaryX = distanceX;
				_lastSecondaryY = distanceY;
			}
			else if(action == MotionEvent.ACTION_POINTER_UP){
				_secondPointerId = INVALID_ID;
			}
		}

		super.onTouchEvent(event);
		return true;
	}
*/

	//number of pixels to scroll to move the caret one unit
	private final static int MOVEMENT_PIXELS = 16;
	//for use in determining whether the displacement is mainly on the x or y axis
	private final static double MIN_ATAN = 0.322; // == atan(1/3)
	private float _xAccum = 0.0f;
	private float _yAccum = 0.0f;
	
	private void moveCaretWithTrackpad(float distanceX, float distanceY){
		//reset accumulators when polarity of displacement changes
		if((_xAccum < 0 && distanceX > 0) || (_xAccum > 0 && distanceX < 0)){
			_xAccum = 0;
		}
		if((_yAccum < 0 && distanceY > 0) || (_yAccum > 0 && distanceY < 0)){
			_yAccum = 0;
		}

		double angle = Math.atan2(Math.abs(distanceX), Math.abs(distanceY));
		
		if(angle >= MIN_ATAN){
			//non-negligible x-axis movement
			float x = _xAccum + distanceX;
			int xUnits = ((int) x) / MOVEMENT_PIXELS;
			_xAccum = x - (xUnits * MOVEMENT_PIXELS);
			
			while(xUnits > 0){
				_textField.moveCaretRight();
				--xUnits;
			}
			while(xUnits < 0){
				_textField.moveCaretLeft();
				++xUnits;
			}
		}

		if((Math.PI/2 - angle) >= MIN_ATAN){
			//non-negligible y-axis movement
			float y = _yAccum + distanceY;
			int yUnits = ((int) y) / MOVEMENT_PIXELS;
			_yAccum = y - (yUnits * MOVEMENT_PIXELS);
			
			for(int i = yUnits; i > 0; --i){
				_textField.moveCaretDown();
			}
			for(int i = yUnits; i < 0; ++i){
				_textField.moveCaretUp();
			}
		}
	}

	@Override
	public boolean onSingleTapConfirmed(MotionEvent e) {
		int x = (int) e.getX() + _textField.getScrollX();
		int y = (int) e.getY() + _textField.getScrollY();

		if(_trackpad.isInTrackpad(x, y)){
			_trackpad.toggleDisplay();
			return true;
		}
		else{
			return super.onSingleTapConfirmed(e);
		}
	}

	@Override
	public boolean onDoubleTap(MotionEvent e) {
		int x = (int) e.getX() + _textField.getScrollX();
		int y = (int) e.getY() + _textField.getScrollY();
		
		//ignore taps on trackpad
		if(_trackpad.isInTrackpad(x, y)){
			return true;
		}
		else{
			return super.onDoubleTap(e);
		}
	}

	@Override
	public void onTextDrawComplete(Canvas canvas) {
		_trackpad.draw(canvas);
	}

	
	
	private class Trackpad{
		private final Bitmap OPEN_IMG = BitmapFactory.decodeResource(
				_textField.getContext().getResources(), R.drawable.trackpad_open);
		private final Bitmap CLOSE_IMG = BitmapFactory.decodeResource(
				_textField.getContext().getResources(), R.drawable.trackpad_close);
		private Drawable _trackpad_right_img, _trackpad_left_img;

		private final static int TRACKPAD_ALPHA = 225;
		private Paint _brush = new Paint();

		private final static int MAX_WIDTH_DP = 240;
		private final static int MAX_HEIGHT_DP = 140;
		private final int MAX_WIDTH;
		private final int MAX_HEIGHT;
		
		private int _width;
		private int _height;
		
		public Trackpad(){
			_width = OPEN_IMG.getWidth();
			_height = OPEN_IMG.getHeight();
			
			float dpScale = _textField.getResources().getDisplayMetrics().density;
			MAX_WIDTH = (int) (MAX_WIDTH_DP * dpScale + 0.5f);
			MAX_HEIGHT = (int) (MAX_HEIGHT_DP * dpScale + 0.5f);

			 TextWarriorException.assertVerbose(_width <= MAX_WIDTH,
			 	"Trackpad button width cannot be more than MAX_WIDTH");
			 TextWarriorException.assertVerbose(_height <= MAX_HEIGHT,
			 	"Trackpad button height cannot be more than MAX_HEIGHT");
			 TextWarriorException.assertVerbose(
					 (CLOSE_IMG.getWidth() == _width)
					 && (CLOSE_IMG.getHeight() == _height),
			 	"Open and close buttons have to be the same size");
			 
			_brush.setAlpha(TRACKPAD_ALPHA);
		}

		public boolean isInTrackpad(int x, int y) {
			Rect bounds = getTrackpadBoundingBox();
			return (x >= bounds.left && x < bounds.right
					&& y >= bounds.top && y < bounds.bottom
					);
		}
		
		public boolean isOpen(){
			return _state == OPENED;
		}


		private final static int CLOSED = 0;
		private final static int OPENED = 1;
		private final static int OPENING = 2;
		private final static int CLOSING = 3;
		private int _state = CLOSED;
		public void toggleDisplay(){
			if(_state == OPENED){
				close();
			}
			else if(_state == CLOSED){
				open();
			}
		}

		private final static long ANIMATION_DURATION = 100000000; //nanoseconds
		long _animationStartTime;
		public void open(){
			if(_state == CLOSED){
				_state = OPENING;
				
				_animationStartTime = System.nanoTime();
				_textField.post(showTrackpad);
			}
		}
		
		private Runnable showTrackpad = new Runnable(){
			public void run(){
				int baseWidth = CLOSE_IMG.getWidth();
				int baseHeight = CLOSE_IMG.getHeight();
				int widthDelta = MAX_WIDTH - baseWidth;
				int heightDelta = MAX_HEIGHT - baseHeight;
				
				float elapsedTime = System.nanoTime() - _animationStartTime;
				
				if(elapsedTime < ANIMATION_DURATION){
					_width = baseWidth +
							(int) (elapsedTime / ANIMATION_DURATION * widthDelta);
					_height = baseHeight +
							(int) (elapsedTime / ANIMATION_DURATION * heightDelta);
					invalidateTrackpad();
					_textField.post(showTrackpad);
				}
				else {
					_state = OPENED;
					_width = MAX_WIDTH;
					_height = MAX_HEIGHT;
					invalidateTrackpad();
				}
			}
		};
		
		public void close(){
			if(_state == OPENED){
				_state = CLOSING;

				_animationStartTime = System.nanoTime();
				_textField.post(hideTrackpad);
			}
		}

		private Runnable hideTrackpad = new Runnable(){
			public void run(){
				int baseWidth = OPEN_IMG.getWidth();
				int baseHeight = OPEN_IMG.getHeight();
				int widthDelta = MAX_WIDTH - baseWidth;
				int heightDelta = MAX_HEIGHT - baseHeight;
				
				float elapsedTime = System.nanoTime() - _animationStartTime;

				invalidateTrackpad();
				if(elapsedTime < ANIMATION_DURATION){
					_width = baseWidth +
							(int) ((1.0 - elapsedTime / ANIMATION_DURATION) * widthDelta);
					_height = baseHeight +
							(int) ((1.0 - elapsedTime / ANIMATION_DURATION) * heightDelta);
					_textField.post(hideTrackpad);
				}
				else {
					_state = CLOSED;
					_width = baseWidth;
					_height = baseHeight;
				}
			}
		};
		
		private void invalidateTrackpad(){
			_textField.invalidate(getTrackpadBoundingBox());
		}
		
		private Rect getTrackpadBoundingBox(){
			int bottom = _textField.getScrollY() + _textField.getHeight();
			int top = bottom - _height;
			
			int left, right;
			if(isRightHanded()){
				right = _textField.getScrollX() + _textField.getWidth();
				left = right - _width;
			}
			else{
				left = _textField.getScrollX();
				right = left + _width;
			}
			
			return new Rect(left, top, right, bottom);
		}

		
		public void draw(Canvas c){
			Rect bounds = getTrackpadBoundingBox();
			float trackpadLeft = bounds.left;
			float trackpadTop = bounds.top;
			
			boolean rightHanded = isRightHanded();
			Drawable trackpad = rightHanded ? getTrackpadRightImg()
					: getTrackpadLeftImg();
			
			Bitmap button;
			int buttonLeft, buttonTop;
			if(_state == CLOSED){
				button = OPEN_IMG;
				buttonLeft = (int) trackpadLeft;
				buttonTop = (int) trackpadTop;
			}
			else{
				button = CLOSE_IMG;
				buttonLeft = rightHanded ? bounds.right - CLOSE_IMG.getWidth()
							: (int) trackpadLeft;
				buttonTop = bounds.bottom - CLOSE_IMG.getHeight();
			}

			c.drawBitmap(button, buttonLeft, buttonTop, _brush);
			trackpad.setBounds((int) trackpadLeft, (int) trackpadTop,
					(int) trackpadLeft + _width, (int) trackpadTop + _height);
			trackpad.draw(c);
		}

		//lazy load images
		final private Drawable getTrackpadRightImg(){
			if(_trackpad_right_img == null){
				_trackpad_right_img = _textField.getContext().getResources()
		 			.getDrawable(R.drawable.trackpad_right);
			}
			return _trackpad_right_img;
		}
		
		final private Drawable getTrackpadLeftImg(){
			if(_trackpad_left_img == null){
				 _trackpad_left_img = _textField.getContext().getResources()
			 		.getDrawable(R.drawable.trackpad_left);
			}
			return _trackpad_left_img;
		}
	}//end inner class
}


<|start_filename|>src/com/myopicmobile/textwarrior/common/Lexer.java<|end_filename|>
/*
 * Copyright (c) 2011 <NAME>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License Version 2.0,
 * with full text available at http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This software is provided "as is". Use at your own risk.
 */
package com.myopicmobile.textwarrior.common;

import com.myopicmobile.textwarrior.common.LanguageCFamily;
import com.myopicmobile.textwarrior.common.DocumentProvider;
import com.myopicmobile.textwarrior.common.Pair;
import com.myopicmobile.textwarrior.common.TextWarriorException;

import java.util.List;
import java.util.Vector;

/**
 * Does lexical analysis of a text for C-like languages.
 * The programming language syntax used is set as a static class variable.
 */
public class Lexer{
	private final static int MAX_KEYWORD_LENGTH = 31;

	public final static int UNKNOWN = -1;
	public final static int NORMAL = 0;
	public final static int KEYWORD = 1;
	public final static int OPERATOR = 2;

	/** A word that starts with a special symbol, inclusive.
	 * Examples:
	 * :ruby_symbol
	 * */
	public final static int SINGLE_SYMBOL_WORD = 10;
	
	/** Tokens that extend from a single start symbol, inclusive, until the end of line.
	 * Up to 2 types of symbols are supported per language, denoted by A and B
	 * Examples:
	 * #include "myCppFile"
	 * #this is a comment in Python
	 * %this is a comment in Prolog
	 * */
	public final static int SINGLE_SYMBOL_LINE_A = 20;
	public final static int SINGLE_SYMBOL_LINE_B = 21;
	
	/** Tokens that extend from a two start symbols, inclusive, until the end of line.
	 * Examples:
	 * //this is a comment in C
	 * */
	public final static int DOUBLE_SYMBOL_LINE = 30;	
	
	/** Tokens that are enclosed between a start and end sequence, inclusive,
	 * that can span multiple lines. The start and end sequences contain exactly
	 * 2 symbols.
	 * Examples:
	 * {- this is a...
	 *  ...multi-line comment in Haskell -}
	 * */
	public final static int DOUBLE_SYMBOL_DELIMITED_MULTILINE = 40;

	/** Tokens that are enclosed by the same single symbol, inclusive, and 
	 * do not span over more than one line.
	 * Examples: 'c', "hello world"
	 * */
	public final static int SINGLE_SYMBOL_DELIMITED_A = 50;
	public final static int SINGLE_SYMBOL_DELIMITED_B = 51;

	private static LanguageCFamily _globalLanguage = LanguageNonProg.getCharacterEncodings();
	synchronized public static void setLanguage(LanguageCFamily lang){
		_globalLanguage = lang;
	}

	synchronized public static LanguageCFamily getLanguage(){
		return _globalLanguage;
	}


	private DocumentProvider _hDoc;
	private LexThread _workerThread = null;
	LexCallback _callback = null;

	public Lexer(LexCallback callback){
		_callback = callback;
	}
	
	public void tokenize(DocumentProvider hDoc){
		if(!Lexer.getLanguage().isProgLang()){
			return;
		}

		//tokenize will modify the state of hDoc; make a copy
		setDocument(new DocumentProvider(hDoc));
		if(_workerThread == null){
			_workerThread = new LexThread(this);
			_workerThread.start();
		}
		else{
			_workerThread.restart();
		}
	}

	void tokenizeDone(List<Pair> result){
		if(_callback != null){
			_callback.lexDone(result);
		}
		_workerThread = null;
	}
	
	public void cancelTokenize(){
		if(_workerThread != null){
			_workerThread.abort();
			_workerThread = null;
		}
	}

	public synchronized void setDocument(DocumentProvider hDoc){
		_hDoc = hDoc;
	}
	
	public synchronized DocumentProvider getDocument(){
		return _hDoc;
	}
	
	
	
	
	
	private class LexThread extends Thread{
		private boolean rescan = false;
		private Lexer _lexManager;
		/** can be set by another thread to stop the scan immediately */
		private Flag _abort;
		/** A collection of Pairs, where Pair.first is the start 
		 *  position of the token, and Pair.second is the type of the token.*/
		private Vector<Pair> _tokens;

		public LexThread(Lexer p){
			_lexManager = p;
			_abort = new Flag();
		}

		public void run(){
			do{
				rescan = false;
				_abort.clear();
				tokenize();
			}
			while(rescan);

			if(!_abort.isSet()){
				// lex complete
				_lexManager.tokenizeDone(_tokens);
			}
		}

		public void restart() {
			rescan = true;
			_abort.set();
		}
		
		public void abort() {
			_abort.set();
		}

		/**
		 * Scans the document referenced by _lexManager for tokens.
		 * The result is stored internally.
		 */
		public void tokenize(){
			DocumentProvider hDoc = getDocument();
			LanguageCFamily language = Lexer.getLanguage();
			Vector<Pair> tokens = new Vector<Pair>();

			if(!language.isProgLang()){
				tokens.addElement(new Pair(0, NORMAL));
				_tokens = tokens;
				return;
			}

			char[] candidateWord = new char[MAX_KEYWORD_LENGTH];
			int currentCharInWord = 0;

			int spanStartPosition = 0;
			int workingPosition = 0;
			int state = UNKNOWN;
			char prevChar = 0;

		    hDoc.seekChar(0);
			while (hDoc.hasNext() && !_abort.isSet()){
				char currentChar = hDoc.next();

				switch(state){
				case UNKNOWN: //fall-through
				case NORMAL: //fall-through
				case KEYWORD: //fall-through
				case SINGLE_SYMBOL_WORD:
					int pendingState = state;
					boolean stateChanged = false;
					if (language.isLineStart(prevChar, currentChar)){
						pendingState = DOUBLE_SYMBOL_LINE;
						stateChanged = true;
					}
					else if (language.isMultilineStartDelimiter(prevChar, currentChar)){
						pendingState = DOUBLE_SYMBOL_DELIMITED_MULTILINE;
						stateChanged = true;
					}
					else if (language.isDelimiterA(currentChar)){
						pendingState = SINGLE_SYMBOL_DELIMITED_A;	
						stateChanged = true;
					}
					else if (language.isDelimiterB(currentChar)){
						pendingState = SINGLE_SYMBOL_DELIMITED_B;	
						stateChanged = true;
					}
					else if (language.isLineAStart(currentChar)){
						pendingState = SINGLE_SYMBOL_LINE_A;
						stateChanged = true;
					}
					else if (language.isLineBStart(currentChar)){
						pendingState = SINGLE_SYMBOL_LINE_B;
						stateChanged = true;
					}
					
					
					if(stateChanged){
						if (pendingState == DOUBLE_SYMBOL_LINE ||
								pendingState == DOUBLE_SYMBOL_DELIMITED_MULTILINE){
							// account for previous char
							spanStartPosition = workingPosition - 1;
//TODO consider less greedy approach and avoid adding token for previous char
							if(tokens.lastElement().getFirst() == spanStartPosition){
								tokens.removeElementAt(tokens.size() - 1);
							}
						}
						else{
							spanStartPosition = workingPosition;
						}

						// If a span appears mid-word, mark the chars preceding 
						// it as NORMAL, if the previous span isn't already NORMAL
						if(currentCharInWord > 0 && state != NORMAL){
							tokens.addElement(new Pair(workingPosition - currentCharInWord, NORMAL));
						}

						state = pendingState;
						tokens.addElement(new Pair(spanStartPosition, state));
						currentCharInWord = 0;
					}
					
					else if (language.isWhitespace(currentChar) || language.isOperator(currentChar)){
						if (currentCharInWord > 0){
							// full word obtained; mark the beginning of the word accordingly
							if( language.isWordStart(candidateWord[0]) ){
								spanStartPosition = workingPosition - currentCharInWord;
								state = SINGLE_SYMBOL_WORD;
								tokens.addElement(new Pair(spanStartPosition, state));
							}
							else if(language.isKeyword( new String(candidateWord, 0, currentCharInWord)) ){
								spanStartPosition = workingPosition - currentCharInWord;
								state = KEYWORD;
								tokens.addElement(new Pair(spanStartPosition, state));
							}
							else if (state != NORMAL){
								spanStartPosition = workingPosition - currentCharInWord;
								state = NORMAL;
								tokens.addElement(new Pair(spanStartPosition, state));
							}
							currentCharInWord = 0;
						}

						// mark operators as normal
						if (state != NORMAL && language.isOperator(currentChar) ){
							state = NORMAL;
							tokens.addElement(new Pair(workingPosition, state));
						}
					}
					else if (currentCharInWord < MAX_KEYWORD_LENGTH){
						// collect non-whitespace chars up to MAX_KEYWORD_LENGTH
						candidateWord[currentCharInWord] = currentChar;
						currentCharInWord++;
					}
					break;
					
				
				case DOUBLE_SYMBOL_LINE: // fall-through
				case SINGLE_SYMBOL_LINE_A: // fall-through
				case SINGLE_SYMBOL_LINE_B:
					if (currentChar == '\n'){
						state = UNKNOWN;
					}
					break;
					

				case SINGLE_SYMBOL_DELIMITED_A:
					if ((language.isDelimiterA(currentChar) && !language.isEscapeChar(prevChar)) ||
						currentChar == '\n'){
						state = UNKNOWN;
					}
					// consume escape of the escape character by assigning
					// currentChar as something else so that it would not be
					// treated as an escape char in the next iteration
					else if (language.isEscapeChar(currentChar) && language.isEscapeChar(prevChar)){
						currentChar = ' ';
					}
					break;
					
					
				case SINGLE_SYMBOL_DELIMITED_B:
					if ((language.isDelimiterB(currentChar) && !language.isEscapeChar(prevChar)) ||
						currentChar == '\n'){
						state = UNKNOWN;
					}
					// consume escape of the escape character by assigning
					// currentChar as something else so that it would not be
					// treated as an escape char in the next iteration
					else if (language.isEscapeChar(currentChar)
							&& language.isEscapeChar(prevChar)){
						currentChar = ' ';
					}
					break;
					
				case DOUBLE_SYMBOL_DELIMITED_MULTILINE:
					if (language.isMultilineEndDelimiter(prevChar, currentChar)){
						state = UNKNOWN;
					}
					break;
					
				default:
					TextWarriorException.assertVerbose(false, "Invalid state in TokenScanner");
					break;
				}
				++workingPosition;
				prevChar = currentChar;
			}
			// end state machine


			if (tokens.isEmpty()){
				// return value cannot be empty
				tokens.addElement(new Pair(0, NORMAL));
			}

			_tokens = tokens;
		}
		
		
	}//end inner class
	

	public interface LexCallback {
		public void lexDone(List<Pair> results);
	}
}
