Subtasks in task edit screen

gtask_related_email
Alex Baker 6 years ago
parent 594441830a
commit d81901b889

@ -32,7 +32,7 @@ import org.tasks.ui.MenuColorizer;
public class BeastModePreferences extends ThemedInjectingAppCompatActivity public class BeastModePreferences extends ThemedInjectingAppCompatActivity
implements Toolbar.OnMenuItemClickListener { implements Toolbar.OnMenuItemClickListener {
private static final String BEAST_MODE_ORDER_PREF = "beast_mode_order_v5"; // $NON-NLS-1$ private static final String BEAST_MODE_ORDER_PREF = "beast_mode_order_v6"; // $NON-NLS-1$
private static final String BEAST_MODE_PREF_ITEM_SEPARATOR = ";"; private static final String BEAST_MODE_PREF_ITEM_SEPARATOR = ";";
@BindView(R.id.toolbar) @BindView(R.id.toolbar)

@ -61,12 +61,14 @@ import org.tasks.ui.DeadlineControlSet;
import org.tasks.ui.EmptyTaskEditFragment; import org.tasks.ui.EmptyTaskEditFragment;
import org.tasks.ui.NavigationDrawerFragment; import org.tasks.ui.NavigationDrawerFragment;
import org.tasks.ui.PriorityControlSet; import org.tasks.ui.PriorityControlSet;
import org.tasks.ui.RemoteListFragment;
import org.tasks.ui.TaskListViewModel; import org.tasks.ui.TaskListViewModel;
import org.tasks.ui.Toaster; import org.tasks.ui.Toaster;
public class MainActivity extends InjectingAppCompatActivity public class MainActivity extends InjectingAppCompatActivity
implements TaskListFragment.TaskListFragmentCallbackHandler, implements TaskListFragment.TaskListFragmentCallbackHandler,
PriorityControlSet.OnPriorityChanged, PriorityControlSet.OnPriorityChanged,
RemoteListFragment.OnListChanged,
TimerControlSet.TimerControlSetCallback, TimerControlSet.TimerControlSetCallback,
DeadlineControlSet.DueDateChangeListener, DeadlineControlSet.DueDateChangeListener,
TaskEditFragment.TaskEditFragmentCallbackHandler, TaskEditFragment.TaskEditFragmentCallbackHandler,
@ -511,4 +513,9 @@ public class MainActivity extends InjectingAppCompatActivity
public void dueDateChanged(long dateTime) { public void dueDateChanged(long dateTime) {
getTaskEditFragment().onDueDateChanged(dateTime); getTaskEditFragment().onDueDateChanged(dateTime);
} }
@Override
public void onListchanged(@Nullable Filter filter) {
getTaskEditFragment().onRemoteListChanged(filter);
}
} }

@ -19,6 +19,7 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
@ -27,6 +28,7 @@ import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.notes.CommentsController; import com.todoroo.astrid.notes.CommentsController;
@ -50,6 +52,7 @@ import org.tasks.injection.InjectingFragment;
import org.tasks.notifications.NotificationManager; import org.tasks.notifications.NotificationManager;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.ui.MenuColorizer; import org.tasks.ui.MenuColorizer;
import org.tasks.ui.SubtaskControlSet;
import org.tasks.ui.TaskEditControlFragment; import org.tasks.ui.TaskEditControlFragment;
public final class TaskEditFragment extends InjectingFragment public final class TaskEditFragment extends InjectingFragment
@ -242,6 +245,10 @@ public final class TaskEditFragment extends InjectingFragment
return getFragment(RepeatControlSet.TAG); return getFragment(RepeatControlSet.TAG);
} }
private SubtaskControlSet getSubtaskControlSet() {
return getFragment(SubtaskControlSet.TAG);
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T extends TaskEditControlFragment> T getFragment(int tag) { private <T extends TaskEditControlFragment> T getFragment(int tag) {
return (T) getChildFragmentManager().findFragmentByTag(getString(tag)); return (T) getChildFragmentManager().findFragmentByTag(getString(tag));
@ -300,7 +307,7 @@ public final class TaskEditFragment extends InjectingFragment
.show(); .show();
} }
public void onPriorityChange(int priority) { void onPriorityChange(int priority) {
getEditTitleControlSet().setPriority(priority); getEditTitleControlSet().setPriority(priority);
} }
@ -314,13 +321,20 @@ public final class TaskEditFragment extends InjectingFragment
getEditTitleControlSet().repeatChanged(repeat); getEditTitleControlSet().repeatChanged(repeat);
} }
public void onDueDateChanged(long dueDate) { void onDueDateChanged(long dueDate) {
RepeatControlSet repeatControlSet = getRepeatControlSet(); RepeatControlSet repeatControlSet = getRepeatControlSet();
if (repeatControlSet != null) { if (repeatControlSet != null) {
repeatControlSet.onDueDateChanged(dueDate); repeatControlSet.onDueDateChanged(dueDate);
} }
} }
void onRemoteListChanged(@Nullable Filter filter) {
SubtaskControlSet subtaskControlSet = getSubtaskControlSet();
if (subtaskControlSet != null) {
subtaskControlSet.onRemoteListChanged(filter);
}
}
void addComment(String message, Uri picture) { void addComment(String message, Uri picture) {
UserActivity userActivity = new UserActivity(); UserActivity userActivity = new UserActivity();
if (picture != null) { if (picture != null) {

@ -72,6 +72,9 @@ public abstract class CaldavDao {
@Query("SELECT * FROM caldav_tasks WHERE cd_task = :taskId AND cd_deleted = 0 LIMIT 1") @Query("SELECT * FROM caldav_tasks WHERE cd_task = :taskId AND cd_deleted = 0 LIMIT 1")
public abstract CaldavTask getTask(long taskId); public abstract CaldavTask getTask(long taskId);
@Query("SELECT cd_remote_id FROM caldav_tasks WHERE cd_task = :taskId AND cd_deleted = 0")
public abstract String getRemoteIdForTask(long taskId);
@Query("SELECT * FROM caldav_tasks WHERE cd_calendar = :calendar AND cd_object = :object LIMIT 1") @Query("SELECT * FROM caldav_tasks WHERE cd_calendar = :calendar AND cd_object = :object LIMIT 1")
public abstract CaldavTask getTask(String calendar, String object); public abstract CaldavTask getTask(String calendar, String object);

@ -28,6 +28,7 @@ import org.tasks.ui.DescriptionControlSet;
import org.tasks.ui.LocationControlSet; import org.tasks.ui.LocationControlSet;
import org.tasks.ui.PriorityControlSet; import org.tasks.ui.PriorityControlSet;
import org.tasks.ui.RemoteListFragment; import org.tasks.ui.RemoteListFragment;
import org.tasks.ui.SubtaskControlSet;
import org.tasks.ui.TaskEditControlFragment; import org.tasks.ui.TaskEditControlFragment;
public class TaskEditControlSetFragmentManager { public class TaskEditControlSetFragmentManager {
@ -47,7 +48,8 @@ public class TaskEditControlSetFragmentManager {
R.id.row_9, R.id.row_9,
R.id.row_10, R.id.row_10,
R.id.row_11, R.id.row_11,
R.id.row_12 R.id.row_12,
R.id.row_13
}; };
private static final int[] TASK_EDIT_CONTROL_SET_FRAGMENTS = private static final int[] TASK_EDIT_CONTROL_SET_FRAGMENTS =
@ -65,7 +67,8 @@ public class TaskEditControlSetFragmentManager {
TagsControlSet.TAG, TagsControlSet.TAG,
RepeatControlSet.TAG, RepeatControlSet.TAG,
CommentBarFragment.TAG, CommentBarFragment.TAG,
RemoteListFragment.TAG RemoteListFragment.TAG,
SubtaskControlSet.TAG
}; };
static { static {
@ -160,6 +163,8 @@ public class TaskEditControlSetFragmentManager {
return new CommentBarFragment(); return new CommentBarFragment();
case RemoteListFragment.TAG: case RemoteListFragment.TAG:
return new RemoteListFragment(); return new RemoteListFragment();
case SubtaskControlSet.TAG:
return new SubtaskControlSet();
default: default:
throw new RuntimeException("Unsupported fragment"); throw new RuntimeException("Unsupported fragment");
} }

@ -18,6 +18,8 @@ import org.tasks.ui.LocationControlSet;
import org.tasks.ui.NavigationDrawerFragment; import org.tasks.ui.NavigationDrawerFragment;
import org.tasks.ui.PriorityControlSet; import org.tasks.ui.PriorityControlSet;
import org.tasks.ui.RemoteListFragment; import org.tasks.ui.RemoteListFragment;
import org.tasks.ui.SubtaskControlSet;
import org.tasks.ui.TaskListViewModel;
@Subcomponent(modules = FragmentModule.class) @Subcomponent(modules = FragmentModule.class)
public interface FragmentComponent { public interface FragmentComponent {
@ -55,4 +57,8 @@ public interface FragmentComponent {
void inject(RemoteListFragment remoteListFragment); void inject(RemoteListFragment remoteListFragment);
void inject(LocationControlSet locationControlSet); void inject(LocationControlSet locationControlSet);
void inject(SubtaskControlSet subtaskControlSet);
void inject(TaskListViewModel taskListViewModel);
} }

@ -0,0 +1,19 @@
package org.tasks.tasklist;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import org.tasks.data.TaskContainer;
class SubtaskDiffCallback extends DiffUtil.ItemCallback<TaskContainer> {
@Override
public boolean areItemsTheSame(@NonNull TaskContainer oldItem, @NonNull TaskContainer newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(
@NonNull TaskContainer oldItem, @NonNull TaskContainer newItem) {
return oldItem.equals(newItem);
}
}

@ -0,0 +1,157 @@
package org.tasks.tasklist;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.Paint;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.google.android.material.chip.Chip;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.ui.CheckableImageView;
import org.tasks.R;
import org.tasks.data.TaskContainer;
import org.tasks.locale.Locale;
import org.tasks.ui.CheckBoxes;
public class SubtaskViewHolder extends RecyclerView.ViewHolder {
private final Activity context;
private final Locale locale;
private final Callbacks callbacks;
private final DisplayMetrics metrics;
public TaskContainer task;
@BindView(R.id.rowBody)
ViewGroup rowBody;
@BindView(R.id.title)
TextView nameView;
@BindView(R.id.completeBox)
CheckableImageView completeBox;
@BindView(R.id.chip_button)
Chip chip;
private int indent;
SubtaskViewHolder(
Activity context,
Locale locale,
ViewGroup view,
Callbacks callbacks,
DisplayMetrics metrics) {
super(view);
this.context = context;
this.locale = locale;
this.callbacks = callbacks;
this.metrics = metrics;
ButterKnife.bind(this, view);
view.setTag(this);
for (int i = 0; i < view.getChildCount(); i++) {
view.getChildAt(i).setTag(this);
}
}
private float getShiftSize() {
return 20 * metrics.density;
}
private int getIndentSize(int indent) {
return Math.round(indent * getShiftSize());
}
void bindView(TaskContainer task, boolean multiLevelSubtasks) {
this.task = task;
setIndent(multiLevelSubtasks ? task.indent : 0);
if (task.hasChildren()) {
chip.setText(locale.formatNumber(task.children));
chip.setVisibility(View.VISIBLE);
chip.setChipIconResource(
task.isCollapsed()
? R.drawable.ic_keyboard_arrow_up_black_24dp
: R.drawable.ic_keyboard_arrow_down_black_24dp);
} else {
chip.setVisibility(View.GONE);
}
nameView.setText(task.getTitle());
setupTitleAndCheckbox();
}
private void setupTitleAndCheckbox() {
if (task.isCompleted()) {
nameView.setEnabled(false);
nameView.setPaintFlags(nameView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
} else {
nameView.setEnabled(!task.isHidden());
nameView.setPaintFlags(nameView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
}
completeBox.setChecked(task.isCompleted());
completeBox.setImageDrawable(CheckBoxes.getCheckBox(context, task.getTask()));
completeBox.invalidate();
}
@OnClick(R.id.title)
void openSubtask(View v) {
callbacks.openSubtask(task.getTask());
}
@OnClick(R.id.chip_button)
void toggleSubtasks(View v) {
callbacks.toggleSubtask(task.getId(), !task.isCollapsed());
}
@OnClick(R.id.completeBox)
void onCompleteBoxClick(View v) {
if (task == null) {
return;
}
boolean newState = completeBox.isChecked();
if (newState != task.isCompleted()) {
callbacks.complete(task.getTask(), newState);
}
// set check box to actual action item state
setupTitleAndCheckbox();
}
public int getIndent() {
return indent;
}
@SuppressLint("NewApi")
public void setIndent(int indent) {
this.indent = indent;
int indentSize = getIndentSize(indent);
if (atLeastLollipop()) {
MarginLayoutParams layoutParams = (MarginLayoutParams) rowBody.getLayoutParams();
layoutParams.setMarginStart(indentSize);
rowBody.setLayoutParams(layoutParams);
} else {
rowBody.setPadding(indentSize, rowBody.getPaddingTop(), 0, rowBody.getPaddingBottom());
}
}
public interface Callbacks {
void openSubtask(Task task);
void toggleSubtask(long taskId, boolean collapsed);
void complete(Task task, boolean completed);
}
}

@ -0,0 +1,93 @@
package org.tasks.tasklist;
import android.app.Activity;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import org.tasks.R;
import org.tasks.data.TaskContainer;
import org.tasks.locale.Locale;
import org.tasks.tasklist.SubtaskViewHolder.Callbacks;
public class SubtasksRecyclerAdapter extends RecyclerView.Adapter<SubtaskViewHolder>
implements ListUpdateCallback {
private final DisplayMetrics metrics;
private final Activity activity;
private final Locale locale;
private final Callbacks callbacks;
private final AsyncListDiffer<TaskContainer> differ;
private boolean multiLevelSubtasks;
public SubtasksRecyclerAdapter(
Activity activity, Locale locale, SubtaskViewHolder.Callbacks callbacks) {
this.activity = activity;
this.locale = locale;
this.callbacks = callbacks;
differ =
new AsyncListDiffer<>(
this, new AsyncDifferConfig.Builder<>(new SubtaskDiffCallback()).build());
metrics = activity.getResources().getDisplayMetrics();
}
@NonNull
@Override
public SubtaskViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ViewGroup view =
(ViewGroup)
LayoutInflater.from(activity).inflate(R.layout.subtask_adapter_row_body, parent, false);
return new SubtaskViewHolder(activity, locale, view, callbacks, metrics);
}
@Override
public void onBindViewHolder(@NonNull SubtaskViewHolder holder, int position) {
TaskContainer task = getItem(position);
if (task != null) {
holder.bindView(task, multiLevelSubtasks);
}
}
public TaskContainer getItem(int position) {
return differ.getCurrentList().get(position);
}
public void submitList(List<TaskContainer> list) {
differ.submitList(list);
}
@Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
notifyDataSetChanged(); // remove animation is janky
}
@Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count, @Nullable Object payload) {
notifyItemRangeChanged(position, count, payload);
}
@Override
public int getItemCount() {
return differ.getCurrentList().size();
}
public void setMultiLevelSubtasksEnabled(boolean enabled) {
multiLevelSubtasks = enabled;
}
}

@ -3,6 +3,7 @@ package org.tasks.ui;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static org.tasks.activities.RemoteListSupportPicker.newRemoteListSupportPicker; import static org.tasks.activities.RemoteListSupportPicker.newRemoteListSupportPicker;
import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -55,6 +56,18 @@ public class RemoteListFragment extends TaskEditControlFragment {
@Nullable private Filter originalList; @Nullable private Filter originalList;
@Nullable private Filter selectedList; @Nullable private Filter selectedList;
private OnListChanged callback;
public interface OnListChanged {
void onListchanged(@Nullable Filter filter);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callback = (OnListChanged) activity;
}
@Nullable @Nullable
@Override @Override
@ -63,7 +76,7 @@ public class RemoteListFragment extends TaskEditControlFragment {
View view = super.onCreateView(inflater, container, savedInstanceState); View view = super.onCreateView(inflater, container, savedInstanceState);
if (savedInstanceState != null) { if (savedInstanceState != null) {
originalList = savedInstanceState.getParcelable(EXTRA_ORIGINAL_LIST); originalList = savedInstanceState.getParcelable(EXTRA_ORIGINAL_LIST);
selectedList = savedInstanceState.getParcelable(EXTRA_SELECTED_LIST); setSelected(savedInstanceState.getParcelable(EXTRA_SELECTED_LIST));
} else { } else {
if (task.isNew()) { if (task.isNew()) {
if (task.hasTransitory(GoogleTask.KEY)) { if (task.hasTransitory(GoogleTask.KEY)) {
@ -96,18 +109,18 @@ public class RemoteListFragment extends TaskEditControlFragment {
} }
} }
selectedList = originalList; setSelected(originalList);
} }
chip.setOnCloseIconClickListener(this::clearSelected); chip.setOnCloseIconClickListener(v -> setSelected(null));
refreshView();
return view; return view;
} }
private void clearSelected(View ignored) { private void setSelected(@Nullable Filter filter) {
selectedList = null; selectedList = filter;
refreshView(); refreshView();
callback.onListchanged(filter);
} }
@Override @Override
@ -171,13 +184,12 @@ public class RemoteListFragment extends TaskEditControlFragment {
private void setList(Filter list) { private void setList(Filter list) {
if (list == null) { if (list == null) {
this.selectedList = null; setSelected(null);
} else if (list instanceof GtasksFilter || list instanceof CaldavFilter) { } else if (list instanceof GtasksFilter || list instanceof CaldavFilter) {
this.selectedList = list; setSelected(list);
} else { } else {
throw new RuntimeException("Unhandled filter type"); throw new RuntimeException("Unhandled filter type");
} }
refreshView();
} }
private void refreshView() { private void refreshView() {

@ -0,0 +1,336 @@
package org.tasks.ui;
import static com.todoroo.andlib.utility.DateUtilities.now;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.OnClick;
import com.google.common.base.Strings;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Join;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.astrid.activity.MainActivity;
import com.todoroo.astrid.api.CaldavFilter;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.GtasksFilter;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.service.TaskCompleter;
import com.todoroo.astrid.service.TaskCreator;
import com.todoroo.astrid.ui.CheckableImageView;
import java.util.ArrayList;
import javax.inject.Inject;
import org.tasks.LocalBroadcastManager;
import org.tasks.R;
import org.tasks.data.CaldavDao;
import org.tasks.data.CaldavTask;
import org.tasks.data.GoogleTask;
import org.tasks.data.GoogleTaskDao;
import org.tasks.injection.FragmentComponent;
import org.tasks.locale.Locale;
import org.tasks.preferences.Preferences;
import org.tasks.tasklist.SubtaskViewHolder.Callbacks;
import org.tasks.tasklist.SubtasksRecyclerAdapter;
public class SubtaskControlSet extends TaskEditControlFragment implements Callbacks {
public static final int TAG = R.string.TEA_ctrl_subtask_pref;
private static final String EXTRA_NEW_SUBTASKS = "extra_new_subtasks";
@BindView(R.id.recycler_view)
RecyclerView recyclerView;
@BindView(R.id.add_subtask)
TextView addSubtask;
@BindView(R.id.new_subtasks)
LinearLayout newSubtaskContainer;
@Inject Activity activity;
@Inject TaskCompleter taskCompleter;
@Inject LocalBroadcastManager localBroadcastManager;
@Inject GoogleTaskDao googleTaskDao;
@Inject Toaster toaster;
@Inject Preferences preferences;
@Inject TaskCreator taskCreator;
@Inject CaldavDao caldavDao;
@Inject TaskDao taskDao;
@Inject Locale locale;
private TaskListViewModel viewModel;
private final RefreshReceiver refreshReceiver = new RefreshReceiver();
private Filter remoteList;
private GoogleTask googleTask;
private SubtasksRecyclerAdapter recyclerAdapter;
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
viewModel = ViewModelProviders.of(this).get(TaskListViewModel.class);
component.inject(viewModel);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList(EXTRA_NEW_SUBTASKS, getNewSubtasks());
}
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = super.onCreateView(inflater, container, savedInstanceState);
if (savedInstanceState != null) {
for (Task task : savedInstanceState.<Task>getParcelableArrayList(EXTRA_NEW_SUBTASKS)) {
addSubtask(task);
}
}
recyclerAdapter = new SubtasksRecyclerAdapter(activity, locale, this);
if (task.getId() > 0) {
recyclerAdapter.submitList(viewModel.getValue());
viewModel.setFilter(new Filter("subtasks", getQueryTemplate(task)), true);
((DefaultItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
recyclerView.setLayoutManager(new LinearLayoutManager(activity));
recyclerView.setNestedScrollingEnabled(false);
viewModel.observe(this, recyclerAdapter::submitList);
recyclerView.setAdapter(recyclerAdapter);
}
return view;
}
private static QueryTemplate getQueryTemplate(Task task) {
return new QueryTemplate()
.join(
Join.left(
GoogleTask.TABLE,
Criterion.and(
GoogleTask.PARENT.eq(task.getId()),
GoogleTask.TASK.eq(Task.ID),
GoogleTask.DELETED.eq(0))))
.join(
Join.left(
CaldavTask.TABLE,
Criterion.and(
CaldavTask.PARENT.eq(task.getId()),
CaldavTask.TASK.eq(Task.ID),
CaldavTask.DELETED.eq(0))))
.where(Criterion.and(TaskCriteria.activeAndVisible(),
Criterion.or(CaldavTask.TASK.gt(0), GoogleTask.TASK.gt(0))));
}
@Override
protected int getLayout() {
return R.layout.control_set_subtasks;
}
@Override
protected int getIcon() {
return R.drawable.ic_subdirectory_arrow_right_black_24dp;
}
@Override
public int controlId() {
return TAG;
}
@Override
public void apply(Task task) {
for (Task subtask: getNewSubtasks()) {
if (Strings.isNullOrEmpty(subtask.getTitle())) {
continue;
}
taskDao.createNew(subtask);
if (remoteList instanceof GtasksFilter) {
GoogleTask googleTask =
new GoogleTask(subtask.getId(), ((GtasksFilter) remoteList).getRemoteId());
googleTask.setParent(task.getId());
googleTask.setMoved(true);
googleTaskDao.insertAndShift(googleTask, preferences.addGoogleTasksToTop());
} else if (remoteList instanceof CaldavFilter) {
CaldavTask caldavTask =
new CaldavTask(subtask.getId(), ((CaldavFilter) remoteList).getUuid());
caldavTask.setParent(task.getId());
caldavTask.setRemoteParent(caldavDao.getRemoteIdForTask(task.getId()));
caldavDao.insert(caldavTask);
}
}
}
@Override
public boolean hasChanges(Task original) {
return remoteList != null && !getNewSubtasks().isEmpty();
}
private ArrayList<Task> getNewSubtasks() {
ArrayList<Task> subtasks = new ArrayList<>();
int children = newSubtaskContainer.getChildCount();
for (int i = 0 ; i < children ; i++) {
View view = newSubtaskContainer.getChildAt(i);
EditText title = view.findViewById(R.id.title);
CheckableImageView completed = view.findViewById(R.id.completeBox);
Task subtask = taskCreator.createWithValues(title.getText().toString());
if (completed.isChecked()) {
subtask.setCompletionDate(now());
}
subtasks.add(subtask);
}
return subtasks;
}
@Override
public void onResume() {
super.onResume();
localBroadcastManager.registerRefreshReceiver(refreshReceiver);
googleTask = googleTaskDao.getByTaskId(task.getId());
updateUI();
}
@Override
public void onPause() {
super.onPause();
localBroadcastManager.unregisterReceiver(refreshReceiver);
}
@OnClick(R.id.add_subtask)
void addSubtask() {
if (remoteList == null) {
toaster.longToast(R.string.subtasks_enable_synchronization);
return;
}
if (isGoogleTaskChild()) {
toaster.longToast(R.string.subtasks_multilevel_google_task);
return;
}
EditText editText = addSubtask(taskCreator.createWithValues(""));
editText.requestFocus();
InputMethodManager imm =
(InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
}
private EditText addSubtask(Task task) {
ViewGroup view =
(ViewGroup)
LayoutInflater.from(activity)
.inflate(R.layout.editable_subtask_adapter_row_body, newSubtaskContainer, false);
view.findViewById(R.id.clear).setOnClickListener(v -> newSubtaskContainer.removeView(view));
EditText editText = view.findViewById(R.id.title);
editText.setTextKeepState(task.getTitle());
editText.setHorizontallyScrolling(false);
editText.setLines(1);
editText.setMaxLines(Integer.MAX_VALUE);
editText.setFocusable(true);
editText.setEnabled(true);
editText.setOnEditorActionListener(
(arg0, actionId, arg2) -> {
if (actionId == EditorInfo.IME_ACTION_NEXT) {
if (editText.getText().length() != 0) {
addSubtask();
}
return true;
}
return false;
});
CheckableImageView completeBox = view.findViewById(R.id.completeBox);
completeBox.setChecked(task.isCompleted());
updateCompleteBox(task, completeBox, editText);
completeBox.setOnClickListener(v -> updateCompleteBox(task, completeBox, editText));
newSubtaskContainer.addView(view);
return editText;
}
private void updateCompleteBox(Task task, CheckableImageView completeBox, EditText editText) {
boolean isComplete = completeBox.isChecked();
completeBox.setImageDrawable(
CheckBoxes.getCheckBox(activity, isComplete, false, task.getPriority()));
if (isComplete) {
editText.setPaintFlags(editText.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
} else {
editText.setPaintFlags(editText.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
}
}
private boolean isGoogleTaskChild() {
return remoteList instanceof GtasksFilter
&& googleTask != null
&& googleTask.getParent() > 0
&& googleTask.getListId().equals(((GtasksFilter) remoteList).getRemoteId());
}
private void updateUI() {
if (remoteList == null || isGoogleTaskChild()) {
recyclerView.setVisibility(View.GONE);
newSubtaskContainer.setVisibility(View.GONE);
} else {
recyclerView.setVisibility(View.VISIBLE);
newSubtaskContainer.setVisibility(View.VISIBLE);
recyclerAdapter.setMultiLevelSubtasksEnabled(!(remoteList instanceof GtasksFilter));
refresh();
}
}
public void onRemoteListChanged(@Nullable Filter filter) {
this.remoteList = filter;
if (recyclerView != null) {
updateUI();
}
}
private void refresh() {
viewModel.invalidate();
}
@Override
public void openSubtask(Task task) {
((MainActivity) getActivity()).getTaskListFragment().onTaskListItemClicked(task);
}
@Override
public void toggleSubtask(long taskId, boolean collapsed) {
taskDao.setCollapsed(taskId, collapsed);
localBroadcastManager.broadcastRefresh();
}
@Override
public void complete(Task task, boolean completed) {
taskCompleter.setComplete(task, completed);
}
protected class RefreshReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
refresh();
}
}
}

@ -272,9 +272,14 @@ public class TaskListViewModel extends ViewModel {
} }
public void invalidate() { public void invalidate() {
if (filter == null) {
return;
}
disposable.add( disposable.add(
Single.fromCallable( Single.fromCallable(
() -> taskDao.fetchTasks(hasSubtasks -> getQuery(preferences, filter, hasSubtasks))) () ->
taskDao.fetchTasks(hasSubtasks -> getQuery(preferences, filter, hasSubtasks)))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(tasks::postValue, Timber::e)); .subscribe(tasks::postValue, Timber::e));

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,15l-6,6 -1.42,-1.42L15.17,16H4V4h2v10h9.17l-3.59,-3.58L13,9l6,6z"/>
</vector>

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:overScrollMode="never"
android:scrollbars="none"
app:elevation="@dimen/elevation_task_list" />
<LinearLayout
android:id="@+id/new_subtasks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/recycler_view"
android:orientation="vertical">
</LinearLayout>
<TextView
android:id="@+id/add_subtask"
style="@style/TaskEditTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/new_subtasks"
android:clickable="true"
android:focusable="true"
android:gravity="start"
android:hint="@string/TEA_add_subtask"
android:paddingStart="0dp"
android:paddingLeft="0dp"
android:paddingEnd="@dimen/keyline_second"
android:paddingRight="@dimen/keyline_second"
android:textAlignment="viewStart" />
</RelativeLayout>

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rowBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/keyline_first">
<com.todoroo.astrid.ui.CheckableImageView
android:id="@+id/completeBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:paddingStart="0dp"
android:paddingLeft="0dp"
android:paddingEnd="@dimen/keyline_first"
android:paddingRight="@dimen/keyline_first" />
<include
layout="@layout/control_set_clear_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true" />
<EditText
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/completeBox"
android:layout_toEndOf="@id/completeBox"
android:layout_toLeftOf="@id/clear"
android:layout_toStartOf="@id/clear"
android:background="@null"
android:focusable="true"
android:gravity="start|top"
android:hint="@string/enter_title_hint"
android:imeOptions="flagNoExtractUi"
android:inputType="textCapSentences"
android:paddingStart="0dp"
android:paddingLeft="0dp"
android:paddingEnd="@dimen/keyline_first"
android:paddingRight="@dimen/keyline_first"
android:singleLine="false"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/task_edit_text_size" />
</RelativeLayout>

@ -10,7 +10,7 @@
<include layout="@layout/toolbar"/> <include layout="@layout/toolbar"/>
<ScrollView <androidx.core.widget.NestedScrollView
android:scrollbarStyle="outsideOverlay" android:scrollbarStyle="outsideOverlay"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="0dp" android:layout_height="0dp"
@ -90,6 +90,11 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<FrameLayout
android:id="@+id/row_13"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout <LinearLayout
android:id="@+id/comments" android:id="@+id/comments"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -97,7 +102,7 @@
android:orientation="vertical"/> android:orientation="vertical"/>
</LinearLayout> </LinearLayout>
</ScrollView> </androidx.core.widget.NestedScrollView>
<FrameLayout <FrameLayout
android:id="@+id/comment_bar" android:id="@+id/comment_bar"

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/rowBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/keyline_first">
<com.todoroo.astrid.ui.CheckableImageView
android:id="@+id/completeBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:paddingStart="0dp"
android:paddingLeft="0dp"
android:paddingEnd="@dimen/keyline_first"
android:paddingRight="@dimen/keyline_first" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_button"
style="@style/ChipStyle"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:textColor="?android:textColorSecondary"
app:chipBackgroundColor="@color/window_background"
app:chipIconTint="@color/icon_tint"
app:chipStrokeColor="?android:textColorSecondary"
app:chipStrokeWidth="@dimen/chip_stroke"
app:iconEndPadding="0dp"
app:iconStartPadding="@dimen/chip_text_padding"
app:textEndPadding="@dimen/chip_text_padding"
app:textStartPadding="@dimen/chip_text_padding" />
<TextView
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@id/chip_button"
android:layout_toLeftOf="@id/chip_button"
android:layout_toEndOf="@id/completeBox"
android:layout_toRightOf="@id/completeBox"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="start|top"
android:paddingStart="0dp"
android:paddingLeft="0dp"
android:paddingEnd="@dimen/keyline_first"
android:paddingRight="@dimen/keyline_first"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/task_edit_text_size" />
</RelativeLayout>

@ -162,6 +162,7 @@
<item>@string/TEA_control_location</item> <item>@string/TEA_control_location</item>
<item>@string/tags</item> <item>@string/tags</item>
<item>@string/synchronization</item> <item>@string/synchronization</item>
<item>@string/subtasks</item>
<item>@string/TEA_control_reminders</item> <item>@string/TEA_control_reminders</item>
<item>@string/TEA_control_files</item> <item>@string/TEA_control_files</item>
<item>@string/TEA_control_notes</item> <item>@string/TEA_control_notes</item>
@ -175,6 +176,7 @@
<string name="TEA_ctrl_when_pref">TEA_ctrl_when_pref</string> <string name="TEA_ctrl_when_pref">TEA_ctrl_when_pref</string>
<string name="TEA_ctrl_repeat_pref">TEA_ctrl_repeat_pref</string> <string name="TEA_ctrl_repeat_pref">TEA_ctrl_repeat_pref</string>
<string name="TEA_ctrl_importance_pref">TEA_ctrl_importance_pref</string> <string name="TEA_ctrl_importance_pref">TEA_ctrl_importance_pref</string>
<string name="TEA_ctrl_subtask_pref">TEA_ctrl_subtask_pref</string>
<string name="TEA_ctrl_lists_pref">TEA_ctrl_lists_pref</string> <string name="TEA_ctrl_lists_pref">TEA_ctrl_lists_pref</string>
<string name="TEA_ctrl_notes_pref">TEA_ctrl_notes_pref</string> <string name="TEA_ctrl_notes_pref">TEA_ctrl_notes_pref</string>
<string name="TEA_ctrl_files_pref">TEA_ctrl_files_pref</string> <string name="TEA_ctrl_files_pref">TEA_ctrl_files_pref</string>
@ -199,6 +201,7 @@
<item>@string/TEA_ctrl_locations_pref</item> <item>@string/TEA_ctrl_locations_pref</item>
<item>@string/TEA_ctrl_lists_pref</item> <item>@string/TEA_ctrl_lists_pref</item>
<item>@string/TEA_ctrl_google_task_list</item> <item>@string/TEA_ctrl_google_task_list</item>
<item>@string/TEA_ctrl_subtask_pref</item>
<item>@string/TEA_ctrl_reminders_pref</item> <item>@string/TEA_ctrl_reminders_pref</item>
<item>@string/TEA_ctrl_files_pref</item> <item>@string/TEA_ctrl_files_pref</item>
<item>@string/TEA_ctrl_notes_pref</item> <item>@string/TEA_ctrl_notes_pref</item>

@ -42,6 +42,7 @@ File %1$s contained %2$s.\n\n
<string name="FLA_new_filter">Create new filter</string> <string name="FLA_new_filter">Create new filter</string>
<string name="TEA_title_hint">Task name</string> <string name="TEA_title_hint">Task name</string>
<string name="TEA_importance_label">Priority</string> <string name="TEA_importance_label">Priority</string>
<string name="TEA_add_subtask">Add subtask</string>
<string name="TEA_no_date">No date</string> <string name="TEA_no_date">No date</string>
<string name="TEA_hideUntil_label">Hide until</string> <string name="TEA_hideUntil_label">Hide until</string>
<string name="TEA_hideUntil_display">Hide until %s</string> <string name="TEA_hideUntil_display">Hide until %s</string>
@ -318,6 +319,7 @@ File %1$s contained %2$s.\n\n
<string name="google_drive_backup">Copy to Google Drive</string> <string name="google_drive_backup">Copy to Google Drive</string>
<string name="miscellaneous">Miscellaneous</string> <string name="miscellaneous">Miscellaneous</string>
<string name="synchronization">Synchronization</string> <string name="synchronization">Synchronization</string>
<string name="subtasks">Subtasks</string>
<string name="enabled">Enabled</string> <string name="enabled">Enabled</string>
<string name="font_size">Font size</string> <string name="font_size">Font size</string>
<string name="row_spacing">Row spacing</string> <string name="row_spacing">Row spacing</string>
@ -556,4 +558,7 @@ File %1$s contained %2$s.\n\n
<string name="caldav_account_repeating_tasks">Let server schedule recurring tasks</string> <string name="caldav_account_repeating_tasks">Let server schedule recurring tasks</string>
<string name="expand_subtasks">Expand subtasks</string> <string name="expand_subtasks">Expand subtasks</string>
<string name="collapse_subtasks">Collapse subtasks</string> <string name="collapse_subtasks">Collapse subtasks</string>
<string name="subtasks_enable_synchronization">Enable synchronization to add subtasks</string>
<string name="subtasks_multilevel_google_task">Multi-level subtasks not supported by Google Tasks</string>
<string name="enter_title_hint">Enter title</string>
</resources> </resources>

Loading…
Cancel
Save