/*
* Copyright (C) 2007 The Android Open Source Project
*
* 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.
*/
package com.applang.tagesberichte;
import java.util.Map;
import java.util.TreeSet;
import com.applang.UserContext;
import com.applang.berichtsheft.R;
import com.applang.provider.NotePad.NoteColumns;
import com.applang.provider.NotePadProvider;
import android.app.Activity;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.EditText;
import static com.applang.Util.*;
import static com.applang.Util1.*;
import static com.applang.Util2.*;
import static com.applang.VelocityUtil.*;
/**
* A generic activity for editing a note in a database. This can be used
* either to simply view a note {@link Intent#ACTION_VIEW}, view and edit a note
* {@link Intent#ACTION_EDIT}, or create a new note {@link Intent#ACTION_INSERT}.
*/
public class NoteEditor extends Activity
{
private static final String TAG = NoteEditor.class.getSimpleName();
/**
* Standard projection for the interesting columns of a normal note.
*/
private static final String[] PROJECTION = new String[] {
NoteColumns._ID, // 0
NoteColumns.NOTE, // 1
};
/** The index of the note column */
private static final int COLUMN_INDEX_NOTE = 1;
// This is our state data that is stored when freezing.
private static final String ORIGINAL_CONTENT = "origContent";
// Identifiers for our menu items.
private static final int REVERT_ID = Menu.FIRST;
private static final int DISCARD_ID = Menu.FIRST + 1;
private static final int DELETE_ID = Menu.FIRST + 2;
private static final int BAUSTEIN_ID = Menu.FIRST + 3;
private static final int EVALUATE_ID = Menu.FIRST + 4;
private static final int SCHLAGWORT_ID = Menu.FIRST + 5;
private static final int ANWEISUNG_ID = Menu.FIRST + 6;
private static final int STRUKTUR_ID = Menu.FIRST + 7;
// The different distinct states the activity can be run in.
public static final int STATE_EDIT = 0;
public static final int STATE_INSERT = 1;
private int mState;
private boolean mNoteOnly = false;
private Uri mUri;
private Cursor mCursor;
private LinedEditText mText;
private String mOriginalContent;
/**
* A custom EditText that draws lines between each line of text that is displayed.
*/
public static class LinedEditText extends EditText {
private Rect mRect;
private Paint mPaint;
// we need this constructor for LayoutInflater
public LinedEditText(Context context, AttributeSet attrs) {
super(context, attrs);
mRect = new Rect();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(0x800000FF);
}
@Override
protected void onDraw(Canvas canvas) {
int count = getLineCount();
Rect r = mRect;
Paint paint = mPaint;
for (int i = 0; i < count; i++) {
int baseline = getLineBounds(i, r);
canvas.drawLine(r.left, baseline + 1, r.right, baseline + 1, paint);
}
super.onDraw(canvas);
}
public String getFirstWord() {
Editable text = getText();
int start = 0;
int end = -1;
for (int i = 0; i < text.length(); i++) {
if (Character.isWhitespace(text.charAt(i))) {
if (end < start)
start = i + 1;
else
break;
}
else
end = i + 1;
}
return end < start ? "" : text.subSequence(start, end).toString();
}
public boolean hasWord() {
return getFirstWord().length() > 0;
}
public String getSelectedText() {
if (hasSelection()) {
int start = getSelectionStart();
int end = getSelectionEnd();
return getText().subSequence(Math.min(start, end), Math.max(start, end)).toString();
}
else
return "";
}
public void insertAtCaretPosition(CharSequence text) {
int length = text.length();
if (length > 0) {
int start = getSelectionStart();
int end = getSelectionEnd();
setText(getText().replace(Math.min(start, end),
Math.max(start, end), text, 0, length));
setSelection(start + length);
}
}
}
int tableIndex;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = getIntent();
tableIndex = NotePadProvider.tableIndex(0, intent.getData());
final String action = intent.getAction();
if (Intent.ACTION_EDIT.equals(action)) {
mState = STATE_EDIT;
mUri = intent.getData();
}
else if (Intent.ACTION_INSERT.equals(action)) {
mState = STATE_INSERT;
mUri = getContentResolver().insert(intent.getData(),
new ContentValues());
// If we were unable to create a new note, then just finish
// this activity. A RESULT_CANCELED will be sent back to the
// original activity if they requested a result.
if (mUri == null) {
Log.e(TAG, "Failed to insert new note into " + getIntent().getData());
finish();
return;
}
startActivityForResult(new Intent(TitleEditor.EDIT_TITLE_ACTION, mUri)
.putExtra("state", NoteEditor.STATE_INSERT), 0);
}
else {
// Whoops, unknown action! Bail.
Log.e(TAG, "Unknown action, exiting");
finish();
return;
}
setContentView(R.layout.note_editor);
mText = (LinedEditText) findViewById(R.id.note);
mText.addTextChangedListener(new TextWatcher() {
boolean first = true;
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable s) {
// if (tableIndex == 1)
// analyze(s);
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (first)
mText.setSelection(Math.max(0, s.length() - 1));
else if (tableIndex == 1) {
if (count - before == 1 && start + count > 0) {
char ch = s.charAt(start + count - 1);
if (VRI.equals(ch)) {
requestBaustein(0);
} else if (VDI.equals(ch)) {
requestDirective(0);
}
}
}
first = false;
}
});
if (savedInstanceState != null) {
mOriginalContent = savedInstanceState.getString(ORIGINAL_CONTENT);
}
}
int[] requestCode = new int[] {0,0,0};
private void requestBaustein(int code) {
requestCode[0] = code;
popupContextMenu(this, mText);
}
private void requestSchlagwort(int code) {
requestCode[1] = code;
popupContextMenu(this, mText);
}
private void requestDirective(int code) {
requestCode[2] = code;
popupContextMenu(this, mText);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, view, menuInfo);
menu.clear();
int q, id = 0;
if (requestCode[0] > 0) {
id = R.string.menu_baustein;
bausteine = NotePadProvider.bausteinMap(getContentResolver(), "");
for (String key : new TreeSet<String>(bausteine.keySet()))
menu.add(key);
q = 0;
}
else if (requestCode[1] > 0) {
id = R.string.menu_schlagwort;
getMenuInflater().inflate(R.menu.contextmenu_noteeditor, menu);
MenuItem mi = menu.findItem(R.id.menu_item_selected);
if (mi != null)
mi.setEnabled(mText.hasSelection());
mi = menu.findItem(R.id.menu_item_first);
if (mi != null)
mi.setEnabled(mText.hasWord());
q = 1;
}
else if (requestCode[2] > 0) {
id = R.string.menu_anweisung;
anweisungen = UserContext.directives();
for (String key : anweisungen.keySet())
menu.add(key);
q = 2;
}
else {
for (int i = 0; i < requestCode.length; i++)
requestCode[i] = 0;
return;
}
menu.setHeaderTitle(getResources().getString(id));
requestCode[q] = -requestCode[q];
}
private ValMap bausteine = null;
Map<String,String> anweisungen = null;
public static final String SPAN = "caret";
@Override
public boolean onContextItemSelected(MenuItem item) {
String text = item.getTitle().toString();
int q;
if (requestCode[0] < 0) {
if (requestCode[0] < -1) {
if (bausteine != null) {
text = bausteine.get(text).toString();
new UserContext.EvaluationTask(this, bausteine, null, null, new Job<Object>() {
public void perform(Object text, Object[] params) {
mText.insertAtCaretPosition(text.toString());
updateNote(mText.getText().toString(), true);
}
}).execute(text, getString(R.string.title_evaluator));
requestCode[0] = 0;
return true;
}
else
text = VRI + enclose("{", text, "}");
}
else {
text = enclose("{", text, "}");
}
mText.insertAtCaretPosition(text);
q = 0;
}
else if (requestCode[1] < 0) {
String word = "";
switch (item.getItemId()) {
case R.id.menu_item_first:
word = mText.getFirstWord();
break;
case R.id.menu_item_selected:
word = mText.getSelectedText();
break;
}
if (word.length() > 0) {
Long id = parseId(-1L, mUri);
String description = getNoteDescription(getContentResolver(), tableIndex, id);
Uri uri = NotePadProvider.contentUri(2);
uri = ContentUris.withAppendedId(uri, id);
id = NotePadProvider.getIdOfNote(getContentResolver(), 2,
NoteColumns.REF_ID + "=? and " + NoteColumns.TITLE + "=?",
strings("" + id, word));
int state = id < 0 ? NoteEditor.STATE_INSERT : NoteEditor.STATE_EDIT;
startActivity(new Intent()
.setClass(this, TitleEditor.class)
.setData(uri)
.putExtra("title", word)
.putExtra("header", description)
.putExtra("state", state));
}
q = 1;
}
else if (requestCode[2] < 0) {
String signature = anweisungen.get(text);
synthesize(signature);
q = 2;
}
else {
return super.onContextItemSelected(item);
}
requestCode[q] = 0;
return true;
}
private void synthesize(String anweisung) {
UserContext.buildDirective(anweisung, this, new ValMap(), new Job<Object>() {
public void perform(Object t, Object[] params) {
if (t != null) {
String text = t.toString();
if (requestCode[2] < -1)
text = VDI + text;
mText.insertAtCaretPosition(text);
updateNote(mText.getText().toString(), true);
}
}
});
}
protected void analyze() {
String[] strings = getResources().getStringArray(R.array.title_edit_array);
final String text = mText.getText().toString();
new Baustein.AnalysisTask(this, new Job<Boolean>() {
public void perform(Boolean problem, Object[] params) {
if (problem) {
int offset = getTextOffsets(text, getProblemCoordinates())[0];
mText.setSelection(offset, offset + 1);
}
else {
Intent intent = new Intent()
.setClass(NoteEditor.this, Baustein.class)
.setData(NotePadProvider.contentUri(2));
startActivityForResult(intent, 2);
}
}
}).execute(text, strings[tableIndex]);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case 1:
mCursor = managedQuery(mUri, PROJECTION, "", null, null);
break;
case 2:
if (resultCode == RESULT_OK) {
int[] span = data.getIntArrayExtra(SPAN);
String string = mText.getText().toString();
int[] offsets = getTextOffsets(string, span);
int length = string.length();
mText.setSelection(
Math.max(0, offsets[0]),
Math.min(length, offsets[1]));
}
break;
default:
setResult(resultCode, (new Intent()).setAction(mUri.toString()));
if (resultCode == RESULT_CANCELED) {
deleteNote();
finish();
}
break;
}
}
@Override
protected void onResume() {
super.onResume();
mCursor = managedQuery(mUri,
PROJECTION,
"", null,
null);
// If we didn't have any trouble retrieving the data, it is now
// time to get at the stuff.
if (mCursor != null) {
// Make sure we are at the one and only row in the cursor.
mCursor.moveToFirst();
// Modify our overall title depending on the mode we are running in.
if (mState == STATE_EDIT) {
setTitle(getText(R.string.title_edit));
} else if (mState == STATE_INSERT) {
setTitle(getText(R.string.title_create));
}
// This is a little tricky: we may be resumed after previously being
// paused/stopped. We want to put the new text in the text view,
// but leave the user where they were (retain the cursor position
// etc). This version of setText does that for us.
String note = mCursor.getString(COLUMN_INDEX_NOTE);
mText.setTextKeepState(note);
// If we hadn't previously retrieved the original text, do so
// now. This allows the user to revert their changes.
if (mOriginalContent == null) {
mOriginalContent = note;
}
}
else {
setTitle(getText(R.string.error_title));
mText.setText(getText(R.string.error_message));
}
if (mTextUndoRedo == null)
mTextUndoRedo = new Helper.TextViewUndoRedo(mText);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
// Save away the original text, so we still have it if the activity
// needs to be killed while paused.
outState.putString(ORIGINAL_CONTENT, mOriginalContent);
}
@Override
protected void onPause() {
super.onPause();
// The user is going somewhere else, so make sure their current
// changes are safely saved away in the provider. We don't need
// to do this if only editing.
if (mCursor != null) {
String text = mText.getText().toString();
int length = text.length();
// If this activity is finished, and there is no text, then we
// do something a little special: simply delete the note entry.
// Note that we do this both for editing and inserting... it
// would be reasonable to only do it when inserting.
if (isFinishing() && (length == 0) && !mNoteOnly) {
setResult(RESULT_CANCELED);
deleteNote();
// Get out updates into the provider.
}
else {
updateNote(text, mNoteOnly);
}
}
}
public void updateNote(String text, boolean noteOnly) {
ContentValues values = new ContentValues();
// This stuff is only done when working with a full-fledged note.
if (!noteOnly) {
// Bump the modification time to now.
values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
// Write our text back into the provider.
values.put(NoteColumns.NOTE, text);
// Commit all of our changes to persistent storage. When the update completes
// the content provider will notify the cursor of the change, which will
// cause the UI to be updated.
getContentResolver().update(mUri,
values,
null, null);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
// Build the menus that are shown when editing.
if (mState == STATE_EDIT) {
menu.add(0, REVERT_ID, 0, R.string.menu_revert)
.setShortcut('0', 'r')
.setIcon(android.R.drawable.ic_menu_revert);
if (!mNoteOnly) {
menu.add(0, DELETE_ID, 0, R.string.menu_delete)
.setShortcut('1', 'd')
.setIcon(android.R.drawable.ic_menu_delete);
}
// Build the menus that are shown when inserting.
} else {
menu.add(0, DISCARD_ID, 0, R.string.menu_discard)
.setShortcut('0', 'd')
.setIcon(android.R.drawable.ic_menu_delete);
}
menu.add(0, BAUSTEIN_ID, 0, R.string.menu_baustein)
.setShortcut('2', 'b');
if (tableIndex == 1) {
menu.add(0, ANWEISUNG_ID, 0, R.string.menu_anweisung)
.setShortcut('5', 'a');
menu.add(0, STRUKTUR_ID, 0, R.string.menu_struktur)
.setShortcut('7', 'k');
menu.add(0, EVALUATE_ID, 0, R.string.menu_evaluate)
.setShortcut('3', 'e');
}
if (tableIndex == 0)
menu.add(0, SCHLAGWORT_ID, 0, R.string.menu_schlagwort)
.setShortcut('4', 's');
// If we are working on a full note, then append to the
// menu items for any other activities that can do stuff with it
// as well. This does a query on the system for any activities that
// implement the ALTERNATIVE_ACTION for our data, adding a menu item
// for each one that is found.
if (!mNoteOnly) {
Intent intent = new Intent(null, getIntent().getData());
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0,
new ComponentName(this, NoteEditor.class), null, intent, 0, null);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle all of the possible menu actions.
switch (item.getItemId()) {
case DELETE_ID:
Long id = parseId(-1L, mUri);
String description = getNoteDescription(getContentResolver(), tableIndex, id);
description = getResources().getString(R.string.areUsure, description);
areUsure(this, description, new Job<Void>() {
@Override
public void perform(Void t, Object[] params) throws Exception {
deleteNote();
finish();
}
});
break;
case DISCARD_ID:
cancelNote();
break;
case REVERT_ID:
cancelNote();
break;
case BAUSTEIN_ID:
requestBaustein(2);
break;
case ANWEISUNG_ID:
requestDirective(2);
break;
case STRUKTUR_ID:
analyze();
break;
case SCHLAGWORT_ID:
requestSchlagwort(2);
break;
case EVALUATE_ID:
startActivityForResult(new Intent()
.setAction(BausteinEvaluator.EVALUATE_ACTION)
.setData(mUri), 1);
break;
}
return super.onOptionsItemSelected(item);
}
/**
* Take care of canceling work on a note. Deletes the note if we
* had created it, otherwise reverts to the original text.
*/
private final void cancelNote() {
if (mCursor != null) {
if (mState == STATE_EDIT) {
// Put the original note text back into the database
mCursor.close();
mCursor = null;
ContentValues values = new ContentValues();
values.put(NoteColumns.NOTE, mOriginalContent);
getContentResolver().update(mUri,
values,
null, null);
} else if (mState == STATE_INSERT) {
// We inserted an empty note, make sure to delete it
deleteNote();
}
}
setResult(RESULT_CANCELED);
finish();
}
private final void deleteNote() {
if (mCursor != null) {
mCursor.close();
mCursor = null;
getContentResolver().delete(mUri, "", null);
mText.setText("");
}
}
public static String getNoteDescription(ContentResolver contentResolver, final int tableIndex, long id) {
Object[] params = new Object[1];
NotePadProvider.fetchNoteById(id, contentResolver, 0, new Job<Cursor>() {
@Override
public void perform(Cursor c, Object[] params) throws Exception {
params[0] = NotesList.description(tableIndex,
c.getLong(3),
c.getString(1));
}
}, params);
return params[0] != null ? params[0].toString() : "";
}
private Helper.TextViewUndoRedo mTextUndoRedo = null;
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
if (mTextUndoRedo != null)
mTextUndoRedo.redo();
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
if (mTextUndoRedo != null)
mTextUndoRedo.undo();
return true;
} else {
return super.onKeyDown(keyCode, event);
}
}
}