1
# -*- coding: utf-8 -*-
2
# -----------------------------------------------------------------------------
3
# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4
# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
6
# This program is free software: you can redistribute it and/or modify it under
7
# the terms of the GNU General Public License as published by the Free Software
8
# Foundation, either version 3 of the License, or (at your option) any later
11
# This program is distributed in the hope that it will be useful, but WITHOUT
12
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16
# You should have received a copy of the GNU General Public License along with
17
# this program. If not, see <http://www.gnu.org/licenses/>.
18
# -----------------------------------------------------------------------------
22
import xml.sax.saxutils as saxutils
26
from GTG.core import CoreConfig
27
from GTG.core.task import Task
28
from GTG.gtk.browser.CellRendererTags import CellRendererTags
29
from liblarch_gtk import TreeView
30
from GTG.gtk import colors
31
from GTG.tools import dates
34
class AutoExpandTreeView(TreeView):
35
"""TreeView which hide the expander column when not needed"""
37
def __init__(self, tree, desc):
38
TreeView.__init__(self, tree, desc)
39
self.show_expander = False
40
self.treemodel.connect("row-has-child-toggled", self.__show_expander_col)
41
self.__show_expander_col(self.treemodel, None, None)
43
def __has_child(self, model, path, iter):
44
if model.iter_has_child(iter):
45
self.show_expander = True
48
def __show_expander_col(self, treemodel, path, iter):
49
self.show_expander = False
50
treemodel.foreach(self.__has_child)
51
self.set_show_expanders(self.show_expander)
53
class TreeviewFactory():
55
def __init__(self,requester,config):
57
self.mainview = self.req.get_tasks_tree()
60
#Initial unactive color
61
#This is a crude hack. As we don't have a reference to the
62
#treeview to retrieve the style, we save that color when we
64
self.unactive_color = "#888a85"
66
# List of keys for connecting/disconnecting Tag tree
69
# Cache tags treeview for on_rename_tag callback
72
#############################
73
#Functions for tasks columns
74
################################
76
def _has_hidden_subtask(self,task):
78
display_count = self.mainview.node_n_children(task.get_id())
81
for tid in task.get_children():
82
sub_task = self.req.get_task(tid)
83
if sub_task and sub_task.get_status() == Task.STA_ACTIVE:
84
real_count = real_count + 1
85
return display_count < real_count
87
def task_bg_color(self,tags,bg):
88
if self.config.get('bg_color_enable'):
89
return colors.background_color(tags,bg)
93
#return an ordered list of tags of a task
94
def task_tags_column(self,node):
95
tags = node.get_tags()
96
tags.sort(key = lambda x: x.get_name())
100
def task_title_column(self, node):
101
return saxutils.escape(node.get_title())
104
def task_label_column(self, node):
106
#we mark in bold tasks which are due now and those marked as Now (fuzzy
108
due = node.get_due_date()
109
if (due.days_left == 0 or due == dates.NOW):
110
str_format = "<b>%s</b>"
111
if self._has_hidden_subtask(node):
112
str_format = "<span color='%s'>%s</span>"\
113
%(self.unactive_color,str_format)
114
title = str_format % saxutils.escape(node.get_title())
116
# color = self.treeview.style.text[gtk.STATE_INSENSITIVE].to_string()
118
if node.get_status() == Task.STA_ACTIVE:
119
count = self.mainview.node_n_children(node.get_id(),recursive=True)
121
title += " (%s)" % count
123
if self.config.get("contents_preview_enable"):
124
excerpt = saxutils.escape(node.get_excerpt(lines=1, \
125
strip_tags=True, strip_subtasks=True))
126
title += " <span size='small' color='%s'>%s</span>" \
127
%(self.unactive_color, excerpt)
128
elif node.get_status() == Task.STA_DISMISSED:
129
title = "<span color='%s'>%s</span>"%(self.unactive_color, title)
133
def task_sdate_column(self,node):
134
return node.get_start_date().to_readable_string()
136
def task_duedate_column(self,node):
137
return node.get_due_date().to_readable_string()
139
def task_cdate_column(self,node):
140
return node.get_closed_date().to_readable_string()
142
def start_date_sorting(self,task1,task2,order):
143
sort = self.__date_comp(task1,task2,'start',order)
146
def due_date_sorting(self,task1,task2,order):
147
sort = self.__date_comp(task1,task2,'due',order)
150
def closed_date_sorting(self,task1,task2,order):
151
sort = self.__date_comp(task1,task2,'closed',order)
154
def title_sorting(self,task1,task2,order):
155
return cmp(task1.get_title(),task2.get_title())
157
def __date_comp(self,task1,task2,para,order):
158
'''This is a quite complex method to sort tasks by date,
159
handling fuzzy date and complex situation.
160
Return -1 if nid1 is before nid2, return 1 otherwise
164
t1 = task1.get_start_date()
165
t2 = task2.get_start_date()
167
t1 = task1.get_due_date()
168
t2 = task2.get_due_date()
169
elif para == 'closed':
170
t1 = task1.get_closed_date()
171
t2 = task2.get_closed_date()
173
raise ValueError('invalid date comparison parameter: %s')%para
179
def reverse_if_descending(s):
180
"""Make a cmp() result relative to the top instead of following
181
user-specified sort direction"""
182
if order == gtk.SORT_ASCENDING:
188
# Put fuzzy dates below real dates
189
if isinstance(t1, dates.FuzzyDate) \
190
and not isinstance(t2, dates.FuzzyDate):
191
sort = reverse_if_descending(1)
192
elif isinstance(t2, dates.FuzzyDate) \
193
and not isinstance(t1, dates.FuzzyDate):
194
sort = reverse_if_descending(-1)
196
if sort == 0: # Group tasks with the same tag together for visual cleanness
197
t1_tags = task1.get_tags_name()
199
t2_tags = task2.get_tags_name()
201
sort = reverse_if_descending(cmp(t1_tags, t2_tags))
203
if sort == 0: # Break ties by sorting by title
204
t1_title = task1.get_title()
205
t2_title = task2.get_title()
206
t1_title = locale.strxfrm(t1_title)
207
t2_title = locale.strxfrm(t2_title)
208
sort = reverse_if_descending(cmp(t1_title, t2_title))
212
#############################
213
#Functions for tags columns
214
#############################
215
def tag_name(self,node):
216
label = node.get_attribute("label")
217
if label.startswith('@'):
220
if node.get_attribute("nonworkview") == "True":
221
return "<span color='%s'>%s</span>" %(self.unactive_color, label)
225
def get_tag_count(self,node):
226
# FIXME: is this good idea?
227
if node.get_id() == 'search':
230
toreturn = node.get_active_tasks_count()
231
return "<span color='%s'>%s</span>" %(self.unactive_color,toreturn)
233
def is_tag_separator_filter(self,tag):
234
return tag.get_attribute('special') == 'sep'
236
def tag_sorting(self,t1,t2,order):
237
t1_sp = t1.get_attribute("special")
238
t2_sp = t2.get_attribute("special")
239
t1_name = locale.strxfrm(t1.get_name())
240
t2_name = locale.strxfrm(t2.get_name())
241
if not t1_sp and not t2_sp:
242
return cmp(t1_name, t2_name)
243
elif not t1_sp and t2_sp:
245
elif t1_sp and not t2_sp:
248
t1_order = t1.get_attribute("order")
249
t2_order = t2.get_attribute("order")
250
return cmp(t1_order, t2_order)
252
def ontag_task_dnd(self,source,target):
253
if target.startswith('@'):
254
task = self.req.get_task(source)
256
elif target == 'gtg-tags-none':
257
task = self.req.get_task(source)
258
for t in task.get_tags_name():
261
############################################
262
######## The Factory #######################
263
############################################
264
def tags_completion_treeview(self, tree):
266
desc['tagname'] = {'value': [str, self.tag_name]}
268
return TreeView(tree, desc)
270
def tags_treeview(self,tree):
276
col['renderer'] = ['markup', gtk.CellRendererText()]
277
col['value'] = [str, lambda node: node.get_id()]
278
col['visible'] = False
280
col['sorting_func'] = self.tag_sorting
286
render_tags = CellRendererTags()
287
render_tags.set_property('ypad', 3)
288
col['title'] = _("Tags")
289
col['renderer'] = ['tag',render_tags]
290
col['value'] = [gobject.TYPE_PYOBJECT,lambda node: node]
291
col['expandable'] = False
292
col['resizable'] = False
299
render_text = gtk.CellRendererText()
300
render_text.set_property('ypad', 3)
302
# FIXME Is there any way how to disable renaming for certain tags?
303
render_text.set_property('editable', True)
304
render_text.connect("edited", self.on_rename_tag)
305
col['renderer'] = ['markup',render_text]
306
col['value'] = [str,self.tag_name]
307
col['expandable'] = True
308
col['new_column'] = False
313
col_name = 'tagcount'
315
render_text = gtk.CellRendererText()
316
render_text.set_property('xpad', 3)
317
render_text.set_property('ypad', 3)
318
render_text.set_property('xalign', 1.0)
319
col['renderer'] = ['markup',render_text]
320
col['value'] = [str,self.get_tag_count]
321
col['expandable'] = False
322
col['new_column'] = False
326
self.enable_update_tags()
328
return self.build_tag_treeview(tree,desc)
330
def on_rename_tag(self, renderer, path, new_name):
331
model = self.tags_view.get_model()
332
my_iter = model.get_iter(path)
333
tag_id = model.get_value(my_iter, 0)
334
tag = self.req.get_tag(tag_id)
336
if tag.is_search_tag():
337
self.req.rename_tag(tag_id, new_name)
339
print "FIXME: renaming tags is not implemented"
341
def enable_update_tags(self):
342
self.tag_cllbcks = []
344
tasks = self.req.get_tasks_tree()
345
for event in 'node-added-inview', 'node-modified-inview', 'node-deleted-inview':
346
handle = tasks.register_cllbck(event, self._update_tags)
347
self.tag_cllbcks.append((event, handle))
349
def disable_update_tags(self):
350
tasks = self.req.get_tasks_tree()
351
for event, handle in self.tag_cllbcks:
352
tasks.deregister_cllbck(event, handle)
353
self.tag_cllbcks = []
355
def _update_tags(self, node_id, path):
356
tree = self.req.get_tag_tree().get_basetree()
357
tree.refresh_node('gtg-tags-all')
358
tree.refresh_node('gtg-tags-none')
360
search_parent = self.req.get_tag(CoreConfig.SEARCH_TAG)
361
for search_tag in search_parent.get_children():
362
tree.refresh_node(search_tag)
364
task = self.req.get_task(node_id)
366
for t in self.req.get_task(node_id).get_tags():
367
tree.refresh_node(t.get_name())
369
def active_tasks_treeview(self,tree):
370
#Build the title/label/tags columns
371
desc = self.common_desc_for_tasks(tree)
374
col_name = 'startdate'
376
col['title'] = _("Start date")
377
col['expandable'] = False
378
col['resizable'] = False
379
col['value'] = [str,self.task_sdate_column]
381
col['sorting_func'] = self.start_date_sorting
387
col['title'] = _("Due")
388
col['expandable'] = False
389
col['resizable'] = False
390
col['value'] = [str,self.task_duedate_column]
392
col['sorting_func'] = self.due_date_sorting
395
#Returning the treeview
396
treeview = self.build_task_treeview(tree,desc)
397
treeview.set_sort_column('duedate')
400
def closed_tasks_treeview(self,tree):
401
#Build the title/label/tags columns
402
desc = self.common_desc_for_tasks(tree)
405
col_name = 'closeddate'
407
col['title'] = _("Closed date")
408
col['expandable'] = False
409
col['resizable'] = False
410
col['value'] = [str,self.task_cdate_column]
412
col['sorting_func'] = self.closed_date_sorting
415
#Returning the treeview
416
treeview = self.build_task_treeview(tree,desc)
417
treeview.set_sort_column('closeddate')
421
#This build the first tag/title columns, common
422
#to both active and closed tasks treeview
423
def common_desc_for_tasks(self,tree):
425
#invisible 'title' column
428
render_text = gtk.CellRendererText()
429
render_text.set_property("ellipsize", pango.ELLIPSIZE_END)
430
col['renderer'] = ['markup',render_text]
431
col['value'] = [str,self.task_title_column]
432
col['visible'] = False
434
col['sorting_func'] = self.title_sorting
437
# "tags" column (no title)
440
render_tags = CellRendererTags()
441
render_tags.set_property('xalign', 0.0)
442
col['renderer'] = ['tag_list',render_tags]
443
col['value'] = [gobject.TYPE_PYOBJECT,self.task_tags_column]
444
col['expandable'] = False
445
col['resizable'] = False
452
col['title'] = _("Title")
453
render_text = gtk.CellRendererText()
454
render_text.set_property("ellipsize", pango.ELLIPSIZE_END)
455
col['renderer'] = ['markup',render_text]
456
col['value'] = [str,self.task_label_column]
457
col['expandable'] = True
458
col['resizable'] = True
459
col['sorting'] = 'title'
464
def build_task_treeview(self,tree,desc):
465
treeview = AutoExpandTreeView(tree,desc)
466
#Now that the treeview is done, we can polish
467
treeview.set_main_search_column('label')
468
treeview.set_expander_column('label')
469
treeview.set_dnd_name('gtg/task-iter-str')
471
treeview.set_bg_color(self.task_bg_color,'tags')
472
# Global treeview properties
473
treeview.set_property("enable-tree-lines", False)
474
treeview.set_rules_hint(False)
475
treeview.set_multiple_selection(True)
476
#Updating the unactive color (same for everyone)
477
self.unactive_color = \
478
treeview.style.text[gtk.STATE_INSENSITIVE].to_string()
481
def build_tag_treeview(self,tree,desc):
482
treeview = AutoExpandTreeView(tree,desc)
483
# Global treeview properties
484
treeview.set_property("enable-tree-lines", False)
485
treeview.set_rules_hint(False)
486
treeview.set_row_separator_func(self.is_tag_separator_filter)
487
treeview.set_headers_visible(False)
488
treeview.set_dnd_name('gtg/tag-iter-str')
489
treeview.set_dnd_external('gtg/task-iter-str',self.ontag_task_dnd)
490
#Updating the unactive color (same for everyone)
491
self.unactive_color = \
492
treeview.style.text[gtk.STATE_INSENSITIVE].to_string()
493
treeview.set_sort_column('tag_id')
494
self.tags_view = treeview