Change tags with multi-select

gtask_related_email
Alex Baker 4 years ago
parent c19649065a
commit 274d98160a

@ -1,24 +1,31 @@
package org.tasks.data;
import static com.google.common.collect.Sets.newHashSet;
import static com.natpryce.makeiteasy.MakeItEasy.with;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.tasks.makers.TagDataMaker.NAME;
import static org.tasks.makers.TagDataMaker.UID;
import static org.tasks.makers.TagDataMaker.newTagData;
import static org.tasks.makers.TagMaker.TAGDATA;
import static org.tasks.makers.TagMaker.TAGUID;
import static org.tasks.makers.TagMaker.TASK;
import static org.tasks.makers.TagMaker.newTag;
import static org.tasks.makers.TaskMaker.ID;
import static org.tasks.makers.TaskMaker.newTask;
import androidx.core.util.Pair;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import java.util.Set;
import javax.inject.Inject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.injection.InjectingTestCase;
import org.tasks.injection.TestComponent;
import org.tasks.makers.TagMaker;
@RunWith(AndroidJUnit4.class)
public class TagDataDaoTest extends InjectingTestCase {
@ -80,12 +87,62 @@ public class TagDataDaoTest extends InjectingTestCase {
tagDataDao.createNew(tagOne);
tagDataDao.createNew(tagTwo);
tagDao.insert(newTag(with(TAGDATA, tagOne), with(TASK, taskOne)));
tagDao.insert(newTag(with(TAGDATA, tagTwo), with(TASK, taskTwo)));
tagDao.insert(TagMaker.newTag(with(TAGDATA, tagOne), with(TASK, taskOne)));
tagDao.insert(TagMaker.newTag(with(TAGDATA, tagTwo), with(TASK, taskTwo)));
assertEquals(singletonList(tagOne), tagDataDao.getTagDataForTask(taskOne.getId()));
}
@Test
public void getEmptyTagSelections() {
Pair<Set<String>, Set<String>> selections = tagDataDao.getTagSelections(ImmutableList.of(1L));
assertTrue(selections.first.isEmpty());
assertTrue(selections.second.isEmpty());
}
@Test
public void getPartialTagSelections() {
newTag(1, "tag1", "tag2");
newTag(2, "tag2", "tag3");
assertEquals(
newHashSet("tag1", "tag3"), tagDataDao.getTagSelections(ImmutableList.of(1L, 2L)).first);
}
@Test
public void getEmptyPartialSelections() {
newTag(1, "tag1");
newTag(2, "tag1");
assertTrue(tagDataDao.getTagSelections(ImmutableList.of(1L, 2L)).first.isEmpty());
}
@Test
public void getCommonTagSelections() {
newTag(1, "tag1", "tag2");
newTag(2, "tag2", "tag3");
assertEquals(
newHashSet("tag2"), tagDataDao.getTagSelections(ImmutableList.of(1L, 2L)).second);
}
@Test
public void getEmptyCommonSelections() {
newTag(1, "tag1");
newTag(2, "tag2");
assertTrue(tagDataDao.getTagSelections(ImmutableList.of(1L, 2L)).second.isEmpty());
}
private void newTag(long taskId, String... tags) {
Task task = newTask(with(ID, taskId));
taskDao.createNew(task);
for (String tag : tags) {
tagDao.insert(TagMaker.newTag(with(TASK, task), with(TAGUID, tag)));
}
}
@Override
protected void inject(TestComponent component) {
component.inject(this);

@ -11,10 +11,12 @@ import org.tasks.data.TagData;
public class TagDataMaker {
public static final Property<TagData, String> NAME = newProperty();
public static final Property<TagData, String> UID = newProperty();
private static final Instantiator<TagData> instantiator = lookup -> {
TagData tagData = new TagData();
tagData.setName(lookup.valueOf(NAME, "tag"));
tagData.setRemoteId(lookup.valueOf(UID, (String) null));
return tagData;
};

@ -12,21 +12,25 @@ import org.tasks.data.TagData;
public class TagMaker {
public static final Property<Tag, String> NAME = newProperty();
public static final Property<Tag, TagData> TAGDATA = newProperty();
public static final Property<Tag, Task> TASK = newProperty();
public static final Property<Tag, String> TAGUID = newProperty();
private static final Instantiator<Tag> instantiator = lookup -> {
Tag tag = new Tag();
Task task = lookup.valueOf(TASK, (Task) null);
assert(task != null);
tag.setTask(task.getId());
tag.setTaskUid(task.getUuid());
TagData tagData = lookup.valueOf(TAGDATA, (TagData) null);
assert(tagData != null);
tag.setTagUid(tagData.getRemoteId());
return tag;
};
private static final Instantiator<Tag> instantiator =
lookup -> {
Tag tag = new Tag();
Task task = lookup.valueOf(TASK, (Task) null);
assert (task != null);
tag.setTask(task.getId());
tag.setTaskUid(task.getUuid());
tag.setTagUid(lookup.valueOf(TAGUID, (String) null));
TagData tagData = lookup.valueOf(TAGDATA, (TagData) null);
if (tagData != null) {
tag.setTagUid(tagData.getRemoteId());
}
assert(tag.getTagUid() != null);
return tag;
};
@SafeVarargs
public static Tag newTag(PropertyValue<? super Tag, ?>... properties) {

@ -8,6 +8,7 @@ package com.todoroo.astrid.activity;
import static android.app.Activity.RESULT_OK;
import static androidx.core.content.ContextCompat.getColor;
import static com.google.common.collect.Lists.newArrayList;
import static com.todoroo.andlib.utility.AndroidUtilities.assertMainThread;
import static org.tasks.activities.RemoteListSupportPicker.newRemoteListSupportPicker;
import static org.tasks.caldav.CaldavCalendarSettingsActivity.EXTRA_CALDAV_CALENDAR;
@ -36,6 +37,7 @@ import androidx.appcompat.widget.SearchView.OnQueryTextListener;
import androidx.appcompat.widget.Toolbar;
import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.util.Pair;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
@ -69,7 +71,9 @@ import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.tasks.LocalBroadcastManager;
@ -81,6 +85,7 @@ import org.tasks.activities.TagSettingsActivity;
import org.tasks.analytics.Tracker;
import org.tasks.analytics.Tracking;
import org.tasks.caldav.CaldavCalendarSettingsActivity;
import org.tasks.data.TagDataDao;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.dialogs.SortDialog;
import org.tasks.injection.ForActivity;
@ -90,6 +95,7 @@ import org.tasks.intents.TaskIntents;
import org.tasks.preferences.Device;
import org.tasks.preferences.Preferences;
import org.tasks.sync.SyncAdapters;
import org.tasks.tags.TagPickerActivity;
import org.tasks.tasklist.DragAndDropRecyclerAdapter;
import org.tasks.tasklist.PagedListRecyclerAdapter;
import org.tasks.tasklist.TaskListRecyclerAdapter;
@ -121,6 +127,7 @@ public final class TaskListFragment extends InjectingFragment
private static final int REQUEST_MOVE_TASKS = 10103;
private static final int REQUEST_FILTER_SETTINGS = 10104;
private static final int REQUEST_TAG_SETTINGS = 10105;
private static final int REQUEST_TAG_TASKS = 10106;
private static final int SEARCH_DEBOUNCE_TIMEOUT = 300;
private final RefreshReceiver refreshReceiver = new RefreshReceiver();
@ -141,6 +148,7 @@ public final class TaskListFragment extends InjectingFragment
@Inject TaskAdapterProvider taskAdapterProvider;
@Inject TaskDao taskDao;
@Inject TaskDuplicator taskDuplicator;
@Inject TagDataDao tagDataDao;
@BindView(R.id.swipe_layout)
SwipeRefreshLayout swipeRefreshLayout;
@ -588,6 +596,16 @@ public final class TaskListFragment extends InjectingFragment
}
}
break;
case REQUEST_TAG_TASKS:
if (resultCode == RESULT_OK) {
tagDataDao.applyTags(
taskDao.fetch(
(ArrayList<Long>) data.getSerializableExtra(TagPickerActivity.EXTRA_TASKS)),
data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_PARTIALLY_SELECTED),
data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED));
finishActionMode();
}
break;
default:
super.onActivityResult(requestCode, resultCode, data);
}
@ -660,9 +678,21 @@ public final class TaskListFragment extends InjectingFragment
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
ArrayList<Long> selected = taskAdapter.getSelected();
switch (item.getItemId()) {
case R.id.edit_tags:
Pair<Set<String>, Set<String>> tags = tagDataDao.getTagSelections(selected);
Intent intent = new Intent(context, TagPickerActivity.class);
intent.putExtra(TagPickerActivity.EXTRA_TASKS, selected);
intent.putParcelableArrayListExtra(
TagPickerActivity.EXTRA_PARTIALLY_SELECTED,
newArrayList(tagDataDao.getByUuid(tags.first)));
intent.putParcelableArrayListExtra(
TagPickerActivity.EXTRA_SELECTED, newArrayList(tagDataDao.getByUuid(tags.second)));
startActivityForResult(intent, REQUEST_TAG_TASKS);
return true;
case R.id.move_tasks:
Filter singleFilter = taskMover.getSingleFilter(taskAdapter.getSelected());
Filter singleFilter = taskMover.getSingleFilter(selected);
(singleFilter == null
? newRemoteListSupportPicker(this, REQUEST_MOVE_TASKS)
: newRemoteListSupportPicker(singleFilter, this, REQUEST_MOVE_TASKS))
@ -672,8 +702,7 @@ public final class TaskListFragment extends InjectingFragment
dialogBuilder
.newDialog(R.string.delete_selected_tasks)
.setPositiveButton(
android.R.string.ok,
(dialogInterface, i) -> deleteSelectedItems(taskAdapter.getSelected()))
android.R.string.ok, (dialogInterface, i) -> deleteSelectedItems(selected))
.setNegativeButton(android.R.string.cancel, null)
.show();
return true;
@ -681,8 +710,7 @@ public final class TaskListFragment extends InjectingFragment
dialogBuilder
.newDialog(R.string.copy_selected_tasks)
.setPositiveButton(
android.R.string.ok,
((dialogInterface, i) -> copySelectedItems(taskAdapter.getSelected())))
android.R.string.ok, ((dialogInterface, i) -> copySelectedItems(selected)))
.setNegativeButton(android.R.string.cancel, null)
.show();
return true;

@ -10,6 +10,7 @@ import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.primitives.Longs.asList;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -39,7 +40,7 @@ public class TaskAdapter {
return selected.size();
}
public List<Long> getSelected() {
public ArrayList<Long> getSelected() {
return newArrayList(selected);
}

@ -114,7 +114,7 @@ public final class TagsControlSet extends TaskEditControlFragment {
@OnClick(R.id.tag_row)
void onClickRow() {
Intent intent = new Intent(getContext(), TagPickerActivity.class);
intent.putParcelableArrayListExtra(TagPickerActivity.EXTRA_TAGS, selectedTags);
intent.putParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED, selectedTags);
startActivityForResult(intent, REQUEST_TAG_PICKER_ACTIVITY);
}
@ -164,7 +164,7 @@ public final class TagsControlSet extends TaskEditControlFragment {
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQUEST_TAG_PICKER_ACTIVITY) {
if (resultCode == RESULT_OK && data != null) {
selectedTags = data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_TAGS);
selectedTags = data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED);
refreshDisplayView();
}
} else {

@ -1,5 +1,14 @@
package org.tasks.data;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.newHashSet;
import static org.tasks.db.DbUtils.MAX_SQLITE_ARGS;
import static org.tasks.db.DbUtils.batch;
import androidx.core.util.Pair;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
@ -7,11 +16,13 @@ import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Transaction;
import androidx.room.Update;
import com.google.common.collect.Collections2;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.helper.UUIDHelper;
import io.reactivex.Single;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.tasks.filters.AlphanumComparator;
import org.tasks.filters.TagFilters;
@ -48,6 +59,9 @@ public abstract class TagDataDao {
@Query("SELECT * FROM tagdata WHERE remoteId = :uuid LIMIT 1")
public abstract TagData getByUuid(String uuid);
@Query("SELECT * FROM tagdata WHERE remoteId IN (:uuids)")
public abstract List<TagData> getByUuid(Collection<String> uuids);
@Query("SELECT * FROM tagdata WHERE name IS NOT NULL AND name != '' ORDER BY UPPER(name) ASC")
public abstract List<TagData> tagDataOrderedByName();
@ -57,6 +71,51 @@ public abstract class TagDataDao {
@Query("DELETE FROM tags WHERE tag_uid = :tagUid")
abstract void deleteTags(String tagUid);
@Query("DELETE FROM tags WHERE task IN (:tasks) AND tag_uid NOT IN (:tagUids)")
abstract void keepTags(List<Long> tasks, List<String> tagUids);
public Pair<Set<String>, Set<String>> getTagSelections(List<Long> tasks) {
List<String> allTags = getAllTags(tasks);
Collection<Set<String>> tags = Collections2.transform(allTags, t -> newHashSet(t.split(",")));
Set<String> partialTags = newHashSet(concat(tags));
Set<String> commonTags = null;
if (tags.isEmpty()) {
commonTags = Collections.emptySet();
} else {
for (Set<String> s : tags) {
if (commonTags == null) {
commonTags = s;
} else {
commonTags.retainAll(s);
}
}
}
partialTags.removeAll(commonTags);
return Pair.create(partialTags, commonTags);
}
@Query(
"SELECT IFNULL(GROUP_CONCAT(DISTINCT(tag_uid)), '') FROM tasks"
+ " INNER JOIN tags ON tags.task = tasks._id"
+ " WHERE tasks._id IN (:tasks)"
+ " GROUP BY tasks._id")
abstract List<String> getAllTags(List<Long> tasks);
@Transaction
public void applyTags(List<Task> tasks, List<TagData> partiallySelected, List<TagData> selected) {
List<String> keep =
newArrayList(transform(concat(partiallySelected, selected), TagData::getRemoteId));
Iterable<Long> ids = transform(tasks, Task::getId);
batch(ids, MAX_SQLITE_ARGS - keep.size(), b -> keepTags(b, keep));
for (Task task : tasks) {
Set<TagData> added =
difference(newHashSet(selected), newHashSet(getTagDataForTask(task.getId())));
if (added.size() > 0) {
insert(transform(added, td -> new Tag(task, td)));
}
}
}
@Transaction
public void delete(TagData tagData) {
deleteTags(tagData.getRemoteId());
@ -81,6 +140,9 @@ public abstract class TagDataDao {
@Insert
abstract long insert(TagData tag);
@Insert
public abstract void insert(Iterable<Tag> tags);
public void createNew(TagData tag) {
if (Task.isUuidEmpty(tag.getRemoteId())) {
tag.setRemoteId(UUIDHelper.newUUID());

@ -12,7 +12,7 @@ import org.tasks.Callback;
public class DbUtils {
private static final int MAX_SQLITE_ARGS = 990;
public static final int MAX_SQLITE_ARGS = 990;
public static <F, T> List<T> collect(Collection<F> items, Function<List<F>, List<T>> func) {
if (items.size() < MAX_SQLITE_ARGS) {

@ -0,0 +1,179 @@
package org.tasks.tags;
import static org.tasks.preferences.ResourceResolver.getData;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.widget.CompoundButton;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatCheckBox;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import org.tasks.R;
public class CheckBoxTriStates extends AppCompatCheckBox {
private int alpha;
private State state;
private OnCheckedChangeListener clientListener;
private final OnCheckedChangeListener privateListener =
new CompoundButton.OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
switch (state) {
case PARTIALLY_CHECKED:
case UNCHECKED:
setState(State.CHECKED, true);
break;
case CHECKED:
setState(State.UNCHECKED, true);
break;
}
}
};
public CheckBoxTriStates(Context context) {
super(context);
init();
}
public CheckBoxTriStates(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CheckBoxTriStates(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public State getState() {
return state;
}
public void setState(State state, boolean notify) {
if (this.state != state) {
this.state = state;
if (notify && this.clientListener != null) {
this.clientListener.onCheckedChanged(this, this.isChecked());
}
}
updateBtn();
}
@Override
public void setOnCheckedChangeListener(@Nullable OnCheckedChangeListener listener) {
if (this.privateListener != listener) {
this.clientListener = listener;
}
super.setOnCheckedChangeListener(privateListener);
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.state = state.ordinal();
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
this.state = State.values()[ss.state];
updateBtn();
requestLayout();
}
private void init() {
alpha = (int) (255 * ResourcesCompat.getFloat(getResources(), R.dimen.alpha_secondary));
setState(State.UNCHECKED, false);
setOnCheckedChangeListener(this.privateListener);
}
private void updateBtn() {
int btnDrawable;
int alpha = 255;
switch (state) {
case PARTIALLY_CHECKED:
btnDrawable = R.drawable.ic_indeterminate_check_box_24px;
break;
case CHECKED:
btnDrawable = R.drawable.ic_outline_check_box_24px;
break;
default:
btnDrawable = R.drawable.ic_outline_check_box_outline_blank_24px;
alpha = this.alpha;
break;
}
Drawable original = getResources().getDrawable(btnDrawable);
Drawable drawable = original.mutate();
drawable.setAlpha(alpha);
DrawableCompat.setTint(
drawable,
state == State.UNCHECKED
? getResources().getColor(R.color.icon_tint)
: getData(getContext(), R.attr.colorAccent));
setButtonDrawable(drawable);
}
public enum State {
PARTIALLY_CHECKED,
CHECKED,
UNCHECKED
}
static class SavedState extends BaseSavedState {
@SuppressWarnings("hiding")
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
int state;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
state = in.readInt();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeValue(state);
}
@Override
public String toString() {
return "CheckboxTriState.SavedState{"
+ Integer.toHexString(System.identityHashCode(this))
+ " state="
+ state
+ "}";
}
}
}

@ -18,6 +18,7 @@ import org.tasks.billing.Inventory;
import org.tasks.data.TagData;
import org.tasks.injection.ActivityComponent;
import org.tasks.injection.ThemedInjectingAppCompatActivity;
import org.tasks.tags.CheckBoxTriStates.State;
import org.tasks.themes.Theme;
import org.tasks.themes.ThemeCache;
import org.tasks.themes.ThemeColor;
@ -25,7 +26,9 @@ import org.tasks.ui.MenuColorizer;
public class TagPickerActivity extends ThemedInjectingAppCompatActivity {
public static final String EXTRA_TAGS = "extra_tags";
public static final String EXTRA_SELECTED = "extra_tags";
public static final String EXTRA_PARTIALLY_SELECTED = "extra_partial";
public static final String EXTRA_TASKS = "extra_tasks";
@BindView(R.id.toolbar)
Toolbar toolbar;
@ -41,14 +44,18 @@ public class TagPickerActivity extends ThemedInjectingAppCompatActivity {
@Inject Inventory inventory;
private TagPickerViewModel viewModel;
private ArrayList<Long> taskIds;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
taskIds = (ArrayList<Long>) intent.getSerializableExtra(EXTRA_TASKS);
if (savedInstanceState == null) {
ArrayList<TagData> tags = getIntent().getParcelableArrayListExtra(EXTRA_TAGS);
viewModel.setTags(tags);
viewModel.setSelected(
intent.getParcelableArrayListExtra(EXTRA_SELECTED),
intent.getParcelableArrayListExtra(EXTRA_PARTIALLY_SELECTED));
}
setContentView(R.layout.activity_tag_picker);
@ -73,16 +80,16 @@ public class TagPickerActivity extends ThemedInjectingAppCompatActivity {
editText.setText(viewModel.getText());
}
private Void onToggle(TagData tagData, Boolean checked) {
private State onToggle(TagData tagData, Boolean checked) {
boolean newTag = tagData.getId() == null;
viewModel.toggle(tagData, checked);
State state = viewModel.toggle(tagData, checked);
if (newTag) {
clear();
}
return null;
return state;
}
@OnTextChanged(R.id.search_input)
@ -94,7 +101,9 @@ public class TagPickerActivity extends ThemedInjectingAppCompatActivity {
public void onBackPressed() {
if (Strings.isNullOrEmpty(viewModel.getText())) {
Intent data = new Intent();
data.putParcelableArrayListExtra(EXTRA_TAGS, viewModel.getTags());
data.putExtra(EXTRA_TASKS, taskIds);
data.putParcelableArrayListExtra(EXTRA_PARTIALLY_SELECTED, viewModel.getPartiallySelected());
data.putParcelableArrayListExtra(EXTRA_SELECTED, viewModel.getSelected());
setResult(RESULT_OK, data);
finish();
} else {

@ -5,7 +5,6 @@ import static com.todoroo.andlib.utility.AndroidUtilities.atLeastJellybeanMR1;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -19,22 +18,23 @@ import butterknife.OnClick;
import kotlin.jvm.functions.Function2;
import org.tasks.R;
import org.tasks.data.TagData;
import org.tasks.tags.CheckBoxTriStates.State;
public class TagPickerViewHolder extends RecyclerView.ViewHolder {
private final Context context;
private final Function2<TagData, Boolean, Void> callback;
private final Function2<TagData, Boolean, State> callback;
@BindView(R.id.text)
TextView text;
@BindView(R.id.checkbox)
CheckBox checkBox;
CheckBoxTriStates checkBox;
private TagData tagData;
TagPickerViewHolder(
Context context, @NonNull View view, Function2<TagData, Boolean, Void> callback) {
Context context, @NonNull View view, Function2<TagData, Boolean, State> callback) {
super(view);
this.callback = callback;
@ -54,10 +54,12 @@ public class TagPickerViewHolder extends RecyclerView.ViewHolder {
@OnCheckedChanged(R.id.checkbox)
void onCheckedChanged() {
callback.invoke(tagData, checkBox.isChecked());
State newState = callback.invoke(tagData, checkBox.isChecked());
updateCheckbox(newState);
}
public void bind(@NonNull TagData tagData, int color, @Nullable Integer icon, boolean checked) {
public void bind(
@NonNull TagData tagData, int color, @Nullable Integer icon, State state) {
this.tagData = tagData;
if (tagData.getId() == null) {
text.setText(context.getString(R.string.create_new_tag, tagData.getName()));
@ -65,8 +67,11 @@ public class TagPickerViewHolder extends RecyclerView.ViewHolder {
checkBox.setVisibility(View.GONE);
} else {
text.setText(tagData.getName());
checkBox.setChecked(checked);
checkBox.setVisibility(View.VISIBLE);
if (state == State.CHECKED) {
checkBox.setChecked(true);
} else {
updateCheckbox(state);
}
if (icon == null) {
icon = R.drawable.ic_outline_label_24px;
}
@ -80,4 +85,9 @@ public class TagPickerViewHolder extends RecyclerView.ViewHolder {
text.setCompoundDrawablesWithIntrinsicBounds(wrapped, null, null, null);
}
}
private void updateCheckbox(State state) {
checkBox.setState(state, false);
checkBox.setVisibility(View.VISIBLE);
}
}

@ -3,6 +3,7 @@ package org.tasks.tags;
import static com.google.common.collect.Iterables.any;
import static com.google.common.collect.Lists.newArrayList;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
@ -19,6 +20,7 @@ import java.util.Set;
import javax.inject.Inject;
import org.tasks.data.TagData;
import org.tasks.data.TagDataDao;
import org.tasks.tags.CheckBoxTriStates.State;
public class TagPickerViewModel extends ViewModel {
@ -27,21 +29,29 @@ public class TagPickerViewModel extends ViewModel {
@Inject TagDataDao tagDataDao;
private final Set<TagData> selected = new HashSet<>();
private final Set<TagData> partiallySelected = new HashSet<>();
private String text;
private Set<TagData> selected = new HashSet<>();
public void observe(LifecycleOwner owner, Observer<List<TagData>> observer) {
tags.observe(owner, observer);
}
public void setTags(List<TagData> tags) {
selected.addAll(tags);
public void setSelected(List<TagData> selected, @Nullable List<TagData> partiallySelected) {
this.selected.addAll(selected);
if (partiallySelected != null) {
this.partiallySelected.addAll(partiallySelected);
}
}
public ArrayList<TagData> getTags() {
public ArrayList<TagData> getSelected() {
return newArrayList(selected);
}
public ArrayList<TagData> getPartiallySelected() {
return newArrayList(partiallySelected);
}
public String getText() {
return text;
}
@ -74,16 +84,27 @@ public class TagPickerViewModel extends ViewModel {
return selected.contains(tagData);
}
void toggle(TagData tagData, boolean checked) {
public State getState(TagData tagData) {
if (partiallySelected.contains(tagData)) {
return State.PARTIALLY_CHECKED;
}
return selected.contains(tagData) ? State.CHECKED : State.UNCHECKED;
}
State toggle(TagData tagData, boolean checked) {
if (tagData.getId() == null) {
tagData = new TagData(tagData.getName());
tagDataDao.createNew(tagData);
}
partiallySelected.remove(tagData);
if (checked) {
selected.add(tagData);
return State.CHECKED;
} else {
selected.remove(tagData);
return State.UNCHECKED;
}
}
}

@ -13,6 +13,7 @@ import kotlin.jvm.functions.Function2;
import org.tasks.R;
import org.tasks.billing.Inventory;
import org.tasks.data.TagData;
import org.tasks.tags.CheckBoxTriStates.State;
import org.tasks.themes.CustomIcons;
import org.tasks.themes.ThemeCache;
@ -23,14 +24,14 @@ public class TagRecyclerAdapter extends RecyclerView.Adapter<TagPickerViewHolder
private final TagPickerViewModel viewModel;
private final ThemeCache themeCache;
private final Inventory inventory;
private final Function2<TagData, Boolean, Void> callback;
private final Function2<TagData, Boolean, State> callback;
TagRecyclerAdapter(
Context context,
TagPickerViewModel viewModel,
ThemeCache themeCache,
Inventory inventory,
Function2<TagData, Boolean, Void> callback) {
Function2<TagData, Boolean, State> callback) {
this.context = context;
this.viewModel = viewModel;
this.themeCache = themeCache;
@ -49,8 +50,7 @@ public class TagRecyclerAdapter extends RecyclerView.Adapter<TagPickerViewHolder
@Override
public void onBindViewHolder(@NonNull TagPickerViewHolder holder, int position) {
TagData tagData = differ.getCurrentList().get(position);
boolean checked = viewModel.isChecked(tagData);
holder.bind(tagData, getColor(tagData), getIcon(tagData), checked);
holder.bind(tagData, getColor(tagData), getIcon(tagData), viewModel.getState(tagData));
}
@Override

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19L5,19L5,5h14v14zM7,11h10v2L7,13z"/>
</vector>

@ -7,7 +7,7 @@
android:clickable="true"
android:focusable="true">
<CheckBox
<org.tasks.tags.CheckBoxTriStates
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

@ -2,6 +2,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/edit_tags"
android:icon="@drawable/ic_outline_label_24px"
android:title="@string/tags"
app:showAsAction="ifRoom" />
<item
android:id="@+id/move_tasks"
android:icon="@drawable/ic_outline_cloud_24px"

Loading…
Cancel
Save