Skip to content

Commit

Permalink
Render Checkboxes using more common Markdown style, implements #875
Browse files Browse the repository at this point in the history
In addition this changes the check list UI. Instead of splitting the description into
a description and check list part, we treat each paragraph either as text paragraph or check list item. Checkable items and text items can be converted into each other.
  • Loading branch information
dmfs committed Mar 20, 2020
1 parent edd77d9 commit 0090f9b
Show file tree
Hide file tree
Showing 41 changed files with 1,142 additions and 21 deletions.
13 changes: 5 additions & 8 deletions opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand Down
35 changes: 35 additions & 0 deletions opentasks/src/main/java/org/dmfs/tasks/model/DescriptionItem.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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.
*/
Expand Down
13 changes: 2 additions & 11 deletions opentasks/src/main/java/org/dmfs/tasks/model/XmlModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<DataKind>) childDescriptor, datakind);
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <marten@dmfs.org>
*/
public final class DescriptionFieldAdapter extends FieldAdapter<List<DescriptionItem>>
{
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<DescriptionItem> 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<DescriptionItem> defaultValue)
{
if (fieldName == null)
{
throw new IllegalArgumentException("fieldName must not be null");
}
mFieldName = fieldName;
mDefaultValue = defaultValue;
}


@Override
public List<DescriptionItem> get(ContentSet values)
{
// return the check list
return parseDescription(values.getAsString(mFieldName));
}


@Override
public List<DescriptionItem> 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<DescriptionItem> getDefault(ContentSet values)
{
return mDefaultValue;
}


@Override
public void set(ContentSet values, List<DescriptionItem> 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<DescriptionItem> 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<DescriptionItem> parseDescription(String description)
{
List<DescriptionItem> result = new ArrayList<DescriptionItem>(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<DescriptionItem> 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);
}
}

}
Loading

0 comments on commit 0090f9b

Please sign in to comment.