Ticket #471: UserList.py

File UserList.py, 33.1 kB (added by dx, 11 months ago)
Line 
1# -*- coding: utf-8 -*-
2
3#   This file is part of emesene.
4#
5#    Emesene is free software; you can redistribute it and/or modify
6#    it under the terms of the GNU General Public License as published by
7#    the Free Software Foundation; either version 2 of the License, or
8#    (at your option) any later version.
9#
10#    Emesene is distributed in the hope that it will be useful,
11#    but WITHOUT ANY WARRANTY; without even the implied warranty of
12#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13#    GNU General Public License for more details.
14#
15#    You should have received a copy of the GNU General Public License
16#    along with emesene; if not, write to the Free Software
17#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18
19import gtk
20import time
21import pango
22import gobject
23import os
24
25import UserMenu
26import GroupMenu
27import TreeViewTooltips
28from Parser import UserListDataType
29import SmileyRenderer
30import FancyAvatarRenderer
31
32import emesenelib.common
33from emesenelib.common import escape, debug
34
35import traceback
36
37class UserListModelTree(gtk.TreeStore):
38    '''this class is the model for a tree like userlist'''
39   
40    def __init__(self):
41        gtk.TreeStore.__init__(self, gtk.gdk.Pixbuf,
42            gobject.TYPE_PYOBJECT, str, str, gobject.TYPE_PYOBJECT,
43            bool, int, int, bool, str)
44        # if its a group: None, parserdata, name, type ('group'),
45        # the group object, show pixbuf (False), offline contacts,
46        # total contacts, false, ''
47       
48        # if its a user:  icon, parserdata, mail, type ('user'),
49        # the contact object, show pixbuf (True), 0 if offline 1
50        # otherwise, 0, true if blocked, status
51       
52class UserListModelList(gtk.ListStore):
53    '''this class is the model for a flat userlist'''
54   
55    def __init__(self):
56        gtk.ListStore.__init__(self, gtk.gdk.Pixbuf,
57            gobject.TYPE_PYOBJECT, str, str, gobject.TYPE_PYOBJECT,
58            bool, int, int, bool, str)
59        # if its a group: None, parserdata, name, type ('group'),
60        # the group object, show pixbuf (False), offline contacts,
61        # total contacts, false
62
63        # if its a user:  icon, parserdata, mail, type ('user'),
64        # the contact object, show pixbuf (True), 0 if offline 1
65        # otherwise, 0, true if blocked, status
66       
67class UserList(gtk.TreeView):
68    '''the userlist widget'''
69   
70    # the numbers that identify the columns of the model
71    PIX = 0
72    LABEL = 1
73    ID = 2
74    TYPE = 3
75    OBJ = 4
76    SHOW = 5
77    OFFLINE_COUNT = 6   # for group rows
78    # notoffline because if i put online you will think
79    # only on the NLN status
80    NOTOFFLINE_FLAG = 6    # for contact rows
81    TOTAL_COUNT = 7
82    BLOCKED = 8
83    STATUS = 9
84
85    __gsignals__ = { 'item-selected' : (gobject.SIGNAL_RUN_LAST, \
86        gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
87    }
88   
89       
90    def __init__(self, controller, theme, config, isTreeStore = True):
91        gtk.TreeView.__init__(self)
92       
93        self.theme = theme
94        self.config = config
95        self.controller = controller
96        self.unifiedParser = controller.getUnifiedParser()
97        self.smileysCache = {}
98
99        # dont you even try to change this value!
100        self.isTreeStore = isTreeStore
101       
102        if isTreeStore:
103            self.model = UserListModelTree()
104        else:
105            self.model = UserListModelList()
106       
107        self.tooltips = TreeViewTooltips.TreeViewTooltips(self.theme,
108            self, UserList.OBJ, UserList.ID, UserList.TYPE)
109           
110        self.treeModelFilter = self.model.filter_new()
111        self.treeModelFilter.set_visible_func(self.visible_func)
112        self.set_model(self.treeModelFilter)
113           
114        self.filterText = ''
115        # a dict with the name of the group as a key and a boolean
116        # to True if its expanded and false if collapsed
117        self.groupState = {}
118        self.restoreGroupState()
119       
120
121        statusColor = self.personalMessageColor
122       
123        if self.showCountContact:
124            self.groupLabel = '<b>%s ( %d/%d )</b>'
125        else:
126            self.groupLabel = '<b>%s</b>'
127        self.userLabel = '%s' +\
128        '\n<span size="small" foreground="' + statusColor + '">%s%s</span>'
129           
130        # label cellrenderer
131        self.crt = SmileyRenderer.SmileyRenderer()
132        # icon cellrenderer
133        self.crp = FancyAvatarRenderer.FancyAvatarRenderer(self.controller)
134       
135        column = gtk.TreeViewColumn()
136        column.set_title('Icons & Text')
137        column.set_expand(True)
138       
139        expColumn = gtk.TreeViewColumn()
140        expColumn.set_max_width(16)       
141       
142        self.append_column(expColumn)
143        self.append_column(column)
144        self.set_expander_column(expColumn)
145       
146        column.pack_start(self.crp, False)
147        column.pack_start(self.crt)
148       
149        column.add_attribute(self.crp, 'pixbuf', UserList.PIX)
150        column.add_attribute(self.crp, 'blocked', UserList.BLOCKED)
151        column.add_attribute(self.crp, 'status', UserList.STATUS)
152        column.set_attributes(self.crt, markup=UserList.LABEL,
153            obj=UserList.OBJ)
154        column.add_attribute(self.crp, 'visible', UserList.SHOW)
155        #self.crt.set_property('ellipsize', pango.ELLIPSIZE_END)
156       
157        self.set_search_column(UserList.ID)
158        self.set_headers_visible(False)
159       
160        if self.isTreeStore:
161            self.TARGETS = [('TREE_MODEL_ROW', gtk.TARGET_SAME_WIDGET, 0),
162                            ('text/plain',0, 1),
163                            ('TEXT', 0, 2),
164                            ('STRING', 0, 3),
165                            ('COMPOUND_TEXT', 0, 4),
166                            ('UTF8_STRING', 0, 5)]
167            self.enable_model_drag_source( gtk.gdk.BUTTON1_MASK|
168                gtk.gdk.CONTROL_MASK, self.TARGETS,
169                gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_MOVE|
170                gtk.gdk.ACTION_COPY)
171            self.enable_model_drag_dest(self.TARGETS,
172                gtk.gdk.ACTION_DEFAULT )
173           
174            self.connect("drag_data_get", self.onDragDataGet)
175            self.connect("drag_data_received", self.onDragDataReceived)
176            self.connect("drag_drop", self.onDragDrop)
177           
178        self.connect("row_activated" , self.onRowActivated)
179        self.connect("button-press-event" , self.onButtonPress)
180        self.connect("row-expanded" , self.onRowExpanded)
181        self.connect("row-collapsed" , self.onRowCollapsed)
182       
183        self.last_fill = 0
184        if os.name != 'nt':  # gtk bug here?
185            self.model.set_sort_func(UserList.OBJ, self.sortMethod)
186            self.model.set_sort_column_id(UserList.OBJ, gtk.SORT_ASCENDING)
187   
188    def compareContact(self, x, y, status, type):
189        def _getPriority(status, contact):
190            status_priority = {
191                'NLN':        0,
192                'AWY':        2,
193                'BSY':        6,
194                'BRB':        3,
195                'PHN':        4,
196                'LUN':        5,
197                'HDN':        7,
198                'IDL':        1,
199                'FLN':        8,
200            }
201            if not status:
202                if contact.status == 'FLN':
203                    return 8
204                else:
205                    return 0
206            else:
207                return status_priority[contact.status]
208           
209        if type:
210            value = lambda x: x.email
211        else:
212            value = lambda x: x.nick
213           
214        if _getPriority(status, x) > _getPriority(status,y):
215            return 1
216        elif _getPriority(status, x) < _getPriority(status,y):
217            return -1
218        else:
219            if value(x) == value(y):
220                return 0
221            elif value(x) > value(y):
222                return 1
223            else:
224                return -1
225         
226    def sortMethod(self, treemodel, iter1, iter2, user_data=None):
227        ''' method used for sorting the userlist'''
228
229        status = self.sortNickGroupByStatusPriority
230        type = self.sortNickGroupByContact
231        obj1 = self.model[iter1][UserList.OBJ]
232        obj2 = self.model[iter2][UserList.OBJ]
233       
234        if self.model[iter1][UserList.TYPE] == 'group' or \
235            self.model[iter2][UserList.TYPE] == 'group':
236            return -1
237        try:
238            return  self.compareContact(obj1, obj2, status, type)
239        except:
240            return -1
241 
242    def onDragDataGet(self, treeview, context, selection, target_id, etime):
243        # TODO: what does the 8 magic number mean?
244        (rowType, contact) = self.getSelected()
245        if rowType == 'user':
246            if target_id == 0:
247                selection.set('STRING', 8, contact.email + '|' + self.getGroupSelected().name)
248            else:
249                selection.set('STRING', 8, contact.nick + ' <' + contact.email + '>')
250        else:
251            if target_id == 0:
252                selection.set('STRING', 8, '')
253            else:
254                selection.set('STRING', 8, self.getGroupSelected().name)
255   
256    def onDragDrop(self, widget, context, x, y, time):
257        widget.drag_get_data( context, context.targets[0], time )
258        return True
259   
260    def onDragDataReceived(self, treeview, context, x, y, selection,
261        info, etime):
262        # don't move contacts when ordered by status   
263        print 'x: '+str(x)
264        print 'y: '+str(y)
265        print 'info: '+str(info)
266        if self.orderByStatus or selection.data == '':
267            context.finish( False, False, etime )
268            return
269        if info != 0:
270            context.finish( False, False, etime )
271            return
272       
273        model = treeview.get_model()
274        mail, group = selection.data.split('|')
275        drop_info = treeview.get_dest_row_at_pos(x, y)
276       
277        if not drop_info:
278            context.finish( False, False, etime )
279            return
280       
281        path, position = drop_info
282        iter = model.get_iter(path)
283        type, object = self.getTupleByIter(iter)
284       
285        if type == 'group':
286            destGroup = object
287        elif type == 'user':
288            destGroup = self.getGroupSelected(iter)
289       
290        if context.action == gtk.gdk.ACTION_COPY:
291            self.controller.contact_manager.add_to_group(mail, destGroup.name)
292        else:
293            self.controller.contact_manager.move_to_group(mail, group,
294                destGroup.name)
295       
296        context.finish( True, False, etime )
297   
298    # -- CONFIG PROPERTIES -- #
299    def getSortNickGroupByStatusPriority(self):
300        return self.config.user['sortNickGroupByStatusPriority']
301    sortNickGroupByStatusPriority = property(fget=getSortNickGroupByStatusPriority)
302   
303    def getSortNickGroupByContact(self):
304        return self.config.user['sortNickGroupByContact']
305    sortNickGroupByContact = property(fget=getSortNickGroupByContact)
306   
307    def getShowByNick(self):
308        return self.config.user['showByNick']
309    showByNick = property(fget=getShowByNick)
310
311    def getOrderByStatus(self):
312        return self.config.user['orderByStatus']
313    orderByStatus = property(fget=getOrderByStatus)
314
315    def getShowOffline(self):
316        return self.config.user['showOffline']
317    showOffline = property(fget=getShowOffline)
318   
319    def getShowEmptyGroups(self):
320        return self.config.user['showEmptyGroups']
321    showEmptyGroups = property(fget=getShowEmptyGroups)
322
323    def getShowCountContact(self):
324        return self.config.user['showCountContact']
325    showCountContact = property(fget=getShowCountContact)
326
327    def getAvatarsInUserList(self):
328        return self.config.user['avatarsInUserList']
329    avatarsInUserList = property(fget=getAvatarsInUserList)
330
331    def getPersonalMessageColor(self):
332        return self.config.user['personalMessageColor']
333    personalMessageColor = property(fget=getPersonalMessageColor)
334       
335    def getSmallIcons(self):
336        return self.config.user['smallIcons']
337    smallIcons = property(fget=getSmallIcons)
338   
339    def getParseSmilies(self):
340        return self.config.user['parseSmilies']
341    parseSmilies = property(fget=getParseSmilies)
342
343    def getUserListAvatarSize(self):
344        return self.config.user['userListAvatarSize']
345    avatarSize = property(fget=getUserListAvatarSize)
346   
347    # -- FILTER STUFF -- #
348   
349    def visible_func(self, model, iter):
350        obj = model[iter][UserList.OBJ]
351        text = self.filterText.lower()
352        if text == '' and obj and not self.showOffline and \
353           model[iter][UserList.TYPE] == 'user' and obj.status == 'FLN':
354            return False
355        elif model[iter][UserList.TYPE] == 'group':
356            totalCount = model[iter][UserList.TOTAL_COUNT]
357            offlineCount = model[iter][UserList.OFFLINE_COUNT]
358            if totalCount == offlineCount:
359                if (self.showOffline and totalCount > 0) or \
360                   self.showEmptyGroups or text != '':
361                    return True
362                else:
363                    return False
364            return True
365        elif text == '':
366            return True
367        elif obj and \
368             (obj.email.lower().find(text) != -1 or \
369              obj.nick.lower().find(text) != -1 or \
370              obj.alias.lower().find(text) != -1):
371            return True
372
373        return False
374
375    def refilter(self):
376        self.treeModelFilter.refilter()
377        self.expandExpandedGroups()
378       
379    def setFilterText(self, text):
380        self.filterText = text
381        self.treeModelFilter.refilter()
382        self.expandExpandedGroups()
383   
384    # -- OTHER FUNCTIONS -- #
385    # TODO: group them
386
387    def expandExpandedGroups(self):
388        '''iterate over the groups and expand the ones that have True on
389        self.groupState'''
390       
391        if self.isTreeStore:
392            for gRow in self.treeModelFilter:
393                try:
394                    obj = gRow[UserList.OBJ]
395                except:
396                    return
397                if not (obj.name in self.groupState and \
398                        self.groupState[obj.name] == False) and \
399                        obj.name != 'offline':
400                    self.expand_row(gRow.path, False)
401       
402    def sortGroupByName(self, groupDict):
403        '''this method return a list of groupNames sorted according the
404        configuration'''
405
406        l = groupDict.values()
407        l.sort( lambda x, y: cmp(x.name, y.name) )
408        return l
409       
410    def sortByStatus(self, groupDict):
411        '''receive a groupDict and return a dict of contacts sorted by status'''
412       
413        online = emesenelib.ContactData.Group(_('Online'))
414        #offline = emesenelib.ContactData.Group(_('Offline'))
415        for group in groupDict.values():
416            for user in group.users.values():
417                if False: # user.status == 'FLN':
418                    offline.setUser(user.email, user)
419                else:
420                    online.setUser(user.email, user)
421               
422        #return [online, offline]
423        return [online]
424       
425    def getSortedContact(self, group, status , type):
426        ''' return a list of contact by status and/or type '''
427        contactIds = group.users.values()
428        contactIds.sort( lambda x,y: self.compareContact(x, y, status, type))
429        return  [x.email for x in contactIds]
430   
431    def getUserSelected(self):
432        '''return the user name if a user is selected or an empty string
433        if no user is selected'''
434       
435        (rowType, contact) = self.getSelected()
436       
437        if rowType == 'user':
438            return contact.email
439        else:
440            return ''
441       
442    def groupIsSmaller(self, group1, group2):
443        '''return True if the group1 < group2 according to the criteria
444        specified by the preferences'''
445       
446        return group1.name < group2.name
447       
448    def contactIsSmaller(self, contact1, contact2):
449        '''return True if the contact1 < contact2 according to the criteria
450        specified by the preferences'''
451       
452        return contact1.email < contact2.email
453       
454    def allContactsOffline(self, contacts):
455        '''return True if all contacts are offline'''
456       
457        return len([x for x in contacts if x.status != 'FLN']) == 0
458       
459    def add(self, values, iterator=None):
460        '''add an item to the model'''
461        if self.isTreeStore:
462            return self.model.append(iterator, values)
463        else:
464            return self.model.append(values)
465       
466    def fill(self, groupDict=None, _force=False):
467        '''clear and fill the user list, groupDict is a dict containing
468        emesenelib.ContactData.Group objects as value and his name as a key'''
469
470        if groupDict is None:
471            groupDict = self.controller.msn.contactManager.groups
472
473        self.set_model(None)
474        #traceback.print_stack()
475        self.model.clear()
476
477        # refresh crp dimention; fill is called when
478        # theme prefrences (smallIcons) change, set crp dimention here
479        if self.smallIcons:
480            self.crp.set_property('dimention', self.avatarSize / 2)
481        else:
482            self.crp.set_property('dimention', self.avatarSize)
483
484        if self.showCountContact:
485            self.groupLabel = '<b>%s ( %d/%d )</b>'
486        else:
487            self.groupLabel = '<b>%s</b>'
488        self.userLabel = '%s\n<span size="small" foreground="' + \
489            self.personalMessageColor + '">%(status)s%s</span>'
490       
491        # self.showEmptyGroups ask to config, so we use a temp value
492        showEmptyGroups = self.showEmptyGroups
493        showOffline = self.showOffline
494       
495        if self.orderByStatus:
496            sortedGroups = self.sortByStatus(groupDict)
497        else:
498            sortedGroups = self.sortGroupByName(groupDict)
499           
500        for group in sortedGroups:
501            contacts = self.getSortedContact(group,self.sortNickGroupByStatusPriority,self.sortNickGroupByContact)
502           
503            groupIter = self.add(self.getGroupRow(group))
504
505            offlineCount = 0
506            totalCount = 0
507
508            for contactId in contacts:
509                contact = group.users[contactId]
510                totalCount += 1
511                if contact.status == 'FLN':
512                    offlineCount +=1
513                # groupIter is not used id we are a list
514                self.add(self.getContactRow(contact), groupIter)
515               
516            self.model.set_value(groupIter, UserList.TOTAL_COUNT, totalCount)
517            self.model.set_value(groupIter, UserList.OFFLINE_COUNT, offlineCount)
518       
519        self.set_model(self.treeModelFilter)
520        self.expandExpandedGroups()
521               
522    def getMenuData(self):
523        '''Returns useful data for building menus in the format
524        (<selected item type>, [[<item name>], <item's group>])'''
525       
526        data = self.getSelected()
527       
528        if data:
529            (rowType, obj) = data
530            if rowType == 'user':
531                return (rowType, obj.email, self.getGroupSelected().name)
532            elif rowType == 'group':
533                return (rowType, obj.name)
534        else:
535            return ('',)
536         
537    def getGroupLabel(self, group):
538        '''return the pango string to format the group'''
539        if self.showCountContact:
540            return self.groupLabel % (escape(group.name), \
541                group.getOnlineUsersNumber(), group.getSize())
542        else:
543            return self.groupLabel % escape(group.name)
544
545    def getGroupRow(self, group):
546        '''return the list that contain all the fields to add it to the model'''
547        return [None, self.getGroupLabel(group), group.name, 'group', group, False, 0, 0, False, '']
548   
549    def getContactLabel(self, contact, showAlias=True, tooltip=False):
550        '''returns a smileyrenderer list (if tooltip=False) or
551        a plain pango string (if tooltip=True)'''
552        status = ''
553       
554        if not self.orderByStatus:
555            if contact.status != 'NLN':
556                index = self.controller.status_ordered[0].index(contact.status)
557                # you fail at i18n
558                status = '(' + self.unifiedParser.getParser( \
559                    self.controller.status_ordered[2][index]).get() + ')' + \
560                    (contact.personalMessage and ' - ')
561       
562        if contact.alias != '' and showAlias and contact.alias[-2:]=='$N':
563                label= contact.alias[:-2]  + ' - ' +  contact.nick
564        elif contact.alias != '' and showAlias:
565                label=contact.alias
566        elif self.showByNick:
567            label = contact.nick
568        else:
569            label = contact.email
570        psm = contact.personalMessage
571
572        hasSmilies = self.parseSmilies
573        smallIcons = self.smallIcons
574
575        if smallIcons and not tooltip:
576            if not psm and not status:
577                template = self.userLabel.replace("\n", '')
578            else:
579                template = self.userLabel.replace("\n", " - ")
580        else:
581            template = self.userLabel
582
583        template = template.replace("%(status)s", status)
584       
585        # TODO: this shows how the current parser design sucks
586        if tooltip:
587            # YES! YOU'VE GUESSED! THIS IS A HACK!
588            text = label.replace('\n', '') + '\n' + psm.replace('\n', '')
589            text = self.unifiedParser.getParser(text).get().split('\n')
590
591            try:
592                label = text[0]
593                psm = text[1]
594            except IndexError:
595                pass
596            return template % (label, psm)
597        else:
598            tuple = (template, label, psm)
599           
600            #send to parser
601            parser = self.unifiedParser.getParser(tuple, UserListDataType)
602            return parser.get(hasSmilies, self.smileysCache)
603         
604    def getContactImage(self, contact):   
605        '''return the image that will be displayed on the userlist'''
606        fallback = 'online'
607        if self.avatarsInUserList and \
608           self.theme.hasUserDisplayPicture(contact):
609            return self.theme.getUserDisplayPicture(contact, \
610                self.avatarSize, self.avatarSize)
611        elif contact.status == 'FLN':
612            if contact.mobile:
613                fallback = 'mobile'
614            else:
615                fallback = 'offline'
616
617        return self.theme.getImage(fallback)
618         
619    def getContactRow(self, contact):
620        '''return the list that contain all the fields to add it to the model'''
621        notofflineFlag = 0 # he is offline
622
623        if contact.status != 'FLN':
624            notofflineFlag = 1 # he is not offline
625        return [self.getContactImage(contact), self.getContactLabel(contact), contact.email, 'user', contact, True, notofflineFlag, 0,
626                contact.blocked, contact.status]
627         
628    def getGroupIter(self, group, add=False):
629        '''try to find a group and return the iter, iff not found and add == True
630        add it and return the iter, if add==false and not found return None'''
631       
632        if self.isTreeStore:
633            for gRow in self.treeModelFilter:
634                obj = gRow[UserList.OBJ]
635                if obj.name == group.name:
636                    return gRow.iter
637        else:
638            for row in self.treeModelFilter:
639                obj = row[UserList.OBJ]
640                if row[UserList.TYPE] == 'group' and obj.name == group.name:
641                    return row.iter
642       
643        if add:
644            return self.addGroup(group)           
645
646        return None
647         
648    def getTupleByIter(self, iterator):
649        '''returns a tuple (type, object) corresponding to the given iterator
650        type is a string, it can be either 'group' or 'user'
651        object is a group or user instance, depending on type'''
652        return (self.treeModelFilter[iterator][UserList.TYPE], self.treeModelFilter[iterator][UserList.OBJ])
653         
654    def getSelected(self):
655        '''return a tuple containing the type ("group" or "user") and the object
656        or None'''
657       
658        rows = self.get_selection().get_selected_rows()[1]
659        iterator = None
660
661        if len(rows) == 1:
662            iterator = rows[0]
663
664        if iterator is None:
665            debug('invalid iter')
666            return None
667           
668        return self.getTupleByIter(iterator)
669       
670    def getGroupSelected(self, iterator=None):
671        '''if a group is selected return the group object, if a user is
672        selected, return the group in wich its contained'''
673       
674        if iterator == None:
675            iterator = self.get_selection().get_selected()[1]
676
677        objType = self.treeModelFilter[iterator][UserList.TYPE]
678        if objType == 'user':
679            return self.treeModelFilter[iterator].parent[UserList.OBJ]
680        elif objType == 'group':
681            return self.treeModelFilter[iterator][UserList.OBJ]
682       
683    def updateGroup(self, oldGroup, group):
684        '''update the values of group, we use oldGroup because the name could
685        have changed, the users inside the group are not modified'''
686        # TODO: it erase the children nodes
687        for row in self.model:
688            obj = row[UserList.OBJ]
689            if row[UserList.TYPE] == 'group' and obj.name == oldGroup.name:
690                self.model.set_value(row.iter, UserList.LABEL, self.getGroupLabel(group))
691                self.model.set_value(row.iter, UserList.OBJ, group)
692                break
693   
694    def updateGroupNum(self,contact):
695        for row in self.model:
696            obj = row[UserList.OBJ]
697            if row[UserList.TYPE] == 'group' and obj.getUser(contact.email) != None :
698                obj.setUser(contact.email,contact)
699                self.model.set_value(row.iter, UserList.LABEL, self.getGroupLabel(obj))
700   
701    def updateContact(self, contact):
702        '''update the values of a contact, dont use old because the mail cant
703        change'''
704        if not contact:
705            print 'Contact is None (UserList:703)'
706            return
707
708        doRefilter = False
709        if self.isTreeStore:
710            for gRow in self.model:
711                for row in gRow.iterchildren():
712                    if row[UserList.TYPE] == 'user' and \
713                       row[UserList.OBJ].email == contact.email:
714                        doRefilter = self._updateContactRow(contact, row, gRow)
715        else:
716            for row in self.model:
717                obj = row[UserList.OBJ]
718                if row[UserList.TYPE] == 'user' and obj.email == contact.email:
719                    oldStatus = obj.status
720                    doRefilter = self._updateContactRow(contact, row)
721
722        if doRefilter:
723            if False: #self.orderByStatus:
724                #print '===fill==='
725                self.fill(self.controller.msn.contactManager.groups)
726            else:
727                self.refilter()
728        self.updateGroupNum(contact)
729   
730    def _updateContactRow(self, contact, row, gRow=None):
731        '''updates a userlist row with new data, returns
732        bool that determines if refilter is needed or not'''
733        row[UserList.PIX] = self.getContactImage(contact)
734        row[UserList.LABEL] = self.getContactLabel(contact)
735        row[UserList.OBJ] = contact
736        row[UserList.BLOCKED] = contact.blocked
737        row[UserList.STATUS] = contact.status
738        if gRow == None:
739            return False
740
741        # if he is offline and he wasnt offline until now
742        if contact.status == 'FLN' and row[UserList.NOTOFFLINE_FLAG] == 1:
743            # set the is offline flas to 0 (he is offline)
744            row[UserList.NOTOFFLINE_FLAG] = 0
745
746            # increment the offline count by 1
747            self.model.set_value(gRow.iter, UserList.OFFLINE_COUNT, \
748                gRow[UserList.OFFLINE_COUNT] + 1)
749           
750            # if all now, are offline refilter
751            online = gRow[UserList.TOTAL_COUNT] - gRow[UserList.OFFLINE_COUNT]
752            if online == 0 or self.orderByStatus:
753                return True
754           
755        # if he wasnt offline and the flag says that he was offline
756        elif contact.status != 'FLN' and row[UserList.NOTOFFLINE_FLAG] == 0:
757            # set that he is not offline
758            row[UserList.NOTOFFLINE_FLAG] = 1
759
760            # decrease the offline count by 1
761            self.model.set_value(gRow.iter, UserList.OFFLINE_COUNT, \
762                gRow[UserList.OFFLINE_COUNT] - 1)
763
764            # if only one contact is online in the group we refresh
765            online = gRow[UserList.TOTAL_COUNT] - gRow[UserList.OFFLINE_COUNT]
766            if online == 1 or self.orderByStatus:
767                return True
768
769        return False
770
771    def addGroup(self, group):
772        '''add a group to the list, return the gtk.TreeIter'''
773       
774        for row in self.treeModelFilter:
775            obj = row[UserList.OBJ]
776            if row[UserList.TYPE] == 'group' and self.groupIsSmaller(group, obj):
777                if self.isTreeStore:
778                    return self.model.insert_before(None, row.iter, self.getGroupRow(group))
779                else:
780                    return self.model.insert_before(row.iter, self.getGroupRow(group))
781       
782        if self.isTreeStore:
783            return self.model.append(None, self.getGroupRow(group))
784        else:
785            return self.model.append(self.getGroupRow(group))
786   
787    def addContact(self, group, contact):
788        '''add a contact to the given group, if this is not on the list, its added'''
789       
790        gRow = self.treeModelFilter[self.getGroupIter(group, True)]
791       
792        if self.isTreeStore:
793            for row in gRow.iterchildren():
794                obj = row[UserList.OBJ]
795                if row[UserList.TYPE] == 'user' and self.contactIsSmaller(contact, obj):
796                    return self.model.insert_before(gRow.iter, row.iter, self.getContactRow(contact))
797        else:
798            treeModelRow = self.treeModelFilter[gRow.iter]
799            treeModelRow = treeModelRow.next
800            row = self.treeModelFilter[treeModelRow.iter]
801           
802            while treeModelRow:
803                row = self.treeModelFilter[treeModelRow.iter]
804                obj = row[UserList.OBJ]
805                # if we reached the next group we add it before
806                if row[UserList.TYPE] == 'group':
807                    break         
808                if row[UserList.TYPE] == 'user' and self.contactIsSmaller(contact, obj):
809                    break
810                   
811                treeModelRow = treeModelRow.next
812           
813            return self.model.insert_before(row.iter, self.getContactRow(contact))
814                   
815    # ------------------------ Callbacks -------------------------------------
816   
817    def onRowActivated(self, treeview, path, view_column):
818        self.emit('item-selected', self.getSelected())
819       
820    def onButtonPress(self, treeview, event):
821        if event.button == 3 and self.isTreeStore:
822            paths = treeview.get_path_at_pos(int(event.x), int(event.y))
823           
824            if paths == None:
825                debug('invalid path')
826            elif len(paths) > 0:
827                iterator = self.treeModelFilter.get_iter(paths[0])
828                (rowType,obj) = self.getTupleByIter(iterator)
829               
830                if rowType == 'user':
831                    menu = UserMenu.UserMenu(self.controller, obj, \
832                        self.getGroupSelected(iterator))
833                    menu.popup(None, None, None, event.button, event.time)
834                elif rowType == 'group' and not self.orderByStatus:
835                    menu = GroupMenu.GroupMenu(self.controller , obj)
836                    menu.popup(None, None, None, event.button, event.time)
837            else:
838                debug('empty paths?')
839                   
840    def onRowExpanded(self,treeview, iterator, path):
841        obj = self.treeModelFilter[path][UserList.OBJ]
842        self.groupState[obj.name] = True
843       
844    def onRowCollapsed(self,treeview, iterator, path):
845        obj = self.treeModelFilter[path][4]
846        self.groupState[obj.name] = False
847
848    def restoreGroupState(self):
849        '''restore self.groupState'''
850        value = self.config.user['collapsedGroups']
851        collapsed = value.split(',')
852        for gid in collapsed:
853            name = self.controller.msn.contactManager.getGroupName(gid)
854            if name is not None:   
855                self.groupState[name] = False
856
857    def saveGroupState(self):
858        '''save ids of collapsed groups'''
859        collapsed = []
860        for group in self.groupState:
861            if not self.groupState[group]:
862                gid = self.controller.msn.contactManager.getGroupId(group)
863                collapsed.append(gid)
864       
865        self.config.user['collapsedGroups'] = ','.join(collapsed)
866       
867
868gobject.type_register(UserList)
869