← Back to team overview

zim-wiki team mailing list archive

Re: Tasklist update

 

Hi Dimitrij,

1. Yes, I do see your point. I also think that the Tag autocompletion is not directly related to the task list. I think it's a matter of if, how and when the plugins become official part of ZimWiki. Jaap asked me to combine these together to the task list plugin. I think it's easier for him to managed them.

2. []

3. You are welcome, and thank you for your feedback my (virtual) friend :)

4. It's a pitty that I can't find the reason why the preferred key binding is not working. If anyone has an idea why?

5. Could it be that I implemented this just for you? Unfortunately I can't find this implementation. Could you send the autocompletion.py file to me so I can review it please?

6. I reviewed other autocompletion implementations (e.g. Workflowy or Visual Studio). Both do not support cycling through the list with the tab key. Do you have another example where it does?

7. Yes, the bullet in front of the comment does not look right. But with the comment line being a bullet, it's easier to parse for a comment. I tried to change the parsing but came to the conclusion that it's better to have > or >> as a new bullet format (as you describe in 10.) I will look into it if I can do this. But this will be a change within the pageview / widget / format modules. Therefore the task list plugin / comment plugin would not run properly without the modified modules.

8. I added a selection for the formatting to the preferences. Please see attached and let me know what you think.

9. I added some extra options into the preferences so you can strip down the comment string just to ">" if you prefer. Please have a look. This might give some weird results for standalone bullet text starting with ">", but shouldn't be an issue. Background: I'm looking for the comment string to identify a comment.

10. See 9. I will also have a look for the "quote" or "comment" format, but no promises

Thank you and Regards,
Murat



On 24.03.2017 20:42, Dimitrij Lisov wrote:
Dear Murat,

1. I see, that your described way is working too. From a user experience point of view I nevertheless propose to separate those plugins, since it's not obvious for a new Zim user, that such an useful feature, read autocompletion of tags, even exists as a plugin, if it will be remain "hidden" in the tasklist plugin. I'm convinced, that there are a lot of Zim users, who are using tags, but not the tasklist plugin. They certainly won't discover the autocompletion plugin, when it won't be listed in the "official" Zim plugins list <https://github.com/jaap-karssenberg/zim-wiki/wiki/Plugins> as a separate plugin. Do you understand my apprehension? Additionally, with a separate plugin, the plugin's options would also be decluttered.

2. That's a pity, but thanks for trying! Maybe another one can contribute on that?

3. It works great in the new version, thanks for that my (virtual) friend!

4. Yes, I have changed the key-bindings, see https://bugs.launchpad.net/zim/+bug/1322201/comments/11. When I reverse the change to the default state, I'm able to type an @ in the search field again.

5. In the new version of your tasklist plugin, autocompleting with "space" isn't possible anymore. Please restore the old behavior, like it was possible in the latest autocompletion plugin. This is for me the most time saving feature of your whole plugin :)

6. I think, that it's already enough to have two ways, with "enter" and "space", to autocomple tags. Introducing "tab" could also be useful for somebody, who is used to use "tab", but then I propose that "tab" should exactly behave like "enter" - it should complete the tag only with one press, and not two, like it's implemented now. I personally nearly always use "space" for autocompletion, since it completes the tag and also adds a space after it, while enter only completes the tag, without a space at the end.

I've played around with the updated *tasklist plugin* and have the following to share:

7. From an aesthetical point of view, the bullet point could be removed in the comment line. The indent already emphasizes the comment line clearly enough, in addition to the comment string itself.

8. The superscript of the date looks, imho, a little bit out of place, because the following comment is written in normal text. Please consider adding an option to have the date in normal text.

9. I also would appreciate an option to entirely remove the date in the comment, like you've already implemented with the time. This would again declutter the comment line and therefore make it more appealing to the eye.

10. Is it intended to have an ">" before the comment text, like shown here <http://i.imgur.com/LFrTDDm.png>? The comment line would, imho, look much cleaner the following way: comment (2017.03.24): what if the number of all minds in the universe is one?
or
comment: what if the number of all minds in the universe is one?

It would be even better, if Zim finally gets an "quote" or "comment" format, that normally starts with >, see here <https://guides.github.com/features/mastering-markdown/>, to better highlight comments. Maybe this could be implemented during Zim's upgrade to GTK3 and the like. Maybe Jaap is reading those lines and can give a short comment on that.

I'm delighted to see Zim constantly evolving and want to say again a warm-hearted "Thank you" to all contributers and of course to Jaap himself!

D

Murat Güven <muratg@xxxxxxxxx <mailto:muratg@xxxxxxxxx>> schrieb am Fr., 24. März 2017 um 17:38 Uhr:

    Dear Dimitrij,

    thank you for your feedback.

    1. Thank you for using the tag autocompletion plugin. I merged
    this into the tasklist plugin, but you won't really see a
    difference as you can use autocompletion without
    starting the tasklist. You just need to activate the tasklist
    plugin within the plugin preferences. Please make sure that the
    autocompletion plugin is deactivated, as
    the same keyboard shortcut is being used. Does this satisfy you?

    2. I reviewed my code and tried to find a way to cycle through the
    list of tags with the tab key, but unfortunately the completion
    method does not support this out of the box.
    There might be a way to handle this, but this is currently out of
    my knowledge. But you can use the tab key instead of the return
    key as well with the new version now.

    3. Yes, this is an excellent idea. I implemented this into the new
    version. I hope it's ok that you can just double click a tag which
    you want to replace ;)

    4. I'm not sure if you have changed the keyboard bindings? I chose
    ALT + q for the plugin activation due to the issues I had with ALT
    GR + @. Please review your bindings and get
    back to me.

    Regards,
    Murat



    On 22.03.2017 22:19, Dimitrij Lisov wrote:
    Dear Murat,

    thank you for your great work!

    I have some proposals regarding the tag autocompletion plugin:

    1. Please make it a standalone plugin again. The autocompletion
    of tags is also very useful without the tasklist plugin. I tag a
    lot of my pages and the autocompletion makes this process very
    convenient. I'm sure, that most Zim users share my point of view.

    2. In addition to "arrow up and down" respectively, also "tab"
    should cycle through the suggested tags in the autocompletion
    pop-up. This just feels more intuitive. The "tab" key is also
    used in other programs to complete a suggestion or cycle through
    them.

    3. When a whole word is marked (f.i. by double-clicking on it)
    while opening the autocompletion pop-up, the selected tag doesn't
    replace the initial marked word, but is placed before the word.
    It would make more sense, when the initial word would be
    completely replaced by the tag.

    4. Found the following bug: After invoking the search (Ctrl+F)
    it's not possible to write an "@" there, when the autocompletion
    plugin is enabled.

    Wishing you all the best!

    Murat Güven <muratg@xxxxxxxxx <mailto:muratg@xxxxxxxxx>> schrieb
    am Di., 21. März 2017 um 13:06 Uhr:

        Dear zim community,

        please join me in improving the task list plugin. I have
        implemented some additional functionality,
        which you may find useful.

        What is new?

          * The comments which are added to a task (via the task
            comment plugin) are now collected together with the tasks
            and are displayed in an additional column.
          * The tags within a task are now displayed in a separate column
          * You can now tick a task directly within the dialog. As
            the core task ticking functionality of zim is used for
            this, zim switches to the page of the task to be ticked.
            Due to the sorting of the displayed tasks, ticked tasks
            are sorted at the end of the list before they are removed
            after index refresh (within seconds)
          * The dialog is now split into 2 task trees. The second
            tree displays the history of ticked tasks
          * When a task is ticked within the dialog, the tick date is
            stored within the index. This tick date is also displayed
            within the task history tree.
            In addition, I updated the pageview module. With this
            update the tick date is added / removed directly to /
            from the task within your note.
            This date is also collected from the task list plugin and
            is displayed within the task history tree.
          * The print functionality now displays the open tasks with
            more information as well.

        I also included / updated following plugins into the task
        list plugin:

          * Tag auto completion (Shortcut: ALT + q)
            --> Supports you in adding tags by suggesting existing tags
            --> Removed unwanted frame around the tag entry (within
            Windows)
          * Task due date (Shortcut: ALT + period)
            --> Supports you in adding a due date to a tag (either
            plain within the text, or with additional entries or as a
            calendar)
          * Task comment (Shortcut: CTRL + SHIFT + >)
            --> The task can now be commented (together with the date
            / time of the comment)
            --> The task comments string is added in superstring
            format, so it does not stand out so much

        You should deactive / delete the separate plugins if you
        already use them. I will maintain them only within the task
        list plugin.

        *Direct links:*
        https://launchpadlibrarian.net/311490871/pageview.py

        https://launchpadlibrarian.net/311658520/tasklist.py

        *Link to the online plugins:*

        https://github.com/jaap-karssenberg/zim-wiki/wiki/Tasklist-plugin-with-tag-autocompletion

        Feedback welcome!

        Regards,
        Murat



        _______________________________________________
        Mailing list: https://launchpad.net/~zim-wiki
        <https://launchpad.net/%7Ezim-wiki>
        Post to     : zim-wiki@xxxxxxxxxxxxxxxxxxx
        <mailto:zim-wiki@xxxxxxxxxxxxxxxxxxx>
        Unsubscribe : https://launchpad.net/~zim-wiki
        <https://launchpad.net/%7Ezim-wiki>
        More help   : https://help.launchpad.net/ListHelp



# -*- coding: utf-8 -*-

# Copyright 2009-2017 Jaap Karssenberg <jaap.karssenberg@xxxxxxxxx>

# Additions by Murat Guven <muratg@xxxxxxxxx>
# V1.99 Taskcomment: Added option to add / remove date, added all styles for the comment string and the comment
#		Added Suffix for Task comment to allow stripping down a task comment prefix to just '>'
# V1.98 Preferences: Corrected text in preferences
# V1.97 Tasklist: re-arranged elements within the dialog to fit for low resolution screens
# V1.96 Tasklist: fixed index issue when tasklist was activated before updating the plugin by increasing SQL_FORMAT_VERSION to 0.61
# V1.95 Taskcomment: code cleaning and fix for comment in strong / emphasis
# V1.94 Tasklist: label comments are also collected
# V1.93 Autocompletion: If text is selected after activation of AC, the selected text is replaced. If selected text
# was itself a tag and the @ sign wasn't selected with, it remains after inserting the new selected tag
# V1.92 Ticking issue while column resizing solved, Pane positions are saved again
# V1.91 Filtering works now with comments as well
# V1.9  Added "tick / untick all tasks". Reworked tags list: Added tags from ticked tasks to tag list
# V1.84 Fixed [no date] entry for children
# V1.83 Fixed wrong view in Task list history when parent task is open and child task is ticked.
# V1.82 Tag list rearranged in vbox tasklist dialog
# V1.81 using Zim widgets for autocompletion, duedate. Removed grey border from autocompletion entry (only visible in Windows)
# V1.8 added history of ticked tasks with ticked date (together with pageview)
# V1.74 little bug fix in comment dates format taken from dates.list
# V1.73 little bug fix in print function
# V1.72 added time for task comment
# V1.71 bug fixed with string truncating in comment
# V1.7 added possiblity to tick tasks
# V1.6 merged autocompletion for tags
# V1.5 merged with due date plugin
# V1.4 merged with task comment plugin
# V1.3 added comment column reading comment bullets under tasks + added tags column to dialog
# V1.21 added standard tag to look for on start
# V1.2 added new entry with autocompletion for tags

from __future__ import with_statement

import gtk
import pango
import logging
import re
import gtk.gdk
import sqlite3

import zim.datetimetz as zim_datetime
from datetime import datetime as dtime
import datetime
import time

from zim.utils import natural_sorted
from zim.parsing import parse_date
from zim.plugins import PluginClass, extends, ObjectExtension, WindowExtension
from zim.actions import action
from zim.notebook import Path
from zim.gui.widgets import ui_environment, \
	Dialog, MessageDialog, QuestionDialog, \
	InputEntry, Button, IconButton, MenuButton, \
	BrowserTreeView, SingleClickTreeView, Window, ScrolledWindow, HPaned, VPaned, \
	encode_markup_text, decode_markup_text
from zim.gui.clipboard import Clipboard
from zim.signals import DelayedCallback, SIGNAL_AFTER
from zim.formats import get_format, \
	UNCHECKED_BOX, CHECKED_BOX, XCHECKED_BOX, BULLET, BLOCK, \
	PARAGRAPH, NUMBEREDLIST, BULLETLIST, LISTITEM, STRIKE, \
	Visitor, VisitorSkip
from zim.config import StringAllowEmpty, ConfigManager

from zim.plugins.calendar import daterange_from_path


logger = logging.getLogger('zim.plugins.tasklist')

KEYVALS_AT = map (gtk.gdk.keyval_from_name, ('at'))
KEYVALS_ESC = map (gtk.gdk.keyval_from_name, ('Escape'))
KEYSTATES = gtk.gdk.CONTROL_MASK | gtk.gdk.MOD2_MASK
ALTQ = '<alt>q'
ALTat = '<alt>at'

SUPER = _('Superscript')
SUB = _('Subsript')
BOLD = _('Bold')
ITALIC = _('Italic')
NOF = _('No Format')


SQL_FORMAT_VERSION = (0, 61)
SQL_FORMAT_VERSION_STRING = "0.61"

# task column is needed for the pageview module to compare when a task is ticked
SQL_CREATE_TABLES = '''
create table if not exists tasklist (
	id INTEGER PRIMARY KEY,
	source INTEGER,
	parent INTEGER,
	haschildren BOOLEAN,
	open BOOLEAN,
	actionable BOOLEAN,
	prio INTEGER,
	due TEXT,
	tags TEXT,
	description TEXT,
	comment TEXT,
	tickmark BOOLEAN,
	tickdate TEXT,
	task TEXT
);
create table if not exists tasktickdate (
	id INTEGER PRIMARY KEY,
	tickmark BOOLEAN,
	tickdate TEXT,
	task TEXT
);
'''


_tag_re = re.compile(r'(?<!\S)@(\w+)\b', re.U)
_date_re = re.compile(r'\s*\[d:(.+)\]')
_tdate_re = re.compile(r'\s*\[x:(.+)\]')



_NO_DATE = '9999' # Constant for empty due date - value chosen for sorting properties
_NO_TAGS = '__no_tags__' # Constant that serves as the "no tags" tag - _must_ be lower case

# FUTURE: add an interface for this plugin in the WWW frontend

# TODO allow more complex queries for filter, in particular (NOT tag AND tag)
# See function filter_item for the AND implementation (all instead of any at 'if visible and self.tag_filter:'
# TODO: think about what "actionable" means
#       - no open dependencies
#       - no defer date in the future
#       - no child item ?? -- hide in flat list ?
#       - no @waiting ?? -> use defer date for this use case


# TODO
# commandline option
# - open dialog
# - output to stdout with configurable format
# - force update, intialization


class TaskListPlugin(PluginClass):

	plugin_info = {
		'name': _('Task List'), # T: plugin name
		'description': _('''\
This plugin adds a dialog showing all open tasks in
this notebook. Open tasks can be either open checkboxes
or items marked with tags like "TODO" or "FIXME".

Additions by Murat Güven:

Auto completion for tags:
	When you press the @ key within a note, a list of available tags are shown and can be selected.
	Currently the activation is set to <ALT> + Q, as the Windows version does not work properly with <ALT-Gr> + Q.

Due date:
	To easily add or update a due date to a task in this format "[d:date]" via keyboard shortcut <ctrl> + period

	Options (see configuration):
	 - add x number of days to current date
	 - show due date in entry for quicker date selection (+ press c for calendar)
	 - show calendar for selecting due date

	The due date format can be either maintained within the dates.list file
	(used by the date function <ctrl>D) or changed in Preferences.
	Standard is [d: %Y-%m-%d]

Task comment:
	To easily add a comment below a task via keyboard shortcut  <ctrl> + <shift> + >

	Either configure your format in preferences or re-use the date format within the dates.list file.
	Standard is [comment: %Y-%m-%d]
	
	The task comments are shown within the task list.

Ticking tasks:
	Tasks can be ticked / unticked within the tasklist window
	History of ticked tasks with ticked date are shown
	(with update of core pageview, ticked dates are handled outside of tasklist too)

[ChangeLog]
# V1.98 Preferences: Corrected text in preferences
# V1.97 Tasklist: re-arranged elements within the dialog to fit for low resolution screens
# V1.96 Tasklist: fixed index issue when tasklist was activated before updating the plugin by increasing SQL_FORMAT_VERSION to 0.61
# V1.95 Taskcomment: code cleaning and fix for comment in strong / emphasis
# V1.94 Tasklist: label comments are also collected
# V1.93 Autocompletion: If text is selected after activation of AC, the selected text is replaced. If selected text
# was itself a tag and the @ sign wasn't selected with, it remains after inserting the new selected tag
# V1.92 Ticking issue while column resizing solved, Pane positions are saved again
# V1.91 Filtering works now with comments as well
# V1.9  Added "tick / untick all tasks". Reworked tags list: Added tags from ticked tasks to tag list
# V1.84 Fixed [no date] entry for children
# V1.83 Fixed wrong view in Task list history when parent task is open and child task is ticked.
# V1.82 Tag list rearranged in vbox tasklist dialog
# V1.81 using Zim widgets for autocompletion, duedate. Removed grey border from autocompletion entry (only visible in Windows)
# V1.8 added history of ticked tasks with ticked date (together with pageview)
# V1.7 added possiblity to tick tasks
# V1.6 merged with tag autocompletion plugin
# V1.5 merged with due date plugin
# V1.4 merged with task comment plugin
# V1.3 added column for task comments + added column for tags to dialog + Print
# V1.21 added standard tag to look for on start
# V1.2 added new entry with autocompletion for tags

This is a core plugin shipping with zim.
'''), # T: plugin description
		'author': 'Jaap Karssenberg',
		'help': 'Plugins:Task List'
	}

	plugin_preferences = (
		# key, type, label, default
		('all_checkboxes', 'bool', _('Consider all checkboxes as tasks'), True),
			# T: label for plugin preferences dialog
		('tag_by_page', 'bool', _('Turn page name into tags for task items'), False),
			# T: label for plugin preferences dialog
		('deadline_by_page', 'bool', _('Implicit due date for task items in calendar pages'), False),
			# T: label for plugin preferences dialog
		('use_workweek', 'bool', _('Flag tasks due on Monday or Tuesday before the weekend'), True),
		('show_history', 'bool', _('Show history of ticked tasks'), True),
			# T: label for plugin preferences dialog
		('labels', 'string', _('Labels marking tasks'), 'FIXME, TODO', StringAllowEmpty),
			# T: label for plugin preferences dialog - labels are e.g. "FIXME", "TODO", "TASKS"
		('next_label', 'string', _('Label for next task'), 'Next:', StringAllowEmpty),
			# T: label for plugin preferences dialog - label is by default "Next"
		('nonactionable_tags', 'string', _('Tags for non-actionable tasks'), '', StringAllowEmpty),
			# T: label for plugin preferences dialog
		('included_subtrees', 'string', _('Subtree(s) to index'), '', StringAllowEmpty),
			# T: subtree to search for tasks - default is the whole tree (empty string means everything)
		('excluded_subtrees', 'string', _('Subtree(s) to ignore'), '', StringAllowEmpty),
			# T: subtrees of the included subtrees to *not* search for tasks - default is none
		('standard_tag', 'string', _('Set tag to be searched for on start (without @)'), '', StringAllowEmpty),
			# T: subtrees of the included subtrees to *not* search for tasks - default is none
		('task_comment_string', 'string', _('Name the string for task comments'), 'comment'),
		('task_comment_string_style', 'choice', _('Style for the task comment string'), SUPER, (SUPER, SUB, BOLD, ITALIC, NOF)),
		('task_comment_date', 'bool', _('Add date to task comment'), True),
		('task_comment_date_format', 'string', _('Format for the task comment date string'), '%Y-%m-%d', StringAllowEmpty),
		('task_comment_dateslist', 'bool', _('Use due-date format in dates.list file for the date string'), False),
		('task_comment_time', 'bool', _('Add time to task comment date'), False),
		('task_comment_time_format', 'string', _('Format for the time string'), '%H:%M', StringAllowEmpty),
		('task_comment_style', 'choice', _('Task comment style'), NOF, (NOF, BOLD, ITALIC, SUPER, SUB)),
		('task_comment_suff', 'string', _('Suffix for the task comment'), '>', StringAllowEmpty),
		('due_date_plus', 'int', _('Due date: Add days to today [d:today + days]'), 0, (0, 365)),
		('due_date_entry', 'bool', _('Due date: Show entry popup'), False),
		('due_date_cal', 'bool', _('Due date: Show calendar popup'), False),
		('completion_non_modal', 'bool', _('Due date: Disable lock of main window, if back focus does not work'), False),
		('single_match', 'bool', _('Tag auto completion: Single match shall not be shown in a popup'), False),
	)
	_rebuild_on_preferences = ['all_checkboxes', 'labels', 'next_label', 'deadline_by_page', 'nonactionable_tags',
				   'included_subtrees', 'excluded_subtrees' ]
		# Rebuild database table if any of these preferences changed.
		# But leave it alone if others change.

	def extend(self, obj):
		name = obj.__class__.__name__
		if name == 'MainWindow':
			index = obj.ui.notebook.index # XXX
			i_ext = self.get_extension(IndexExtension, index=index)
			mw_ext = MainWindowExtension(self, obj, i_ext)
			self.extensions.add(mw_ext)
		else:
			PluginClass.extend(self, obj)


@extends('Index')
class IndexExtension(ObjectExtension):

	# define signals we want to use - (closure type, return type and arg types)
	__signals__ = {
		'tasklist-changed': (None, None, ()),
	}

	def __init__(self, plugin, index):
		ObjectExtension.__init__(self, plugin, index)
		self.plugin = plugin
		self.index = index

		self.preferences = plugin.preferences

		self.task_labels = None
		self.task_label_re = None
		self.next_label = None
		self.next_label_re = None
		self.nonactionable_tags = []
		self.included_re = None
		self.excluded_re = None
		self.db_initialized = False
		self._current_preferences = None

		db_version = self.index.properties['plugin_tasklist_format']
		if db_version == '%i.%i' % SQL_FORMAT_VERSION:
			self.db_initialized = True

		self._set_preferences()
		self.connectto(plugin.preferences, 'changed', self.on_preferences_changed)

		self.connectto_all(self.index, (
			('initialize-db', self.initialize_db, None, SIGNAL_AFTER),
			('page-indexed', self.index_page),
			('page-deleted', self.remove_page),
		))
		# We don't care about pages that are moved

	def on_preferences_changed(self, preferences):
		if self._current_preferences is None \
		or not self.db_initialized:
			return

		new_preferences = self._serialize_rebuild_on_preferences()
		if new_preferences != self._current_preferences:
			self._drop_table()
		self._set_preferences()  # Sets _current_preferences

	def _set_preferences(self):
		self._current_preferences = self._serialize_rebuild_on_preferences()

		string = self.preferences['labels'].strip(' ,')
		if string:
			self.task_labels = [s.strip() for s in self.preferences['labels'].split(',')]

		else:
			self.task_labels = []

		if self.preferences['next_label']:
			self.next_label = self.preferences['next_label']
				# Adding this avoid the need for things like "TODO: Next: do this next"
			self.next_label_re = re.compile(r'^' + re.escape(self.next_label) + r':?\s+' )
			self.task_labels.append(self.next_label)
		else:
			self.next_label = None
			self.next_label_re = None

		if self.preferences['nonactionable_tags']:
			self.nonactionable_tags = [
				t.strip('@').lower()
					for t in self.preferences['nonactionable_tags'].replace(',', ' ').strip().split()]
		else:
			self.nonactionable_tags = []

		if self.task_labels:
			regex = r'^(' + '|'.join(map(re.escape, self.task_labels)) + r')(?!\w)'
			self.task_label_re = re.compile(regex)
		else:
			self.task_label_re = None

		if self.preferences['included_subtrees']:
			included = [i.strip().strip(':') for i in self.preferences['included_subtrees'].split(',')]
			included.sort(key=lambda s: len(s), reverse=True) # longest first
			included_re = '^(' + '|'.join(map(re.escape, included)) + ')(:.+)?$'
			#~ print '>>>>>', "included_re", repr(included_re)
			self.included_re = re.compile(included_re)
		else:
			self.included_re = None

		if self.preferences['excluded_subtrees']:
			excluded = [i.strip().strip(':') for i in self.preferences['excluded_subtrees'].split(',')]
			excluded.sort(key=lambda s: len(s), reverse=True) # longest first
			excluded_re = '^(' + '|'.join(map(re.escape, excluded)) + ')(:.+)?$'
			#~ print '>>>>>', "excluded_re", repr(excluded_re)
			self.excluded_re = re.compile(excluded_re)
		else:
			self.excluded_re = None

	def _serialize_rebuild_on_preferences(self):
		# string mapping settings that influence building the table
		string = ''
		for pref in self.plugin._rebuild_on_preferences:
			string += str(self.preferences[pref])
		return string

	def initialize_db(self, index):
		with index.db_commit:
			index.db.executescript(SQL_CREATE_TABLES)
		self.index.properties['plugin_tasklist_format'] = '%i.%i' % SQL_FORMAT_VERSION
		self.db_initialized = True

	def teardown(self):
		self._drop_table()

	def _drop_table(self):
		self.index.properties['plugin_tasklist_format'] = 0

		try:
			self.index.db.execute('DROP TABLE "tasklist"')
			self.index.db.execute('DROP TABLE "tasktickdate"')
		except:
			if self.db_initialized:
				logger.exception('Could not drop table:')

		self.db_initialized = False

	def _excluded(self, path):
		if self.included_re and self.excluded_re:
			# judge which match is more specific
			# this allows including subnamespace of excluded namespace
			# and vice versa
			inc_match = self.included_re.match(path.name)
			exc_match = self.excluded_re.match(path.name)
			if not exc_match:
				return not bool(inc_match)
			elif not inc_match:
				return bool(exc_match)
			else:
				return len(inc_match.group(1)) < len(exc_match.group(1))
		elif self.included_re:
			return not bool(self.included_re.match(path.name))
		elif self.excluded_re:
			return bool(self.excluded_re.match(path.name))
		else:
			return False

	def index_page(self, index, path, page):
		if not self.db_initialized: return
		#~ print '>>>>>', path, page, page.hascontent

		tasksfound = self.remove_page(index, path, _emit=False)
		if self._excluded(path):
			if tasksfound:
				self.emit('tasklist-changed')
			return

		parsetree = page.get_parsetree()
		if not parsetree:
			return

		#~ print '!! Checking for tasks in', path
		dates = daterange_from_path(path)
		if dates and self.preferences['deadline_by_page']:
			deadline = dates[2]
		else:
			deadline = None
		
		tasks = self._extract_tasks(parsetree, deadline)
		
		if tasks:
			# Do insert with a single commit
			with self.index.db_commit:
				self._insert(path, 0, tasks)

		if tasks or tasksfound:
			self.emit('tasklist-changed')

	def _insert(self, page, parentid, children):
		# Helper function to insert tasks in table
		c = self.index.db.cursor()
		for task, grandchildren in children:
			task[4] = ','.join(sorted(task[4])) # set to text
			task[6] = ''.join(sorted(task[6], reverse=True)) # sort comments against date and set to text
			
			c.execute(
				'insert into tasklist(source, parent, haschildren, open, actionable, prio, due, tags, description, comment, tickmark, tickdate, task)'
				'values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
				(page.id, parentid, bool(grandchildren)) + tuple(task)
			)
			if grandchildren:
				self._insert(page, c.lastrowid, grandchildren) # recurs


	def _extract_tasks(self, parsetree, defaultdate=None):
		'''Extract all tasks from a parsetree.
		@param parsetree: a L{zim.formats.ParseTree} object
		@param defaultdate: default due date for the whole page (e.g. for calendar pages) as string
		@returns: nested list of tasks, each task is given as a 2-tuple, 1st item is a tuple
		with following properties: C{(open, actionable, prio, due, description)}, 2nd item
		is a list of child tasks (if any).
		'''

		parser = TasksParser(
			self.task_label_re,
			self.next_label_re,
			self.nonactionable_tags,
			self.preferences['all_checkboxes'],
			defaultdate,
			self.preferences
		)
		parser.parse(parsetree)
		return parser.get_tasks()

	def remove_page(self, index, path, _emit=True):
		if not self.db_initialized: return

		tasksfound = False
		with index.db_commit:
			cursor = index.db.cursor()
			cursor.execute(
				'delete from tasklist where source=?', (path.id,) )
			tasksfound = cursor.rowcount > 0

		if tasksfound and _emit:
			self.emit('tasklist-changed')

		return tasksfound

	def list_tasks(self, parent=None):
		'''List tasks
		@param parent: the parent task (as returned by this method) or C{None} to list
		all top level tasks
		@returns: a list of tasks at this level as sqlite Row objects
		'''
		if parent: parentid = parent['id']
		else: parentid = 0

		if self.db_initialized:
			cursor = self.index.db.cursor()
			cursor.execute('select * from tasklist where parent=? order by prio, due, description', (parentid,))
				# Want order by prio & due - add desc to keep sorting more or less stable
			for row in cursor:
				yield row

	def get_task(self, taskid):
		cursor = self.index.db.cursor()
		cursor.execute('select * from tasklist where id=?', (taskid,))
		return cursor.fetchone()

	def get_path(self, task):
		'''Get the L{Path} for the source of a task
		@param task: the task (as returned by L{list_tasks()}
		@returns: an L{IndexPath} object
		'''
		return self.index.lookup_id(task['source'])

	def put_new_tickdate_to_db(self, task, tickmark=True):
		date = zim_datetime.now()
		cursor = self.index.db.cursor()
		cursor.execute('UPDATE tasktickdate set tickdate=? WHERE task=?', (date, unicode(task)))
		if not cursor.rowcount:
			cursor.execute('INSERT into tasktickdate(tickmark, tickdate, task) values (?, ?, ?)', (tickmark, unicode(date), unicode(task)))

	def put_existing_tickdate_to_db(self, task, date, tickmark=True):
		cursor = self.index.db.cursor()
		cursor.execute('UPDATE tasktickdate set tickdate=? WHERE task=?', (date, unicode(task)))
		if not cursor.rowcount:
			cursor.execute('INSERT into tasktickdate(tickmark, tickdate, task) values (?, ?, ?)', (tickmark, unicode(date), unicode(task)))

	def get_tickdate_from_db(self, task):
		cursor = self.index.db.cursor()
		cursor.execute('SELECT * FROM tasktickdate WHERE task=?', (task,))
		return cursor.fetchone()

	def del_tickdate_from_db(self, task):
		cursor = self.index.db.cursor()
		cursor.execute('DELETE FROM tasktickdate WHERE task=?', (unicode(task),))



@extends('MainWindow')
class MainWindowExtension(WindowExtension):

	uimanager_xml = '''
		<ui>
			<menubar name='menubar'>
				<menu action='view_menu'>
					<placeholder name="plugin_items">
						<menuitem action="show_task_list" />
					</placeholder>
				</menu>
				<menu action='tools_menu'>
					<placeholder name='plugin_items'>
						<menuitem action='task_comment'/>
					</placeholder>
					<placeholder name='plugin_items'>
						<menuitem action='due_date'/>
					</placeholder>
					<placeholder name='plugin_items'>
						<menuitem action='auto_completion'/>
					</placeholder>
				</menu>
			</menubar>
			<toolbar name='toolbar'>
				<placeholder name='tools'>
					<toolitem action='show_task_list'/>
				</placeholder>
			</toolbar>
		</ui>
	'''

	def __init__(self, plugin, window, index_ext):
		WindowExtension.__init__(self, plugin, window)
		self.index_ext = index_ext
	@action(_('Task List'), stock='zim-task-list', readonly=True) # T: menu item
	def show_task_list(self):
		if not self.index_ext.db_initialized:
			MessageDialog(self.window, (
				_('Need to index the notebook'),
				# T: Short message text on first time use of task list plugin
				_('This is the first time the task list is opened.\n'
				  'Therefore the index needs to be rebuild.\n'
				  'Depending on the size of the notebook this can\n'
				  'take up to several minutes. Next time you use the\n'
				  'task list this will not be needed again.' )
				# T: Long message text on first time use of task list plugin
			) ).run()
			logger.info('Tasklist not initialized, need to rebuild index')
			finished = self.window.ui.reload_index(flush=True) # XXX
			# Flush + Reload will also initialize task list
			if not finished:
				self.index_ext.db_initialized = False
				return
			
		dialog = TaskListDialog.unique(self, self.window, self.index_ext, self.plugin.preferences)
		dialog.present()

	#~ accel_key = 'at'
	ac_accel_key = ALTQ

	@action(_('Task Comment'), accelerator='<ctrl>greater')  # T: menu item
	def task_comment(self):
		tc = TaskComment(self.window, self.plugin.preferences)
		tc.add()

	@action(_('_Due Date'), accelerator='<ctrl>period')  # T: menu item
	def due_date(self):
		dd = DueDate(self.window, self.plugin.preferences)
		dd.add()

	@action(_('_Auto Completion'), accelerator=ac_accel_key)  # T: menu item
	def auto_completion(self):
		ac = AutoCompletion(self.window, self.plugin.preferences)
		ac.show()


class AutoCompletion():

	def __init__(self, window, plugin_preferences):

		self.plugin_prefs = plugin_preferences
		self.window = window
		self.zim_buffer = self.window.pageview.view.get_buffer()
		self.zim_index = self.window.ui.notebook.index
		self.main_window = self.window.pageview.view

	def show(self):
		# get selection if any before it is removed by the entry completion selection
		try:
			self.start, self.end = self.zim_buffer.get_selection_bounds()
			self.text_is_selected = True
		except:
			self.text_is_selected = False
		
		self.completion_window = Window()
		self.entry = InputEntry()
		# No frame around entry
		self.entry.set_inner_border(None)
		self.completion_window.add(self.entry)

		# remove grey border from completion window around entry
		self.completion_window.set_geometry_hints(self.entry, max_height=1)		

		#to prevent that main window is used during autocompletion
		if not self.plugin_prefs['completion_non_modal']:
			self.completion_window.set_modal(True)
		self.completion_window.set_keep_above(True)

		self.cursor = self.zim_buffer.get_iter_at_mark(self.zim_buffer.get_insert())
		x, y, height = self.ac_get_iter_pos(self.main_window, self.cursor)
		self.completion_window.move(x, y)

		self.entry_completion = gtk.EntryCompletion()
		# this brings the hit from the list into the entry box so
		# it can be selected hitting RETURN
		self.entry_completion.set_inline_completion(True)

		# add tags from zim index to the completion list
		tag_liststore = gtk.ListStore(str)
		self.ac_set_tag_liststore(tag_liststore)

		# if there is only one hit, don't show popup
		if self.plugin_prefs['single_match']:
			self.entry_completion.set_popup_single_match(False)

		self.entry.set_completion(self.entry_completion)
		self.entry_completion.set_text_column(0)

		# just the plane entry box
		self.completion_window.set_decorated(False)

		# as lowercase entries are not matched after the 3rd character somehow with the standard completion
		# entry_completion.set_match_func(self.new_completion_match_func, 0)		# not working yet

		self.completion_window.show_all()

		# if list element is selected via mouse klick or cursor + return
		self.entry_completion.connect('match-selected', self.ac_match_selected)

		# when return key is pressed
		self.entry.connect('activate', self.ac_activate_return)

		#listen if ESC key is pressed for the entry completion
		self.entry.add_events(gtk.gdk.KEY_PRESS_MASK)
		self.entry.connect('key_press_event', self.ac_kpe_entry)

	# not working yet. Want to use this as the entry completion is not really working with lower case
	# def new_completion_match_func(completion, entry_str, iter, data):
	# 	self.column = data
	# 	self.model = completion.get_model()
	# 	self.modelstr = self.model[iter][self.column]
	# 	return self.modelstr.upper()(entry_str.upper())

	def ac_set_tag_liststore(self, tag_liststore):
		self.zim_tags = self.zim_index.list_all_tags()
		tag_liststore.clear()
		for self.tag in self.zim_tags:
			tag_liststore.append([self.tag.name])
		self.entry_completion.set_model(tag_liststore)

	def ac_kpe_entry(self, widget, event):
		if event.keyval:
			if event.keyval == gtk.gdk.keyval_from_name('Escape'):
				self.completion_window.destroy()
			if event.keyval == gtk.gdk.keyval_from_name('Tab'):
				self.entry.activate()

	def ac_match_selected(self, completion, model, iter):
		if self.text_is_selected:
			self.zim_buffer.delete(self.start, self.end)
			if  self.char_is_at_before_selection():
				self.tag = model[iter][0]
		else:
			self.tag = "@" + model[iter][0]

		self.completion_window.destroy()

		#get the position of the cursor from the parent window/textbuffer
		cursor = self.zim_buffer.get_iter_at_mark(self.zim_buffer.get_insert())
		self.zim_buffer.insert(cursor, self.tag)


	def ac_activate_return(self, widget):
		#get the selected tag string
		self.tag = self.entry.get_text()
		# if not return was hit in an empty box without text
		if self.tag:		
			if self.text_is_selected:
				self.zim_buffer.delete(self.start, self.end)
				if not self.char_is_at_before_selection():
					self.tag = "@" + self.tag
			else:
				self.tag = "@" + self.tag

			#get the position of the cursor from the parent window/textbuffer
			cursor = self.zim_buffer.get_iter_at_mark(self.zim_buffer.get_insert())
			#add tag string into textbuffer
			self.zim_buffer.insert(cursor, self.tag)
			#close autocompletion window

		#self.tag = ''
		self.completion_window.destroy()


	def char_is_at_before_selection(self):
		cursor_orig = self.zim_buffer.get_iter_at_mark(self.zim_buffer.get_insert())

		cursor_orig.backward_chars(1)
		cursor = self.zim_buffer.get_iter_at_mark(self.zim_buffer.get_insert())
		self.zim_buffer.place_cursor(cursor)

		start = self.zim_buffer.get_iter_at_mark(self.zim_buffer.get_insert())
		end = cursor_orig
		char = self.zim_buffer.get_text(start, end)

		if char == "@":
			return True
		return False


	def ac_get_iter_pos(self, textview, cursor):
		self.top_x, self.top_y = textview.get_toplevel().get_position()
		self.iter_location = textview.get_iter_location(cursor)
		self.mark_x, self.mark_y = self.iter_location.x, self.iter_location.y + self.iter_location.height
		#calculate buffer-coordinates to coordinates within the window
		self.win_location = textview.buffer_to_window_coords(gtk.TEXT_WINDOW_WIDGET,
														 int(self.mark_x), int(self.mark_y))
		#now find the right window --> Editor Window and the right pos on screen
		self.win = textview.get_window(gtk.TEXT_WINDOW_WIDGET);
		self.view_pos = self.win.get_position()
		self.xx = self.win_location[0] + self.view_pos[0]
		self.yy = self.win_location[1] + self.view_pos[1] + self.iter_location.height
		self.x = self.top_x + self.xx
		self.y = self.top_y + self.yy
		return (self.x, self.y, self.iter_location.height)


class DueDate():

	def __init__(self, window, plugin_preferences):
		self.plugin_prefs = plugin_preferences
		self.window = window


	def add(self):


		#self.cal_window = gtk.Window()
		self.cal_window = Window()
		self.cal_window.set_decorated(False)
		self.cal_window.set_modal(True)
		self.cal_window.set_keep_above(True)

		self.cal = gtk.Calendar()
		self.cal.display_options(gtk.CALENDAR_SHOW_WEEK_NUMBERS | gtk.CALENDAR_SHOW_HEADING | gtk.CALENDAR_SHOW_DAY_NAMES)
		self.cal_window.add(self.cal)

		#self.entry_window = gtk.Window()
		self.entry_window = Window()
		self.entry_window.set_decorated(False)
		self.entry_window.set_modal(True)
		self.entry_window.set_keep_above(True)

		self.vbox = gtk.VBox()
		self.label_day_name = gtk.Label()
		
		self.entry_hbox = gtk.HBox()
		#self.entry_year = gtk.Entry(4)
		self.entry_year = InputEntry()
		self.entry_year.set_max_length(4)
		self.entry_year.set_width_chars(4)
		#self.entry_month = gtk.Entry(2)
		self.entry_month = InputEntry()
		self.entry_month.set_max_length(2)
		self.entry_month.set_width_chars(2)
		#self.entry_day = gtk.Entry(2)
		self.entry_day = InputEntry()
		self.entry_day.set_max_length(2)
		self.entry_day.set_width_chars(2)
		#self.entry_week_no = gtk.Entry(2)
		self.entry_week_no = InputEntry()
		self.entry_week_no.set_max_length(2)
		self.entry_week_no.set_width_chars(2)
		self.entry_week_no.set_sensitive(False)

		self.label_due_date_plus = gtk.Label()
		self.label_due_date_plus.set_text('Today   +    ')
		#self.entry_due_date_plus = gtk.Entry(6)
		self.entry_due_date_plus = InputEntry()
		self.entry_due_date_plus.set_max_length(6)
		self.entry_due_date_plus.set_width_chars(6)
		self.due_date_plus_hbox = gtk.HBox()
		self.due_date_plus_hbox.pack_start(self.label_due_date_plus, expand = False, padding = 2)
		self.due_date_plus_hbox.pack_start(self.entry_due_date_plus, expand = True, padding = 1)

		self.due_date_plus = self.plugin_prefs['due_date_plus']

		self.vbox.pack_start(self.label_day_name, expand = False, padding=0)
		self.vbox.pack_start(self.entry_hbox, expand = False, padding=0)
		self.vbox.pack_start(self.due_date_plus_hbox, expand = False, padding = 2)

		'''
		[0: current_date_element, 1: prev_date_element]
		1: year, 2: month, 3: day, 4: due_date_plus, 5: week_no (not really used any more from history)
		'''
		# in fact I don't need 2 history levels any more, but I keep it for later use
		self.entry_history = {	1: [0, False],
								2: [0, False],
								3: [0, False],
								4: [0, False],
								5: [0, False]
								}

		def up_down_arrow_key(arrow_key_dict):
			# get the elements from the dict / list
			function = arrow_key_dict[0]
			entry = arrow_key_dict[1]
			ddplus_calc_value = arrow_key_dict[2]

			# call the right function from the list with its params
			function(ddplus_calc_value, entry)
			
			# in case someone enters a new day and then directly uses Up arrow key
			self.update_due_date_plus()							

			# put back focus to the correct entry as it's set to entry_due_date_plus in above function	
			entry.grab_focus()

		def left_right_arrow_key(arrow_key_dict):
			function = arrow_key_dict[0]
			entry = arrow_key_dict[1]
			
			entry.grab_focus()
			#select data in entry
			function(0 , -1)


		def cal_lrud_key(value, widget):
			self.due_date_plus += value

			new_date = self.get_date_with_ddplus(None, self.due_date_plus)

			widget.select_month(new_date.month-1, new_date.year)
			self.cal_select_day(new_date.day)
			pass

		
		'''
		this dict provides me a set of functions which I call according to the id of the key press event
		this way I avoid repeating code...
		'''
		self.entry_key_inputs = {	65362 : (up_down_arrow_key, {'day'		: [self.ce_kpe_day_and_ddplus, self.entry_day, 1],  # up arrow key
																 'month'	: [self.ce_kpe_month, self.entry_month, 1],
																 'year'		: [self.ce_kpe_year, self.entry_year, 1],		
																'ddplus'	: [self.ce_kpe_day_and_ddplus, self.entry_due_date_plus, 1]}),
									65364 : (up_down_arrow_key, {'day'		: [self.ce_kpe_day_and_ddplus, self.entry_day, -1], # down arrow key
																 'month'	: [self.ce_kpe_month, self.entry_month, -1],
																 'year'		: [self.ce_kpe_year, self.entry_year, -1],		
																 'ddplus'	: [self.ce_kpe_day_and_ddplus, self.entry_due_date_plus, -1]}),
									65361 : (left_right_arrow_key, {'day' :		[self.entry_month.select_region, self.entry_month],							# when event comes from entry_day, I need to go to month (left)
																	'month' :	[self.entry_year.select_region, self.entry_year],							# when event comes from entry_month, I need to go to year (left)
																	'year' :	[self.entry_due_date_plus.select_region, self.entry_due_date_plus],		# when event comes from entry_year, I need to go to ddplus (left)
																	'ddplus' :	[self.entry_day.select_region, self.entry_day]}),						# when event comes from entry_due_date_plus, I need to go to day (left)
									65363 : (left_right_arrow_key, {'day' :		[self.entry_due_date_plus.select_region, self.entry_due_date_plus],	# when event comes from entry_day, I need to go to entry_due_date_plus (right)
																	'ddplus' :	[self.entry_year.select_region, self.entry_year],		# when event comes from entry_due_date_plus, I need to go to entry_year (right)
																	'year' :	[self.entry_month.select_region, self.entry_month],		# when event comes from entry_year, I need to go to entry_month (right)
																	'month' :	[self.entry_day.select_region, self.entry_day]})			# when event comes from entry_month, I need to go to entry_day (right)
									}

		self.cal_key_inputs = {		65361 : (cal_lrud_key, -1),	# left arrow key
									65363 : (cal_lrud_key, +1),	# right arrow key
									65362 : (cal_lrud_key,  -7),  # up arrow key
									65364 : (cal_lrud_key, +7)	# down arrow key
								}

		
		# TODO: need to set focus chain manually to prevent focus on due_date_plus entry when arrow down is clicked
		# not quite satisfied with the current result though
		self.vbox.set_focus_chain([self.entry_due_date_plus, self.entry_day])		

		self.entry_hbox.pack_start(self.entry_year, expand=True, padding=1)
		self.entry_hbox.pack_start(self.entry_month, expand=True, padding=1)
		self.entry_hbox.pack_start(self.entry_day, expand=True, padding=1)
		self.entry_hbox.pack_start(self.entry_week_no, expand=True, padding=2)

		self.entry_window.add(self.vbox)

		self.buf = self.window.pageview.view.get_buffer()
		
		# needed to highlight due date
		self.text_tag = self.buf.create_tag(background = "grey")	
		
		self.page_view = self.window.pageview.view
		self.cursor = self.buf.get_iter_at_mark(self.buf.get_insert())

		self.due_date_begin_iter = ""
		self.due_date_end_iter = ""
		# preserve cursor position
		self.cursor_orig = self.buf.get_iter_at_mark(self.buf.get_insert())
		self.format = self.get_format()
		self.exist_due_date = None
		
		self.get_due_date_at_line(self.buf)
		

		if self.plugin_prefs['due_date_entry']:
			self.show_entries()
		elif self.plugin_prefs['due_date_cal']:
			self.show_cal()
		else:
			self.insert_due_date(self.exist_due_date)

	def show_entries(self):
		# put data into entries
		self.fill_entries(self.exist_due_date)

		# move entries to cursor position
		x, y, height = self.get_iter_pos()
		self.entry_window.move(x, y)

		self.entry_day.connect("key_press_event", self.ce_keypress, "day")
		self.entry_month.connect("key_press_event", self.ce_keypress, "month")
		self.entry_year.connect("key_press_event", self.ce_keypress, "year")
		self.entry_due_date_plus.connect("key_press_event", self.ce_keypress, "ddplus")

		self.entry_year.connect("activate", self.ce_activate_insert_due_date)	
		self.entry_month.connect("activate", self.ce_activate_insert_due_date)	
		self.entry_day.connect("activate", self.ce_activate_insert_due_date)
		self.entry_due_date_plus.connect("activate", self.ce_activate_insert_due_date)

		self.entry_year.connect("event", self.ce_event, self.entry_year)
		self.entry_month.connect("event", self.ce_event, self.entry_month)
		self.entry_day.connect("event", self.ce_event, self.entry_day)
		self.entry_due_date_plus.connect("event", self.ce_event, self.entry_due_date_plus)

		self.entry_window.show_all()

	def show_day_name(self, year, month, day):
		date = self.get_specific_date(year, month, day)
		day_name = date.strftime("%A")
		self.label_day_name.set_text(str(day_name))

	def show_cal(self):
		self.entry_window.destroy()						
		# TODO: it's more logical to show the calendar in addition, but needs some re-design, 
		# that if calender entry is selected with cursor, it should also run through the entry date_elements
		# only if date is clicked in calender, add selection to buffer
		due_date = self.get_date_with_ddplus()					
		
		# due_date.month-1 as calendar starts counting with 0 as January ....
		self.cal.select_month(due_date.month-1, due_date.year)		
		self.cal.select_day(due_date.day)

		x, y, height = self.get_iter_pos()
		# x-200 when above TODO is done
		self.cal_window.move(x, y)						
		self.cal_window.show_all()

		self.cal_mark_today()
		self.cal.connect("key_press_event", self.c_kpe_calendar, self.window)
		self.cal.connect("day-selected-double-click", self.ce_selected_cal_date)

	def show_date_element_data_in_entry(self, entry, date_element_data, history_index):
		'''
		data 0: current_date_element, 
		data 1: prev_date_element]
		index 1: year, 2: month, 3: day, 4: due_date_plus
		'''
		self.put_date_element_data_into_hist(date_element_data, history_index)
		entry.set_text(str(date_element_data))
		
	def show_all_date_element_data(self, year, month, day, ddplus, focus_entry=None):
		year, month, day, ddplus = self.validate_date_element_data(year, month, day, ddplus)
		# only validated data should be in history...
		self.put_all_date_element_data_into_hist(year, month, day, ddplus)

		week_no = self.get_week_no(year, month, day)
		self.show_day_name(year, month, day)

		# calculate the number of due date plus from current date to today
		self.calculate_due_date_plus_from_today(year, month, day)						

		self.show_date_element_data_in_entry(self.entry_due_date_plus, self.due_date_plus, 4)
		self.show_date_element_data_in_entry(self.entry_week_no, week_no, 5)

		self.show_date_element_data_in_entry(self.entry_year, year, 1)
		self.show_date_element_data_in_entry(self.entry_month, month, 2)
		self.show_date_element_data_in_entry(self.entry_day, day, 3)

		focus_entry.grab_focus()
		focus_entry.select_region(0 , -1)

	def get_week_no(self, year, month, day):
		date = self.get_specific_date(year, month, day)
		week_no = date.isocalendar()[1]
		return week_no

	def validate_date_element_data(self, year, month, day, ddplus):
	
		# make data int. By doing this in the beginning, I ensure that empty elements or chars are already handled. Only if 0 is entered, I need to check below	
		try:
			year = int(year)
		except ValueError:
			# If I ensure that only integer is put into history, I don't need to integer this again
			year = self.entry_history[1][0]				
		try:
			month = int(month)
		except ValueError:
			month = self.entry_history[2][0]
		try:
			day = int(day)
		except ValueError:
			day = self.entry_history[3][0]
		try:
			ddplus = int(ddplus)
		except ValueError:
			ddplus = self.entry_history[4][0]

		# identify, if data was passed. If None or empty, get history data
		if year == 0:
			year = self.entry_history[1][0]
		if month == 0:
			month = self.entry_history[2][0]
		if day == 0:
			day = self.entry_history[3][0]

		# now ensure that year, month and day is a valid number. datetime takes only year > 1900, month obviously <= 12
		if year < 1900:
			year = 1900

		if month > 12:
			month = 12

		last_day_of_month = self.get_last_day_of_month(year, month)

		if day > last_day_of_month:
			day = last_day_of_month

		return year, month, day, ddplus

	def put_all_date_element_data_into_hist(self, year, month, day, ddplus):
		'''
		The history is needed if user removes data from entry and then press enter or clicks to another entry
		data 0: current_date_element, 
		data 1: prev_date_element]
		index 1: year, 2: month, 3: day, 4: due_date_plus, 5: week_no not needed any more

		'''

		list_of_elements = [year, month, day, ddplus]

		for history_index in range(1-4):
			# put data into history and cycle old elements through all levels of the history dict
			# depth is the max number of history levels: -1 as 0 is the first level  -> depth = len -1 --> flexibility if I want to increase history depth per entry in the dict
			depth = len(self.entry_history[history_index]) -1				
			# start with level 0, so level 0 is the first entry in the history
			level = 0														
			# depth = 0 is filled after this loop with current date_element_data
			while level < depth:											
				# fill up the history from the bottom. So very last is put into one above
				self.entry_history[history_index][depth-level] = self.entry_history[history_index][depth-level-1]
				level += 1
			self.entry_history[history_index][0] = list_of_elements[history_index-1]

	def put_date_element_data_into_hist(self, date_element_data, history_index):
		'''
		The history is needed if user removes data from entry and then press enter or clicks to another entry
		data 0: current_date_element, 
		data 1: prev_date_element]
		index 1: year, 2: month, 3: day, 4: due_date_plus, 5: week_no not needed any more
		'''
		# put data into history and cycle old elements through all levels of the history dict
		# depth is the max number of history levels: -1 as 0 is the first level  -> depth = len -1 --> flexibility if I want to increase history depth per entry in the dict
		depth = len(self.entry_history[history_index]) -1				
		# start with level 0, so level 0 is the first entry in the history
		level = 0														
		# depth = 0 is filled after this loop with current date_element_data
		while level < depth:											
			# fill up the history from the bottom. So very last is put into one above
			self.entry_history[history_index][depth-level] = self.entry_history[history_index][depth-level-1]
			level += 1
		self.entry_history[history_index][0] = date_element_data

	def print_history(self):
		# to check if history is working correct
		for index in range(1, 5):
			for level in range(len(self.entry_history[index])):
				print 'Index: {0}, Level: {1}, data: , {2}'.format(index, level, self.entry_history[index][level])

	def print_data(self, method=None, data1=None, data2=None, data3=None):
		print 'method: {0}, data1: {1}, data2: {2}, data3: , {3}'.format(method, data1, data2, data3)

	def insert_due_date(self, due_date = None):
		# delete existing due date
		if self.due_date_begin_iter:
			self.del_due_date_at_line(self.buf)
		if not due_date:
			due_date = self.get_date_with_ddplus()
		# get the user's due date format
		# format = self.get_format() --> already in self.format
		# now put the due date into the format
		date_with_format = zim_datetime.strftime(self.format, due_date)
		self.buf.insert(self.cursor, date_with_format)	
		self.high_light_chars(2)
		return

	def calculate_due_date_plus_from_today(self, year, month, day):
		date = self.get_specific_date(year, month, day)
		date_today = zim_datetime.date.today()
		today = self.get_specific_date(date_today.year, date_today.month, date_today.day)
		date_diff = date - today
		self.due_date_plus = date_diff.days

	def ce_event(self, widget, event, entry):
		if event.type == gtk.gdk.BUTTON_RELEASE:
			# in case someone enters new data into the other entries and then directly clicks into due_date_plus entry
			self.update_due_date_plus()						
			entry.grab_focus()

	def update_due_date_plus(self):
		year, month, day, ddplus = self.get_all_date_element_data()
		self.calculate_due_date_plus_from_today(year, month, day)
		self.show_all_date_element_data(year, month, day, ddplus, focus_entry = self.entry_due_date_plus)

	def ce_activate_insert_due_date(self, widget):								# ce = connect entry...

		#add due date string into textbuffer at cursor
		year, month, day, ddplus = self.get_all_date_element_data()
		year_hist, month_hist, day_hist, ddplus_hist = self.get_all_date_element_data_from_hist()

		# if nothing has been changed manually, the date can be taken and inserted into buffer
		if year == year_hist and month == month_hist and day == day_hist and ddplus == ddplus_hist:
			due_date = self.put_date_to_format(year, month, day)
			if self.due_date_begin_iter:
				self.del_due_date_at_line(self.buf)
			self.buf.insert(self.cursor, due_date)
			self.entry_window.destroy()

		# If due date plus = 0 entered --> reset to today
		if ddplus == 0:									
			self.due_date_plus = 0
			self.fill_entries(zim_datetime.date.today())
			return

		# something was entered into due_date_plus entry
		if ddplus <> ddplus_hist:
			self.due_date_plus = ddplus
			self.due_date = self.get_date_with_ddplus(None, ddplus)		
			self.fill_entries(self.due_date)
		else:
			self.calculate_due_date_plus_from_today(year, month, day)
			self.show_all_date_element_data(year, month, day, ddplus, focus_entry = self.entry_due_date_plus)
		return															

	def ce_selected_cal_date(self, calendar):
		(year, month, day) = calendar.get_date()
		date = self.put_date_to_format(year, month+1, day)

		if self.due_date_begin_iter:
			self.del_due_date_at_line(self.buf)
		
		cursor = self.buf.get_iter_at_mark(self.buf.get_insert())
		self.buf.insert(cursor, date + " ")
		self.cal_window.destroy()

	def put_date_to_format(self, year, month, day):
		date = self.get_specific_date(year,month,day)
		#format = self.get_format()
		return date.strftime(self.format)

	def fill_entries(self, date = None):
		if not date:
			date = self.get_date_with_ddplus()
		self.show_all_date_element_data(date.year, date.month, date.day, self.due_date_plus, focus_entry = self.entry_day)


	def ce_keypress(self, widget, event, entry_type):
		''' 
		entry_type = day, month, year, ddplus
		'''
		try:
			# I get the function which I want to call and the list of arguments according to the keyval from self.inputs
			function_for_arrow_key, args_for_arrow_key_function = self.entry_key_inputs[event.keyval]
			# I call the necessary function with the list of arguments, using the entry type to identify the right element in the dict within the list of arguments
			function_for_arrow_key(args_for_arrow_key_function[entry_type])

		except (KeyError):
			# all other keys:'c' or ESC or Pos1
			self.ce_events_common_key(event.keyval)

	def ce_events_common_key(self, keyval):
		if keyval == gtk.gdk.keyval_from_name('Escape'):
			try:
				self.buf.remove_tag(self.text_tag, self.due_date_begin_iter[0], self.due_date_end_iter[1])
			except (ValueError, AttributeError, TypeError):
				dummy = 0

			self.buf.place_cursor(self.cursor_orig)

			self.entry_window.destroy()
		elif keyval == gtk.gdk.keyval_from_name('c'):
			self.show_cal()
		elif keyval == 65360:			#	Pos1 key
			# reset entries to today
			self.due_date_plus = 0
			self.fill_entries(zim_datetime.date.today())


	def ce_kpe_day_and_ddplus(self, value, entry):
		self.due_date_plus += value

		new_date = self.get_date_with_ddplus(None, self.due_date_plus)
		self.show_all_date_element_data(new_date.year, new_date.month, new_date.day, self.due_date_plus, focus_entry = entry)

	def ce_kpe_month(self, value, entry):
		year, month, day, due_date_plus = self.get_all_date_element_data()

		self.due_date_plus += value * self.get_last_day_of_month(year, month)

		new_date = self.get_date_with_ddplus(None, self.due_date_plus)
		self.show_all_date_element_data(new_date.year, new_date.month, new_date.day, self.due_date_plus, focus_entry = entry)

	def ce_kpe_year(self, value, entry):
		year, month, day, due_date_plus = self.get_all_date_element_data()
		day_of_year = self.get_yday(year, 12, 31)

		self.due_date_plus += value * day_of_year

		new_date = self.get_date_with_ddplus(None, self.due_date_plus)
		self.show_all_date_element_data(new_date.year, new_date.month, new_date.day, self.due_date_plus, focus_entry = entry)


	def cal_mark_today(self):
		# just in case, make a clear start
		self.cal.clear_marks()								
		(year, month, day) = self.cal.get_date()
		today = zim_datetime.date.today()
		# if calendar is displaying current month, mark today
		if year == today.year and month == today.month-1:						
			self.cal.mark_day(today.day)

		# c - connect
	def c_kpe_calendar(self, widget, event, window):	
		# just in case, make a clear start
		widget.clear_marks()								
		(year, month, day) = self.cal.get_date()
		today = zim_datetime.date.today()


		# if calendar is displaying current month, mark today
		if year == today.year and month == today.month-1:						
			self.cal.mark_day(today.day)

		if event.keyval == gtk.gdk.keyval_from_name('Escape'):
			self.cal_window.destroy()
		if event.keyval == gtk.gdk.keyval_from_name('Return'):
			self.ce_selected_cal_date(widget)
		if event.keyval == 65360:			#	Pos1 key
			# reset calendar to today
			self.due_date_plus = 0
			due_date = self.get_date_with_ddplus()									
			# -1 as calendar starts counting with 0 as January ....
			self.cal.select_month(due_date.month-1, due_date.year)		
			self.cal_select_day(due_date.day)

		## Picture up and down keys
		if event.keyval == 65365: # Picture up key
			next_month = month + 1
			if next_month == 12:
				next_month = 0
				year = year + 1
			widget.select_month(next_month, year)
			# or statement in order to take the last day of month for further months
			if (day > self.get_last_day_of_month(year, next_month+1)) or (day == self.get_last_day_of_month(year, month+1)):		
				day = self.get_last_day_of_month(year, next_month+1)
			self.cal_select_day(day)


		if event.keyval == 65366: # Picture down key
			prev_month = month - 1
			if prev_month == -1:
				prev_month = 11
				year = year - 1
			widget.select_month(prev_month, year)
			# +1 as calendar counts starting with 0
			if (day > self.get_last_day_of_month(year, prev_month+1)) or (day == self.get_last_day_of_month(year, month+1)):			
				day = self.get_last_day_of_month(year, prev_month+1)
			self.cal_select_day(day)


		try:
			# I get the function which I want to call and the list of arguments according to the keyval from self.cal_key_inputs
			function_for_arrow_key, args_for_arrow_key_function = self.cal_key_inputs[event.keyval]
			# I call the necessary function with the list of arguments
			function_for_arrow_key(args_for_arrow_key_function, widget)

		except (KeyError):
			pass


	def cal_select_day(self, selected_day):
		# just in case, make a clear start
		self.cal.clear_marks()								
		(year, month, day) = self.cal.get_date()
		today = zim_datetime.date.today()
		# if calendar is displaying current month, mark today
		if year == today.year and month == today.month-1:						
			self.cal.mark_day(today.day)
		self.cal.select_day(selected_day)

	def high_light_chars(self, chars_n):
		# now move 1 char back and place the cursor
		self.cursor.backward_chars(chars_n-1)
		self.buf.place_cursor(self.cursor)

		# now get the iter at cursor pos
		bound = self.buf.get_iter_at_mark(self.buf.get_insert())

		# now2 move 2 char back and place the cursor
		self.cursor.backward_chars(chars_n)
		self.buf.place_cursor(self.cursor)

		# now get the iter at cursor pos
		ins = self.buf.get_iter_at_mark(self.buf.get_insert())
		
		# now select 2 chars
		self.buf.select_range(ins, bound)

	def get_specific_date(self, year, month, day):
		try:
			date = zim_datetime.datetime(year, month, day)
		except ValueError:
			date = zim_datetime.datetime(1900, month, day)
		return date

	def get_date_with_ddplus(self, current_date=None, due_date_plus=None):
		if not current_date:
			current_date = zim_datetime.date.today()
		# get the current date + x days according to the days given in prefs or from entry due_date_plus
		if not due_date_plus:
			try:
				due_date = current_date + zim_datetime.timedelta(days=self.due_date_plus)
				if due_date.year < 1900:
					raise ValueError
			except ValueError:
				due_date = self.get_specific_date(1900, 1, 1)
				date_today = zim_datetime.date.today()
				today = self.get_specific_date(date_today.year, date_today.month, date_today.day)

				date_diff  = due_date - today
				self.due_date_plus = date_diff.days
		else:
			try: 
				due_date = current_date + zim_datetime.timedelta(days=due_date_plus)
				if due_date.year < 1900:
					raise ValueError
			except ValueError:
				due_date = self.get_specific_date(1900, 1, 1)
				date_today = zim_datetime.date.today()
				today = self.get_specific_date(date_today.year, date_today.month, date_today.day)

				date_diff  = due_date - today
				self.due_date_plus = date_diff.days
		return due_date

	def get_last_day_of_month(self, year, month):
		""" Work out the last day of the month """
		last_days = [31, 30, 29, 28, 27]
		for i in last_days:
				try:
						end = dtime(year, month, i)		
				except ValueError:
						continue
				else:
						return i
		return None


	def get_yday(self, year, month, day):
		date = self.get_specific_date(year,month,day)
		date_tt = date.timetuple()
		year_of_day = date_tt.tm_yday
		return year_of_day
		
	def get_all_date_element_data(self):
		year = self.entry_year.get_text()
		month = self.entry_month.get_text()
		day = self.entry_day.get_text()
		ddplus = self.entry_due_date_plus.get_text()
		
		year, month, day, ddplus = self.validate_date_element_data(year, month, day, ddplus)
		return year, month, day, ddplus

	def get_all_date_element_data_from_hist(self):
		year_hist = self.entry_history[1][0]
		month_hist = self.entry_history[2][0]
		day_hist = self.entry_history[3][0]
		ddplus_hist = self.entry_history[4][0]

		return year_hist, month_hist, day_hist, ddplus_hist

	def get_format(self):
		#get the dates.list config file
		file = ConfigManager().get_config_file('<profile>/dates.list')
		format = "[d: %Y-%m-%d]" # This is the given standard format
		# look for a due date format in dates.list file
		for line in file.readlines():
			line = line.strip()
			if not line.startswith("[d:"):
				continue
			if format in line:
				return format
			else:
				format = line
		return format

	def get_iter_pos(self):
		self.top_x, self.top_y = self.page_view.get_toplevel().get_position()
		self.iter_location = self.page_view.get_iter_location(self.cursor)
		self.mark_x, self.mark_y = self.iter_location.x, self.iter_location.y + self.iter_location.height
		#calculate buffer-coordinates to coordinates within the window
		self.win_location = self.page_view.buffer_to_window_coords(gtk.TEXT_WINDOW_WIDGET,
														 int(self.mark_x), int(self.mark_y))
		#now find the right window --> Editor Window and the right pos on screen
		self.win = self.page_view.get_window(gtk.TEXT_WINDOW_WIDGET);
		self.view_pos = self.win.get_position()
		self.xx = self.win_location[0] + self.view_pos[0]
		self.yy = self.win_location[1] + self.view_pos[1] + self.iter_location.height
		self.x = self.top_x + self.xx
		self.y = self.top_y + self.yy
		return (self.x, self.y, self.iter_location.height)

	def del_due_date_at_line(self, buffer):
		try:
			self.due_date_begin_iter, self.due_date_end_iter = self.get_due_date_at_line(buffer)
			buffer.delete(self.due_date_begin_iter, self.due_date_end_iter)
			buffer.place_cursor(self.due_date_begin_iter)
			self.cursor = self.buf.get_iter_at_mark(self.buf.get_insert())
		except TypeError:
			return

	def get_due_date_at_line(self, buffer):
		self.due_date_begin_iter = False
		self.due_date_end_iter = False

		# in order to get a due date entry no matter where the cursor was placed within the line,
		# move the cursor to the end of the line and look for [d:
		cursor = buffer.get_iter_at_mark(buffer.get_insert())

		# if there is nothing but a linefeed, just return, nothing to check
		if cursor.ends_line() and cursor.starts_line():
			return
		
		# this is a workaround to get the start of the line...first move to end of previous line()
		cursor.backward_line()
		# then move to next line(), which positions at the start of the line ...
		cursor.forward_line()
		# now place the cursor at start of line
		buffer.place_cursor(cursor)
		line_start = buffer.get_iter_at_mark(buffer.get_insert())

		# move to end of line and then start search backwards.
		cursor.forward_to_line_end()
		buffer.place_cursor(cursor)
			
		self.due_date_begin_iter = cursor.backward_search("[d:", gtk.TEXT_SEARCH_TEXT_ONLY, limit = line_start)
		if self.due_date_begin_iter:
			# this might lead to a selection of more than wanted if there is a ] somewhere after the due date string.
			# leaving it this way right now as this is not very likely to happen.
			self.due_date_end_iter = cursor.backward_search("]", gtk.TEXT_SEARCH_TEXT_ONLY, limit = line_start)

		try:
			buffer.apply_tag(self.text_tag, self.due_date_begin_iter[0], self.due_date_end_iter[1])
			exist_due_date_txt = buffer.get_slice(self.due_date_begin_iter[0], self.due_date_end_iter[1])
			self.exist_due_date =  self.put_txt_into_date(exist_due_date_txt)


		except (UnboundLocalError, AttributeError, ValueError, TypeError):
			buffer.place_cursor(self.cursor_orig)	
			return self.due_date_begin_iter, self.due_date_end_iter
		
		# put the cursor to the end of the existing due date entry, so the entry or calendar can be shown there
		self.cursor = self.due_date_end_iter[1]
		return self.due_date_begin_iter[0], self.due_date_end_iter[1]

	def put_txt_into_date(self, date_txt):
		plain_format = self.format.strip("[d: ").strip("]")
		plain_due_date = date_txt.strip("[d: ").strip("]")
		exist_due_date = datetime.datetime.strptime(plain_due_date, plain_format).date()
		return exist_due_date


class TaskComment():

	def __init__(self, window, plugin_preferences):
		self.plugin_prefs = plugin_preferences
		self.window = window


	def add(self):
		_style = {SUB : 'sub', SUPER : 'sup', NOF : None, BOLD : 'strong', ITALIC : 'emphasis'}
		current_date = zim_datetime.date.today()

		# get the user's task comment format
		format = self.get_format()
		self.task_comment_with_date = zim_datetime.strftime(format, current_date)

		# get the text buffer
		self.buffer = self.window.pageview.view.get_buffer()

		current_style = self.buffer.get_textstyle()

		line_number = self.buffer.get_insert_iter().get_line()
		indent_level = self.buffer.get_indent(line_number)
		new_line = line_number + 1

		# create new sub-bullet
		iter = self.buffer.get_iter_at_line(line_number)
		iter.forward_to_line_end()
		self.buffer.place_cursor(iter)								# works also if cursor is placed inside a tag
		self.buffer.insert_at_cursor("\n")	
		self.buffer.set_indent(new_line, indent_level + 1)
		
		# bullet * is used for now.
		# TODO: declare > or >> as new bullet
		self.buffer.set_bullet(new_line, '*') # set bullet type

		# place cursor in right spot
		iter = self.buffer.get_iter_at_line(new_line)
		#iter.forward_to_line_end()
		self.buffer.place_cursor(iter)

		# get the cursor position
		cursor = self.buffer.get_insert_iter()
		## add text at cursor position


		string_style = self.plugin_prefs['task_comment_string_style']
		comment_style = self.plugin_prefs['task_comment_style']

		self.buffer.set_textstyle(_style[string_style])
		self.buffer.insert(cursor, self.task_comment_with_date)
		self.buffer.set_textstyle(_style[comment_style])
		# to make the current style work...

		if self.plugin_prefs['task_comment_suff']:
			comment_suffix = self.plugin_prefs['task_comment_suff'] + " "
		else:
			comment_suffix = " "
		self.buffer.insert(cursor, comment_suffix)
		page = self.window.pageview.get_page()


	def get_format(self):
		format = self.set_format(date_string = self.plugin_prefs['task_comment_date_format']) # this is the format set in preferences
		if self.plugin_prefs['task_comment_date']:
			if self.plugin_prefs['task_comment_dateslist']:
				# get the dates.list config file
				file = ConfigManager().get_config_file('<profile>/dates.list')
				for line in file.readlines():
					line = line.strip()
					if not line.startswith("[d:"):
						continue
					# lazy approach to remove ] from the end of the date string
					line = re.sub(']', '',line)
					# and then strip [d: from format in dates.list and build format string set in preferences
					format = self.set_format(date_string = line.lstrip("[d: ").lstrip("]")) 
		return format

	def set_format(self, date_string):
		if self.plugin_prefs['task_comment_date']:
			date_string = "[" + date_string + "]"
			if self.plugin_prefs['task_comment_time']:
				if self.plugin_prefs['task_comment_time_format']:
					time_format = self.plugin_prefs['task_comment_time_format']
				else:
					time_format = '%H:%M'
				current_time = zim_datetime.datetime.now().strftime(time_format)
				date_string = date_string + "[" + current_time + "]"

			format = self.plugin_prefs['task_comment_string'] + " " + date_string
		else:
			format = self.plugin_prefs['task_comment_string']

		return format



class TasksParser(Visitor):
	'''Parse tasks from a parsetree'''

	def __init__(self, task_label_re, next_label_re, nonactionable_tags, all_checkboxes, defaultdate, preferences):
		self.task_label_re = task_label_re
		self.next_label_re = next_label_re
		self.nonactionable_tags = nonactionable_tags
		self.all_checkboxes = all_checkboxes

		defaults = (True, True, 0, defaultdate or _NO_DATE, set(), None)
			# (open, actionable, prio, due, tags, description)

		self._tasks = []
		self._tasks_comments = []
		self.preferences = preferences

		self._stack = [(-1, defaults, self._tasks)]
			# Stack for parsed tasks with tuples like (level, task, children)
			# We need to include the list level in the stack because we can
			# have mixed bullet lists with checkboxes, so task nesting is
			# not the same as list nesting

		# Parsing state
		self._text = [] # buffer with pieces of text
		self._depth = 0 # nesting depth for list items
		self._last_node = (None, None) # (tag, attrib) of last item seen by start()
		self._intasklist = False # True if we are in a tasklist with a header
		self._tasklist_tags = None # global tags from the tasklist header


	def parse(self, parsetree):
		#~ filter = TreeFilter(
			#~ TextCollectorFilter(self),
			#~ tags=['p', 'ul', 'ol', 'li'],
			#~ exclude=['strike']
		#~ )
		parsetree.visit(self)

	def get_tasks(self):
		'''Get the tasks that were collected by visiting the tree
		@returns: nested list of tasks, each task is given as a 2-tuple,
		1st item is a tuple with following properties:
		C{(open, actionable, prio, due, description)},
		2nd item is a list of child tasks (if any).
		'''
		return self._tasks


	def start(self, tag, attrib):
		if tag == STRIKE:
			raise VisitorSkip # skip this node
		elif tag in (PARAGRAPH, NUMBEREDLIST, BULLETLIST, LISTITEM):
			if tag == PARAGRAPH:
				self._intasklist = False

			# Parse previous chuck of text (para level text)
			if self._text:
				if tag in (NUMBEREDLIST, BULLETLIST) \
				and self._last_node[0] == PARAGRAPH \
				and self._check_para_start(self._text):
					pass
				else:
					self._parse_para_text(self._text)

				self._text = [] # flush

			# Update parser state
			if tag in (NUMBEREDLIST, BULLETLIST):
				self._depth += 1
			elif tag == LISTITEM:
				self._pop_stack() # see comment in end()
			self._last_node = (tag, attrib)
		else:
			pass # do nothing for other tags (we still get the text)

	def text(self, text):
		self._text.append(text)

	def end(self, tag):
		if tag == PARAGRAPH:
			if self._text:
				self._parse_para_text(self._text)
				self._text = [] # flush
			self._depth = 0
			self._pop_stack()
		elif tag in (NUMBEREDLIST, BULLETLIST):
			self._depth -= 1
			self._pop_stack()
		elif tag == LISTITEM:
			if self._text:
				attrib = self._last_node[1]
				self._parse_list_item(attrib, self._text)
				self._text = [] # flush
			# Don't pop here, next item may be child
			# Instead pop when next item opens

		# want to parse comments without a bullet, but seems to be the wrong way
		# to look for BLOCK text. Rather declare comment char >> or > as a new bullet
		#elif tag == BLOCK:
		#	if self._text:
		#		self._parse_para_text(self._text)
		#		self._parse_block_text(self._text)
		#		self._text = [] # flush
		else:
			pass # do nothing for other tags

	def _pop_stack(self):
		# Drop stack to current level
		assert self._depth >= 0, 'BUG: stack count corrupted'
		level = self._depth
		if level > 0:
			level -= 1 # first list level should be same as level of line items in para
		while self._stack[-1][0] >= level:
			self._stack.pop()

	def _check_para_start(self, strings):
		# Check first line for task list header
		# SHould look like "TODO @foo @bar:"
		# FIXME shouldn't we depend on tag elements in the tree ??
		line = u''.join(strings).strip()

		if not '\n' in line \
		and self._matches_label(line):
			words = line.strip(':').split()
			words.pop(0) # label
			if all(w.startswith('@') for w in words):
				self._intasklist = True
				self._tasklist_tags = set(w.strip('@') for w in words)
			else:
				self._intasklist = False
		else:
			self._intasklist = False

		return self._intasklist

	def _parse_para_text(self, strings):
		# Paragraph text to be parsed - just look for lines with label
		for line in u''.join(strings).splitlines():
			if self._matches_label(line):
				self._parse_task(line)

	def _parse_list_item(self, attrib, text):
		# List item to parse - check bullet, then match label
		bullet = attrib.get('bullet')
		line = u''.join(text)

		# get comment
		self._parse_comment(line)

		if (bullet in (UNCHECKED_BOX, CHECKED_BOX, XCHECKED_BOX)
			and (self._intasklist or self.all_checkboxes)):
			open = (bullet == UNCHECKED_BOX)
			# added bullet to params to identify if a task is ticked for the tick box
			self._parse_task(line, bullet, open=open)
		elif self._matches_label(line):
			self._parse_task(line)


	#def _parse_block_text(self, text):
	#	# Block text to parse
	#	line = u''.join(text).strip()

	#	if '\n' in line and self._matches_label(line):
	#		line_end = line.find('\n')
	#		print "lind_end = ", line_end
	#	else:
	#		line_end = 0
			
	#	print ">>>> parsing block text ", line
	#		# get comment
	#	self._parse_comment(line[line_end:])


	def _matches_label(self, line):
		return self.task_label_re and self.task_label_re.match(line)

	def _matches_next_label(self, line):
		return self.next_label_re and self.next_label_re.match(line)

	def _parse_t_date(self, text):
		return text.partition("[x:")[2].partition("]")[0]
		pass

	def _parse_comment(self, line):
		comment_string = self.preferences['task_comment_string']
		if comment_string in line:
			return_value = line.find(comment_string)
			parent_level, parent, parent_children = self._stack[-1]
			c_string_len_formatting = 1
			c_string_len = len(self.preferences['task_comment_string']) + c_string_len_formatting
			comment_strip = line[c_string_len:] + "\n"
			try:
				parent[6].append(comment_strip)
			except IndexError:
				try:
					# Can't really understand why parent does not show the task if it's a label.
					# The following seem to work.
					parent = parent_children[-1][0]
					parent[6].append(comment_strip)
					pass
				except IndexError:
					logger.exception('Could not add comment to task! Index error!')
					pass

	def _parse_task(self, text, bullet=None, open=True):
		level = self._depth
		if level > 0:
			level -= 1 # first list level should be same as level of line items in para

		parent_level, parent, parent_children = self._stack[-1]
		
		# Get prio
		prio = text.count('!')
		if prio == 0:
			prio = parent[2] # default to parent prio
				
		# Get due date
		due = _NO_DATE
		datematch = _date_re.search(text) # first match
		if datematch:
			date = parse_date(datematch.group(0))
			if date:
				due = '%04i-%02i-%02i' % date # (y, m, d)

		if due == _NO_DATE:
			due = parent[3] # default to parent date (or default for root)

		# Find tags
		tags = set(_tag_re.findall(text))
		if self._intasklist and self._tasklist_tags:
			tags |= self._tasklist_tags
		tags |= parent[4] # add parent tags

		# Check actionable
		if not parent[1]: # default parent not actionable
			actionable = False
		elif any(t.lower() in self.nonactionable_tags for t in tags):
			actionable = False
		elif self._matches_next_label(text) and parent_children:
			previous = parent_children[-1][0]
			actionable = not previous[0] # previous task not open
		else:
			actionable = True

		# Parents are not closed if it has open child items
		if self._depth > 0 and open:
			for l, t, c in self._stack[1:]:
				t[0] = True
		
		# quick way to find out if task is already ticked (is needed to display already ticked parent tasks, while child is unticked)

		tickdate = ""
		if bullet in (CHECKED_BOX, XCHECKED_BOX):
			tickmark = True	
			# Get ticked date (if pageview adds tickdate to task within text)
			t_datematch = _tdate_re.search(text) # first match
			if t_datematch:
				tickdate = self._parse_t_date(t_datematch.group(0))
		else:
			tickmark = False


		# And finally add to stack
		comment=[]

		# adding text as this contains the whole task string
		task = [open, actionable, prio, due, tags, text, comment, tickmark, tickdate, text]						
		#task = [open, actionable, prio, due, tags, text, comment, tickmark,]						
		children = []
		task[6] = []	# flush old comments
		parent_children.append((task, children))
		if self._depth > 0: # (don't add paragraph level items to the stack)
			self._stack.append((level, task, children))





class TaskListDialog(Dialog):

	def __init__(self, window, index_ext, preferences):

		screen_width = gtk.gdk.screen_width()
		screen_height = gtk.gdk.screen_height()
		TASKLISTDIALOG_WIDTH = screen_width / 4
		TASKLISTDIALOG_HEIGHT = screen_height / 4

		Dialog.__init__(self, window, _('Task List'), # T: dialog title
			buttons=gtk.BUTTONS_CLOSE, help=':Plugins:Task List',
			defaultwindowsize=(TASKLISTDIALOG_WIDTH, TASKLISTDIALOG_HEIGHT) )
		self.preferences = preferences
		self.index_ext = index_ext

		# Divider between Tasks and Tasks History
		# BUG? The position is not preserved
		self.vpane = VPaned()
		self.uistate.setdefault('vpane_pos', TASKLISTDIALOG_HEIGHT/2)
		self.vpane.set_position(self.uistate['vpane_pos'])
		
		# Divider between Tags and Tasks
		self.hpane = HPaned()
		self.uistate.setdefault('hpane_pos', TASKLISTDIALOG_WIDTH/2)
		self.hpane.set_position(self.uistate['hpane_pos'])


		self.hpane.add2(self.vpane)
		
		vbox_task_top = gtk.VBox(spacing = 5)
		hbox_task_top = gtk.HBox(spacing = 5)
		vbox_task_bottom = gtk.VBox(spacing = 5)
		
		self.vpane.add1(vbox_task_top)
		self.vpane.add2(vbox_task_bottom)

		vbox_top = gtk.VBox(spacing=5)
		# Entries hbox
		hbox_entries = gtk.HBox(spacing=5)

		vbox_top.pack_start(hbox_entries, False)


		self.vbox.pack_start(vbox_top, False)
		# now pack the hpane to vbox to have it below the entries
		self.vbox.pack_end(self.hpane, True)
		
		# Task list
		self.uistate.setdefault('only_show_act', False)
		self.uistate.setdefault('tick_all', False)
		opener = window.get_resource_opener()
		self.task_list = TaskListTreeView(
			window, self.index_ext, opener,
			#tick_all=self.uistate['tick_all'],
			filter_actionable=self.uistate['only_show_act'],
			tag_by_page=preferences['tag_by_page'],
			use_workweek=preferences['use_workweek'],
		)
		self.task_list.set_headers_visible(True) # Fix for maemo
		self.task_list.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)



		# Task list history
		if self.preferences['show_history']:
			hbox_hist = gtk.HBox(spacing=10)
			vbox_task_bottom.pack_start(gtk.Label(_('History of ticked tasks')), False) # T: Input label

			self.hpane_hist = HPaned()
			self.uistate.setdefault('hpane_hist_pos', 75)
			self.hpane_hist.set_position(self.uistate['hpane_hist_pos'])

		# Ticked Task history list 
		if self.preferences['show_history']:
			self.task_list_hist = TaskListHistoryTreeView(
				window, self.index_ext, opener,
				filter_actionable=self.uistate['only_show_act'],
				tag_by_page=preferences['tag_by_page'],
				use_workweek=preferences['use_workweek'],
			)
			self.task_list_hist.set_headers_visible(True) # Fix for maemo

			vbox_task_bottom.pack_end(ScrolledWindow(self.task_list_hist), True)
			self.task_list_hist.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_HORIZONTAL)

		# Tag auto completion
		hbox_entries.pack_start(gtk.Label(_('Tags')+': '), False) # T: Input label
		tag_entry = InputEntry()
		hbox_entries.pack_start(tag_entry, False)
		tag_entry.set_icon_to_clear()

		if self.preferences['show_history']:
			self.tag_list = TagListTreeView(self.index_ext, self.task_list, tag_entry, self.preferences, self.task_list_hist)
		else:
			self.tag_list = TagListTreeView(self.index_ext, self.task_list, tag_entry, self.preferences)
		self.hpane.add1(ScrolledWindow(self.tag_list))

		# clear filter when icon is pressed
		def clear_entry(entry, iconpos, event):
			entry.set_text('')
			self.task_list.set_tag_filter(None, None)
			self.task_list_hist.set_tag_filter(None, None)

		tag_entry.connect("icon-press", clear_entry)

		# Filter input
		hbox_entries.pack_start(gtk.Label(_('Filter')+': '), False) # T: Input label
		filter_entry = InputEntry()
		filter_entry.set_icon_to_clear()
		hbox_entries.pack_start(filter_entry, False)
		
		filter_cb = DelayedCallback(500,
			lambda o: self.task_list.set_filter(filter_entry.get_text()))
		filter_entry.connect('changed', filter_cb)

		if self.preferences['show_history']:
			filter_cb_hist = DelayedCallback(500,
				lambda o: self.task_list_hist.set_filter(filter_entry.get_text()))
			filter_entry.connect('changed', filter_cb_hist)
		

		# Dropdown with options - TODO
		#~ menu = gtk.Menu()
		#~ showtree = gtk.CheckMenuItem(_('Show _Tree')) # T: menu item in options menu
		#~ menu.append(showtree)
		#~ menu.append(gtk.SeparatorMenuItem())
		#~ showall = gtk.RadioMenuItem(None, _('Show _All Items')) # T: menu item in options menu
		#~ showopen = gtk.RadioMenuItem(showall, _('Show _Open Items')) # T: menu item in options menu
		#~ menu.append(showall)
		#~ menu.append(showopen)
		#~ menubutton = MenuButton(_('_Options'), menu) # T: Button label
		#~ hbox.pack_start(menubutton, False)


		# Tick all tasks
		self.tick_all_toggle = gtk.CheckButton(_('Tick all tasks'))
		# Don't want to mess around to get a consistent tick state depending on if list is empty or not
		self.tick_all_toggle.set_inconsistent(True)
		self.tick_all_toggle.connect('toggled', lambda o: self.task_list._toggle_all_tasks(o.get_active(),self))

		hbox_task_top.pack_start(self.tick_all_toggle, False)


		# Untick all tasks
		self.untick_all_toggle = gtk.CheckButton(_('Untick all tasks'))
		self.untick_all_toggle.set_inconsistent(True)
		untick_toggle_handler_id = self.untick_all_toggle.connect('toggled', lambda o: self.task_list_hist._toggle_all_tasks(o.get_active(), self))

		vbox_task_bottom.pack_start(self.untick_all_toggle, False)


		self.act_toggle = gtk.CheckButton(_('Only Show Actionable Tasks'))
			# T: Checkbox in task list
		self.act_toggle.set_active(self.uistate['only_show_act'])
		self.act_toggle.connect('toggled', lambda o: self.task_list.set_filter_actionable(o.get_active()))
		
		#vbox_top.pack_end(self.act_toggle, False)

		vbox_task_top.pack_start(self.act_toggle, False)
		vbox_task_top.pack_start(hbox_task_top, False)
		vbox_task_top.pack_end(ScrolledWindow(self.task_list), True)

		# Statistics label
		self.statistics_label = gtk.Label()
		#vbox_entries_labels.pack_end(self.statistics_label, False)
		hbox_task_top.pack_end(self.statistics_label, False)
		


		def set_statistics():
			total, stats = self.task_list.get_statistics()
			text = ngettext('%i open item', '%i open items', total) % total
				# T: Label for statistics in Task List, %i is the number of tasks
			text += ' (' + '/'.join(map(str, stats)) + ')'
			self.statistics_label.set_text(text)

		set_statistics()

		def on_tasklist_changed(o):
			self.task_list.refresh()
			self.tag_list.refresh(self.task_list)
			self.task_list_hist.refresh()
			set_statistics()

		callback = DelayedCallback(10, on_tasklist_changed)
			# Don't really care about the delay, but want to
			# make it less blocking - should be async preferably
			# now it is at least on idle
		self.connectto(index_ext, 'tasklist-changed', callback)

	def do_response(self, response):
		self.uistate['hpane_pos'] = self.hpane.get_position()
		self.uistate['hpane_hist_pos'] = self.hpane_hist.get_position()
		self.uistate['vpane_pos'] = self.vpane.get_position()
		self.uistate['only_show_act'] = self.act_toggle.get_active()
		#self.uistate['tick_all'] = self.tick_all_toggle.get_active()
		Dialog.do_response(self, response)


class TagListTreeView(SingleClickTreeView):
	'''TreeView with a single column 'Tags' which shows all tags available
	in a TaskListTreeView. Selecting a tag will filter the task list to
	only show tasks with that tag.
	'''

	_type_separator = 0
	_type_label = 1
	_type_tag = 2
	_type_untagged = 3

	def __init__(self, index_ext, task_list, tag_entry, preferences, task_list_hist=None):
		model = gtk.ListStore(str, int, int, int) # tag name, number of tasks, type, weight
		SingleClickTreeView.__init__(self, model)
		self.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
		self.index_ext = index_ext
		self.task_list = task_list

		# depending of preferences
		if not task_list_hist:
			self.task_list_hist = self.task_list
		else:
			self.task_list_hist = task_list_hist
		self.preferences = preferences
		self.tag_entry = tag_entry

		# add additional entry with auto completion for tags within tasks
		self.completion = gtk.EntryCompletion()
		# it's better to not have a popup for a single match here
		self.completion.set_inline_completion(True)
		self.completion.set_model(model)
		self.completion.set_popup_single_match(False)
		self.tag_entry.set_completion(self.completion)
		self.completion.set_text_column(0)

		# if user has selected a tag from the popup list
		self.completion.connect('match-selected', self.match_selected)

		#if only remaining match is shown and RETURN is hit
		self.tag_entry.connect('activate', self.activate_return)

		column = gtk.TreeViewColumn(_('Tags'))
			# T: Column header for tag list in Task List dialog
		self.append_column(column)

		cr1 = gtk.CellRendererText()
		cr1.set_property('ellipsize', pango.ELLIPSIZE_END)
		column.pack_start(cr1, True)
		column.set_attributes(cr1, text=0, weight=3) # tag name, weight

		cr2 = self.get_cell_renderer_number_of_items()
		column.pack_start(cr2, False)
		column.set_attributes(cr2, text=1) # number of tasks

		self.set_row_separator_func(lambda m, i: m[i][2] == self._type_separator)

		self._block_selection_change = False
		self.get_selection().connect('changed', self.on_selection_changed)


		# get the standard_tag from preferences and set entry
		standard_tag = self.preferences['standard_tag']
		if standard_tag:
			self.tag_entry.set_text(standard_tag)
			self.activate_return(self.tag_entry)

		
		self.refresh(task_list)


	def match_selected(self, completion, model, iter):
		tags = []
		tags.append(model[iter][0])
		# there can be still a additional filter set
		labels = self.get_labels()
		self.task_list.set_tag_filter(tags, labels)
		self.task_list_hist.set_tag_filter(tags, labels)

	def activate_return(self, entry):
		# if return is hit without anything, don't show untagged tasks accidentally
		# TODO: This is another 'hidden' functionality for an easy way to show them
		# but it needs to be implemented in a proper way

		if entry.get_text() == "":
			return
		tags = []
		tag_entered = entry.get_text()

		tags.append(tag_entered)
		# there can be still a additional filter set
		labels = self.get_labels()
		self.task_list.set_tag_filter(tags, labels)
		self.task_list_hist.set_tag_filter(tags, labels)
		tree_selection = self.get_selection()

		# get the path for the tag from the tag list to highlight in list
		model = self.get_model()
		path = 0
		for element in model:
			if tag_entered == element[0]:
				break
			path += 1
		tree_selection.select_path(path)
		

	def get_tags(self):
		'''Returns current selected tags, or None for all tags'''
		tags = []
		for row in self._get_selected():
			if row[2] == self._type_tag:
				tags.append(row[0].decode('utf-8'))
			elif row[2] == self._type_untagged:
				tags.append(_NO_TAGS)
		return tags or None

	def get_labels(self):
		'''Returns current selected labels'''
		labels = []
		for row in self._get_selected():
			if row[2] == self._type_label:
				labels.append(row[0].decode('utf-8'))
		return labels or None

	def _get_selected(self):
		selection = self.get_selection()
		if selection:
			model, paths = selection.get_selected_rows()
			if not paths or (0,) in paths:
				return []
			else:
				self.tag_entry.clear()
				return [model[path] for path in paths]
		else:
			return []

	def refresh(self, task_list):
		self._block_selection_change = True
		selected = [(row[0], row[2]) for row in self._get_selected()] # remember name and type

		# Rebuild model
		model = self.get_model()
		if model is None: return
		model.clear()

		n_all = self.task_list.get_n_tasks()
		#n_all_hist = self.task_list_hist.get_n_tasks()

		model.append((_('All Tasks'), n_all, self._type_label, pango.WEIGHT_BOLD)) # T: "tag" for showing all tasks

		used_labels = self.task_list.get_labels()
		for label in self.index_ext.task_labels: # explicitly keep sorting from preferences
			if label in used_labels \
			and label != self.index_ext.next_label:
				model.append((label, used_labels[label], self._type_label, pango.WEIGHT_BOLD))

		tags = self.task_list.get_tags()
		if _NO_TAGS in tags:
			n_untagged = tags.pop(_NO_TAGS)
			model.append((_('Untagged'), n_untagged, self._type_untagged, pango.WEIGHT_NORMAL))
			# T: label in tasklist plugins for tasks without a tag

		model.append(('', 0, self._type_separator, 0)) # separator
		model.append(('', 0, self._type_separator, 0)) # separator

		for tag in natural_sorted(tags):
			model.append((tag, tags[tag], self._type_tag, pango.WEIGHT_NORMAL))

		model.append(('', 0, self._type_separator, 0)) # separator for tags of closed tags

		used_labels = self.task_list_hist.get_labels()
		for label in self.index_ext.task_labels: # explicitly keep sorting from preferences
			if label in used_labels \
			and label != self.index_ext.next_label:
				model.append((label, used_labels[label], self._type_label, pango.WEIGHT_BOLD))

		tags_hist = self.task_list_hist.get_tags()

		if _NO_TAGS in tags_hist:
			n_untagged = tags_hist.pop(_NO_TAGS)
			model.append((_('Untagged'), n_untagged, self._type_untagged, pango.WEIGHT_NORMAL))
			# T: label in tasklist plugins for tasks without a tag

		# append tags from task list history, but only if it's not already in the list of tags from task list			
		for tag in natural_sorted(tags_hist):
			if tag not in tags:
				model.append((tag, tags_hist[tag], self._type_tag, pango.WEIGHT_NORMAL))

		# Restore selection
		def reselect(model, path, iter):
			row = model[path]
			name_type = (row[0], row[2])
			if name_type in selected:
				self.get_selection().select_iter(iter)

		if selected:
			model.foreach(reselect)
		self._block_selection_change = False

	def on_selection_changed(self, selection):
		if not self._block_selection_change:
			tags = self.get_tags()
			labels = self.get_labels()
			self.task_list.set_tag_filter(tags, labels)
			self.task_list_hist.set_tag_filter(tags, labels)

HIGH_COLOR = '#EF5151' # red (derived from Tango style guide - #EF2929)
MEDIUM_COLOR = '#FCB956' # orange ("idem" - #FCAF3E)
ALERT_COLOR = '#FCEB65' # yellow ("idem" - #FCE94F)
# FIXME: should these be configurable ?


class TaskListTreeView(BrowserTreeView):

	VIS_COL = 0 # visible
	TICKED_COL = 1			# additions
	PRIO_COL = 2
	TASK_COL = 3
	DATE_COL = 4
	PAGE_COL = 5
	TAGS0_COL = 6			# additions
	TASK_COMMENT_COL = 7	# additions
	ACT_COL = 8 # actionable
	OPEN_COL = 9 # item not closed
	TASKID_COL = 10
	TAGS_COL = 11
	TASK0_COL = 12


	def __init__(self, window, index_ext, opener, filter_actionable=False, tag_by_page=False, use_workweek=False):
		self.real_model = gtk.TreeStore(bool, bool, int, str, str, str, str, str, bool, bool, int, object, str)
			# VIS_COL, TICKED_COL, PRIO_COL, TASK_COL, DATE_COL, TAGS0_COL, TASK_COMMENT_COL, PAGE_COL, ACT_COL, OPEN_COL, TASKID_COL, TAGS_COL, TASK0_COL
		model = self.real_model.filter_new()
		model.set_visible_column(self.VIS_COL)
		model = gtk.TreeModelSort(model)
		model.set_sort_column_id(self.PRIO_COL, gtk.SORT_DESCENDING)
		BrowserTreeView.__init__(self, model)
		screen_width = gtk.gdk.screen_width()
		screen_height = gtk.gdk.screen_height()

		self.index_ext = index_ext
		self.opener = opener
		self.filter = None
		self.tag_filter = None
		self.label_filter = None
		self.filter_actionable = filter_actionable
		self.tag_by_page = tag_by_page
		self._tags = {}
		self._labels = {}
		self.win = window
		column_width = 150
		self.tick_column = False


		# Wrap text in column on resize
		def set_column_width(column, width, renderer, pan = True):
			column_width = column.get_width()
			renderer.props.wrap_width = column_width
			if pan:
				renderer.props.wrap_mode = pango.WRAP_WORD
			else:
				renderer.props.wrap_mode = gtk.WRAP_WORD

		# Add some rendering for the task tick box
		cell_renderer = gtk.CellRendererToggle()
		cell_renderer.set_property('activatable', True)
		column = gtk.TreeViewColumn('Tick', cell_renderer)
		column.set_sort_column_id(self.TICKED_COL)
		column.add_attribute(cell_renderer, "active", self.TICKED_COL)
		self.append_column(column)
		cell_renderer.connect("toggled", self.tick_column_clicked)

		# Add some rendering for the Prio column
		def render_prio(col, cell, model, i):
			prio = model.get_value(i, self.PRIO_COL)
			cell.set_property('text', str(prio))
			if prio >= 3: color = HIGH_COLOR
			elif prio == 2: color = MEDIUM_COLOR
			elif prio == 1: color = ALERT_COLOR
			else: color = None
			cell.set_property('cell-background', color)

		cell_renderer = gtk.CellRendererText()
		column = gtk.TreeViewColumn(' ! ', cell_renderer)
		column.set_cell_data_func(cell_renderer, render_prio)
		column.set_sort_column_id(self.PRIO_COL)
		self.append_column(column)

		# Rendering for task description column
		cell_renderer = gtk.CellRendererText()
		cell_renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
		column = gtk.TreeViewColumn(_('Task'), cell_renderer, markup=self.TASK_COL)
				# T: Column header Task List dialog
		column.set_resizable(True)
		column.set_sort_column_id(self.TASK_COL)
		column.set_expand(True)

		#if ui_environment['platform'] == 'maemo':
		#	column.set_min_width(100) # don't let this column get too small
		#else:
		#	column.set_min_width(150) # don't let this column get too small
		
		self.append_column(column)
		self.set_expander_column(column)

		if gtk.gtk_version >= (2, 12) \
		and gtk.pygtk_version >= (2, 12):
			self.set_tooltip_column(self.TASK_COL)

		# Rendering of the Date column
		day_of_week = datetime.date.today().isoweekday()
		if use_workweek and day_of_week == 4:
			# Today is Thursday - 2nd day ahead is after the weekend
			delta1, delta2 = 1, 3
		elif use_workweek and day_of_week == 5:
			# Today is Friday - next day ahead is after the weekend
			delta1, delta2 = 3, 4
		else:
			delta1, delta2 = 1, 2

		today    = str( datetime.date.today() )
		tomorrow = str( datetime.date.today() + datetime.timedelta(days=delta1))
		dayafter = str( datetime.date.today() + datetime.timedelta(days=delta2))
		def render_date(col, cell, model, i):
			date = model.get_value(i, self.DATE_COL)
			if date == _NO_DATE:
				cell.set_property('text', '')
			else:
				cell.set_property('text', date)
				# TODO allow strftime here

			if date <= today: color = HIGH_COLOR
			elif date <= tomorrow: color = MEDIUM_COLOR
			elif date <= dayafter: color = ALERT_COLOR
				# "<=" because tomorrow and/or dayafter can be after the weekend
			else: color = None
			cell.set_property('cell-background', color)

		cell_renderer = gtk.CellRendererText()
		column = gtk.TreeViewColumn(_('Date'), cell_renderer)
			# T: Column header Task List dialog
		column.set_cell_data_func(cell_renderer, render_date)
		column.set_sort_column_id(self.DATE_COL)
		self.append_column(column)

		# Rendering for tag column
		cell_renderer = gtk.CellRendererText()

		cell_renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
		column = gtk.TreeViewColumn(_('Tags'), cell_renderer, markup=self.TAGS0_COL)
		column.set_resizable(True)
		column.set_sort_column_id(self.TAGS0_COL)
		column.set_min_width(50)

		column.connect_after("notify::width", set_column_width, cell_renderer)
		self.append_column(column)

		# Rendering for task comment column
		cell_renderer = gtk.CellRendererText()
		column = gtk.TreeViewColumn(_('Comment'), cell_renderer, text=self.TASK_COMMENT_COL)
		column.set_resizable(True)
		column.set_sort_column_id(self.TASK_COMMENT_COL)
		#column.set_min_width(column_width)
		cell_renderer.props.wrap_width = int(screen_width*0.05)
		cell_renderer.props.wrap_mode = pango.WRAP_WORD

		column.connect_after("notify::width", set_column_width, cell_renderer)
		self.append_column(column)

		# Rendering for page name column
		cell_renderer = gtk.CellRendererText()
		column = gtk.TreeViewColumn(_('Page'), cell_renderer, text=self.PAGE_COL)
				# T: Column header Task List dialog
		column.set_sort_column_id(self.PAGE_COL)
		self.append_column(column)

		# Finalize
		self.refresh()

		# HACK because we can not register ourselves :S
		self.connect_after('row_activated', self.__class__.do_row_activated)

	def refresh(self):
		'''Refresh the model based on index data'''
		# Update data
		self._clear()
		self._append_tasks(None, None, {})

		# Make tags case insensitive
		tags = sorted((t.lower(), t) for t in self._tags)
			# tuple sorting will sort ("foo", "Foo") before ("foo", "foo"),
			# but ("bar", ..) before ("foo", ..)
		prev = ('', '')
		for tag in tags:
			if tag[0] == prev[0]:
				self._tags[prev[1]] += self._tags[tag[1]]
				self._tags.pop(tag[1])
			prev = tag

		# Set view
		self._eval_filter() # keep current selection
		self.expand_all()

	def _clear(self):
		self.real_model.clear() # flush
		self._tags = {}
		self._labels = {}

	def _append_tasks(self, task, iter, path_cache):
		for row in self.index_ext.list_tasks(task):
			if not row['open']:
				continue # Only include open items for now

			if row['source'] not in path_cache:
				path = self.index_ext.get_path(row)
				if path is None:
					# Be robust for glitches - filter these out
					continue
				else:
					path_cache[row['source']] = path

			path = path_cache[row['source']]

			# Update labels
			for label in self.index_ext.task_label_re.findall(row['description']):
				self._labels[label] = self._labels.get(label, 0) + 1

			# Update tag count
			tags = row['tags'].split(',')
			# Want to show tags one below the other instead of separated by comma, so creating tags0 instead
			tags0 = ""	
			for tag in tags:							
				tags0 += "<span color=\"#ce5c00\">" + tag + "</span>" + "\n"

			if self.tag_by_page:
				tags = tags + path.parts

			if tags:
				for tag in tags:
					self._tags[tag] = self._tags.get(tag, 0) + 1
			else:
				self._tags[_NO_TAGS] = self._tags.get(_NO_TAGS, 0) + 1

			# Format description
			task = _date_re.sub('', row['description'], count=1)
			task = _tdate_re.sub('', task, count=1)
			task = re.sub('\s*!+\s*', ' ', task) # get rid of exclamation marks
			task = self.index_ext.next_label_re.sub('', task) # get rid of "next" label in description
			task = encode_markup_text(task)
			if row['actionable']:
				#task = _tag_re.sub(r'<span color="#ce5c00">@\1</span>', task) # highlight tags - same color as used in pageview
				task = _tag_re.sub(r'', task) # get rid of tags in task description --> most probably not the best place to do that...	
				task = self.index_ext.task_label_re.sub(r'<b>\1</b>', task) # highlight labels
			else:
				task = r'<span color="darkgrey">%s</span>' % task

			# Insert all columns
			modelrow = [False, row['tickmark'], row['prio'], task, row['due'], path.name, tags0, row['comment'], row['actionable'], row['open'], row['id'], tags, row['task']]
						# VIS_COL, , TICKMARK, PRIO_COL, TASK_COL, DATE_COL, PAGE_COL, ACT_COL, OPEN_COL, TASKID_COL, TAGS_COL
			modelrow[0] = self._filter_item(modelrow)
			myiter = self.real_model.append(iter, modelrow)

			if row['haschildren']:
				self._append_tasks(row, myiter, path_cache) # recurs

	def _toggle_all_tasks(self, tick_status, dialog):
		model = self.get_model()	
		toggle_all_ok = self._toggle_all_tasks_dialog(dialog)

		if toggle_all_ok:
			model.foreach(self._do_toggle_all_tasks)

	def _toggle_all_tasks_dialog(self, main_dialog):
		task_count = self.get_n_tasks()

		response = QuestionDialog(main_dialog, (_('Tick ' + str(task_count) + ' tasks?'),
						# T: Short message text on first time use of task list plugin
						_('You are about to tick ' + str(task_count) + ' tasks.\n\n'
						  'Are you sure?\n')
						# T: Long message text on first time use of task list plugin
					) ).run()

		return response

	def _do_toggle_all_tasks(self, model, path, iter):
		# skip tasks which are ticked but are show due to unticked children
		if not model[path][self.TICKED_COL]:
			page = Path( model[path][self.PAGE_COL] )
			text = self._get_raw_text(model[path])
			pageview = self.opener.open_page(page)
			pageview.find(text)

			task = model[path][self.TASK0_COL]

			# if pageview has not been updated to add tickdate to task into the text, then the tickdate is
			# preserved at least within the index (for the time being)
			self.index_ext.put_new_tickdate_to_db(task)
			
			self.win.pageview.toggle_checkbox()

	def set_filter_actionable(self, filter):
		'''Set filter state for non-actionable items
		@param filter: if C{False} all items are shown, if C{True} only actionable items
		'''
		self.filter_actionable = filter
		self._eval_filter()

	def set_filter(self, string):
		# TODO allow more complex queries here - same parse as for search
		if string:
			inverse = False
			if string.lower().startswith('not '):
				# Quick HACK to support e.g. "not @waiting"
				inverse = True
				string = string[4:]
			self.filter = (inverse, string.strip().lower())
		else:
			self.filter = None
		self._eval_filter()

	def get_labels(self):
		'''Get all labels that are in use
		@returns: a dict with labels as keys and the number of tasks
		per label as value
		'''
		return self._labels

	def get_tags(self):
		'''Get all tags that are in use
		@returns: a dict with tags as keys and the number of tasks
		per tag as value
		'''
		return self._tags

	def get_n_tasks(self):
		'''Get the number of tasks in the list
		@returns: total number
		'''
		model = self.get_model()
		counter = [0]
		def count(model, path, iter):
			if model[iter][self.OPEN_COL]:
				# only count open items
				counter[0] += 1
		model.foreach(count)
		return counter[0]

	def get_statistics(self):
		statsbyprio = {}

		def count(model, path, iter):
			# only count open items
			row = model[iter]
			if row[self.OPEN_COL]:
				prio = row[self.PRIO_COL]
				statsbyprio.setdefault(prio, 0)
				statsbyprio[prio] += 1

		self.real_model.foreach(count)

		if statsbyprio:
			total = reduce(int.__add__, statsbyprio.values())
			highest = max([0] + statsbyprio.keys())
			stats = [statsbyprio.get(k, 0) for k in range(highest+1)]
			stats.reverse() # highest first
			return total, stats
		else:
			return 0, []

	def set_tag_filter(self, tags=None, labels=None):
		if tags:
			self.tag_filter = [tag.lower() for tag in tags]
		else:
			self.tag_filter = None

		if labels:
			self.label_filter = [label.lower() for label in labels]
		else:
			self.label_filter = None

		self._eval_filter()

	def _eval_filter(self):
		logger.debug('Filtering task list with labels: %s tags: %s, filter: %s', self.label_filter, self.tag_filter, self.filter)

		def filter(model, path, iter):
			visible = self._filter_item(model[iter])
			model[iter][self.VIS_COL] = visible
			if visible:
				parent = model.iter_parent(iter)
				while parent:
					model[parent][self.VIS_COL] = visible
					parent = model.iter_parent(parent)

		self.real_model.foreach(filter)
		self.expand_all()

	def _filter_item(self, modelrow):
		# This method filters case insensitive because both filters and
		# text are first converted to lower case text.
		visible = True

		if not modelrow[self.OPEN_COL] \
		or (not modelrow[self.ACT_COL] and self.filter_actionable):
			visible = False

		description = modelrow[self.TASK_COL].decode('utf-8').lower()
		pagename = modelrow[self.PAGE_COL].decode('utf-8').lower()
		tags = [t.lower() for t in modelrow[self.TAGS_COL]]
		comments = modelrow[self.TASK_COMMENT_COL].decode('utf-8').lower()

		if visible and self.label_filter:
			# Any labels need to be present
			for label in self.label_filter:
				if label in description:
					break
			else:
				visible = False # no label found

		if visible and self.tag_filter:
			# Any tag should match --> changed to all to 'activate'
			# 'hidden' functionality for multiple selection of tags with
			# the use of the ctrl key. 
			if (_NO_TAGS in self.tag_filter and not tags) \
			or all(tag in tags for tag in self.tag_filter):
				visible = True
			else:
				visible = False

		if visible and self.filter:
			# And finally the filter string should match
			# FIXME: we are matching against markup text here - may fail for some cases
			inverse, string = self.filter
			match = string in description or string in pagename or string in comments
			if (not inverse and not match) or (inverse and match):
				visible = False

		return visible

	def tick_column_clicked(self, toggle, path):
		'''
		As resizing the columns emit the clicked signal somehow with the "tick" column as parameter
		I use this Workaround for determining if really column "tick" was ticked. 
		'''
		self.tick_column = True

	def do_row_activated(self, path, column):
		model = self.get_model()
		page = Path( model[path][self.PAGE_COL] )
		text = self._get_raw_text(model[path])
		pageview = self.opener.open_page(page)
		pageview.find(text)
		# I need to get the task text only to compare against the model below
		task_text = model[path][self.TASK_COL]
				
		# now check if column Tick was clicked and tick box and action on page selected
		# Need the workaround with self.tick_column as resizing the columns emit a clicked signal
		# with the column title = Tick
		if column.get_title() == "Tick" and self.tick_column:
			real_path = model.convert_path_to_child_path(path)
			if (self.label_filter or self.tag_filter or self.filter):
				real_path = self.get_real_path(task_text)

			self.real_model[real_path][self.TICKED_COL] = not self.real_model[real_path][self.TICKED_COL]

			task = self.real_model[real_path][self.TASK0_COL]

			# if pageview has not been updated to add tickdate to task into the text, then the tickdate is
			# preserved at least within the index (for the time being)
			self.index_ext.put_new_tickdate_to_db(task)
			self.win.pageview.toggle_checkbox()
			self.tick_column = False
		
	def get_real_path(self, text):
		'''
		this method looks for the correct path in the real_model according to the selected row in treeview.
		As treeview can be filtered against tag etc., the only reliable way to find the right path (even with children) is to
		compare the text selected against the text in self.real_model and then return the path
		'''
		# start from the beginning
		iter = self.real_model.get_iter_first()
		while iter:
			path = self.real_model.get_path(iter)
			if self.real_model[path][self.TASK_COL] == text:
				#parent found
				return path
			if self.real_model.iter_has_child(iter):
				# there are children
				n_children = self.real_model.iter_n_children(iter)
				# how many?
				for child_no in range(0, n_children):
					child_iter = self.real_model.iter_nth_child(iter, child_no)
					child_path = self.real_model.get_path(child_iter)
					if self.real_model[child_path][self.TASK_COL] == text:
						# so there is a child found
						return child_path
			iter = self.real_model.iter_next(iter)

	def _get_raw_text(self, task):
		id = task[self.TASKID_COL]
		row = self.index_ext.get_task(id)
		return row['description']

	def do_initialize_popup(self, menu):
		item = gtk.ImageMenuItem('gtk-copy')
		item.connect('activate', self.copy_to_clipboard)
		menu.append(item)
		self.populate_popup_expand_collapse(menu)

	def copy_to_clipboard(self, *a):
		'''Exports currently visible elements from the tasks list'''
		logger.debug('Exporting to clipboard current view of task list.')
		text = self.get_visible_data_as_csv()
		Clipboard.set_text(text)
			# TODO set as object that knows how to format as text / html / ..
			# unify with export hooks

	def get_visible_data_as_csv(self):
		text = ""
		for indent, prio, desc, date, tags0, comment, page in self.get_visible_data():
			prio = str(prio)
			desc = decode_markup_text(desc)
			desc = '"' + desc.replace('"', '""') + '"'
			text += ",".join((prio, desc, date, tags0, comment, page)) + "\n"
		return text


	# TODO: Show filter (e.g. list of selected tags) which lead to the print out
	def get_visible_data_as_html(self):
		html = '''\
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd";>
<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
		<title>Task List - Zim</title>
		<meta name='Generator' content='Zim [%% zim.version %%]'>
		<style type='text/css'>
			table.tasklist {
				border-width: 1px;
				border-spacing: 2px;
				border-style: solid;
				border-color: gray;
				border-collapse: collapse;
			}
			table.tasklist th {
				border-width: 1px;
				padding: 8px;
				border-style: solid;
				border-color: gray;
				text-align: left;
				background-color: gray;
				color: white;
			}
			table.tasklist td {
				border-width: 1px;
				padding: 8px;
				border-style: solid;
				border-color: gray;
				text-align: left;
			}
			.high {background-color: %s}
			.medium {background-color: %s}
			.alert {background-color: %s}
		</style>
	</head>
	<body>


<h1>Task List - Zim</h1>
<table class="tasklist">
<tr><th>Status</th><th>Prio</th><th>Task</th><th>Date</th><th>Tags</th><th>Comments</th></tr>
''' % (HIGH_COLOR, MEDIUM_COLOR, ALERT_COLOR)

		today    = str( datetime.date.today() )
		tomorrow = str( datetime.date.today() + datetime.timedelta(days=1))
		dayafter = str( datetime.date.today() + datetime.timedelta(days=2))
		for indent, status, prio, desc, date, tags0, comment, page in self.get_visible_data():

			if status == 1: status_str = '<td>Closed</td>'
			if status == 0: status_str = '<td>Open</td>'

			if prio >= 3: prio = '<td class="high">%s</td>' % prio
			elif prio == 2: prio = '<td class="medium">%s</td>' % prio
			elif prio == 1: prio = '<td class="alert">%s</td>' % prio
			else: prio = '<td>%s</td>' % prio

			if date and date <= today: date = '<td class="high">%s</td>' % date
			elif date == tomorrow: date = '<td class="medium">%s</td>' % date
			elif date == dayafter: date = '<td class="alert">%s</td>' % date
			else: date = '<td>%s</td>' % date

			desc = '<td>%s%s</td>' % ('&nbsp;' * (4 * indent), desc)
			if "\n" in tags0:
				tags0 = tags0.replace("\n", "<br />")
			tags0 = '<td>%s</td>' % tags0
			if "\n" in comment:
				comment = comment.replace("\n", "<br />")
			comment = '<td>%s</td>' % comment
			page = '<td>%s</td>' % page

			html += '<tr>' + status_str + prio + desc + date + tags0 + comment + '</tr>\n'

		html += '''\
</table>

	</body>

</html>
'''
		return html

	def get_visible_data(self):
		rows = []

		def collect(model, path, iter):
			indent = len(path) - 1 # path is tuple with indexes

			row = model[iter]
			status = row[self.TICKED_COL]
			prio = row[self.PRIO_COL]
			desc = row[self.TASK_COL].decode('utf-8')
			date = row[self.DATE_COL]
			page = row[self.PAGE_COL].decode('utf-8')
			tags0 = row[self.TAGS0_COL].decode('utf-8')
			comment = row[self.TASK_COMMENT_COL].decode('utf-8')

			if date == _NO_DATE:
				date = ''

			rows.append((indent, status, prio, desc, date, tags0, comment, page))

		model = self.get_model()
		model.foreach(collect)

		return rows


class TaskListHistoryTreeView(BrowserTreeView):

	
	VIS_COL = 0 # visible
	TICKED_COL = 1			
	TICKED_DATE_COL = 2
	PRIO_COL = 3
	TASK_COL = 4
	DATE_COL = 5
	PAGE_COL = 6
	TAGS0_COL = 7 # TAGS separated by \n
	TASK_COMMENT_COL = 8	
	ACT_COL = 9 # actionable
	OPEN_COL = 10 # item not closed
	TASKID_COL = 11
	TAGS_COL = 12
	TASK0_COL = 13 # Tasks string complete (for search in index)	

	def __init__(self, window, index_ext, opener, filter_actionable=False, tag_by_page=False, use_workweek=False):
		self.real_model = gtk.TreeStore(bool, bool, str, int, str, str, str, str, str, bool, bool, int, object, str)
			# VIS_COL, TICKED_COL, TICKED_DATE_COL, PRIO_COL, TASK_COL, DATE_COL, TAGS0_COL, TASK_COMMENT_COL, PAGE_COL, ACT_COL, OPEN_COL, TASKID_COL, TAGS_COL, TASK0_COL
		model = self.real_model.filter_new()
		model.set_visible_column(self.VIS_COL)
		model = gtk.TreeModelSort(model)
		model.set_sort_column_id(self.TICKED_DATE_COL, gtk.SORT_DESCENDING)
		BrowserTreeView.__init__(self, model)
		screen_width = gtk.gdk.screen_width()
		screen_height = gtk.gdk.screen_height()
		self.index_ext = index_ext
		self.opener = opener
		self.filter = None
		self.tag_filter = None
		self.label_filter = None
		self.filter_actionable = filter_actionable
		self.tag_by_page = tag_by_page
		self._tags = {}
		self._labels = {}
		self.win = window
		column_width = 300

		# Wrap text in column on resize
		def set_column_width(column, width, renderer, pan = True):
			column_width = column.get_width()
			renderer.props.wrap_width = column_width
			if pan:
				renderer.props.wrap_mode = pango.WRAP_WORD
			else:
				renderer.props.wrap_mode = gtk.WRAP_WORD

		# Add some rendering for the task tick box
		cell_renderer = gtk.CellRendererToggle()
		cell_renderer.set_property('activatable', True)
		column = gtk.TreeViewColumn('Tick', cell_renderer)
		column.set_sort_column_id(self.TICKED_COL)
		#column.set_resizable(False)
		column.add_attribute(cell_renderer, "active", self.TICKED_COL)
		self.append_column(column)


		# Add some rendering for the task tick date
		cell_renderer = gtk.CellRendererText()
		column = gtk.TreeViewColumn(_('Ticked Date'), cell_renderer, text=self.TICKED_DATE_COL)
		column.set_sort_column_id(self.TICKED_DATE_COL)
		column.set_resizable(True)
		self.append_column(column)


		# Add some rendering for the Prio column
		def render_prio(col, cell, model, i):
			prio = model.get_value(i, self.PRIO_COL)
			cell.set_property('text', str(prio))
			if prio >= 3: color = HIGH_COLOR
			elif prio == 2: color = MEDIUM_COLOR
			elif prio == 1: color = ALERT_COLOR
			else: color = None
			cell.set_property('cell-background', color)

		cell_renderer = gtk.CellRendererText()
		#~ column = gtk.TreeViewColumn(_('Prio'), cell_renderer)
			# T: Column header Task List dialog
		column = gtk.TreeViewColumn(' ! ', cell_renderer)
		column.set_cell_data_func(cell_renderer, render_prio)
		column.set_sort_column_id(self.PRIO_COL)
		#column.set_resizable(True)
		self.append_column(column)

		# Rendering for task description column
		cell_renderer = gtk.CellRendererText()
		cell_renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
		column = gtk.TreeViewColumn(_('Task'), cell_renderer, markup=self.TASK_COL)
				# T: Column header Task List dialog
		column.set_resizable(True)
		column.set_sort_column_id(self.TASK_COL)
		column.set_expand(True)
		#if ui_environment['platform'] == 'maemo':
		#	column.set_min_width(100) # don't let this column get too small
		#else:
		#	column.set_min_width(150) # don't let this column get too small
		
		self.append_column(column)
		self.set_expander_column(column)

		if gtk.gtk_version >= (2, 12) \
		and gtk.pygtk_version >= (2, 12):
			self.set_tooltip_column(self.TASK_COL)

		## Rendering of the Date column
		#day_of_week = datetime.date.today().isoweekday()
		#if use_workweek and day_of_week == 4:
		#	# Today is Thursday - 2nd day ahead is after the weekend
		#	delta1, delta2 = 1, 3
		#elif use_workweek and day_of_week == 5:
		#	# Today is Friday - next day ahead is after the weekend
		#	delta1, delta2 = 3, 4
		#else:
		#	delta1, delta2 = 1, 2

		#today    = str( datetime.date.today() )
		#tomorrow = str( datetime.date.today() + datetime.timedelta(days=delta1))
		#dayafter = str( datetime.date.today() + datetime.timedelta(days=delta2))
		def render_date(col, cell, model, i):
			date = model.get_value(i, self.DATE_COL)
			if date == _NO_DATE:
				cell.set_property('text', '')
			#else:
			#	cell.set_property('text', date)
				# TODO allow strftime here

		#	if date <= today: color = HIGH_COLOR
		#	elif date <= tomorrow: color = MEDIUM_COLOR
		#	elif date <= dayafter: color = ALERT_COLOR
		#		# "<=" because tomorrow and/or dayafter can be after the weekend
		#	else: color = None
		#	cell.set_property('cell-background', color)

		cell_renderer = gtk.CellRendererText()
		column = gtk.TreeViewColumn(_('Date'), cell_renderer, text=self.DATE_COL)
			# T: Column header Task List dialog
		column.set_cell_data_func(cell_renderer, render_date)
		column.set_sort_column_id(self.DATE_COL)
		self.append_column(column)

		# Rendering for tag column
		cell_renderer = gtk.CellRendererText()

		cell_renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
		column = gtk.TreeViewColumn(_('Tags'), cell_renderer, markup=self.TAGS0_COL)
		column.set_resizable(True)
		column.set_sort_column_id(self.TAGS0_COL)
		column.set_min_width(100)

		column.connect_after("notify::width", set_column_width, cell_renderer)
		self.append_column(column)

		# Rendering for task comment column
		cell_renderer = gtk.CellRendererText()
		column = gtk.TreeViewColumn(_('Comment'), cell_renderer, text=self.TASK_COMMENT_COL)
		column.set_resizable(True)
		column.set_sort_column_id(self.TASK_COMMENT_COL)
		#column.set_min_width(column_width)
		cell_renderer.props.wrap_width = int(screen_width*0.05)
		cell_renderer.props.wrap_mode = pango.WRAP_WORD

		column.connect_after("notify::width", set_column_width, cell_renderer)
		self.append_column(column)

		# Rendering for page name column
		cell_renderer = gtk.CellRendererText()
		column = gtk.TreeViewColumn(_('Page'), cell_renderer, text=self.PAGE_COL)
				# T: Column header Task List dialog
		column.set_sort_column_id(self.PAGE_COL)
		self.append_column(column)

		# Finalize
		self.refresh()

		# HACK because we can not register ourselves :S
		self.connect('row_activated', self.__class__.do_row_activated)


	def refresh(self):
		'''Refresh the model based on index data'''
		# Update data
		self._clear()
		self._append_tasks(None, None, {})

		# Make tags case insensitive
		tags = sorted((t.lower(), t) for t in self._tags)
			# tuple sorting will sort ("foo", "Foo") before ("foo", "foo"),
			# but ("bar", ..) before ("foo", ..)
		prev = ('', '')
		for tag in tags:
			if tag[0] == prev[0]:
				self._tags[prev[1]] += self._tags[tag[1]]
				self._tags.pop(tag[1])
			prev = tag

		# Set view
		self._eval_filter() # keep current selection
		self.expand_all()

	def _clear(self):
		self.real_model.clear() # flush
		self._tags = {}
		self._labels = {}

	def _append_tasks(self, task, iter, path_cache):
		for row in self.index_ext.list_tasks(task):
			# only include ticked tasks and open tasks with ticked children
			if not row['tickmark'] and not row['haschildren']:
				continue

			if row['source'] not in path_cache:
				path = self.index_ext.get_path(row)
				if path is None:
					# Be robust for glitches - filter these out
					continue
				else:
					path_cache[row['source']] = path

			path = path_cache[row['source']]

			# Update labels
			for label in self.index_ext.task_label_re.findall(row['description']):
				self._labels[label] = self._labels.get(label, 0) + 1

			# Update tag count
			tags = row['tags'].split(',')
			# Want to show tags one below the other instead of separated by comma, so creating tags0 instead
			tags0 = ""	
			for tag in tags:							
				tags0 += "<span color=\"#ce5c00\">" + tag + "</span>" + "\n"

			if self.tag_by_page:
				tags = tags + path.parts

			if tags:
				for tag in tags:
					self._tags[tag] = self._tags.get(tag, 0) + 1
			else:
				self._tags[_NO_TAGS] = self._tags.get(_NO_TAGS, 0) + 1


			# Format description
			task = _date_re.sub('', row['description'], count=1)
			task = _tdate_re.sub('', task, count=1)
			task = re.sub('\s*!+\s*', ' ', task) # get rid of exclamation marks
			task = self.index_ext.next_label_re.sub('', task) # get rid of "next" label in description
			task = encode_markup_text(task)
			if row['actionable']:
			#if row['tickmark']:
				#task = _tag_re.sub(r'<span color="#ce5c00">@\1</span>', task) # highlight tags - same color as used in pageview
				task = _tag_re.sub(r'', task) # get rid of tags in task description --> most probably not the best place to do that...	
				task = self.index_ext.task_label_re.sub(r'<b>\1</b>', task) # highlight labels
			else:
				task = r'<span color="darkgrey">%s</span>' % task
			
			tickdate = "[no date]"
			if row['tickdate']:
				# This means that pageview has added tickdate to task within text and has been parsed by Taskparser
				tickdate = row['tickdate']
			else:
				# Either pageview is not updated to add tickdates to task, or it's disabled in pageview prefs.
				# Therefore get the tickdat from the index. But if child is ticked,
				# and parent is not but has entry in index, don't show date
				if row['tickmark']:
					tickdate = self.get_tickdate_for_task(row['task'])
					if not tickdate:
						tickdate = "[no date]"

			# Insert all columns
			modelrow = [False, row['tickmark'], tickdate, row['prio'], task, row['due'], path.name, tags0, row['comment'], row['actionable'], row['open'], row['id'], tags, row['task']]
			modelrow[0] = self._filter_item(modelrow)
			myiter = self.real_model.append(iter, modelrow)

			if row['haschildren']:
				self._append_tasks(row, myiter, path_cache) # recurs


	def _toggle_all_tasks(self, tick_status, dialog):
		toggle_all_ok = self._toggle_all_tasks_dialog(dialog)
		
		if toggle_all_ok:
			model = self.get_model()	
			model.foreach(self._do_toggle_all_tasks)

	def _toggle_all_tasks_dialog(self, main_dialog):
		task_count = self.get_n_tasks()

		response = QuestionDialog(main_dialog, (_('Untick ' + str(task_count) + ' tasks?'),
						# T: Short message text on first time use of task list plugin
						_('You are about to untick ' + str(task_count) + ' tasks.\n\n'
						  'Are you sure?\n')
						# T: Long message text on first time use of task list plugin
					) ).run()

		return response
		
	def _do_toggle_all_tasks(self, model, path, iter):

		# skip tasks which are not ticked but are shown due to ticked children
		if model[path][self.TICKED_COL]:
			page = Path( model[path][self.PAGE_COL] )
			text = self._get_raw_text(model[path])
			pageview = self.opener.open_page(page)
			pageview.find(text)

			task = model[path][self.TASK0_COL]

			# if pageview has not been updated to add tickdate to task into the text, then the tickdate is
			# preserved at least within the index (for the time being)
			self.index_ext.put_new_tickdate_to_db(task)
			
			self.win.pageview.toggle_checkbox()


	def get_tickdate_for_task(self, task):
		date = self.index_ext.get_tickdate_from_db(task)
		if date:
			return date[2]
		return False

	def put_tickdate_to_db(self, task, date):
		self.index_ext.put_existing_tickdate_to_db(task, date, tickmark=True)


	def set_filter_actionable(self, filter):
		'''Set filter state for non-actionable items
		@param filter: if C{False} all items are shown, if C{True} only actionable items
		'''
		self.filter_actionable = filter
		self._eval_filter()

	def set_filter(self, string):
		# TODO allow more complex queries here - same parse as for search
		if string:
			inverse = False
			if string.lower().startswith('not '):
				# Quick HACK to support e.g. "not @waiting"
				inverse = True
				string = string[4:]
			self.filter = (inverse, string.strip().lower())
		else:
			self.filter = None
		self._eval_filter()

	def get_labels(self):
		'''Get all labels that are in use
		@returns: a dict with labels as keys and the number of tasks
		per label as value
		'''
		return self._labels

	def get_tags(self):
		'''Get all tags that are in use
		@returns: a dict with tags as keys and the number of tasks
		per tag as value
		'''
		return self._tags

	def get_n_tasks(self):
		'''Get the number of tasks in the list
		@returns: total number
		'''
		model = self.get_model()
		counter = [0]
		def count(model, path, iter):
			if not model[iter][self.OPEN_COL]:
				# only count open items
				counter[0] += 1
		model.foreach(count)
		return counter[0]

	def get_statistics(self):
		statsbyprio = {}

		def count(model, path, iter):
			# only count open items
			row = model[iter]
			if row[self.OPEN_COL]:
				prio = row[self.PRIO_COL]
				statsbyprio.setdefault(prio, 0)
				statsbyprio[prio] += 1

		self.real_model.foreach(count)

		if statsbyprio:
			total = reduce(int.__add__, statsbyprio.values())
			highest = max([0] + statsbyprio.keys())
			stats = [statsbyprio.get(k, 0) for k in range(highest+1)]
			stats.reverse() # highest first
			return total, stats
		else:
			return 0, []

	def set_tag_filter(self, tags=None, labels=None):
		if tags:
			self.tag_filter = [tag.lower() for tag in tags]
		else:
			self.tag_filter = None

		if labels:
			self.label_filter = [label.lower() for label in labels]
		else:
			self.label_filter = None

		self._eval_filter()

	def _eval_filter(self):
		logger.debug('Filtering task list history with labels: %s tags: %s, filter: %s', self.label_filter, self.tag_filter, self.filter)

		def filter(model, path, iter):
			visible = self._filter_item(model[iter])
			model[iter][self.VIS_COL] = visible
			if visible:
				parent = model.iter_parent(iter)
				while parent:
					model[parent][self.VIS_COL] = visible
					parent = model.iter_parent(parent)

		self.real_model.foreach(filter)
		self.expand_all()

	def _filter_item(self, modelrow):
		# This method filters case insensitive because both filters and
		# text are first converted to lower case text.
		visible = True
		
		if not modelrow[self.TICKED_COL]:
			visible = False

		#if not modelrow[self.OPEN_COL] \
		#or (not modelrow[self.ACT_COL] and self.filter_actionable):
		#	visible = False

		description = modelrow[self.TASK_COL].decode('utf-8').lower()
		pagename = modelrow[self.PAGE_COL].decode('utf-8').lower()
		tags = [t.lower() for t in modelrow[self.TAGS_COL]]
		comments = modelrow[self.TASK_COMMENT_COL].decode('utf-8').lower()

		if visible and self.label_filter:
			# Any labels need to be present
			for label in self.label_filter:
				if label in description:
					break
			else:
				visible = False # no label found

		if visible and self.tag_filter:
			# Any tag should match --> changed to all to 'activate'
			# 'hidden' functionality for multiple selection of tags with
			# the use of the ctrl key. 
			if (_NO_TAGS in self.tag_filter and not tags) \
			or all(tag in tags for tag in self.tag_filter):
				visible = True
			else:
				visible = False

		if visible and self.filter:
			# And finally the filter string should match
			# FIXME: we are matching against markup text here - may fail for some cases
			inverse, string = self.filter
			match = string in description or string in pagename or string in comments
			if (not inverse and not match) or (inverse and match):
				visible = False

		return visible

	def do_row_activated(self, path, column):
		model = self.get_model()
		page = Path( model[path][self.PAGE_COL] )
		text = self._get_raw_text(model[path])
		pageview = self.opener.open_page(page)
		pageview.find(text)
		# I need to get the task text only to compare against the model below
		task_text = model[path][self.TASK_COL]
				
		# now check if column Tick was clicked and tick box and action on page selected
		if column.get_title() == "Tick":
			real_path = model.convert_path_to_child_path(path)
			# get the real path to self.real_model no matter what filter or tag is selected
			if (self.label_filter or self.tag_filter or self.filter):
				real_path = self.get_real_path(task_text)
			self.real_model[real_path][self.TICKED_COL] = not self.real_model[real_path][self.TICKED_COL]
			
			task = self.real_model[real_path][self.TASK0_COL]
			
			# the tick date is removed from index no matter if pageview removes tickdate from task within text
			self.index_ext.del_tickdate_from_db(task)

			self.win.pageview.toggle_checkbox()

		
	def get_real_path(self, text):
		'''
		this method looks for the correct path in the real_model according to the selected row in treeview.
		As treeview can be filtered against tag etc., the only reliable way to find the right path (even with children) is to
		compare the text selected against the text in self.real_model and then return the path
		'''
		# start from the beginning
		iter = self.real_model.get_iter_first()
		while iter:
			path = self.real_model.get_path(iter)
			if self.real_model[path][self.TASK_COL] == text:
				#parent found
				return path
			if self.real_model.iter_has_child(iter):
				# there are children
				n_children = self.real_model.iter_n_children(iter)
				# how many?
				for child_no in range(0, n_children):
					child_iter = self.real_model.iter_nth_child(iter, child_no)
					child_path = self.real_model.get_path(child_iter)
					if self.real_model[child_path][self.TASK_COL] == text:
						# so there is a child found
						return child_path
			iter = self.real_model.iter_next(iter)

	def _get_raw_text(self, task):
		id = task[self.TASKID_COL]
		row = self.index_ext.get_task(id)
		return row['description']

	def do_initialize_popup(self, menu):
		item = gtk.ImageMenuItem('gtk-copy')
		item.connect('activate', self.copy_to_clipboard)
		menu.append(item)
		self.populate_popup_expand_collapse(menu)

	def copy_to_clipboard(self, *a):
		'''Exports currently visible elements from the tasks list'''
		logger.debug('Exporting to clipboard current view of task list.')
		text = self.get_visible_data_as_csv()
		Clipboard.set_text(text)
			# TODO set as object that knows how to format as text / html / ..
			# unify with export hooks

	def get_visible_data_as_csv(self):
		text = ""
		for indent, prio, desc, date, tags0, comment, page in self.get_visible_data():
			prio = str(prio)
			desc = decode_markup_text(desc)
			desc = '"' + desc.replace('"', '""') + '"'
			text += ",".join((prio, desc, date, tags0, comment, page)) + "\n"
		return text


	# TODO: Show filter (e.g. list of selected tags) which lead to the print out
	def get_visible_data_as_html(self):
		html = '''\
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd";>
<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
		<title>Task List - Zim</title>
		<meta name='Generator' content='Zim [%% zim.version %%]'>
		<style type='text/css'>
			table.tasklist {
				border-width: 1px;
				border-spacing: 2px;
				border-style: solid;
				border-color: gray;
				border-collapse: collapse;
			}
			table.tasklist th {
				border-width: 1px;
				padding: 8px;
				border-style: solid;
				border-color: gray;
				text-align: left;
				background-color: gray;
				color: white;
			}
			table.tasklist td {
				border-width: 1px;
				padding: 8px;
				border-style: solid;
				border-color: gray;
				text-align: left;
			}
			.high {background-color: %s}
			.medium {background-color: %s}
			.alert {background-color: %s}
		</style>
	</head>
	<body>


<h1>Task List - Zim</h1>
<table class="tasklist">
<tr><th>Status</th><th>Prio</th><th>Task</th><th>Date</th><th>Tags</th><th>Comments</th></tr>
''' % (HIGH_COLOR, MEDIUM_COLOR, ALERT_COLOR)

		today    = str( datetime.date.today() )
		tomorrow = str( datetime.date.today() + datetime.timedelta(days=1))
		dayafter = str( datetime.date.today() + datetime.timedelta(days=2))
		for indent, status, prio, desc, date, tags0, comment, page in self.get_visible_data():

			if status == 1: status_str = '<td>Closed</td>'
			if status == 0: status_str = '<td>Open</td>'

			if prio >= 3: prio = '<td class="high">%s</td>' % prio
			elif prio == 2: prio = '<td class="medium">%s</td>' % prio
			elif prio == 1: prio = '<td class="alert">%s</td>' % prio
			else: prio = '<td>%s</td>' % prio

			if date and date <= today: date = '<td class="high">%s</td>' % date
			elif date == tomorrow: date = '<td class="medium">%s</td>' % date
			elif date == dayafter: date = '<td class="alert">%s</td>' % date
			else: date = '<td>%s</td>' % date

			desc = '<td>%s%s</td>' % ('&nbsp;' * (4 * indent), desc)
			if "\n" in tags0:
				tags0 = tags0.replace("\n", "<br />")
			tags0 = '<td>%s</td>' % tags0
			if "\n" in comment:
				comment = comment.replace("\n", "<br />")
			comment = '<td>%s</td>' % comment
			page = '<td>%s</td>' % page

			html += '<tr>' + status_str + prio + desc + date + tags0 + comment + '</tr>\n'

		html += '''\
</table>

	</body>

</html>
'''
		return html

	def get_visible_data(self):
		rows = []

		def collect(model, path, iter):
			indent = len(path) - 1 # path is tuple with indexes

			row = model[iter]
			status = row[self.TICKED_COL]
			prio = row[self.PRIO_COL]
			desc = row[self.TASK_COL].decode('utf-8')
			date = row[self.DATE_COL]
			page = row[self.PAGE_COL].decode('utf-8')
			tags0 = row[self.TAGS0_COL].decode('utf-8')
			comment = row[self.TASK_COMMENT_COL].decode('utf-8')

			if date == _NO_DATE:
				date = ''

			rows.append((indent, status, prio, desc, date, tags0, comment, page))

		model = self.get_model()
		model.foreach(collect)

		return rows

# Need to register classes defining gobject signals
#~ gobject.type_register(TaskListTreeView)
# NOTE: enabling this line causes this treeview to have wrong theming under default ubuntu them !???

Follow ups

References