~ubuntu-branches/ubuntu/trusty/gtg/trusty

« back to all changes in this revision

Viewing changes to GTG/gtk/browser/treeview_factory.py

  • Committer: Package Import Robot
  • Author(s): Luca Falavigna
  • Date: 2012-04-10 23:08:21 UTC
  • mfrom: (1.1.8)
  • Revision ID: package-import@ubuntu.com-20120410230821-q6it7f4d7elut6pv
Tags: 0.2.9-1
* New upstream release (Closes: #668096).
  - Implement a search text box (Closes: #650279).
  - Window title reflects active tasks (LP: #537096).
  - Fix misbehaviours of the indicator applet (LP: #548836, #676353).
  - Fix crash when selecting notification area plugin twice (LP: #550321).
  - Fix sorting of tasks by date (LP: #556159).
  - Fix excessive delays at startup (LP: #558600).
  - Fix crash with dates having unknown values (LP: #561449).
  - Fix crash issued when pressing delete key (LP: #583103).
  - Keep notification plugin enabled after logoff (LP: #617257).
  - Fix Hamster plugin to work with recent Hamster versions (LP: #620313).
  - No longer use non-unicode strings (LP: #680632).
  - New RTM sync mechanism (LP: #753327).
  - Fix crashes while handling XML storage file (LP: #916474, #917634).
* debian/patches/*:
  - Drop all patches, they have been merged upstream.
* debian/patches/shebang.patch:
  - Fix shebang line.
* debian/patches/manpages.patch:
  - Fix some groff warnings in gtg_new_task man page
* debian/compat:
  - Bump compatibility level to 9.
* debian/control:
  - Bump X-Python-Version to >= 2.6.
  - Add python-liblarch and python-liblarch-gtk to Depends field.
  - Add python-cheetah, python-geoclue, python-gnomekeyring,
    python-launchpadlib and python-suds to Suggests field.
  - Bump Standards-Version to 3.9.3.
* debian/copyright:
  - Refresh copyright information.
  - Format now points to copyright-format site.
* debian/rules:
  - Make gtcli_bash_completion script executable.
* debian/watch:
  - Update watch file.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
 
5
#
 
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
 
9
# version.
 
10
#
 
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
 
14
# details.
 
15
#
 
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
# -----------------------------------------------------------------------------
 
19
import gtk
 
20
import gobject
 
21
import pango
 
22
import xml.sax.saxutils as saxutils
 
23
import locale
 
24
 
 
25
from GTG                              import _
 
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
 
32
 
 
33
 
 
34
class AutoExpandTreeView(TreeView):
 
35
    """TreeView which hide the expander column when not needed"""
 
36
 
 
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)
 
42
 
 
43
    def __has_child(self, model, path, iter):
 
44
        if model.iter_has_child(iter):
 
45
            self.show_expander = True
 
46
            return True
 
47
 
 
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)
 
52
 
 
53
class TreeviewFactory():
 
54
 
 
55
    def __init__(self,requester,config):
 
56
        self.req = requester
 
57
        self.mainview = self.req.get_tasks_tree()
 
58
        self.config = config
 
59
        
 
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 
 
63
        #build the treeview.
 
64
        self.unactive_color = "#888a85"
 
65
 
 
66
        # List of keys for connecting/disconnecting Tag tree
 
67
        self.tag_cllbcks = []
 
68
 
 
69
        # Cache tags treeview for on_rename_tag callback
 
70
        self.tags_view = None
 
71
        
 
72
    #############################
 
73
    #Functions for tasks columns
 
74
    ################################
 
75
        
 
76
    def _has_hidden_subtask(self,task):
 
77
        #not recursive
 
78
        display_count = self.mainview.node_n_children(task.get_id())
 
79
        real_count = 0
 
80
        if task.has_child():
 
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
 
86
    
 
87
    def task_bg_color(self,tags,bg):
 
88
        if self.config.get('bg_color_enable'):
 
89
            return colors.background_color(tags,bg)
 
90
        else:
 
91
            return None
 
92
    
 
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())
 
97
        return tags
 
98
        
 
99
    #task title
 
100
    def task_title_column(self, node):
 
101
        return saxutils.escape(node.get_title())
 
102
        
 
103
    #task title/label
 
104
    def task_label_column(self, node):
 
105
        str_format = "%s"
 
106
        #we mark in bold tasks which are due now and those marked as Now (fuzzy
 
107
        # date)
 
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())
 
115
        #FIXME
 
116
#        color = self.treeview.style.text[gtk.STATE_INSENSITIVE].to_string()
 
117
        color = "red"
 
118
        if node.get_status() == Task.STA_ACTIVE:
 
119
            count = self.mainview.node_n_children(node.get_id(),recursive=True)
 
120
            if count != 0:
 
121
                title += " (%s)" % count
 
122
            
 
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)
 
130
        return title
 
131
        
 
132
    #task start date
 
133
    def task_sdate_column(self,node):
 
134
        return node.get_start_date().to_readable_string()
 
135
        
 
136
    def task_duedate_column(self,node):
 
137
        return node.get_due_date().to_readable_string()
 
138
        
 
139
    def task_cdate_column(self,node):
 
140
        return node.get_closed_date().to_readable_string()
 
141
        
 
142
    def start_date_sorting(self,task1,task2,order):
 
143
        sort = self.__date_comp(task1,task2,'start',order)
 
144
        return sort
 
145
        
 
146
    def due_date_sorting(self,task1,task2,order):
 
147
        sort = self.__date_comp(task1,task2,'due',order)
 
148
        return sort
 
149
    
 
150
    def closed_date_sorting(self,task1,task2,order):
 
151
        sort = self.__date_comp(task1,task2,'closed',order)
 
152
        return sort
 
153
        
 
154
    def title_sorting(self,task1,task2,order):
 
155
        return cmp(task1.get_title(),task2.get_title())
 
156
        
 
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
 
161
        '''
 
162
        if task1 and task2:
 
163
            if para == 'start':
 
164
                t1 = task1.get_start_date()
 
165
                t2 = task2.get_start_date()
 
166
            elif para == 'due':
 
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()
 
172
            else:
 
173
                raise ValueError('invalid date comparison parameter: %s')%para
 
174
            sort = cmp(t2,t1)
 
175
        else:
 
176
            sort = 0
 
177
        
 
178
        #local function
 
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:
 
183
                return s
 
184
            else:
 
185
                return -1*s
 
186
 
 
187
        if sort == 0:
 
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)
 
195
        
 
196
        if sort == 0: # Group tasks with the same tag together for visual cleanness 
 
197
            t1_tags = task1.get_tags_name()
 
198
            t1_tags.sort()
 
199
            t2_tags = task2.get_tags_name()
 
200
            t2_tags.sort()
 
201
            sort = reverse_if_descending(cmp(t1_tags, t2_tags))
 
202
            
 
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))
 
209
        
 
210
        return sort
 
211
        
 
212
    #############################
 
213
    #Functions for tags columns
 
214
    #############################
 
215
    def tag_name(self,node):
 
216
        label = node.get_attribute("label")
 
217
        if label.startswith('@'):
 
218
            label = label[1:]
 
219
 
 
220
        if node.get_attribute("nonworkview") == "True":
 
221
            return "<span color='%s'>%s</span>" %(self.unactive_color, label)
 
222
        else:
 
223
            return label
 
224
        
 
225
    def get_tag_count(self,node):
 
226
# FIXME: is this good idea?
 
227
        if node.get_id() == 'search':
 
228
            return ""
 
229
        else:
 
230
            toreturn = node.get_active_tasks_count()
 
231
            return "<span color='%s'>%s</span>" %(self.unactive_color,toreturn)
 
232
        
 
233
    def is_tag_separator_filter(self,tag):
 
234
        return tag.get_attribute('special') == 'sep'
 
235
        
 
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:
 
244
            return 1
 
245
        elif t1_sp and not t2_sp:
 
246
            return -1
 
247
        else:
 
248
            t1_order = t1.get_attribute("order")
 
249
            t2_order = t2.get_attribute("order")
 
250
            return cmp(t1_order, t2_order)
 
251
            
 
252
    def ontag_task_dnd(self,source,target):
 
253
        if target.startswith('@'):
 
254
            task = self.req.get_task(source)
 
255
            task.add_tag(target)
 
256
        elif target == 'gtg-tags-none':
 
257
            task = self.req.get_task(source)
 
258
            for t in task.get_tags_name():
 
259
                task.remove_tag(t)
 
260
 
 
261
    ############################################
 
262
    ######## The Factory #######################
 
263
    ############################################
 
264
    def tags_completion_treeview(self, tree):
 
265
        desc = {}
 
266
        desc['tagname'] = {'value': [str, self.tag_name]}
 
267
 
 
268
        return TreeView(tree, desc)
 
269
 
 
270
    def tags_treeview(self,tree):
 
271
        desc = {}
 
272
 
 
273
        #Tag id
 
274
        col_name = 'tag_id'
 
275
        col = {}
 
276
        col['renderer'] = ['markup', gtk.CellRendererText()]
 
277
        col['value'] = [str, lambda node: node.get_id()]
 
278
        col['visible'] = False
 
279
        col['order'] = 0
 
280
        col['sorting_func'] = self.tag_sorting
 
281
        desc[col_name] = col
 
282
        
 
283
        #Tags color
 
284
        col_name = 'color'
 
285
        col = {}
 
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
 
293
        col['order'] = 1
 
294
        desc[col_name] = col
 
295
        
 
296
        #Tag names
 
297
        col_name = 'tagname'
 
298
        col = {}
 
299
        render_text = gtk.CellRendererText()
 
300
        render_text.set_property('ypad', 3)
 
301
        # Allow renaming
 
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
 
309
        col['order'] = 2
 
310
        desc[col_name] = col
 
311
        
 
312
        #Tag count
 
313
        col_name = 'tagcount'
 
314
        col = {}
 
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
 
323
        col['order'] = 3
 
324
        desc[col_name] = col
 
325
 
 
326
        self.enable_update_tags()
 
327
 
 
328
        return self.build_tag_treeview(tree,desc)
 
329
 
 
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)
 
335
 
 
336
        if tag.is_search_tag():
 
337
            self.req.rename_tag(tag_id, new_name)
 
338
        else:
 
339
            print "FIXME: renaming tags is not implemented"
 
340
 
 
341
    def enable_update_tags(self):
 
342
        self.tag_cllbcks = []
 
343
 
 
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))
 
348
 
 
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 = []
 
354
 
 
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')
 
359
 
 
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)
 
363
 
 
364
        task = self.req.get_task(node_id)
 
365
        if task:
 
366
            for t in self.req.get_task(node_id).get_tags():
 
367
                tree.refresh_node(t.get_name())
 
368
    
 
369
    def active_tasks_treeview(self,tree):
 
370
        #Build the title/label/tags columns
 
371
        desc = self.common_desc_for_tasks(tree)
 
372
        
 
373
        # "startdate" column
 
374
        col_name = 'startdate'
 
375
        col = {}
 
376
        col['title'] = _("Start date")
 
377
        col['expandable'] = False
 
378
        col['resizable'] = False
 
379
        col['value'] = [str,self.task_sdate_column]
 
380
        col['order'] = 3
 
381
        col['sorting_func'] = self.start_date_sorting
 
382
        desc[col_name] = col
 
383
 
 
384
        # 'duedate' column
 
385
        col_name = 'duedate'
 
386
        col = {}
 
387
        col['title'] = _("Due")
 
388
        col['expandable'] = False
 
389
        col['resizable'] = False
 
390
        col['value'] = [str,self.task_duedate_column]
 
391
        col['order'] = 4
 
392
        col['sorting_func'] = self.due_date_sorting
 
393
        desc[col_name] = col
 
394
 
 
395
        #Returning the treeview
 
396
        treeview = self.build_task_treeview(tree,desc)
 
397
        treeview.set_sort_column('duedate')
 
398
        return treeview
 
399
        
 
400
    def closed_tasks_treeview(self,tree):
 
401
        #Build the title/label/tags columns
 
402
        desc = self.common_desc_for_tasks(tree)
 
403
        
 
404
        # "startdate" column
 
405
        col_name = 'closeddate'
 
406
        col = {}
 
407
        col['title'] = _("Closed date")
 
408
        col['expandable'] = False
 
409
        col['resizable'] = False
 
410
        col['value'] = [str,self.task_cdate_column]
 
411
        col['order'] = 3
 
412
        col['sorting_func'] = self.closed_date_sorting
 
413
        desc[col_name] = col
 
414
 
 
415
        #Returning the treeview
 
416
        treeview = self.build_task_treeview(tree,desc)
 
417
        treeview.set_sort_column('closeddate')
 
418
        return treeview
 
419
        
 
420
    
 
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):
 
424
        desc = {}
 
425
        #invisible 'title' column
 
426
        col_name = 'title'
 
427
        col = {}
 
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
 
433
        col['order'] = 0
 
434
        col['sorting_func'] = self.title_sorting
 
435
        desc[col_name] = col
 
436
        
 
437
        # "tags" column (no title)
 
438
        col_name = 'tags'
 
439
        col = {}
 
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
 
446
        col['order'] = 1
 
447
        desc[col_name] = col
 
448
 
 
449
        # "label" column
 
450
        col_name = 'label'
 
451
        col = {}
 
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'
 
460
        col['order'] = 2
 
461
        desc[col_name] = col
 
462
        return desc
 
463
        
 
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')
 
470
        #Background colors
 
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()
 
479
        return treeview
 
480
        
 
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
 
495
        return treeview