/*
* Copyright (C) 2009 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 cn.edu.tsinghua.hpc.tcontacts.ui.widget;
import java.util.List;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Entity;
import android.telephony.PhoneNumberFormattingTextWatcher;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListAdapter;
import android.widget.RelativeLayout;
import android.widget.TextView;
import cn.edu.tsinghua.hpc.tcontacts.ContactsUtils;
import cn.edu.tsinghua.hpc.tcontacts.R;
import cn.edu.tsinghua.hpc.tcontacts.model.Editor;
import cn.edu.tsinghua.hpc.tcontacts.model.EntityDelta;
import cn.edu.tsinghua.hpc.tcontacts.model.EntityModifier;
import cn.edu.tsinghua.hpc.tcontacts.model.ContactsSource.DataKind;
import cn.edu.tsinghua.hpc.tcontacts.model.ContactsSource.EditField;
import cn.edu.tsinghua.hpc.tcontacts.model.ContactsSource.EditType;
import cn.edu.tsinghua.hpc.tcontacts.model.Editor.EditorListener;
import cn.edu.tsinghua.hpc.tcontacts.model.EntityDelta.ValuesDelta;
/**
* Simple editor that handles labels and any {@link EditField} defined for
* the entry. Uses {@link ValuesDelta} to read any existing
* {@link Entity} values, and to correctly write any changes values.
*/
public class GenericEditorView extends RelativeLayout implements Editor, View.OnClickListener {
protected static final int RES_FIELD = R.layout.item_editor_field;
protected static final int RES_LABEL_ITEM = android.R.layout.simple_list_item_1;
protected LayoutInflater mInflater;
protected static final int INPUT_TYPE_CUSTOM = EditorInfo.TYPE_CLASS_TEXT
| EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
protected TextView mLabel;
protected ViewGroup mFields;
protected View mDelete;
protected View mMore;
protected DataKind mKind;
protected ValuesDelta mEntry;
protected EntityDelta mState;
protected boolean mReadOnly;
protected boolean mHideOptional = true;
protected EditType mType;
// Used only when a user tries to use custom label.
private EditType mPendingType;
public GenericEditorView(Context context) {
super(context);
}
public GenericEditorView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/** {@inheritDoc} */
@Override
protected void onFinishInflate() {
mInflater = (LayoutInflater)getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
mLabel = (TextView)findViewById(R.id.edit_label);
mLabel.setOnClickListener(this);
mFields = (ViewGroup)findViewById(R.id.edit_fields);
mDelete = findViewById(R.id.edit_delete);
mDelete.setOnClickListener(this);
mMore = findViewById(R.id.edit_more);
mMore.setOnClickListener(this);
}
protected EditorListener mListener;
public void setEditorListener(EditorListener listener) {
mListener = listener;
}
public void setDeletable(boolean deletable) {
mDelete.setVisibility(deletable ? View.VISIBLE : View.INVISIBLE);
}
@Override
public void setEnabled(boolean enabled) {
mLabel.setEnabled(enabled);
final int count = mFields.getChildCount();
for (int pos = 0; pos < count; pos++) {
final View v = mFields.getChildAt(pos);
v.setEnabled(enabled);
}
mMore.setEnabled(enabled);
}
/**
* Build the current label state based on selected {@link EditType} and
* possible custom label string.
*/
private void rebuildLabel() {
// Handle undetected types
if (mType == null) {
mLabel.setText(R.string.unknown);
return;
}
if (mType.customColumn != null) {
// Use custom label string when present
final String customText = mEntry.getAsString(mType.customColumn);
if (customText != null) {
mLabel.setText(customText);
return;
}
}
// Otherwise fall back to using default label
mLabel.setText(mType.labelRes);
}
/** {@inheritDoc} */
public void onFieldChanged(String column, String value) {
// Field changes are saved directly
mEntry.put(column, value);
if (mListener != null) {
mListener.onRequest(EditorListener.FIELD_CHANGED);
}
}
private void rebuildValues() {
setValues(mKind, mEntry, mState, mReadOnly);
}
/**
* Prepare this editor using the given {@link DataKind} for defining
* structure and {@link ValuesDelta} describing the content to edit.
*/
public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state, boolean readOnly) {
mKind = kind;
mEntry = entry;
mState = state;
mReadOnly = readOnly;
final boolean enabled = !readOnly;
if (!entry.isVisible()) {
// Hide ourselves entirely if deleted
setVisibility(View.GONE);
return;
} else {
setVisibility(View.VISIBLE);
}
// Display label selector if multiple types available
final boolean hasTypes = EntityModifier.hasEditTypes(kind);
mLabel.setVisibility(hasTypes ? View.VISIBLE : View.GONE);
mLabel.setEnabled(enabled);
if (hasTypes) {
mType = EntityModifier.getCurrentType(entry, kind);
rebuildLabel();
}
// Build out set of fields
mFields.removeAllViews();
boolean hidePossible = false;
for (EditField field : kind.fieldList) {
// Inflate field from definition
EditText fieldView = (EditText)mInflater.inflate(RES_FIELD, mFields, false);
if (field.titleRes > 0) {
fieldView.setHint(field.titleRes);
}
int inputType = field.inputType;
fieldView.setInputType(inputType);
if (inputType == InputType.TYPE_CLASS_PHONE) {
fieldView.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
}
fieldView.setMinLines(field.minLines);
// Read current value from state
final String column = field.column;
final String value = entry.getAsString(column);
fieldView.setText(value);
// Prepare listener for writing changes
fieldView.addTextChangedListener(new TextWatcher() {
public void afterTextChanged(Editable s) {
// Trigger event for newly changed value
onFieldChanged(column, s.toString());
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
// Hide field when empty and optional value
final boolean couldHide = (!ContactsUtils.isGraphic(value) && field.optional);
final boolean willHide = (mHideOptional && couldHide);
fieldView.setVisibility(willHide ? View.GONE : View.VISIBLE);
fieldView.setEnabled(enabled);
hidePossible = hidePossible || couldHide;
mFields.addView(fieldView);
}
// When hiding fields, place expandable
mMore.setVisibility(hidePossible ? View.VISIBLE : View.GONE);
mMore.setEnabled(enabled);
}
/**
* Prepare dialog for entering a custom label. The input value is trimmed: white spaces before
* and after the input text is removed.
* <p>
* If the final value is empty, this change request is ignored;
* no empty text is allowed in any custom label.
*/
private Dialog createCustomDialog() {
final EditText customType = new EditText(getContext());
customType.setInputType(INPUT_TYPE_CUSTOM);
customType.requestFocus();
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle(R.string.customLabelPickerTitle);
builder.setView(customType);
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
final String customText = customType.getText().toString().trim();
if (ContactsUtils.isGraphic(customText)) {
// Now we're sure it's ok to actually change the type value.
mType = mPendingType;
mPendingType = null;
mEntry.put(mKind.typeColumn, mType.rawValue);
mEntry.put(mType.customColumn, customText);
rebuildLabel();
}
}
});
builder.setNegativeButton(android.R.string.cancel, null);
return builder.create();
}
/**
* Prepare dialog for picking a new {@link EditType} or entering a
* custom label. This dialog is limited to the valid types as determined
* by {@link EntityModifier}.
*/
public Dialog createLabelDialog() {
// Build list of valid types, including the current value
final List<EditType> validTypes = EntityModifier.getValidTypes(mState, mKind, mType);
// Wrap our context to inflate list items using correct theme
final Context dialogContext = new ContextThemeWrapper(getContext(),
android.R.style.Theme_Light);
final LayoutInflater dialogInflater = mInflater.cloneInContext(dialogContext);
final ListAdapter typeAdapter = new ArrayAdapter<EditType>(getContext(), RES_LABEL_ITEM,
validTypes) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = dialogInflater.inflate(RES_LABEL_ITEM, parent, false);
}
final EditType type = this.getItem(position);
final TextView textView = (TextView)convertView;
textView.setText(type.labelRes);
return textView;
}
};
final DialogInterface.OnClickListener clickListener =
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
final EditType selected = validTypes.get(which);
if (selected.customColumn != null) {
// Show custom label dialog if requested by type.
//
// Only when the custum value input in the next step is correct one.
// this method also set the type value to what the user requested here.
mPendingType = selected;
createCustomDialog().show();
} else {
// User picked type, and we're sure it's ok to actually write the entry.
mType = selected;
mEntry.put(mKind.typeColumn, mType.rawValue);
rebuildLabel();
}
}
};
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle(R.string.selectLabel);
builder.setSingleChoiceItems(typeAdapter, 0, clickListener);
return builder.create();
}
/** {@inheritDoc} */
public void onClick(View v) {
switch (v.getId()) {
case R.id.edit_label: {
createLabelDialog().show();
break;
}
case R.id.edit_delete: {
// Keep around in model, but mark as deleted
mEntry.markDeleted();
// Remove editor from parent view
final ViewGroup parent = (ViewGroup)getParent();
parent.removeView(this);
if (mListener != null) {
// Notify listener when present
mListener.onDeleted(this);
}
break;
}
case R.id.edit_more: {
mHideOptional = !mHideOptional;
rebuildValues();
break;
}
}
}
}