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