Merge remote branch 'upstream/dev' into dev
@ -0,0 +1,198 @@
|
||||
/**
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.astrid.api;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.todoroo.andlib.utility.DateUtilities;
|
||||
|
||||
import edu.umd.cs.findbugs.annotations.CheckForNull;
|
||||
|
||||
/**
|
||||
* CustomFilterCriteria allow users to build a custom filter by chaining
|
||||
* together criteria
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public final class CustomFilterCriterion implements Parcelable {
|
||||
|
||||
// --- placeholder strings
|
||||
|
||||
/** value to be replaced with the current time as long */
|
||||
public static final String VALUE_NOW = "NOW()"; //$NON-NLS-1$
|
||||
|
||||
/** value to be replaced by end of day as long */
|
||||
public static final String VALUE_EOD = "EOD()"; //$NON-NLS-1$
|
||||
|
||||
/** value to be replaced by end of day yesterday as long */
|
||||
public static final String VALUE_EOD_YESTERDAY = "EODY()"; //$NON-NLS-1$
|
||||
|
||||
/** value to be replaced by end of day tomorrow as long */
|
||||
public static final String VALUE_EOD_TOMORROW = "EODT()"; //$NON-NLS-1$
|
||||
|
||||
/** value to be replaced by end of day day after tomorrow as long */
|
||||
public static final String VALUE_EOD_DAY_AFTER = "EODTT()"; //$NON-NLS-1$
|
||||
|
||||
/** value to be replaced by end of day next week as long */
|
||||
public static final String VALUE_EOD_NEXT_WEEK = "EODW()"; //$NON-NLS-1$
|
||||
|
||||
/** Replace placeholder strings with actual */
|
||||
public static String replacePlaceholders(String value) {
|
||||
if(value.contains(VALUE_NOW))
|
||||
value = value.replace(VALUE_NOW, Long.toString(DateUtilities.now()));
|
||||
if(value.contains(VALUE_EOD) || value.contains(VALUE_EOD_DAY_AFTER) ||
|
||||
value.contains(VALUE_EOD_NEXT_WEEK) || value.contains(VALUE_EOD_TOMORROW) ||
|
||||
value.contains(VALUE_EOD_YESTERDAY)) {
|
||||
Date date = new Date();
|
||||
date.setHours(23);
|
||||
date.setMinutes(59);
|
||||
date.setSeconds(59);
|
||||
long time = date.getTime();
|
||||
value = value.replace(VALUE_EOD_YESTERDAY, Long.toString(time - DateUtilities.ONE_DAY));
|
||||
value = value.replace(VALUE_EOD, Long.toString(time));
|
||||
value = value.replace(VALUE_EOD_TOMORROW, Long.toString(time + DateUtilities.ONE_DAY));
|
||||
value = value.replace(VALUE_EOD_DAY_AFTER, Long.toString(time + 2 * DateUtilities.ONE_DAY));
|
||||
value = value.replace(VALUE_EOD_NEXT_WEEK, Long.toString(time + 7 * DateUtilities.ONE_DAY));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
// --- instance variables
|
||||
|
||||
/**
|
||||
* Criteria Title. If the title contains ?, this is replaced by the entry
|
||||
* label string selected.
|
||||
* <p>
|
||||
* e.g "Due: ?"
|
||||
*/
|
||||
@CheckForNull
|
||||
public String text;
|
||||
|
||||
/**
|
||||
* Criterion SQL. This query should return task id's. If this contains
|
||||
* ?, it will be replaced by the entry value
|
||||
* <p>
|
||||
* Examples:
|
||||
* <ul>
|
||||
* <li><code>SELECT _id FROM tasks WHERE dueDate <= ?</code>
|
||||
* <li><code>SELECT task FROM metadata WHERE value = '?'</code>
|
||||
* </ul>
|
||||
*/
|
||||
@CheckForNull
|
||||
public String sql;
|
||||
|
||||
/**
|
||||
* Values to apply to a task when quick-adding a task from a filter
|
||||
* created from this criterion. ? will be replaced with the entry value.
|
||||
* For example, when a user views tasks tagged 'ABC', the
|
||||
* tasks they create should also be tagged 'ABC'. If set to null, no
|
||||
* additional values will be stored for a task.
|
||||
*/
|
||||
@CheckForNull
|
||||
public ContentValues valuesForNewTasks = null;
|
||||
|
||||
/**
|
||||
* Array of entries for user to select from
|
||||
*/
|
||||
@CheckForNull
|
||||
public String[] entryTitles;
|
||||
|
||||
/**
|
||||
* Array of entry values corresponding to entries
|
||||
*/
|
||||
@CheckForNull
|
||||
public String[] entryValues;
|
||||
|
||||
/**
|
||||
* Icon for this criteria. Can be null for no bitmap
|
||||
*/
|
||||
@CheckForNull
|
||||
public Bitmap icon;
|
||||
|
||||
/**
|
||||
* Criteria name. This is displayed when users are selecting a criteria
|
||||
*/
|
||||
@CheckForNull
|
||||
public String name;
|
||||
|
||||
/**
|
||||
* Create a new CustomFilterCriteria object
|
||||
*
|
||||
* @param title
|
||||
* @param sql
|
||||
* @param valuesForNewTasks
|
||||
* @param entryTitles
|
||||
* @param entryValues
|
||||
* @param icon
|
||||
* @param name
|
||||
*/
|
||||
public CustomFilterCriterion(String title, String sql,
|
||||
ContentValues valuesForNewTasks, String[] entryTitles,
|
||||
String[] entryValues, Bitmap icon, String name) {
|
||||
this.text = title;
|
||||
this.sql = sql;
|
||||
this.valuesForNewTasks = valuesForNewTasks;
|
||||
this.entryTitles = entryTitles;
|
||||
this.entryValues = entryValues;
|
||||
this.icon = icon;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
// --- parcelable
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(text);
|
||||
dest.writeString(sql);
|
||||
dest.writeParcelable(valuesForNewTasks, 0);
|
||||
dest.writeStringArray(entryTitles);
|
||||
dest.writeStringArray(entryValues);
|
||||
dest.writeParcelable(icon, 0);
|
||||
dest.writeString(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parcelable Creator Object
|
||||
*/
|
||||
public static final Parcelable.Creator<CustomFilterCriterion> CREATOR = new Parcelable.Creator<CustomFilterCriterion>() {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public CustomFilterCriterion createFromParcel(Parcel source) {
|
||||
CustomFilterCriterion item = new CustomFilterCriterion(
|
||||
source.readString(), source.readString(),
|
||||
(ContentValues)source.readParcelable(ContentValues.class.getClassLoader()),
|
||||
source.createStringArray(), source.createStringArray(),
|
||||
(Bitmap)source.readParcelable(Bitmap.class.getClassLoader()),
|
||||
source.readString());
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public CustomFilterCriterion[] newArray(int size) {
|
||||
return new CustomFilterCriterion[size];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
@ -0,0 +1,414 @@
|
||||
package com.todoroo.astrid.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import android.app.ListActivity;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.View.OnCreateContextMenuListener;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.timsu.astrid.R;
|
||||
import com.todoroo.andlib.data.Property.CountProperty;
|
||||
import com.todoroo.andlib.service.Autowired;
|
||||
import com.todoroo.andlib.service.ContextManager;
|
||||
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||
import com.todoroo.andlib.sql.Criterion;
|
||||
import com.todoroo.andlib.sql.Join;
|
||||
import com.todoroo.andlib.sql.Query;
|
||||
import com.todoroo.astrid.activity.TaskListActivity;
|
||||
import com.todoroo.astrid.api.CustomFilterCriterion;
|
||||
import com.todoroo.astrid.api.Filter;
|
||||
import com.todoroo.astrid.dao.Database;
|
||||
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
|
||||
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
|
||||
import com.todoroo.astrid.model.Metadata;
|
||||
import com.todoroo.astrid.model.Task;
|
||||
import com.todoroo.astrid.tags.TagService;
|
||||
import com.todoroo.astrid.tags.TagService.Tag;
|
||||
|
||||
/**
|
||||
* Activity that allows users to build custom filters
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class CustomFilterActivity extends ListActivity {
|
||||
|
||||
static final int MENU_GROUP_FILTER = 0;
|
||||
static final int MENU_GROUP_FILTER_OPTION = 1;
|
||||
static final int MENU_GROUP_CONTEXT_TYPE = 2;
|
||||
static final int MENU_GROUP_CONTEXT_DELETE = 3;
|
||||
|
||||
// --- hierarchy of filter classes
|
||||
|
||||
public static class CriterionInstance {
|
||||
public static final int TYPE_ADD = 0;
|
||||
public static final int TYPE_SUBTRACT = 1;
|
||||
public static final int TYPE_INTERSECT = 2;
|
||||
public static final int TYPE_UNIVERSE = 3;
|
||||
|
||||
/** criteria for this instance */
|
||||
public CustomFilterCriterion criterion;
|
||||
|
||||
/** which of the entries is selected */
|
||||
public int selectedIndex = -1;
|
||||
|
||||
/** type of join */
|
||||
public int type = TYPE_INTERSECT;
|
||||
|
||||
/** statistics for {@link FilterView} */
|
||||
public int start, end, max;
|
||||
}
|
||||
|
||||
private TextView filterName;
|
||||
private CustomFilterAdapter adapter;
|
||||
private final ArrayList<CustomFilterCriterion> criteria =
|
||||
new ArrayList<CustomFilterCriterion>();
|
||||
|
||||
// --- activity
|
||||
|
||||
@Autowired
|
||||
Database database;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ContextManager.setContext(this);
|
||||
|
||||
setContentView(R.layout.custom_filter_activity);
|
||||
setTitle(R.string.CFA_title);
|
||||
|
||||
DependencyInjectionService.getInstance().inject(this);
|
||||
database.openForReading();
|
||||
populateCriteria();
|
||||
|
||||
filterName = (TextView)findViewById(R.id.filterName);
|
||||
List<CriterionInstance> startingCriteria = new ArrayList<CriterionInstance>();
|
||||
startingCriteria.add(getStartingUniverse());
|
||||
adapter = new CustomFilterAdapter(this, startingCriteria);
|
||||
setListAdapter(adapter);
|
||||
updateList();
|
||||
|
||||
setUpListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate criteria list with built in and plugin criteria
|
||||
*/
|
||||
@SuppressWarnings("nls")
|
||||
private void populateCriteria() {
|
||||
Resources r = getResources();
|
||||
|
||||
// built in criteria: due date
|
||||
String[] entryValues = new String[] {
|
||||
CustomFilterCriterion.VALUE_EOD_YESTERDAY,
|
||||
CustomFilterCriterion.VALUE_EOD,
|
||||
CustomFilterCriterion.VALUE_EOD_TOMORROW,
|
||||
CustomFilterCriterion.VALUE_EOD_DAY_AFTER,
|
||||
CustomFilterCriterion.VALUE_EOD_NEXT_WEEK,
|
||||
};
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Task.DUE_DATE.name, "?");
|
||||
CustomFilterCriterion criterion = new CustomFilterCriterion(
|
||||
getString(R.string.CFC_dueBefore_text),
|
||||
Query.select(Task.ID).from(Task.TABLE).where(
|
||||
Criterion.and(
|
||||
TaskCriteria.activeAndVisible(),
|
||||
Task.DUE_DATE.gt(0),
|
||||
Task.DUE_DATE.lte("?"))).toString(),
|
||||
values, r.getStringArray(R.array.CFC_dueBefore_entries),
|
||||
entryValues, ((BitmapDrawable)r.getDrawable(R.drawable.tango_calendar)).getBitmap(),
|
||||
getString(R.string.CFC_dueBefore_name));
|
||||
criteria.add(criterion);
|
||||
|
||||
// built in criteria: importance
|
||||
entryValues = new String[] {
|
||||
Integer.toString(Task.IMPORTANCE_DO_OR_DIE),
|
||||
Integer.toString(Task.IMPORTANCE_MUST_DO),
|
||||
Integer.toString(Task.IMPORTANCE_SHOULD_DO),
|
||||
Integer.toString(Task.IMPORTANCE_NONE),
|
||||
};
|
||||
String[] entries = new String[] {
|
||||
"!!!!", "!!!", "!!", "!"
|
||||
};
|
||||
values = new ContentValues();
|
||||
values.put(Task.IMPORTANCE.name, "?");
|
||||
criterion = new CustomFilterCriterion(
|
||||
getString(R.string.CFC_importance_text),
|
||||
Query.select(Task.ID).from(Task.TABLE).where(
|
||||
Criterion.and(TaskCriteria.activeAndVisible(),
|
||||
Task.IMPORTANCE.lte("?"))).toString(),
|
||||
values, entries,
|
||||
entryValues, ((BitmapDrawable)r.getDrawable(R.drawable.tango_warning)).getBitmap(),
|
||||
getString(R.string.CFC_importance_name));
|
||||
criteria.add(criterion);
|
||||
|
||||
// built in criteria: tags
|
||||
Tag[] tags = TagService.getInstance().getGroupedTags(TagService.GROUPED_TAGS_BY_SIZE, Criterion.all);
|
||||
String[] tagNames = new String[tags.length];
|
||||
for(int i = 0; i < tags.length; i++)
|
||||
tagNames[i] = tags[i].tag;
|
||||
values = new ContentValues();
|
||||
values.put(Metadata.KEY.name, TagService.KEY);
|
||||
values.put(TagService.TAG.name, "?");
|
||||
criterion = new CustomFilterCriterion(
|
||||
getString(R.string.CFC_tag_text),
|
||||
Query.select(Metadata.TASK).from(Metadata.TABLE).join(Join.inner(
|
||||
Task.TABLE, Metadata.TASK.eq(Task.ID))).where(Criterion.and(
|
||||
TaskCriteria.activeAndVisible(),
|
||||
MetadataCriteria.withKey(TagService.KEY),
|
||||
TagService.TAG.eq("?"))).toString(),
|
||||
values, tagNames, tagNames,
|
||||
((BitmapDrawable)r.getDrawable(R.drawable.filter_tags1)).getBitmap(),
|
||||
getString(R.string.CFC_tag_name));
|
||||
criteria.add(criterion);
|
||||
}
|
||||
|
||||
private CriterionInstance getStartingUniverse() {
|
||||
CriterionInstance instance = new CriterionInstance();
|
||||
instance.criterion = new CustomFilterCriterion(getString(R.string.CFA_universe_all),
|
||||
null, null, null, null, null, null);
|
||||
instance.type = CriterionInstance.TYPE_UNIVERSE;
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void setUpListeners() {
|
||||
((Button)findViewById(R.id.add)).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
menuItemInstance = null;
|
||||
getListView().showContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
((Button)findViewById(R.id.saveAndView)).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
saveAndView();
|
||||
}
|
||||
});
|
||||
|
||||
getListView().setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v,
|
||||
ContextMenuInfo menuInfo) {
|
||||
if(menu.hasVisibleItems())
|
||||
return;
|
||||
|
||||
if(menuItemInstance == null) {
|
||||
for(int i = 0; i < criteria.size(); i++) {
|
||||
CustomFilterCriterion item = criteria.get(i);
|
||||
SubMenu subMenu = menu.addSubMenu(item.name);
|
||||
if(item.icon != null)
|
||||
subMenu.setIcon(new BitmapDrawable(item.icon));
|
||||
|
||||
for(int j = 0; j < item.entryTitles.length; j++) {
|
||||
subMenu.add(CustomFilterActivity.MENU_GROUP_FILTER_OPTION,
|
||||
i, j, item.entryTitles[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// was invoked by short-pressing row
|
||||
else {
|
||||
CustomFilterCriterion criterion = menuItemInstance.criterion;
|
||||
if(criterion.entryTitles == null ||
|
||||
criterion.entryTitles.length == 0)
|
||||
return;
|
||||
|
||||
menu.setHeaderTitle(criterion.name);
|
||||
menu.setGroupCheckable(CustomFilterActivity.MENU_GROUP_FILTER_OPTION, true, true);
|
||||
|
||||
for(int i = 0; i < criterion.entryTitles.length; i++) {
|
||||
menu.add(CustomFilterActivity.MENU_GROUP_FILTER_OPTION,
|
||||
-1, i, criterion.entryTitles[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- listeners and action events
|
||||
|
||||
CriterionInstance menuItemInstance = null;
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||
if(menu.size() > 0)
|
||||
menu.clear();
|
||||
|
||||
// view holder
|
||||
if(v.getTag() != null) {
|
||||
adapter.onCreateContextMenu(menu, v);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
void saveAndView() {
|
||||
StringBuilder sql = new StringBuilder(" WHERE ");
|
||||
for(int i = 0; i < adapter.getCount(); i++) {
|
||||
CriterionInstance instance = adapter.getItem(i);
|
||||
if(instance.selectedIndex < 0 && instance.criterion.entryValues != null)
|
||||
continue;
|
||||
|
||||
switch(instance.type) {
|
||||
case CriterionInstance.TYPE_ADD:
|
||||
sql.append("OR ");
|
||||
break;
|
||||
case CriterionInstance.TYPE_SUBTRACT:
|
||||
sql.append("AND NOT ");
|
||||
break;
|
||||
case CriterionInstance.TYPE_INTERSECT:
|
||||
sql.append("AND ");
|
||||
break;
|
||||
case CriterionInstance.TYPE_UNIVERSE:
|
||||
}
|
||||
|
||||
// special code for all tasks universe
|
||||
if(instance.criterion.sql == null)
|
||||
sql.append(TaskCriteria.activeAndVisible()).append(' ');
|
||||
else {
|
||||
String subSql = instance.criterion.sql.replace("?",
|
||||
instance.criterion.entryValues[instance.selectedIndex]);
|
||||
subSql = CustomFilterCriterion.replacePlaceholders(subSql);
|
||||
sql.append(Task.ID).append(" IN (").append(subSql).append(") ");
|
||||
}
|
||||
}
|
||||
|
||||
String title;
|
||||
if(filterName.getText().length() > 0)
|
||||
title = filterName.getText().toString();
|
||||
else
|
||||
title = filterName.getHint().toString();
|
||||
|
||||
ContentValues values = new ContentValues(); // TODO
|
||||
Filter filter = new Filter(title, title, null, values);
|
||||
filter.sqlQuery = sql.toString();
|
||||
|
||||
// TODO save
|
||||
|
||||
Intent taskListActivity = new Intent(this, TaskListActivity.class);
|
||||
taskListActivity.putExtra(TaskListActivity.TOKEN_FILTER, filter);
|
||||
startActivity(taskListActivity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate all sizes
|
||||
*/
|
||||
@SuppressWarnings("nls")
|
||||
void updateList() {
|
||||
int max = 0, last = -1;
|
||||
|
||||
StringBuilder sql = new StringBuilder(Query.select(new CountProperty()).from(Task.TABLE).toString()).
|
||||
append(" WHERE ");
|
||||
StringBuilder suggestedTitle = new StringBuilder();
|
||||
|
||||
for(int i = 0; i < adapter.getCount(); i++) {
|
||||
CriterionInstance instance = adapter.getItem(i);
|
||||
if(instance.selectedIndex < 0 && instance.criterion.entryValues != null) {
|
||||
instance.start = last;
|
||||
instance.end = last;
|
||||
continue;
|
||||
}
|
||||
|
||||
String entryTitle = "";
|
||||
if(instance.criterion.entryTitles != null) {
|
||||
entryTitle = instance.criterion.entryTitles[instance.selectedIndex];
|
||||
}
|
||||
String title = instance.criterion.text.replace("?", entryTitle);
|
||||
|
||||
switch(instance.type) {
|
||||
case CriterionInstance.TYPE_ADD:
|
||||
sql.append("OR ");
|
||||
title = getString(R.string.CFA_type_add) + " " + title;
|
||||
break;
|
||||
case CriterionInstance.TYPE_SUBTRACT:
|
||||
sql.append("AND NOT ");
|
||||
title = getString(R.string.CFA_type_subtract) + " " + title;
|
||||
break;
|
||||
case CriterionInstance.TYPE_INTERSECT:
|
||||
sql.append("AND ");
|
||||
break;
|
||||
case CriterionInstance.TYPE_UNIVERSE:
|
||||
}
|
||||
|
||||
suggestedTitle.append(title).append(' ');
|
||||
|
||||
// special code for all tasks universe
|
||||
if(instance.criterion.sql == null)
|
||||
sql.append(TaskCriteria.activeAndVisible()).append(' ');
|
||||
else {
|
||||
String subSql = instance.criterion.sql.replace("?",
|
||||
instance.criterion.entryValues[instance.selectedIndex]);
|
||||
subSql = CustomFilterCriterion.replacePlaceholders(subSql);
|
||||
System.err.println(subSql);
|
||||
sql.append(Task.ID).append(" IN (").append(subSql).append(") ");
|
||||
}
|
||||
|
||||
Cursor cursor = database.getDatabase().rawQuery(sql.toString(), null);
|
||||
try {
|
||||
cursor.moveToNext();
|
||||
instance.start = last == -1 ? cursor.getInt(0) : last;
|
||||
instance.end = cursor.getInt(0);
|
||||
last = instance.end;
|
||||
max = Math.max(max, last);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
for(int i = 0; i < adapter.getCount(); i++) {
|
||||
CriterionInstance instance = adapter.getItem(i);
|
||||
instance.max = max;
|
||||
}
|
||||
|
||||
adapter.notifyDataSetInvalidated();
|
||||
|
||||
if(adapter.getCount() > 1 && filterName.getText().length() == 0)
|
||||
filterName.setHint(suggestedTitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(int featureId, MenuItem item) {
|
||||
if(item.getGroupId() == MENU_GROUP_FILTER_OPTION) {
|
||||
if(menuItemInstance == null) {
|
||||
CustomFilterCriterion criterion = criteria.get(item.getItemId());
|
||||
menuItemInstance = new CriterionInstance();
|
||||
menuItemInstance.criterion = criterion;
|
||||
}
|
||||
|
||||
menuItemInstance.selectedIndex = item.getOrder();
|
||||
if(adapter.getPosition(menuItemInstance) == -1)
|
||||
adapter.add(menuItemInstance);
|
||||
updateList();
|
||||
return true;
|
||||
}
|
||||
|
||||
else if(item.getGroupId() == MENU_GROUP_CONTEXT_TYPE) {
|
||||
CriterionInstance instance = adapter.getItem(item.getOrder());
|
||||
instance.type = item.getItemId();
|
||||
updateList();
|
||||
}
|
||||
|
||||
else if(item.getGroupId() == MENU_GROUP_CONTEXT_DELETE) {
|
||||
CriterionInstance instance = adapter.getItem(item.getOrder());
|
||||
adapter.remove(instance);
|
||||
updateList();
|
||||
}
|
||||
|
||||
return super.onMenuItemSelected(featureId, item);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
/**
|
||||
* See the file "LICENSE" for the full license governing this code.
|
||||
*/
|
||||
package com.todoroo.astrid.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.timsu.astrid.R;
|
||||
import com.todoroo.astrid.core.CustomFilterActivity.CriterionInstance;
|
||||
import com.todoroo.astrid.model.AddOn;
|
||||
|
||||
/**
|
||||
* Adapter for {@link AddOn}s
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class CustomFilterAdapter extends ArrayAdapter<CriterionInstance> {
|
||||
|
||||
private final Activity activity;
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
public CustomFilterAdapter(Activity activity, List<CriterionInstance> objects) {
|
||||
super(activity, R.id.name, objects);
|
||||
this.activity = activity;
|
||||
inflater = (LayoutInflater) activity.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE);
|
||||
}
|
||||
|
||||
// --- view event handling
|
||||
|
||||
View.OnClickListener filterClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ViewHolder viewHolder = (ViewHolder) v.getTag();
|
||||
if(viewHolder == null)
|
||||
return;
|
||||
if(viewHolder.item.type == CriterionInstance.TYPE_UNIVERSE)
|
||||
return;
|
||||
|
||||
// keep the filter options in the name context menu
|
||||
((CustomFilterActivity)activity).menuItemInstance = viewHolder.item;
|
||||
((CustomFilterActivity)activity).getListView().showContextMenu();
|
||||
}
|
||||
};
|
||||
|
||||
public void onCreateContextMenu(ContextMenu menu, View v) {
|
||||
// view holder
|
||||
ViewHolder viewHolder = (ViewHolder) v.getTag();
|
||||
if(viewHolder == null || viewHolder.item.type == CriterionInstance.TYPE_UNIVERSE)
|
||||
return;
|
||||
|
||||
int index = getPosition(viewHolder.item);
|
||||
|
||||
menu.setHeaderTitle(viewHolder.name.getText());
|
||||
if(viewHolder.icon.getVisibility() == View.VISIBLE)
|
||||
menu.setHeaderIcon(viewHolder.icon.getDrawable());
|
||||
|
||||
|
||||
MenuItem item = menu.add(CustomFilterActivity.MENU_GROUP_CONTEXT_TYPE, CriterionInstance.TYPE_INTERSECT, index,
|
||||
activity.getString(R.string.CFA_context_chain,
|
||||
activity.getString(R.string.CFA_type_intersect)));
|
||||
item.setChecked(viewHolder.item.type == CriterionInstance.TYPE_INTERSECT);
|
||||
item = menu.add(CustomFilterActivity.MENU_GROUP_CONTEXT_TYPE, CriterionInstance.TYPE_ADD, index,
|
||||
activity.getString(R.string.CFA_context_chain,
|
||||
activity.getString(R.string.CFA_type_add)));
|
||||
item.setChecked(viewHolder.item.type == CriterionInstance.TYPE_ADD);
|
||||
|
||||
item = menu.add(CustomFilterActivity.MENU_GROUP_CONTEXT_TYPE, CriterionInstance.TYPE_SUBTRACT, index,
|
||||
activity.getString(R.string.CFA_context_chain,
|
||||
activity.getString(R.string.CFA_type_subtract)));
|
||||
item.setChecked(viewHolder.item.type == CriterionInstance.TYPE_SUBTRACT);
|
||||
menu.setGroupCheckable(CustomFilterActivity.MENU_GROUP_CONTEXT_TYPE, true, true);
|
||||
|
||||
menu.add(CustomFilterActivity.MENU_GROUP_CONTEXT_DELETE, 0, index,
|
||||
R.string.CFA_context_delete);
|
||||
}
|
||||
|
||||
// --- view construction
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if(convertView == null) {
|
||||
convertView = inflater.inflate(R.layout.custom_filter_row, parent, false);
|
||||
ViewHolder viewHolder = new ViewHolder();
|
||||
viewHolder.type = (ImageView) convertView.findViewById(R.id.type);
|
||||
viewHolder.icon = (ImageView) convertView.findViewById(R.id.icon);
|
||||
viewHolder.name= (TextView) convertView.findViewById(R.id.name);
|
||||
viewHolder.filterView = (FilterView) convertView.findViewById(R.id.filter);
|
||||
convertView.setTag(viewHolder);
|
||||
}
|
||||
|
||||
ViewHolder viewHolder = (ViewHolder)convertView.getTag();
|
||||
viewHolder.item = getItem(position);
|
||||
initializeView(convertView);
|
||||
|
||||
// listeners
|
||||
convertView.setOnCreateContextMenuListener(activity);
|
||||
convertView.setOnClickListener(filterClickListener);
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
public CriterionInstance item;
|
||||
public ImageView type;
|
||||
public ImageView icon;
|
||||
public TextView name;
|
||||
public FilterView filterView;
|
||||
}
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
private void initializeView(View convertView) {
|
||||
ViewHolder viewHolder = (ViewHolder) convertView.getTag();
|
||||
CriterionInstance item = viewHolder.item;
|
||||
|
||||
String entryTitle = "";
|
||||
if(item.selectedIndex >= 0 && item.criterion.entryTitles != null &&
|
||||
item.selectedIndex < item.criterion.entryTitles.length) {
|
||||
entryTitle = item.criterion.entryTitles[item.selectedIndex];
|
||||
}
|
||||
String title = item.criterion.text.replace("?", entryTitle);
|
||||
|
||||
viewHolder.type.setVisibility(item.type == CriterionInstance.TYPE_UNIVERSE ?
|
||||
View.GONE : View.VISIBLE);
|
||||
switch(item.type) {
|
||||
case CriterionInstance.TYPE_ADD:
|
||||
viewHolder.type.setImageResource(R.drawable.arrow_join);
|
||||
title = activity.getString(R.string.CFA_type_add) + " " + title;
|
||||
break;
|
||||
case CriterionInstance.TYPE_SUBTRACT:
|
||||
viewHolder.type.setImageResource(R.drawable.arrow_branch);
|
||||
title = activity.getString(R.string.CFA_type_subtract) + " " + title;
|
||||
break;
|
||||
case CriterionInstance.TYPE_INTERSECT:
|
||||
viewHolder.type.setImageResource(R.drawable.arrow_down);
|
||||
break;
|
||||
}
|
||||
|
||||
viewHolder.icon.setVisibility(item.criterion.icon == null ? View.GONE :
|
||||
View.VISIBLE);
|
||||
if(item.criterion.icon != null)
|
||||
viewHolder.icon.setImageBitmap(item.criterion.icon);
|
||||
|
||||
viewHolder.name.setText(title);
|
||||
|
||||
viewHolder.filterView.setMax(item.max);
|
||||
viewHolder.filterView.setStart(item.start);
|
||||
viewHolder.filterView.setEnd(item.end);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package com.todoroo.astrid.core;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* Draws filters
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class FilterView extends View {
|
||||
|
||||
private int start = 0, end = 0, max = 1;
|
||||
|
||||
private static final int FILTER_COLOR = Color.rgb(85, 155, 255);
|
||||
private static final int BG_COLOR = Color.WHITE;
|
||||
private static final int TEXT_COLOR = Color.BLACK;
|
||||
|
||||
// --- boilerplate
|
||||
|
||||
public void setStart(int start) {
|
||||
this.start = start;
|
||||
}
|
||||
|
||||
public void setEnd(int end) {
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public void setMax(int max) {
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
public FilterView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
public FilterView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public FilterView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
// --- painting code
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
Paint paint = new Paint();
|
||||
paint.setColor(BG_COLOR);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
|
||||
|
||||
paint.setColor(FILTER_COLOR);
|
||||
Path path = new Path();
|
||||
path.moveTo(getWidth() * (0.5f - 0.5f * start / max), 0);
|
||||
path.lineTo(getWidth() * (0.5f + 0.5f * start / max), 0);
|
||||
path.lineTo(getWidth() * (0.5f + 0.5f * end / max), getHeight());
|
||||
path.lineTo(getWidth() * (0.5f - 0.5f * end / max), getHeight());
|
||||
path.close();
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
paint.setColor(TEXT_COLOR);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setTextSize(16);
|
||||
canvas.drawText(Integer.toString(end), getWidth() / 2, getHeight() / 2, paint);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package com.todoroo.astrid.core;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.todoroo.astrid.api.FilterListItem;
|
||||
|
||||
/**
|
||||
* Special filter that triggers the search functionality when accessed.
|
||||
*
|
||||
* @author Tim Su <tim@todoroo.com>
|
||||
*
|
||||
*/
|
||||
public class IntentFilter extends FilterListItem {
|
||||
|
||||
public PendingIntent intent;
|
||||
|
||||
/**
|
||||
* Constructor for creating a new IntentFilter
|
||||
*
|
||||
* @param listingTitle
|
||||
* Title of this item as displayed on the lists page, e.g. Inbox
|
||||
* @param intent
|
||||
* intent to load
|
||||
*/
|
||||
public IntentFilter(String listingTitle, PendingIntent intent) {
|
||||
this.listingTitle = listingTitle;
|
||||
this.intent = intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for creating a new IntentFilter used internally
|
||||
*/
|
||||
protected IntentFilter(PendingIntent intent) {
|
||||
this.intent = intent;
|
||||
}
|
||||
|
||||
// --- parcelable
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(intent, 0);
|
||||
super.writeToParcel(dest, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parcelable creator
|
||||
*/
|
||||
public static final Parcelable.Creator<IntentFilter> CREATOR = new Parcelable.Creator<IntentFilter>() {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public IntentFilter createFromParcel(Parcel source) {
|
||||
IntentFilter item = new IntentFilter((PendingIntent) source.readParcelable(
|
||||
PendingIntent.class.getClassLoader()));
|
||||
item.readFromParcel(source);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public IntentFilter[] newArray(int size) {
|
||||
return new IntentFilter[size];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
After Width: | Height: | Size: 646 B |
After Width: | Height: | Size: 407 B |
After Width: | Height: | Size: 687 B |
After Width: | Height: | Size: 832 B |
Before Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 626 B |
After Width: | Height: | Size: 555 B |
After Width: | Height: | Size: 518 B |
After Width: | Height: | Size: 778 B |
After Width: | Height: | Size: 715 B |
After Width: | Height: | Size: 778 B |
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- See the file "LICENSE" for the full license governing this code. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:background="@drawable/background_gradient"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Filter Name -->
|
||||
<EditText android:id="@+id/filterName"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/CFA_filterName_hint"/>
|
||||
|
||||
<!-- List -->
|
||||
<ListView android:id="@android:id/list"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="100"
|
||||
android:scrollbars="vertical"
|
||||
android:cacheColorHint="#00000000"/>
|
||||
|
||||
<!-- help text -->
|
||||
<TextView android:id="@+id/help"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="@string/CFA_help"/>
|
||||
|
||||
<!-- buttons -->
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dip"
|
||||
android:padding="5dip"
|
||||
android:background="@drawable/edit_header"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<Button android:id="@+id/add"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:drawableLeft="@drawable/tango_add"
|
||||
android:text="@string/CFA_button_add" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/saveAndView"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/CFA_button_save"
|
||||
android:drawableRight="@drawable/tango_save" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- See the file "LICENSE" for the full license governing this code. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="55dip"
|
||||
android:background="@android:drawable/list_selector_background"
|
||||
android:paddingLeft="4dip"
|
||||
android:paddingRight="6dip"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<!-- filter intersection type icon -->
|
||||
<ImageView android:id="@+id/type"
|
||||
android:layout_width="32dip"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="1"
|
||||
android:paddingLeft="5dip"
|
||||
android:paddingRight="5dip"
|
||||
android:scaleType="center"/>
|
||||
|
||||
<!-- filter icon -->
|
||||
<ImageView android:id="@+id/icon"
|
||||
android:layout_width="32dip"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="1"
|
||||
android:paddingLeft="5dip"
|
||||
android:paddingRight="5dip"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<!-- filter text -->
|
||||
<TextView android:id="@+id/name"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="100"
|
||||
android:paddingLeft="5dip"
|
||||
android:textSize="18sp"
|
||||
android:gravity="center_vertical"/>
|
||||
|
||||
<!-- filter graphic -->
|
||||
<com.todoroo.astrid.core.FilterView android:id="@+id/filter"
|
||||
android:layout_width="64px"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="1"
|
||||
android:paddingLeft="5dip"
|
||||
android:paddingRight="5dip"/>
|
||||
|
||||
</LinearLayout>
|