mirror of https://github.com/tasks/tasks
Wrote code for custom filter. A bunch of code. Time to see if it works
parent
b50e22996c
commit
0f77edb13e
@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* See the file "LICENSE" for the full license governing this code.
|
||||||
|
*/
|
||||||
|
package com.todoroo.astrid.api;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
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 extends FilterListItem {
|
||||||
|
|
||||||
|
// --- constants
|
||||||
|
|
||||||
|
/** 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$
|
||||||
|
|
||||||
|
// --- instance variables
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Criteria Title. If the title contains %s, this is replaced by the entry
|
||||||
|
* label string selected.
|
||||||
|
* <p>
|
||||||
|
* e.g "Due: %s"
|
||||||
|
*/
|
||||||
|
@CheckForNull
|
||||||
|
public String text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Criterion SQL. This query should return task id's. If this contains
|
||||||
|
* %s, it will be replaced by the entry value
|
||||||
|
* <p>
|
||||||
|
* Examples:
|
||||||
|
* <ul>
|
||||||
|
* <li><code>SELECT _id FROM tasks WHERE dueDate <= %s</code>
|
||||||
|
* <li><code>SELECT task FROM metadata WHERE value = '%s'</code>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@CheckForNull
|
||||||
|
public String sql;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Values to apply to a task when quick-adding a task from a filter
|
||||||
|
* created from this criterion. %s 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}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
super.writeToParcel(dest, 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,251 @@
|
|||||||
|
package com.todoroo.astrid.core;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import android.app.ListActivity;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
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.View;
|
||||||
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
|
import com.timsu.astrid.R;
|
||||||
|
import com.todoroo.andlib.data.Property.CountProperty;
|
||||||
|
import com.todoroo.andlib.service.Autowired;
|
||||||
|
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||||
|
import com.todoroo.andlib.sql.Criterion;
|
||||||
|
import com.todoroo.andlib.sql.Query;
|
||||||
|
import com.todoroo.astrid.api.CustomFilterCriterion;
|
||||||
|
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;
|
||||||
|
|
||||||
|
// --- 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;
|
||||||
|
|
||||||
|
/** type of join */
|
||||||
|
public int type;
|
||||||
|
|
||||||
|
/** statistics for {@link FilterView} */
|
||||||
|
public int start, end, max;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CustomFilterAdapter adapter;
|
||||||
|
private final ArrayList<CustomFilterCriterion> criteria =
|
||||||
|
new ArrayList<CustomFilterCriterion>();
|
||||||
|
|
||||||
|
// --- activity
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Database database;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
setContentView(R.layout.custom_filter_activity);
|
||||||
|
setTitle(R.string.CFA_title);
|
||||||
|
|
||||||
|
DependencyInjectionService.getInstance().inject(this);
|
||||||
|
populateCriteria();
|
||||||
|
|
||||||
|
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, "%s");
|
||||||
|
CustomFilterCriterion criterion = new CustomFilterCriterion(
|
||||||
|
getString(R.string.CFC_dueBefore_text),
|
||||||
|
Query.select(Task.ID).from(Task.TABLE).where(Task.DUE_DATE.lte("%s")).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),
|
||||||
|
};
|
||||||
|
values = new ContentValues();
|
||||||
|
values.put(Task.IMPORTANCE.name, "%s");
|
||||||
|
criterion = new CustomFilterCriterion(
|
||||||
|
getString(R.string.CFC_importance_text),
|
||||||
|
Query.select(Task.ID).from(Task.TABLE).where(Task.IMPORTANCE.lte("%s")).toString(),
|
||||||
|
values, r.getStringArray(R.array.EPr_default_importance),
|
||||||
|
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, "%s");
|
||||||
|
criterion = new CustomFilterCriterion(
|
||||||
|
getString(R.string.CFC_tag_text),
|
||||||
|
Query.select(Metadata.TASK).from(Metadata.TABLE).where(Criterion.and(
|
||||||
|
MetadataCriteria.withKey(TagService.KEY),
|
||||||
|
TagService.TAG.eq("%s"))).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) {
|
||||||
|
getListView().showContextMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
getListView().setOnCreateContextMenuListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- listeners and action events
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||||
|
for(int i = 0; i < criteria.size(); i++) {
|
||||||
|
CustomFilterCriterion item = criteria.get(i);
|
||||||
|
MenuItem menuItem = menu.add(MENU_GROUP_FILTER, i, i, item.name);
|
||||||
|
if(item.icon != null)
|
||||||
|
menuItem.setIcon(new BitmapDrawable(item.icon));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ");
|
||||||
|
for(int i = 0; i < adapter.getCount(); i++) {
|
||||||
|
CriterionInstance instance = adapter.getItem(i);
|
||||||
|
|
||||||
|
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
|
||||||
|
sql.append(Task.ID).append(" IN (").append(instance.criterion.sql).append(") ");
|
||||||
|
|
||||||
|
Cursor cursor = database.getDatabase().rawQuery(sql.toString(), null);
|
||||||
|
try {
|
||||||
|
cursor.moveToNext();
|
||||||
|
max = Math.max(max, cursor.getCount());
|
||||||
|
instance.start = last == -1 ? cursor.getInt(0) : last;
|
||||||
|
instance.end = cursor.getInt(0);
|
||||||
|
} finally {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = 0; i < adapter.getCount(); i++) {
|
||||||
|
CriterionInstance instance = adapter.getItem(i);
|
||||||
|
instance.max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.notifyDataSetInvalidated();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onMenuItemSelected(int featureId, MenuItem item) {
|
||||||
|
if(item.getGroupId() == MENU_GROUP_FILTER) {
|
||||||
|
CustomFilterCriterion criterion = criteria.get(item.getItemId());
|
||||||
|
CriterionInstance instance = new CriterionInstance();
|
||||||
|
instance.criterion = criterion;
|
||||||
|
adapter.add(instance);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if(item.getGroupId() == MENU_GROUP_FILTER_OPTION)
|
||||||
|
return adapter.onMenuItemSelected(item);
|
||||||
|
|
||||||
|
|
||||||
|
return super.onMenuItemSelected(featureId, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* 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.view.ContextMenu.ContextMenuInfo;
|
||||||
|
import android.view.View.OnCreateContextMenuListener;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.timsu.astrid.R;
|
||||||
|
import com.todoroo.astrid.api.CustomFilterCriterion;
|
||||||
|
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
|
||||||
|
viewHolder.name.showContextMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
OnCreateContextMenuListener createContextMenuListener = new OnCreateContextMenuListener() {
|
||||||
|
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) {
|
||||||
|
ViewHolder viewHolder = (ViewHolder) ((View)view.getParent()).getTag();
|
||||||
|
CustomFilterCriterion criteria = viewHolder.item.criterion;
|
||||||
|
if(criteria.entryTitles == null ||
|
||||||
|
criteria.entryTitles.length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
menu.setHeaderTitle(criteria.name);
|
||||||
|
menu.setGroupCheckable(CustomFilterActivity.MENU_GROUP_FILTER_OPTION, true, true);
|
||||||
|
|
||||||
|
for(int i = 0; i < criteria.entryTitles.length; i++) {
|
||||||
|
menu.add(CustomFilterActivity.MENU_GROUP_FILTER_OPTION,
|
||||||
|
getPosition(viewHolder.item),
|
||||||
|
i, criteria.entryTitles[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public boolean onMenuItemSelected(MenuItem item) {
|
||||||
|
CriterionInstance instance = getItem(item.getItemId());
|
||||||
|
instance.selectedIndex = item.getOrder();
|
||||||
|
((CustomFilterActivity)activity).updateList();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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.setOnClickListener(filterClickListener);
|
||||||
|
viewHolder.name.setOnCreateContextMenuListener(createContextMenuListener);
|
||||||
|
|
||||||
|
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.selectedIndex < item.criterion.entryTitles.length) {
|
||||||
|
entryTitle = item.criterion.entryTitles[item.selectedIndex];
|
||||||
|
}
|
||||||
|
String title = item.criterion.text.replace("%s", entryTitle);
|
||||||
|
boolean notFirst = getPosition(item) > 1;
|
||||||
|
|
||||||
|
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);
|
||||||
|
if(notFirst)
|
||||||
|
title = activity.getString(R.string.CFA_type_add) + " " + title;
|
||||||
|
break;
|
||||||
|
case CriterionInstance.TYPE_SUBTRACT:
|
||||||
|
viewHolder.type.setImageResource(R.drawable.arrow_branch);
|
||||||
|
if(notFirst)
|
||||||
|
title = activity.getString(R.string.CFA_type_subtract) + " " + title;
|
||||||
|
break;
|
||||||
|
case CriterionInstance.TYPE_INTERSECT:
|
||||||
|
viewHolder.type.setImageResource(R.drawable.arrow_down);
|
||||||
|
if(notFirst)
|
||||||
|
title = activity.getString(R.string.CFA_type_intersect) + " " + title;
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package com.todoroo.astrid.core;
|
||||||
|
|
||||||
|
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 SearchFilter extends FilterListItem {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for creating a new SearchFilter
|
||||||
|
*
|
||||||
|
* @param listingTitle
|
||||||
|
* Title of this item as displayed on the lists page, e.g. Inbox
|
||||||
|
*/
|
||||||
|
public SearchFilter(String listingTitle) {
|
||||||
|
this.listingTitle = listingTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for creating a new SearchFilter
|
||||||
|
*/
|
||||||
|
protected SearchFilter() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- parcelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
super.writeToParcel(dest, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parcelable creator
|
||||||
|
*/
|
||||||
|
public static final Parcelable.Creator<SearchFilter> CREATOR = new Parcelable.Creator<SearchFilter>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public SearchFilter createFromParcel(Parcel source) {
|
||||||
|
SearchFilter item = new SearchFilter();
|
||||||
|
item.readFromParcel(source);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public SearchFilter[] newArray(int size) {
|
||||||
|
return new SearchFilter[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 646 B |
Binary file not shown.
After Width: | Height: | Size: 407 B |
Binary file not shown.
After Width: | Height: | Size: 687 B |
Binary file not shown.
After Width: | Height: | Size: 832 B |
@ -0,0 +1,53 @@
|
|||||||
|
<?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"
|
||||||
|
android:text="@string/CFA_filterName_new"/>
|
||||||
|
|
||||||
|
<!-- 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"/>
|
||||||
|
|
||||||
|
<!-- 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/discard_basic"
|
||||||
|
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="22sp"
|
||||||
|
android:gravity="center_vertical"/>
|
||||||
|
|
||||||
|
<!-- filter graphic -->
|
||||||
|
<com.todoroo.astrid.core.FilterView android:id="@+id/filter"
|
||||||
|
android:layout_width="64dip"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:paddingLeft="5dip"
|
||||||
|
android:paddingRight="5dip"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
Loading…
Reference in New Issue