/*
* Copyright (C) 2012 The CyanogenMod 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.cyanogenmod.filemanager.ui.widgets;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.method.ScrollingMovementMethod;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.cyanogenmod.filemanager.R;
import com.cyanogenmod.filemanager.ui.ThemeManager;
import com.cyanogenmod.filemanager.ui.ThemeManager.Theme;
import com.cyanogenmod.filemanager.util.DialogHelper;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* A widget that performs an inline autocomplete for an {@link EditText}
* control.
*/
public class InlineAutocompleteTextView extends RelativeLayout
implements View.OnClickListener, View.OnLongClickListener,
View.OnKeyListener {
/**
* An interface to communicate events when a text value is changed.
*/
public interface OnTextChangedListener {
/**
* Method invoked when the value of the initial directory was changed.
*
* @param newValue The new value of the text
* @param currentFilterData The current set of string for filter
*/
void onTextChanged(String newValue, List<String> currentFilterData);
}
private List<String> mData;
private OnTextChangedListener mOnTextChangedListener;
private EditText mBackgroundText;
private EditText mForegroundText;
private String mCompletionString = null;
private int mFilter = 0;
private FilteredTextWatcher mTextWatcher;
/**
* Constructor of <code>InlineAutocompleteTextView</code>.
*
* @param context The current context
*/
public InlineAutocompleteTextView(Context context) {
super(context);
init();
}
/**
* Constructor of <code>InlineAutocompleteTextView</code>.
*
* @param context The current context
* @param attrs The attributes of the XML tag that is inflating the view.
*/
public InlineAutocompleteTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* Constructor of <code>InlineAutocompleteTextView</code>.
*
* @param context The current context
* @param attrs The attributes of the XML tag that is inflating the view.
* @param defStyle The default style to apply to this view. If 0, no style
* will be applied (beyond what is included in the theme). This may
* either be an attribute resource, whose value will be retrieved
* from the current theme, or an explicit style resource.
*/
public InlineAutocompleteTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/**
* Method that initializes the view. This method loads all the necessary
* information and create an appropriate layout for the view
*/
private void init() {
//Initialize data
this.mData = new ArrayList<String>();
this.mOnTextChangedListener = null;
this.mTextWatcher = new FilteredTextWatcher();
//Inflate the view
ViewGroup v = (ViewGroup)inflate(getContext(), R.layout.inline_autocomplete, null);
addView(v);
//Retrieve views
this.mBackgroundText = (EditText)findViewById(R.id.inline_autocomplete_bg_text);
this.mBackgroundText.setOnKeyListener(this);
this.mForegroundText = (EditText)findViewById(R.id.inline_autocomplete_fg_text);
this.mForegroundText.setMovementMethod(new ScrollingMovementMethod());
this.mForegroundText.addTextChangedListener(this.mTextWatcher);
this.mForegroundText.setOnKeyListener(this);
this.mForegroundText.requestFocus();
this.mForegroundText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView view, int actionId, KeyEvent event) {
doDone(true);
return false;
}
});
View button = findViewById(R.id.inline_autocomplete_button_tab);
button.setOnClickListener(this);
button.setOnLongClickListener(this);
// Apply the theme
applyTheme();
//Initialize text views
setText(""); //$NON-NLS-1$
}
/**
* {@inheritDoc}
*/
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// Show the soft keyboard (only if the device has't a hardware keyboard)
Configuration config = getContext().getResources().getConfiguration();
if (config.keyboard == Configuration.KEYBOARD_NOKEYS) {
Activity activity = (Activity)getContext();
InputMethodManager imm =
(InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInputFromInputMethod(
this.mForegroundText.getWindowToken(), 0);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// Hide the soft keyboard
Configuration config = getContext().getResources().getConfiguration();
if (config.keyboard == Configuration.KEYBOARD_NOKEYS) {
Activity activity = (Activity)getContext();
InputMethodManager imm =
(InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(this.mForegroundText.getWindowToken(), 0);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.FLAG_EDITOR_ACTION:
//Consume the event
doDone(false);
return true;
case KeyEvent.KEYCODE_TAB:
//Do tab, and consume the event
doTab();
return true;
default:
break;
}
return false;
}
/**
* Method that sets the current text value.
*
* @param value The value to set to current text value
*/
public void setText(String value) {
this.mBackgroundText.setVisibility(View.INVISIBLE);
this.mForegroundText.setText(value);
this.mBackgroundText.setText(""); //$NON-NLS-1$
onTextChanged(value);
this.mForegroundText.requestFocus();
this.mForegroundText.setSelection(value.length());
int lines = this.mBackgroundText.getLineCount();
this.mForegroundText.setLines(lines <= 0 ? 1 : lines);
}
/**
* Method that returns the current text value.
*
* @return The current text value
*/
public String getText() {
return this.mForegroundText.getText().toString();
}
/**
* Method that sets the listener for send text change events.
*
* @param onTextChangedListener The text changed listener
*/
public void setOnTextChangedListener(OnTextChangedListener onTextChangedListener) {
this.mOnTextChangedListener = onTextChangedListener;
}
/**
* Method that returns a string for autocomplete.
*
* @return String The autocomplete string
*/
public String getCompletionString() {
return this.mCompletionString;
}
/**
* Method that sets a string for autocomplete.
*
* @param completionString The autocomplete string
*/
public void setCompletionString(String completionString) {
this.mCompletionString = completionString;
}
/**
* {@inheritDoc}
*/
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.inline_autocomplete_button_tab:
doTab();
break;
default:
break;
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean onLongClick(View v) {
switch (v.getId()) {
case R.id.inline_autocomplete_button_tab:
//Complete with current suggestion
String filter = this.mBackgroundText.getText().toString();
if (this.mBackgroundText.getVisibility() == View.VISIBLE && filter.length() > 0) {
setText(filter);
} else {
post(new Runnable() {
@Override
public void run() {
DialogHelper.showToast(
getContext(),
R.string.inline_autocomplete_tab_nothing_to_complete_msg,
Toast.LENGTH_SHORT);
}
});
}
return true;
default:
break;
}
return false;
}
/**
* Method that need to be invoked when a string was changed
*
* @param value The new string
* @hide
*/
void onTextChanged(String value) {
this.mFilter = 0;
if (this.mOnTextChangedListener != null) {
//Communicate the change
this.mOnTextChangedListener.onTextChanged(
value.toString(), this.mData);
if (this.mCompletionString != null &&
!value.toString().endsWith(this.mCompletionString)) {
//Autocomplete
autocomplete();
} else {
this.mBackgroundText.setVisibility(View.INVISIBLE);
}
}
}
/**
* Method that autocompletes the text, showing the best matches.
*/
private void autocomplete() {
final String currentText = getText();
if (!this.mData.isEmpty()) {
Iterator<String> it = this.mData.iterator();
while (it.hasNext()) {
String filterData = it.next();
if (filterData.startsWith(currentText)) {
this.mBackgroundText.setText(filterData);
this.mBackgroundText.setVisibility(View.VISIBLE);
int lines = this.mBackgroundText.getLineCount();
this.mForegroundText.setLines(lines <= 0 ? 1 : lines);
return;
}
}
}
this.mBackgroundText.setVisibility(View.INVISIBLE);
}
/**
* Method invoked when a tab key event is requested (button or keyboard)
* @hide
*/
void doTab() {
//Complete with current text
String current = this.mForegroundText.getText().toString();
if (current.length() == 0) {
return;
}
//Get the data
List<String> filteredData = filter(this.mData, current);
if (filteredData.size() <= this.mFilter) {
this.mFilter = 0;
}
if (filteredData.size() == 1 && this.mFilter == 0) {
//Autocomplete with the only autocomplete option
setText(filteredData.get(this.mFilter));
} else {
//Show the autocomplete options
if (filteredData.size() > 0) {
this.mBackgroundText.setText(filteredData.get(this.mFilter));
this.mBackgroundText.setVisibility(View.VISIBLE);
this.mFilter++;
}
}
}
/**
* Method that creates a temporary filter based in the current text
*
* @param data The global data array
* @param current The current text
* @return The filtered data array
*/
private static List<String> filter(List<String> data, String current) {
List<String> filter = new ArrayList<String>(data);
int size = filter.size();
for (int i=size-1; i>=0; i--) {
String s = filter.get(i);
if (!s.startsWith(current)) {
filter.remove(i);
}
}
return filter;
}
/**
* Method invoked when a enter key event is requested (button or keyboard)
*
* @param fromEditorAction It this method was invoked from editor action
* @hide
*/
void doDone(boolean fromEditorAction) {
if (fromEditorAction) {
// Hide the soft keyboard
Configuration config = getContext().getResources().getConfiguration();
if (config.keyboard == Configuration.KEYBOARD_NOKEYS) {
Activity activity = (Activity)getContext();
InputMethodManager imm =
(InputMethodManager) activity.getSystemService(
Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(this.mForegroundText.getWindowToken(), 0);
}
}
}
/**
* A class for filter the text introduced by the user
*/
private class FilteredTextWatcher implements TextWatcher {
private String mText;
private int mStart;
private int mCount;
public FilteredTextWatcher() {/**NON BLOCK**/}
/**
* {@inheritDoc}
*/
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
/**NON BLOCK**/
}
/**
* {@inheritDoc}
*/
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
this.mStart = start;
this.mCount = count;
}
/**
* {@inheritDoc}
*/
@Override
public void afterTextChanged(Editable s) {
// Enter and Tab are not allowed, and have their own treatment
final String orig = s.toString();
String text = orig;
final int start = this.mStart;
final int count = this.mCount;
// Avoid recursive calls
if (this.mText != null && this.mText.compareTo(orig) == 0) {
return;
}
// Force to ignore Tab and Enter keys
text = replaceKeyEvent(text, '\t', true);
text = replaceKeyEvent(text, '\r', false);
text = replaceKeyEvent(text, '\n', false);
if (text.compareTo(orig) != 0) {
// Some key has internally changed
this.mText = text;
s.replace(0, orig.length(), text);
}
// Does the user input enter or tab keys?
boolean tab = false;
boolean enter = false;
String userInput = orig.substring(start, start + count);
tab = (userInput.indexOf("\t") != -1); //$NON-NLS-1$
enter = (userInput.indexOf("\r") != -1 || //$NON-NLS-1$
userInput.indexOf("\n") != -1); //$NON-NLS-1$
// Check events
if (enter) {
// Broadcast enter event
doDone(false);
return;
}
if (tab) {
// Broadcast enter event
doTab();
return;
}
// Broadcast the new text value
InlineAutocompleteTextView.this.onTextChanged(text);
}
/**
* Method that replace a key char
*
* @param value The string in which search
* @param key The key char to search
* @param removeAfter If true the string will truncate after the first key char found
* @return String The replaced string
*/
private String replaceKeyEvent(String value, char key, boolean removeAfter) {
String text = value;
int pos = text.indexOf(key);
while (pos != -1) {
if (removeAfter) {
text = text.substring(0, pos);
} else {
text = text.replaceAll(String.valueOf(key), ""); //$NON-NLS-1$
}
pos = text.indexOf(key);
}
return text;
}
}
/**
* Method that applies the current theme to the widget components
*/
public void applyTheme() {
Theme theme = ThemeManager.getCurrentTheme(getContext());
View v = findViewById(R.id.inline_autocomplete_bg_text);
theme.setTextColor(getContext(), (TextView)v, "text_color"); //$NON-NLS-1$
v = findViewById(R.id.inline_autocomplete_fg_text);
theme.setTextColor(getContext(), (TextView)v, "text_color"); //$NON-NLS-1$
v = findViewById(R.id.inline_autocomplete_button_tab);
theme.setImageDrawable(
getContext(), (ImageView)v, "ab_tab_drawable"); //$NON-NLS-1$
}
}