/* * 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; } } } }