Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 8ddf52d3 authored by Marten Gajda's avatar Marten Gajda Committed by GitHub
Browse files

Render Checkboxes using more common Markdown style, implements #875 (#901)

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.
parent 57ab48bf
Loading
Loading
Loading
Loading
+5 −8
Original line number Diff line number Diff line
@@ -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 class DefaultModel extends Model
                .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));
+35 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+8 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import org.dmfs.tasks.model.adapters.BooleanFieldAdapter;
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.AdjustPercentComplete;
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.
     */
+2 −11
Original line number Diff line number Diff line
@@ -24,7 +24,6 @@ import android.content.pm.PackageManager;
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.builder.AbstractObjectBuilder;
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 @@ public class XmlModel extends Model
                            {
                                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);
@@ -511,10 +504,8 @@ public class XmlModel extends Model
        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));
+248 −0
Original line number Diff line number Diff line
/*
 * 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