diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java b/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java index f49fd8af1..23cccf4e3 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java @@ -38,8 +38,8 @@ public class DefaultModel extends Model private final static LayoutDescriptor TEXT_EDIT = new LayoutDescriptor(R.layout.text_field_editor); private final static LayoutDescriptor TEXT_EDIT_SINGLE_LINE = new LayoutDescriptor(R.layout.text_field_editor).setOption(LayoutDescriptor.OPTION_MULTILINE, false); - private final static LayoutDescriptor CHECKLIST_VIEW = new LayoutDescriptor(R.layout.checklist_field_view); - private final static LayoutDescriptor CHECKLIST_EDIT = new LayoutDescriptor(R.layout.checklist_field_editor); + private final static LayoutDescriptor DESCRIPTION_VIEW = new LayoutDescriptor(R.layout.description_field_view); + private final static LayoutDescriptor DESCRIPTION_EDIT = new LayoutDescriptor(R.layout.description_field_editor); private final static LayoutDescriptor CHOICES_VIEW = new LayoutDescriptor(R.layout.choices_field_view); private final static LayoutDescriptor CHOICES_EDIT = new LayoutDescriptor(R.layout.choices_field_editor); private final static LayoutDescriptor PROGRESS_VIEW = new LayoutDescriptor(R.layout.percentage_field_view); @@ -106,14 +106,11 @@ public void inflate() .setEditorLayout(TEXT_EDIT).setIcon(R.drawable.ic_detail_location)); // description - addField(new FieldDescriptor(context, R.id.task_field_description, R.string.task_description, TaskFieldAdapters.DESCRIPTION) - .setViewLayout(TEXT_VIEW.setOption(LayoutDescriptor.OPTION_LINKIFY, Linkify.ALL)).setEditorLayout(TEXT_EDIT) + addField(new FieldDescriptor(context, R.id.task_field_checklist, R.string.task_description, TaskFieldAdapters.DESCRIPTION_CHECKLIST) + .setViewLayout(DESCRIPTION_VIEW) + .setEditorLayout(DESCRIPTION_EDIT) .setIcon(R.drawable.ic_detail_description)); - // description - addField(new FieldDescriptor(context, R.id.task_field_checklist, R.string.task_checklist, TaskFieldAdapters.CHECKLIST).setViewLayout(CHECKLIST_VIEW) - .setEditorLayout(CHECKLIST_EDIT).setIcon(R.drawable.ic_detail_checklist)); - // start addField(new FieldDescriptor(context, R.id.task_field_dtstart, R.string.task_start, TaskFieldAdapters.DTSTART).setViewLayout(TIME_VIEW) .setEditorLayout(TIME_EDIT).setIcon(R.drawable.ic_detail_start)); diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/DescriptionItem.java b/opentasks/src/main/java/org/dmfs/tasks/model/DescriptionItem.java new file mode 100644 index 000000000..d5d2c0d68 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/model/DescriptionItem.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.model; + +/** + * A bloody POJO o_O to store a description/check list item + */ +public final class DescriptionItem +{ + public final boolean checkbox; + public boolean checked; + public String text; + + + public DescriptionItem(boolean checkbox, boolean checked, String text) + { + this.checkbox = checkbox; + this.checked = checked; + this.text = text; + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java b/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java index f1195736d..3310211c9 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java @@ -24,6 +24,7 @@ import org.dmfs.tasks.model.adapters.ChecklistFieldAdapter; import org.dmfs.tasks.model.adapters.ColorFieldAdapter; import org.dmfs.tasks.model.adapters.CustomizedDefaultFieldAdapter; +import org.dmfs.tasks.model.adapters.DescriptionFieldAdapter; import org.dmfs.tasks.model.adapters.DescriptionStringFieldAdapter; import org.dmfs.tasks.model.adapters.FieldAdapter; import org.dmfs.tasks.model.adapters.FloatFieldAdapter; @@ -37,6 +38,7 @@ import org.dmfs.tasks.model.constraints.After; import org.dmfs.tasks.model.constraints.BeforeOrShiftTime; import org.dmfs.tasks.model.constraints.ChecklistConstraint; +import org.dmfs.tasks.model.constraints.DescriptionConstraint; import org.dmfs.tasks.model.defaults.DefaultAfter; import org.dmfs.tasks.model.defaults.DefaultBefore; @@ -110,6 +112,12 @@ public final class TaskFieldAdapters public final static ChecklistFieldAdapter CHECKLIST = (ChecklistFieldAdapter) new ChecklistFieldAdapter(Tasks.DESCRIPTION) .addContraint(new ChecklistConstraint(STATUS, PERCENT_COMPLETE)); + /** + * Adapter for the checklist of a task. + */ + public final static DescriptionFieldAdapter DESCRIPTION_CHECKLIST = (DescriptionFieldAdapter) new DescriptionFieldAdapter(Tasks.DESCRIPTION) + .addContraint(new DescriptionConstraint(STATUS, PERCENT_COMPLETE)); + /** * Private adapter for the start date of a task. We need this to reference DTSTART from DUE. */ diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/XmlModel.java b/opentasks/src/main/java/org/dmfs/tasks/model/XmlModel.java index be0ebb97b..152814ffc 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/XmlModel.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/XmlModel.java @@ -24,7 +24,6 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ServiceInfo; import android.content.res.XmlResourceParser; -import android.util.Log; import org.dmfs.tasks.R; import org.dmfs.tasks.contract.TaskContract.Tasks; @@ -39,7 +38,6 @@ import org.dmfs.xmlobjects.pull.ParserContext; import org.dmfs.xmlobjects.pull.Recyclable; import org.dmfs.xmlobjects.pull.XmlObjectPull; -import org.dmfs.xmlobjects.pull.XmlObjectPullParserException; import org.dmfs.xmlobjects.pull.XmlPath; import java.util.HashMap; @@ -179,11 +177,6 @@ else if ("dtstart".equals(datakind.datakind)) { state.hasStart = true; } - else if ("description".equals(datakind.datakind) && !datakind.hideCheckList) - { - Log.i(TAG, "found old description data kind, adding checklist"); - object.addField(FIELD_INFLATER_MAP.get("checklist").inflate(appContext, object.mModelContext, datakind)); - } } // we don't need the datakind object anymore, so recycle it context.recycle((ElementDescriptor) childDescriptor, datakind); @@ -511,10 +504,8 @@ public FieldInflater addEditLayoutOption(String key, int value) FIELD_INFLATER_MAP.put("location", new FieldInflater(TaskFieldAdapters.LOCATION, R.id.task_field_location, R.string.task_location, R.layout.opentasks_location_field_view, R.layout.text_field_editor, R.drawable.ic_detail_location).addDetailsLayoutOption( LayoutDescriptor.OPTION_LINKIFY, 0)); - FIELD_INFLATER_MAP.put("description", new FieldInflater(TaskFieldAdapters.DESCRIPTION, R.id.task_field_description, R.string.task_description, - R.layout.text_field_view, R.layout.text_field_editor, R.drawable.ic_detail_description)); - FIELD_INFLATER_MAP.put("checklist", new FieldInflater(TaskFieldAdapters.CHECKLIST, R.id.task_field_checklist, R.string.task_checklist, - R.layout.checklist_field_view, R.layout.checklist_field_editor, R.drawable.ic_detail_checklist)); + FIELD_INFLATER_MAP.put("description", new FieldInflater(TaskFieldAdapters.DESCRIPTION_CHECKLIST, R.id.task_field_description, R.string.task_description, + R.layout.description_field_view, R.layout.description_field_editor, R.drawable.ic_detail_description)); FIELD_INFLATER_MAP.put("dtstart", new FieldInflater(TaskFieldAdapters.DTSTART, R.id.task_field_dtstart, R.string.task_start, R.layout.time_field_view, R.layout.time_field_editor, R.drawable.ic_detail_start)); diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/adapters/DescriptionFieldAdapter.java b/opentasks/src/main/java/org/dmfs/tasks/model/adapters/DescriptionFieldAdapter.java new file mode 100644 index 000000000..7982665c2 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/model/adapters/DescriptionFieldAdapter.java @@ -0,0 +1,248 @@ +/* + * Copyright 2019 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.model.adapters; + +import android.content.ContentValues; +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + +import org.dmfs.tasks.model.ContentSet; +import org.dmfs.tasks.model.DescriptionItem; +import org.dmfs.tasks.model.OnContentChangeListener; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * Knows how to load and store check list from/to a combined description/check list field. + * + * @author Marten Gajda + */ +public final class DescriptionFieldAdapter extends FieldAdapter> +{ + private final static Pattern CHECKMARK_PATTERN = Pattern.compile("([-*] )?\\[([xX ])\\](.*)"); + + /** + * The field name this adapter uses to store the values. + */ + private final String mFieldName; + + /** + * The default value, if any. + */ + private final List mDefaultValue; + + + /** + * Constructor for a new StringFieldAdapter without default value. + * + * @param fieldName + * The name of the field to use when loading or storing the value. + */ + public DescriptionFieldAdapter(String fieldName) + { + this(fieldName, null); + } + + + /** + * Constructor for a new StringFieldAdapter without default value. + * + * @param fieldName + * The name of the field to use when loading or storing the value. + * @param defaultValue + * The default check list + */ + public DescriptionFieldAdapter(String fieldName, List defaultValue) + { + if (fieldName == null) + { + throw new IllegalArgumentException("fieldName must not be null"); + } + mFieldName = fieldName; + mDefaultValue = defaultValue; + } + + + @Override + public List get(ContentSet values) + { + // return the check list + return parseDescription(values.getAsString(mFieldName)); + } + + + @Override + public List get(Cursor cursor) + { + int columnIdx = cursor.getColumnIndex(mFieldName); + if (columnIdx < 0) + { + throw new IllegalArgumentException("The fieldName column missing in cursor."); + } + return parseDescription(cursor.getString(columnIdx)); + } + + + @Override + public List getDefault(ContentSet values) + { + return mDefaultValue; + } + + + @Override + public void set(ContentSet values, List value) + { + if (value != null && !value.isEmpty()) + { + StringBuilder sb = new StringBuilder(1024); + serializeDescription(sb, value); + + values.put(mFieldName, sb.toString()); + } + else + { + // store the current value just without check list + values.put(mFieldName, (String) null); + } + } + + + @Override + public void set(ContentValues values, List value) + { + if (value != null && !value.isEmpty()) + { + StringBuilder sb = new StringBuilder(1024); + + serializeDescription(sb, value); + + values.put(mFieldName, sb.toString()); + } + else + { + values.put(mFieldName, ""); + } + + } + + + @Override + public void registerListener(ContentSet values, OnContentChangeListener listener, boolean initalNotification) + { + values.addOnChangeListener(listener, mFieldName, initalNotification); + } + + + @Override + public void unregisterListener(ContentSet values, OnContentChangeListener listener) + { + values.removeOnChangeListener(listener, mFieldName); + } + + + private static List parseDescription(String description) + { + List result = new ArrayList(16); + if (TextUtils.isEmpty(description)) + { + return result; + } + Matcher matcher = CHECKMARK_PATTERN.matcher(""); + StringBuilder currentParagraph = new StringBuilder(); + boolean currentHasCheckedMark = false; + boolean currentIsChecked = false; + for (String line : description.split("\n")) + { + matcher.reset(line); + + if (matcher.lookingAt()) + { + // start a new paragraph, if we already had one + if (currentParagraph.length() > 0) + { + result.add(new DescriptionItem(currentHasCheckedMark, currentIsChecked, + currentHasCheckedMark ? currentParagraph.toString().trim() : currentParagraph.toString())); + } + currentHasCheckedMark = true; + currentIsChecked = "x".equals(matcher.group(2).toLowerCase()); + currentParagraph.setLength(0); + currentParagraph.append(matcher.group(3)); + } + else + { + if (currentHasCheckedMark) + { + // start a new paragraph, if the last one had a tick mark + if (currentParagraph.length() > 0) + { + // close last paragraph + result.add(new DescriptionItem(currentHasCheckedMark, currentIsChecked, currentParagraph.toString().trim())); + } + currentHasCheckedMark = false; + currentParagraph.setLength(0); + } + if (currentParagraph.length() > 0) + { + currentParagraph.append("\n"); + } + currentParagraph.append(line); + } + } + + // close paragraph + if (currentHasCheckedMark || currentParagraph.length() > 0) + { + result.add(new DescriptionItem(currentHasCheckedMark, currentIsChecked, + currentHasCheckedMark ? currentParagraph.toString().trim() : currentParagraph.toString())); + } + return result; + } + + + private static void serializeDescription(StringBuilder sb, List checklist) + { + if (checklist == null || checklist.isEmpty()) + { + return; + } + + boolean first = true; + for (DescriptionItem item : checklist) + { + if (first) + { + first = false; + } + else + { + sb.append('\n'); + } + if (item.checkbox) + { + sb.append(item.checked ? "- [x] " : "- [ ] "); + } + sb.append(item.text); + } + } + +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/constraints/DescriptionConstraint.java b/opentasks/src/main/java/org/dmfs/tasks/model/constraints/DescriptionConstraint.java new file mode 100644 index 000000000..5a0a8a425 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/model/constraints/DescriptionConstraint.java @@ -0,0 +1,97 @@ +/* + * Copyright 2019 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.model.constraints; + +import org.dmfs.tasks.contract.TaskContract.Tasks; +import org.dmfs.tasks.model.ContentSet; +import org.dmfs.tasks.model.DescriptionItem; +import org.dmfs.tasks.model.adapters.IntegerFieldAdapter; + +import java.util.List; + + +/** + * Adjust percent complete & status when a checklist is changed. + * + * @author Marten Gajda + */ +public class DescriptionConstraint extends AbstractConstraint> +{ + private final IntegerFieldAdapter mPercentCompleteAdapter; + private final IntegerFieldAdapter mStatusAdapter; + + + public DescriptionConstraint(IntegerFieldAdapter statusAdapter, IntegerFieldAdapter percentCompleteAdapter) + { + mPercentCompleteAdapter = percentCompleteAdapter; + mStatusAdapter = statusAdapter; + } + + + @Override + public List apply(ContentSet currentValues, List oldValue, List newValue) + { + if (oldValue != null && newValue != null && !oldValue.isEmpty() && !newValue.isEmpty() && !oldValue.equals(newValue)) + { + int checked = 0; + int checkbox = 0; + for (DescriptionItem item : newValue) + { + if (item.checkbox) + { + ++checkbox; + if (item.checked) + { + ++checked; + } + } + } + + if (checkbox > 0) + { + int newPercentComplete = (checked * 100) / checkbox; + + if (mStatusAdapter != null) + { + Integer oldStatus = mStatusAdapter.get(currentValues); + + if (oldStatus == null) + { + oldStatus = mStatusAdapter.getDefault(currentValues); + } + + Integer newStatus = newPercentComplete == 100 ? Tasks.STATUS_COMPLETED : newPercentComplete > 0 || oldStatus != null + && oldStatus == Tasks.STATUS_COMPLETED ? Tasks.STATUS_IN_PROCESS : oldStatus; + if (oldStatus == null && newStatus != null || oldStatus != null && !oldStatus.equals(newStatus) && oldStatus != Tasks.STATUS_CANCELLED) + { + mStatusAdapter.set(currentValues, newStatus); + } + } + + if (mPercentCompleteAdapter != null) + { + Integer oldPercentComplete = mPercentCompleteAdapter.get(currentValues); + if (oldPercentComplete == null || oldPercentComplete != newPercentComplete) + { + mPercentCompleteAdapter.set(currentValues, newPercentComplete); + } + } + } + } + return newValue; + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/widget/DescriptionFieldView.java b/opentasks/src/main/java/org/dmfs/tasks/widget/DescriptionFieldView.java new file mode 100644 index 000000000..402c898a9 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/widget/DescriptionFieldView.java @@ -0,0 +1,442 @@ +/* + * Copyright 2019 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.widget; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Rect; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; + +import com.jmedeisis.draglinearlayout.DragLinearLayout; +import com.jmedeisis.draglinearlayout.DragLinearLayout.OnViewSwapListener; + +import org.dmfs.android.bolts.color.colors.AttributeColor; +import org.dmfs.tasks.R; +import org.dmfs.tasks.model.ContentSet; +import org.dmfs.tasks.model.DescriptionItem; +import org.dmfs.tasks.model.FieldDescriptor; +import org.dmfs.tasks.model.adapters.DescriptionFieldAdapter; +import org.dmfs.tasks.model.layout.LayoutOptions; + +import java.util.List; + +import androidx.core.view.ViewCompat; + + +/** + * View widget for descriptions with checklists. + * + * @author Marten Gajda + */ +public class DescriptionFieldView extends AbstractFieldView implements OnCheckedChangeListener, OnViewSwapListener, OnClickListener +{ + private DescriptionFieldAdapter mAdapter; + private DragLinearLayout mContainer; + + private List mCurrentValue; + + private boolean mBuilding = false; + private LayoutInflater mInflater; + private InputMethodManager mImm; + + + public DescriptionFieldView(Context context) + { + super(context); + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + } + + + public DescriptionFieldView(Context context, AttributeSet attrs) + { + super(context, attrs); + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + } + + + public DescriptionFieldView(Context context, AttributeSet attrs, int defStyle) + { + super(context, attrs, defStyle); + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + } + + + @Override + protected void onFinishInflate() + { + super.onFinishInflate(); + mContainer = findViewById(R.id.checklist); + mContainer.setOnViewSwapListener(this); + + mContainer.findViewById(R.id.add_item).setOnClickListener(this); + } + + + @Override + public void setFieldDescription(FieldDescriptor descriptor, LayoutOptions layoutOptions) + { + super.setFieldDescription(descriptor, layoutOptions); + mAdapter = (DescriptionFieldAdapter) descriptor.getFieldAdapter(); + } + + + @SuppressWarnings("deprecation") + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) + { + if (mCurrentValue == null || mBuilding) + { + return; + } + + int childCount = mContainer.getChildCount(); + for (int i = 0; i < childCount; ++i) + { + if (mContainer.getChildAt(i).findViewById(android.R.id.checkbox) == buttonView) + { + mCurrentValue.get(i).checked = isChecked; + ((TextView) mContainer.getChildAt(i).findViewById(android.R.id.title)).setTextAppearance(getContext(), + isChecked ? R.style.checklist_checked_item_text : R.style.dark_text); + if (mValues != null) + { + mAdapter.validateAndSet(mValues, mCurrentValue); + } + return; + } + } + } + + + @Override + public void updateValues() + { + mAdapter.validateAndSet(mValues, mCurrentValue); + } + + + @Override + public void onContentLoaded(ContentSet contentSet) + { + super.onContentLoaded(contentSet); + } + + + @Override + public void onContentChanged(ContentSet contentSet) + { + if (mValues != null) + { + List newValue = mAdapter.get(mValues); + if (newValue != null && !newValue.equals(mCurrentValue)) // don't trigger unnecessary updates + { + updateCheckList(newValue); + mCurrentValue = newValue; + } + } + } + + + private void updateCheckList(List list) + { + setVisibility(VISIBLE); + + mBuilding = true; + + int count = 0; + for (final DescriptionItem item : list) + { + View itemView = mContainer.getChildAt(count); + if (itemView == null || itemView.getId() != R.id.checklist_element) + { + itemView = createItemView(); + mContainer.addView(itemView, mContainer.getChildCount() - 1); + mContainer.setViewDraggable(itemView, itemView.findViewById(R.id.drag_handle)); + } + + bindItemView(itemView, item); + + ++count; + } + + while (mContainer.getChildCount() > count + 1) + { + View view = mContainer.getChildAt(count); + mContainer.removeDragView(view); + } + + mBuilding = false; + } + + + @Override + public void onSwap(View view1, int position1, View view2, int position2) + { + if (mCurrentValue != null) + { + DescriptionItem item1 = mCurrentValue.get(position1); + DescriptionItem item2 = mCurrentValue.get(position2); + + // swap items in the list + mCurrentValue.set(position2, item1); + mCurrentValue.set(position1, item2); + } + } + + + /** + * Inflates a new check list element view. + * + * @return + */ + private View createItemView() + { + return mInflater.inflate(R.layout.description_field_view_element, mContainer, false); + } + + + @SuppressWarnings("deprecation") + private void bindItemView(final View itemView, final DescriptionItem item) + { + // set the checkbox status + CheckBox checkbox = itemView.findViewById(android.R.id.checkbox); + // make sure we don't receive our own updates + checkbox.setOnCheckedChangeListener(null); + checkbox.setChecked(item.checked && item.checkbox); + checkbox.jumpDrawablesToCurrentState(); + checkbox.setOnCheckedChangeListener(DescriptionFieldView.this); + checkbox.setVisibility(item.checkbox ? VISIBLE : GONE); + + // configure the title + final EditText text = itemView.findViewById(android.R.id.title); + text.setTextAppearance(getContext(), item.checked && item.checkbox ? R.style.checklist_checked_item_text : R.style.dark_text); + if (text.getTag() != null) + { + text.removeTextChangedListener((TextWatcher) text.getTag()); + } + text.setText(item.text); + ColorStateList colorStateList = new ColorStateList( + new int[][] { new int[] { android.R.attr.state_focused }, new int[] { -android.R.attr.state_focused } }, + new int[] { new AttributeColor(getContext(), R.attr.colorPrimary).argb(), 0 }); + ViewCompat.setBackgroundTintList(text, colorStateList); + text.setOnFocusChangeListener(new OnFocusChangeListener() + { + + @Override + public void onFocusChange(View v, boolean hasFocus) + { + View tools = itemView.findViewById(R.id.tools); + tools.setVisibility(hasFocus ? VISIBLE : GONE); + String newText = text.getText().toString(); + if (!hasFocus && !newText.equals(item.text) && mValues != null && !mCurrentValue.equals(mAdapter.get(mValues))) + { + item.text = newText; + } + + if (hasFocus) + { + v.postDelayed( + () -> tools.requestRectangleOnScreen(new Rect(0, 0, tools.getWidth(), tools.getHeight()), false), + 200); + } + } + }); + if (item.checkbox) + { + text.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); + } + else + { + text.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + } + text.setOnEditorActionListener(new OnEditorActionListener() + { + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) + { + if (actionId == EditorInfo.IME_ACTION_NEXT) + { + int pos = mContainer.indexOfChild(itemView); + insertEmptyItem(item.checkbox, pos + 1); + return true; + } + return false; + } + }); + text.setImeOptions(EditorInfo.IME_ACTION_NEXT); + + TextWatcher watcher = new TextWatcher() + { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) + { + + } + + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) + { + + } + + + @Override + public void afterTextChanged(Editable editable) + { + item.text = editable.toString(); + } + }; + + text.setTag(watcher); + text.addTextChangedListener(watcher); + + // bind the remove button + View removeButton = itemView.findViewById(R.id.delete); + removeButton.setOnClickListener(new OnClickListener() + { + @Override + public void onClick(View v) + { + // mImm.hideSoftInputFromWindow(text.getWindowToken(), 0); + mCurrentValue.remove(item); + + mContainer.removeDragView(itemView); + mAdapter.validateAndSet(mValues, mCurrentValue); + } + }); + + // bind the remove button + TextView toggleCheckableButton = itemView.findViewById(R.id.toggle_checkable); + toggleCheckableButton.setText(item.checkbox ? R.string.opentasks_hide_tick_box : R.string.opentasks_show_tick_box); + toggleCheckableButton.setCompoundDrawablesWithIntrinsicBounds(item.checkbox ? R.drawable.ic_text_24px : R.drawable.ic_list_24px, 0, 0, 0); + toggleCheckableButton.setOnClickListener(new OnClickListener() + { + @Override + public void onClick(View v) + { + // mImm.hideSoftInputFromWindow(text.getWindowToken(), 0); + int idx = mCurrentValue.indexOf(item); + mCurrentValue.remove(item); + if (!item.checkbox) + { + String[] lines = item.text.split("\n"); + + if (lines.length == 1) + { + DescriptionItem newItem = new DescriptionItem(true, item.checked, item.text); + mCurrentValue.add(idx, newItem); + } + else + { + for (String i : lines) + { + DescriptionItem newItem = new DescriptionItem(true, false, i); + mCurrentValue.add(idx, newItem); + idx += 1; + + } + } + } + else + { + DescriptionItem newItem = new DescriptionItem(false, item.checked, item.text); + mCurrentValue.add(idx, newItem); + bindItemView(itemView, newItem); + } + updateCheckList(mCurrentValue); + mAdapter.validateAndSet(mValues, mCurrentValue); + } + }); + } + + + /** + * Insert an empty item at the given position. Nothing will be inserted if the check list already contains an empty item at the given position. The new (or + * exiting) emtpy item will be focused and the keyboard will be opened. + * + * @param withCheckBox + * @param pos + */ + private void insertEmptyItem(boolean withCheckBox, int pos) + { + if (mCurrentValue.size() > pos && mCurrentValue.get(pos).text.length() == 0) + { + // there already is an empty item at this pos focus it and return + View view = mContainer.getChildAt(pos); + focusTitle(view); + return; + } + + // create a new empty item + DescriptionItem item = new DescriptionItem(withCheckBox, false, ""); + mCurrentValue.add(pos, item); + View newItem = createItemView(); + bindItemView(newItem, item); + + // append it to the list + mContainer.addDragView(newItem, newItem.findViewById(R.id.drag_handle), pos); + + focusTitle(newItem); + } + + + @Override + public void onClick(View v) + { + int id = v.getId(); + if (id == R.id.add_item) + { + insertEmptyItem(!mCurrentValue.isEmpty(), mCurrentValue.size()); + } + } + + + /** + * Focus the title element of the given view and open the keyboard if necessary. + * + * @param view + */ + private void focusTitle(View view) + { + View titleView = view.findViewById(android.R.id.title); + if (titleView != null) + { + titleView.requestFocus(); + mImm.showSoftInput(titleView, InputMethodManager.SHOW_IMPLICIT); + } + } + +} diff --git a/opentasks/src/main/res/drawable-anydpi/ic_delete_24px.xml b/opentasks/src/main/res/drawable-anydpi/ic_delete_24px.xml new file mode 100644 index 000000000..a13fe784e --- /dev/null +++ b/opentasks/src/main/res/drawable-anydpi/ic_delete_24px.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/opentasks/src/main/res/drawable-anydpi/ic_drag_indicator_24px.xml b/opentasks/src/main/res/drawable-anydpi/ic_drag_indicator_24px.xml new file mode 100644 index 000000000..fc92b408c --- /dev/null +++ b/opentasks/src/main/res/drawable-anydpi/ic_drag_indicator_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/opentasks/src/main/res/drawable-anydpi/ic_list_24px.xml b/opentasks/src/main/res/drawable-anydpi/ic_list_24px.xml new file mode 100644 index 000000000..d3ef126c0 --- /dev/null +++ b/opentasks/src/main/res/drawable-anydpi/ic_list_24px.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/opentasks/src/main/res/drawable-anydpi/ic_remove_24px.xml b/opentasks/src/main/res/drawable-anydpi/ic_remove_24px.xml new file mode 100644 index 000000000..07aa64f7d --- /dev/null +++ b/opentasks/src/main/res/drawable-anydpi/ic_remove_24px.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/opentasks/src/main/res/drawable-anydpi/ic_text_24px.xml b/opentasks/src/main/res/drawable-anydpi/ic_text_24px.xml new file mode 100644 index 000000000..11ceb0e3b --- /dev/null +++ b/opentasks/src/main/res/drawable-anydpi/ic_text_24px.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/opentasks/src/main/res/drawable-hdpi/ic_delete_24px.png b/opentasks/src/main/res/drawable-hdpi/ic_delete_24px.png new file mode 100644 index 000000000..98eb9589d Binary files /dev/null and b/opentasks/src/main/res/drawable-hdpi/ic_delete_24px.png differ diff --git a/opentasks/src/main/res/drawable-hdpi/ic_24_drag_handle_black50.png b/opentasks/src/main/res/drawable-hdpi/ic_drag_indicator_24px.png similarity index 100% rename from opentasks/src/main/res/drawable-hdpi/ic_24_drag_handle_black50.png rename to opentasks/src/main/res/drawable-hdpi/ic_drag_indicator_24px.png diff --git a/opentasks/src/main/res/drawable-hdpi/ic_list_24px.png b/opentasks/src/main/res/drawable-hdpi/ic_list_24px.png new file mode 100644 index 000000000..a3052d5bb Binary files /dev/null and b/opentasks/src/main/res/drawable-hdpi/ic_list_24px.png differ diff --git a/opentasks/src/main/res/drawable-hdpi/ic_remove_24px.png b/opentasks/src/main/res/drawable-hdpi/ic_remove_24px.png new file mode 100644 index 000000000..611b5be6c Binary files /dev/null and b/opentasks/src/main/res/drawable-hdpi/ic_remove_24px.png differ diff --git a/opentasks/src/main/res/drawable-hdpi/ic_text_24px.png b/opentasks/src/main/res/drawable-hdpi/ic_text_24px.png new file mode 100644 index 000000000..a14439882 Binary files /dev/null and b/opentasks/src/main/res/drawable-hdpi/ic_text_24px.png differ diff --git a/opentasks/src/main/res/drawable-ldpi/ic_24_drag_handle_black50.png b/opentasks/src/main/res/drawable-ldpi/ic_drag_indicator_24px.png similarity index 100% rename from opentasks/src/main/res/drawable-ldpi/ic_24_drag_handle_black50.png rename to opentasks/src/main/res/drawable-ldpi/ic_drag_indicator_24px.png diff --git a/opentasks/src/main/res/drawable-mdpi/ic_delete_24px.png b/opentasks/src/main/res/drawable-mdpi/ic_delete_24px.png new file mode 100644 index 000000000..0782f081a Binary files /dev/null and b/opentasks/src/main/res/drawable-mdpi/ic_delete_24px.png differ diff --git a/opentasks/src/main/res/drawable-mdpi/ic_24_drag_handle_black50.png b/opentasks/src/main/res/drawable-mdpi/ic_drag_indicator_24px.png similarity index 100% rename from opentasks/src/main/res/drawable-mdpi/ic_24_drag_handle_black50.png rename to opentasks/src/main/res/drawable-mdpi/ic_drag_indicator_24px.png diff --git a/opentasks/src/main/res/drawable-mdpi/ic_list_24px.png b/opentasks/src/main/res/drawable-mdpi/ic_list_24px.png new file mode 100644 index 000000000..8b5c28de2 Binary files /dev/null and b/opentasks/src/main/res/drawable-mdpi/ic_list_24px.png differ diff --git a/opentasks/src/main/res/drawable-mdpi/ic_remove_24px.png b/opentasks/src/main/res/drawable-mdpi/ic_remove_24px.png new file mode 100644 index 000000000..5a5cde9d7 Binary files /dev/null and b/opentasks/src/main/res/drawable-mdpi/ic_remove_24px.png differ diff --git a/opentasks/src/main/res/drawable-mdpi/ic_text_24px.png b/opentasks/src/main/res/drawable-mdpi/ic_text_24px.png new file mode 100644 index 000000000..acbcb9fe3 Binary files /dev/null and b/opentasks/src/main/res/drawable-mdpi/ic_text_24px.png differ diff --git a/opentasks/src/main/res/drawable-xhdpi/ic_delete_24px.png b/opentasks/src/main/res/drawable-xhdpi/ic_delete_24px.png new file mode 100644 index 000000000..c7bc393fd Binary files /dev/null and b/opentasks/src/main/res/drawable-xhdpi/ic_delete_24px.png differ diff --git a/opentasks/src/main/res/drawable-xhdpi/ic_24_drag_handle_black50.png b/opentasks/src/main/res/drawable-xhdpi/ic_drag_indicator_24px.png similarity index 100% rename from opentasks/src/main/res/drawable-xhdpi/ic_24_drag_handle_black50.png rename to opentasks/src/main/res/drawable-xhdpi/ic_drag_indicator_24px.png diff --git a/opentasks/src/main/res/drawable-xhdpi/ic_list_24px.png b/opentasks/src/main/res/drawable-xhdpi/ic_list_24px.png new file mode 100644 index 000000000..1f727dfe4 Binary files /dev/null and b/opentasks/src/main/res/drawable-xhdpi/ic_list_24px.png differ diff --git a/opentasks/src/main/res/drawable-xhdpi/ic_remove_24px.png b/opentasks/src/main/res/drawable-xhdpi/ic_remove_24px.png new file mode 100644 index 000000000..ebefccae8 Binary files /dev/null and b/opentasks/src/main/res/drawable-xhdpi/ic_remove_24px.png differ diff --git a/opentasks/src/main/res/drawable-xhdpi/ic_text_24px.png b/opentasks/src/main/res/drawable-xhdpi/ic_text_24px.png new file mode 100644 index 000000000..37f9ab5e4 Binary files /dev/null and b/opentasks/src/main/res/drawable-xhdpi/ic_text_24px.png differ diff --git a/opentasks/src/main/res/drawable-xxhdpi/ic_delete_24px.png b/opentasks/src/main/res/drawable-xxhdpi/ic_delete_24px.png new file mode 100644 index 000000000..0fbc42f81 Binary files /dev/null and b/opentasks/src/main/res/drawable-xxhdpi/ic_delete_24px.png differ diff --git a/opentasks/src/main/res/drawable-xxhdpi/ic_24_drag_handle_black50.png b/opentasks/src/main/res/drawable-xxhdpi/ic_drag_indicator_24px.png similarity index 100% rename from opentasks/src/main/res/drawable-xxhdpi/ic_24_drag_handle_black50.png rename to opentasks/src/main/res/drawable-xxhdpi/ic_drag_indicator_24px.png diff --git a/opentasks/src/main/res/drawable-xxhdpi/ic_list_24px.png b/opentasks/src/main/res/drawable-xxhdpi/ic_list_24px.png new file mode 100644 index 000000000..27c26fbe7 Binary files /dev/null and b/opentasks/src/main/res/drawable-xxhdpi/ic_list_24px.png differ diff --git a/opentasks/src/main/res/drawable-xxhdpi/ic_remove_24px.png b/opentasks/src/main/res/drawable-xxhdpi/ic_remove_24px.png new file mode 100644 index 000000000..d05ba7990 Binary files /dev/null and b/opentasks/src/main/res/drawable-xxhdpi/ic_remove_24px.png differ diff --git a/opentasks/src/main/res/drawable-xxhdpi/ic_text_24px.png b/opentasks/src/main/res/drawable-xxhdpi/ic_text_24px.png new file mode 100644 index 000000000..d7dfc149d Binary files /dev/null and b/opentasks/src/main/res/drawable-xxhdpi/ic_text_24px.png differ diff --git a/opentasks/src/main/res/drawable-xxxhdpi/ic_24_drag_handle_black50.png b/opentasks/src/main/res/drawable-xxxhdpi/ic_drag_indicator_24px.png similarity index 100% rename from opentasks/src/main/res/drawable-xxxhdpi/ic_24_drag_handle_black50.png rename to opentasks/src/main/res/drawable-xxxhdpi/ic_drag_indicator_24px.png diff --git a/opentasks/src/main/res/layout/checklist_field_view_element.xml b/opentasks/src/main/res/layout/checklist_field_view_element.xml index abcfa4e68..247d83802 100644 --- a/opentasks/src/main/res/layout/checklist_field_view_element.xml +++ b/opentasks/src/main/res/layout/checklist_field_view_element.xml @@ -24,7 +24,7 @@ android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:padding="8dp" - android:src="@drawable/ic_24_drag_handle_black50"/> + android:src="@drawable/ic_drag_indicator_24px"/> + + + + + + + + + + + + \ No newline at end of file diff --git a/opentasks/src/main/res/layout/description_field_view.xml b/opentasks/src/main/res/layout/description_field_view.xml new file mode 100644 index 000000000..ba49edf9e --- /dev/null +++ b/opentasks/src/main/res/layout/description_field_view.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/opentasks/src/main/res/layout/description_field_view_element.xml b/opentasks/src/main/res/layout/description_field_view_element.xml new file mode 100644 index 000000000..8d8290dbb --- /dev/null +++ b/opentasks/src/main/res/layout/description_field_view_element.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/opentasks/src/main/res/layout/editor_header.xml b/opentasks/src/main/res/layout/editor_header.xml index 9e4bd8b66..1b418552e 100644 --- a/opentasks/src/main/res/layout/editor_header.xml +++ b/opentasks/src/main/res/layout/editor_header.xml @@ -6,7 +6,6 @@ style="@style/editor_label_text" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginBottom="-8dip" android:ellipsize="marquee" android:fadingEdge="horizontal" android:paddingLeft="4dip" diff --git a/opentasks/src/main/res/values-de/strings.xml b/opentasks/src/main/res/values-de/strings.xml index e6f1b7dd3..b25781076 100644 --- a/opentasks/src/main/res/values-de/strings.xml +++ b/opentasks/src/main/res/values-de/strings.xml @@ -276,5 +276,8 @@ Angeheftete Aufgaben Beginn und Fälligkeiten + Löschen + Text + Checkliste diff --git a/opentasks/src/main/res/values/strings.xml b/opentasks/src/main/res/values/strings.xml index 4821a1043..1a4090ca3 100644 --- a/opentasks/src/main/res/values/strings.xml +++ b/opentasks/src/main/res/values/strings.xml @@ -43,6 +43,16 @@ Send Send to + + Text + + + Checklist + + + Delete + + Add item