1
2# Copyright 2009-2017 Jaap Karssenberg <jaap.karssenberg@gmail.com>
3
4# TODO: allow more complex queries for filter, in particular (NOT tag AND tag)
5#       allow multiple tabs in dialog / side pane with configurable query
6#
7# TODO: add an interface for this plugin in the WWW frontend
8#
9# TODO: commandline option
10# - open dialog
11# - output to stdout with configurable format
12# - force update, intialization
13
14# TODO: test coverage for the start date label (and due with "<")
15# TODO: test coverage for start / due date from journal page
16# TODO: test coverage for sorting in list_open_tasks
17# TODO: test coverage include / exclude sections
18# TODO: update manual
19
20
21
22from zim.plugins import PluginClass, find_extension
23from zim.actions import action
24from zim.config import StringAllowEmpty
25from zim.signals import DelayedCallback
26from zim.notebook import NotebookExtension
27
28from zim.gui.notebookview import NotebookViewExtension
29from zim.gui.widgets import RIGHT_PANE, PANE_POSITIONS
30
31from .indexer import TasksIndexer, TasksView
32from .gui import TaskListDialog, TaskListWidget
33
34
35class TaskListPlugin(PluginClass):
36
37	plugin_info = {
38		'name': _('Task List'), # T: plugin name
39		'description': _('''\
40This plugin adds a dialog showing all open tasks in
41this notebook. Open tasks can be either open checkboxes
42or items marked with tags like "TODO" or "FIXME".
43
44This is a core plugin shipping with zim.
45'''), # T: plugin description
46		'author': 'Jaap Karssenberg',
47		'help': 'Plugins:Task List'
48	}
49
50	plugin_preferences = (
51		# key, type, label, default
52		('button_in_headerbar', 'bool', _('Show tasklist button in headerbar'), True),
53			# T: preferences option
54		('embedded', 'bool', _('Show tasklist in sidepane'), False),
55			# T: preferences option
56		('pane', 'choice', _('Position in the window'), RIGHT_PANE, PANE_POSITIONS),
57			# T: preferences option
58		('with_due', 'bool', _('Show due date in sidepane'), False),
59			# T: preferences option
60	)
61
62	parser_preferences = (
63		# key, type, label, default
64		('all_checkboxes', 'bool', _('Consider all checkboxes as tasks'), True),
65			# T: label for plugin preferences dialog
66		('labels', 'string', _('Labels marking tasks'), 'FIXME, TODO', StringAllowEmpty),
67			# T: label for plugin preferences dialog - labels are e.g. "FIXME", "TODO"
68		('integrate_with_journal', 'choice', _('Use date from journal pages'), 'start', ( # T: label for preference with multiple options
69			('none', _('do not use')),        # T: choice for "Use date from journal pages"
70			('start', _('as start date for tasks')),  # T: choice for "Use date from journal pages"
71			('due', _('as due date for tasks'))       # T: choice for "Use date from journal pages"
72		)),
73		('included_subtrees', 'string', _('Section(s) to index'), '', StringAllowEmpty),
74			# T: Notebook sections to search for tasks - default is the whole tree (empty string means everything)
75		('excluded_subtrees', 'string', _('Section(s) to ignore'), '', StringAllowEmpty),
76			# T: Notebook sections to exclude when searching for tasks - default is none
77	)
78
79	plugin_notebook_properties = parser_preferences + (
80		('nonactionable_tags', 'string', _('Tags for non-actionable tasks'), '', StringAllowEmpty),
81			# T: label for plugin preferences dialog
82		('tag_by_page', 'bool', _('Turn page name into tags for task items'), False),
83			# T: label for plugin preferences dialog
84		('use_workweek', 'bool', _('Flag tasks due on Monday or Tuesday before the weekend'), False),
85			# T: label for plugin preferences dialog
86	)
87
88	hide_preferences = ('nonactionable_tags', 'tag_by_page', 'use_workweek')
89		# These are deprecated, but I don't dare to remove them yet
90		# so hide them in the configuration dialog instead
91
92
93class TaskListNotebookExtension(NotebookExtension):
94
95	__signals__ = {
96		'tasklist-changed': (None, None, ()),
97	}
98
99	def __init__(self, plugin, notebook):
100		NotebookExtension.__init__(self, plugin, notebook)
101
102		self.properties = self.plugin.notebook_properties(notebook)
103		self._parser_key = self._get_parser_key()
104
105		self.index = notebook.index
106		if self.index.get_property(TasksIndexer.PLUGIN_NAME) != TasksIndexer.PLUGIN_DB_FORMAT:
107			self.index._db.executescript(TasksIndexer.TEARDOWN_SCRIPT) # XXX
108			self.index.flag_reindex()
109
110		self.indexer = None
111		self._setup_indexer(self.index, self.index.update_iter)
112		self.connectto(self.index, 'new-update-iter', self._setup_indexer)
113
114		self.connectto(self.properties, 'changed', self.on_properties_changed)
115
116	def _setup_indexer(self, index, update_iter):
117		if self.indexer is not None:
118			self.disconnect_from(self.indexer)
119			self.indexer.disconnect_all()
120
121		self.indexer = TasksIndexer.new_from_index(index, self.properties)
122		update_iter.add_indexer(self.indexer)
123		self.connectto(self.indexer, 'tasklist-changed')
124
125	def on_properties_changed(self, properties):
126		# Need to construct new parser, re-index pages
127		if self._parser_key != self._get_parser_key():
128			self._parser_key = self._get_parser_key()
129
130			self.disconnect_from(self.indexer)
131			self.indexer.disconnect_all()
132			self.indexer = TasksIndexer.new_from_index(self.index, properties)
133			self.index.flag_reindex()
134			self.connectto(self.indexer, 'tasklist-changed')
135
136	def on_tasklist_changed(self, indexer):
137		self.emit('tasklist-changed')
138
139	def _get_parser_key(self):
140		return tuple(
141			self.properties[t[0]]
142				for t in self.plugin.parser_preferences
143		)
144
145	def teardown(self):
146		self.indexer.disconnect_all()
147		self.notebook.index.update_iter.remove_indexer(self.indexer)
148		self.index._db.executescript(TasksIndexer.TEARDOWN_SCRIPT) # XXX
149		self.index.set_property(TasksIndexer.PLUGIN_NAME, None)
150
151
152class TaskListNotebookViewExtension(NotebookViewExtension):
153
154	def __init__(self, plugin, pageview):
155		NotebookViewExtension.__init__(self, plugin, pageview)
156		self._widget = None
157		self.currently_with_due = plugin.preferences['with_due']
158		self.on_preferences_changed(plugin.preferences)
159		self.connectto(plugin.preferences, 'changed', self.on_preferences_changed)
160
161	@action(_('Task List'), icon='task-list-symbolic', menuhints='view:headerbar') # T: menu item
162	def show_task_list(self):
163		# TODO: add check + dialog for index probably_up_to_date
164
165		index = self.pageview.notebook.index
166		tasksview = TasksView.new_from_index(index)
167		properties = self.plugin.notebook_properties(self.pageview.notebook)
168		dialog = TaskListDialog.unique(self, self.pageview, tasksview, properties)
169		dialog.present()
170
171	def on_preferences_changed(self, preferences):
172		reset_widget = self.currently_with_due != preferences['with_due']
173		self.currently_with_due = preferences['with_due']
174		if self._widget and (not preferences['embedded'] or reset_widget):
175			self.remove_sidepane_widget(self._widget)
176			self._widget = None
177		if preferences['embedded'] or reset_widget:
178			if self._widget is None:
179				self._init_widget()
180				self.add_sidepane_widget(self._widget, 'pane')
181			else:
182				self._widget.task_list.refresh()
183
184		self.set_action_in_headerbar(self.show_task_list, preferences['button_in_headerbar'])
185
186	def _init_widget(self):
187		index = self.pageview.notebook.index
188		tasksview = TasksView.new_from_index(index)
189		properties = self.plugin.notebook_properties(self.pageview.notebook)
190		self._widget = TaskListWidget(tasksview, self.navigation,
191			properties, self.plugin.preferences['with_due'], self.uistate)
192
193		def on_tasklist_changed(o):
194			self._widget.task_list.refresh()
195
196		callback = DelayedCallback(10, on_tasklist_changed)
197			# Don't really care about the delay, but want to
198			# make it less blocking - now it is at least on idle
199
200		nb_ext = find_extension(self.pageview.notebook, TaskListNotebookExtension)
201		self.connectto(nb_ext, 'tasklist-changed', callback)
202