mpcjanssen / simpletask-android

GNU General Public License v3.0
547 stars 128 forks source link

Implement topydo-style dependencies #362

Open inducer opened 8 years ago

inducer commented 8 years ago

https://github.com/bram85/topydo/wiki/dep-%28subcommand%29

mpcjanssen commented 8 years ago

Implementing do should be do-able (no pun intended). Including ls would be more difficult. I don't know how this should look in a Android UI. I am open to suggestions.

inducer commented 8 years ago

Actually, I like the existing UI just fine. This was supposed about dep:4 and id:4 tags. (Further down the page)

mpcjanssen commented 8 years ago

What would the dep id functionality do? Complete the deps if you complete the id? And complete the id if all the deps are closed? Could you describe your wished workflow in Simpletask by an example?

inducer commented 8 years ago

Mainly, hide (or provide a criterion to hide, by the sort criteria) tasks with unmet dependencies.

E.g.

get room for meeting id:123
email meeting  notice dep:123
mpcjanssen commented 8 years ago

I still don't understand exactly. You would like to see parent tasks and dependent tasks where the dependent task is not complete?

So

abcd id:1
x efgh dep:1
ijkl dep:1

will show as

abcd id:1
ijkl dep:1
inducer commented 8 years ago

No, actually the other way around. I'd only like to see abcd. Once that's done, I'd like to start seeing the tasks that depend on that.

mpcjanssen commented 8 years ago

Ah I see so you only want to see tasks which have completed parents (so which are active). Interesting idea. What I can consider is:

Filtering

  1. Extend the lua functionality so you can access the full task list and build it yourself
  2. Or, add filter functionality to show active tasks where a task is active if all dep: tasks are completed or if there are no dep: tasks

UI support

  1. Add a button to give a unique id:
  2. Add a button to insert a dep: where you can chose from all tasks with an id:
mpcjanssen commented 8 years ago

Hmm the active task definition is probably:

A task is active if all it's child tasks are completed. So a task with id:x is active if all dep:x tasks are completed.

inducer commented 8 years ago

I'd phrase it as follows: If a task has a dep:something then it is active if and only if there isn't another task active with id:something. A task without dep:something is the same as before.

Regarding UI support: I'd think you'll typically forget to add IDs to tasks that you'll want to involve in dependcies, so I wouldn't filter to tasks with IDs. Instead add them when needed.

Next, I've found it's common to want to add depencies both ways. Topydo lets you say: before:NN (where NN is one of its ephemeral IDs) and after:NN, and it'll set up id: and dep: for you. This feels "right" in a sense.

bram85 commented 8 years ago

For the record, topydo uses the p tag instead of dep to designate its parent item.

In @inducer's example, get room will be hidden by default in topydo, because email meeting notice has not yet been completed. You cannot complete the first item before you finish the second. topydo will only suggest to complete the child items when you request to complete a parent item (not vice versa).

topydo has quite some logic to handle these id and p tags, internally implemented as a directed graph. This can be used to keep the set of id and p tags clean. Again, in the above example, if you complete email meeting notice, then the id tag will be removed from get room to clean things up (and reuse that number for another dependency in the future).

How to represent this in a GUI? At first I'd say to hide parent items as @inducer suggested. Second, and an idea I have in mind for topydo, is to show child items with indent w.r.t. to their parent item (kind of a tree view).

I'm not sure on how to set dependencies between items. Drag and drop? Choose parent from a list? You don't want the users to write the id and p tags themselves, that's something that dep and do take care of. Toodledo allows items to be dragged to their to-be parent item.

mpcjanssen commented 8 years ago

So summarizing:

UI

Some additional questions:

bram85 commented 8 years ago

Can there be more than one level of parent-child? Or in other words can a task have an id and a p?

Yes, this is possible. You risk cyclic dependencies; in that case, topydo doesn't hide parents when one of their own descendant items depend on them.

What happens when completing a recurring parent task, will the children also be recreated?

No, it doesn't. When completing a parent task, topydo is unaware of any child tasks that already have been completed (because id and p are cleaned up quite aggressively). Maybe it could work if parent and children have exactly the same rec pattern, in that case it could leave the id and p tags alone so the next occurrences are still related. But I fear that could become messy.

mpcjanssen commented 8 years ago

@bram85

topydo is unaware of any child tasks that already have been completed (because id and p are cleaned up quite aggressively).

Does this mean any id: or p: tags are cleaned on completing?

bram85 commented 8 years ago

Yes, and on deletion. But only if archiving to a done.txt file is enabled (which is the default).

It's not really a requirement, but it reduces the noise of dependency tags in the items.

smichel17 commented 8 years ago

Related: #122

Ekleog commented 6 years ago

This would be great to have IMO. My reading of #349 is that all that's missing to implement this as a LUA filter is a getTaskList() LUA function that would return the list of all tasks?

mpcjanssen commented 6 years ago

@Ekleog correct. The challenge with doing this is to make it in such a way that it performs well. Filtering needs to be fast to prevent hanging of the application.

Ekleog commented 6 years ago

I've never done any LUA yet, but https://github.com/kikito/memoize.lua appears to mention it's possible to have memoized functions in LUA. With this, I would implement it the following way, if I had a getTaskList() (pseudo-code, again I've never done any LUA yet):

local function getAllRemainingIds_unmemoized()
  local ids = emptyHashSet()
  for i = getTaskList() do
    if isNotCompleted(i) and hasIdTag(i) then
      addToHashSet(ids, getIdTag(i))
    end
  end
  return ids
end

local getAllRemainingIds() = memoize(getAllRemainingIds_unmemoized)

local function onFilter(task)
  if hasPTag(task) then
    return not isInHashSet(getAllRemainingIds(), getPTag(task))
  else
    return true
  end
end

This algorithm is in O(n) average for the first task filtering (filling up getAllRemainingIds), and in O(1) average for all subsequent task filtering.

This means that it is, on average, O(1) per task. It doesn't handle cleanly circular dependencies, but… meh, if you've got circular dependencies in your tasks you've got bigger problems anyway.

That said, there may be issues of consistency if the memoized cache isn't cleared between two distinct filtering operations… in this case, maybe it'd be better to have a more powerful doFilter filter, that would take as input the list of all tasks, and would return as output the list of tasks to output?