root/branches/gajim_0.11/src/tooltips.py

Revision 7940, 21.3 kB (checked in by asterix, 22 months ago)

merge diff from trunk

  • Property svn:eol-style set to LF
Line 
1##      tooltips.py
2##
3## Copyright (C) 2005-2006 Dimitur Kirov <dkirov@gmail.com>
4## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
5## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org>
6##
7## This program is free software; you can redistribute it and/or modify
8## it under the terms of the GNU General Public License as published
9## by the Free Software Foundation; version 2 only.
10##
11## This program is distributed in the hope that it will be useful,
12## but WITHOUT ANY WARRANTY; without even the implied warranty of
13## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14## GNU General Public License for more details.
15##
16
17import gtk
18import gobject
19import os
20import time
21import locale
22
23import gtkgui_helpers
24
25from common import gajim
26from common import helpers
27from common import i18n
28
29class BaseTooltip:
30        ''' Base Tooltip class;
31                Usage:
32                        tooltip = BaseTooltip()
33                        ....
34                        tooltip.show_tooltip(data, widget_height, widget_y_position)
35                        ....
36                        if tooltip.timeout != 0:
37                                tooltip.hide_tooltip()
38               
39                * data - the text to be displayed  (extenders override this argument and
40                        display more complex contents)
41                * widget_height  - the height of the widget on which we want to show tooltip
42                * widget_y_position - the vertical position of the widget on the screen
43               
44                Tooltip is displayed aligned centered to the mouse poiner and 4px below the widget.
45                In case tooltip goes below the visible area it is shown above the widget.
46        '''
47        def __init__(self):
48                self.timeout = 0
49                self.preferred_position = [0, 0]
50                self.win = None
51                self.id = None
52               
53        def populate(self, data):
54                ''' this method must be overriden by all extenders
55                This is the most simple implementation: show data as value of a label
56                '''
57                self.create_window()
58                self.win.add(gtk.Label(data))
59               
60        def create_window(self):
61                ''' create a popup window each time tooltip is requested '''
62                self.win = gtk.Window(gtk.WINDOW_POPUP)
63                self.win.set_border_width(3)
64                self.win.set_resizable(False)
65                self.win.set_name('gtk-tooltips')
66               
67                self.win.set_events(gtk.gdk.POINTER_MOTION_MASK)
68                self.win.connect_after('expose_event', self.expose)
69                self.win.connect('size-request', self.on_size_request)
70                self.win.connect('motion-notify-event', self.motion_notify_event)
71                self.screen = self.win.get_screen()
72       
73        def _get_icon_name_for_tooltip(self, contact):
74                ''' helper function used for tooltip contacts/acounts
75                Tooltip on account has fake contact with sub == '', in this case we show
76                real status of the account
77                '''
78                if contact.ask == 'subscribe':
79                        return 'requested'
80                elif contact.sub in ('both', 'to', ''):
81                        return contact.show
82                return 'not in roster'
83
84        def motion_notify_event(self, widget, event):
85                self.hide_tooltip()
86
87        def on_size_request(self, widget, requisition):
88                half_width = requisition.width / 2 + 1
89                if self.preferred_position[0] < half_width: 
90                        self.preferred_position[0] = 0
91                elif self.preferred_position[0]  + requisition.width > \
92                        self.screen.get_width() + half_width:
93                        self.preferred_position[0] = self.screen.get_width() - \
94                                requisition.width
95                else:
96                        self.preferred_position[0] -= half_width
97                        self.screen.get_height()
98                if self.preferred_position[1] + requisition.height > \
99                        self.screen.get_height():
100                        # flip tooltip up
101                        self.preferred_position[1] -= requisition.height  + \
102                                self.widget_height + 8
103                if self.preferred_position[1] < 0:
104                        self.preferred_position[1] = 0
105                self.win.move(self.preferred_position[0], self.preferred_position[1])
106
107        def expose(self, widget, event):
108                style = self.win.get_style()
109                size = self.win.get_size()
110                style.paint_flat_box(self.win.window, gtk.STATE_NORMAL, gtk.SHADOW_OUT,
111                        None, self.win, 'tooltip', 0, 0, -1, 1)
112                style.paint_flat_box(self.win.window, gtk.STATE_NORMAL, gtk.SHADOW_OUT,
113                        None, self.win, 'tooltip', 0, size[1] - 1, -1, 1)
114                style.paint_flat_box(self.win.window, gtk.STATE_NORMAL, gtk.SHADOW_OUT,
115                        None, self.win, 'tooltip', 0, 0, 1, -1)
116                style.paint_flat_box(self.win.window, gtk.STATE_NORMAL, gtk.SHADOW_OUT,
117                        None, self.win, 'tooltip', size[0] - 1, 0, 1, -1)
118                return True
119       
120        def show_tooltip(self, data, widget_height, widget_y_position):
121                ''' show tooltip on widget.
122                data contains needed data for tooltip contents
123                widget_height is the height of the widget on which we show the tooltip
124                widget_y_position is vertical position of the widget on the screen
125                '''
126                # set tooltip contents
127                self.populate(data)
128               
129                # get the X position of mouse pointer on the screen
130                pointer_x = self.screen.get_display().get_pointer()[1]
131               
132                # get the prefered X position of the tooltip on the screen in case this position is >
133                # than the height of the screen, tooltip will be shown above the widget
134                preferred_y = widget_y_position + widget_height + 4
135               
136                self.preferred_position = [pointer_x, preferred_y]
137                self.widget_height = widget_height
138                self.win.ensure_style()
139                self.win.show_all()
140
141        def hide_tooltip(self):
142                if self.timeout > 0:
143                        gobject.source_remove(self.timeout)
144                        self.timeout = 0
145                if self.win:
146                        self.win.destroy()
147                        self.win = None
148                self.id = None
149
150class StatusTable:
151        ''' Contains methods for creating status table. This
152        is used in Roster and NotificationArea tooltips '''
153        def __init__(self):
154                self.current_row = 1
155                self.table = None
156                self.text_label = None
157                self.spacer_label = '   '
158               
159        def create_table(self):
160                self.table = gtk.Table(4, 1)
161                self.table.set_property('column-spacing', 2)
162       
163        def add_text_row(self, text):
164                self.text_label = gtk.Label()
165                self.text_label.set_line_wrap(True)
166                self.text_label.set_alignment(0, 0)
167                self.text_label.set_selectable(False)
168                self.text_label.set_markup(text)
169                self.table.attach(self.text_label, 1, 4, 1, 2)
170               
171        def get_status_info(self, resource, priority, show, status):
172                str_status = resource + ' (' + unicode(priority) + ')'
173                if status:
174                        status = status.strip()
175                        if status != '':
176                                # make sure 'status' is unicode before we send to to reduce_chars
177                                if isinstance(status, str):
178                                        status = unicode(status, encoding='utf-8')
179                                # reduce to 100 chars, 1 line
180                                status = helpers.reduce_chars_newlines(status, 100, 1)
181                                str_status = gtkgui_helpers.escape_for_pango_markup(str_status)
182                                status = gtkgui_helpers.escape_for_pango_markup(status)
183                                str_status += ' - <i>' + status + '</i>'
184                return str_status
185       
186        def add_status_row(self, file_path, show, str_status, status_time = None, show_lock = False):
187                ''' appends a new row with status icon to the table '''
188                self.current_row += 1
189                state_file = show.replace(' ', '_')
190                files = []
191                files.append(os.path.join(file_path, state_file + '.png'))
192                files.append(os.path.join(file_path, state_file + '.gif'))
193                image = gtk.Image()
194                image.set_from_pixbuf(None)
195                for file in files:
196                        if os.path.exists(file):
197                                image.set_from_file(file)
198                                break
199                spacer = gtk.Label(self.spacer_label)
200                image.set_alignment(1, 0.5)
201                self.table.attach(spacer, 1, 2, self.current_row, 
202                        self.current_row + 1, 0, 0, 0, 0)
203                self.table.attach(image, 2, 3, self.current_row, 
204                        self.current_row + 1, gtk.FILL, gtk.FILL, 2, 0)
205                status_label = gtk.Label()
206                status_label.set_markup(str_status)
207                status_label.set_alignment(0, 0)
208                status_label.set_line_wrap(True)
209                self.table.attach(status_label, 3, 4, self.current_row,
210                        self.current_row + 1, gtk.FILL | gtk.EXPAND, 0, 0, 0)
211                if show_lock:
212                        lock_image = gtk.Image()
213                        lock_image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, 
214                                gtk.ICON_SIZE_MENU)
215                        self.table.attach(lock_image, 4, 5, self.current_row,
216                                self.current_row + 1, 0, 0, 0, 0)
217       
218class NotificationAreaTooltip(BaseTooltip, StatusTable):
219        ''' Tooltip that is shown in the notification area '''
220        def __init__(self):
221                BaseTooltip.__init__(self)
222                StatusTable.__init__(self)
223
224        def fill_table_with_accounts(self, accounts):
225                iconset = gajim.config.get('iconset')
226                if not iconset:
227                        iconset = 'dcraven'
228                file_path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16')
229                for acct in accounts:
230                        message = acct['message']
231                        # before reducing the chars we should assure we send unicode, else
232                        # there are possible pango TBs on 'set_markup'
233                        if isinstance(message, str):
234                                message = unicode(message, encoding = 'utf-8')
235                        message = helpers.reduce_chars_newlines(message, 100, 1)
236                        message = gtkgui_helpers.escape_for_pango_markup(message)
237                        if gajim.con_types.has_key(acct['name']) and \
238                                gajim.con_types[acct['name']] in ('tls', 'ssl'):
239                                show_lock = True
240                        else:
241                                show_lock = False
242                        if message:
243                                self.add_status_row(file_path, acct['show'], 
244                                        gtkgui_helpers.escape_for_pango_markup(acct['name']) + \
245                                        ' - ' + message, show_lock=show_lock)
246                        else:
247                                self.add_status_row(file_path, acct['show'], 
248                                        gtkgui_helpers.escape_for_pango_markup(acct['name']) 
249                                        , show_lock=show_lock)
250
251        def populate(self, data):
252                self.create_window()
253                self.create_table()
254                accounts = helpers.get_accounts_info()
255                if len(accounts) > 1:
256                        self.table.resize(2, 1)
257                        self.fill_table_with_accounts(accounts)
258                self.hbox = gtk.HBox()
259                self.table.set_property('column-spacing', 1)
260
261                text = helpers.get_notification_icon_tooltip_text()
262                text = gtkgui_helpers.escape_for_pango_markup(text)
263               
264                self.add_text_row(text)
265                self.hbox.add(self.table)
266                self.win.add(self.hbox)
267
268class GCTooltip(BaseTooltip):
269        ''' Tooltip that is shown in the GC treeview '''
270        def __init__(self):
271                self.account = None
272                self.text_label = gtk.Label()
273                self.text_label.set_line_wrap(True)
274                self.text_label.set_alignment(0, 0)
275                self.text_label.set_selectable(False)
276                self.avatar_image = gtk.Image()
277
278                BaseTooltip.__init__(self)
279               
280        def populate(self, contact):
281                if not contact:
282                        return
283                self.create_window()
284                vcard_table = gtk.Table(3, 1)
285                vcard_table.set_property('column-spacing', 2)
286                vcard_table.set_homogeneous(False)
287                vcard_current_row = 1
288                properties = []
289
290                nick_markup = '<b>' + \
291                        gtkgui_helpers.escape_for_pango_markup(contact.get_shown_name()) \
292                        + '</b>' 
293                properties.append((nick_markup, None))
294
295                if contact.status: # status message
296                        status = contact.status.strip()
297                        if status != '':
298                                # escape markup entities
299                                status = helpers.reduce_chars_newlines(status, 100, 5)
300                                status = '<i>' +\
301                                        gobject.markup_escape_text(status) + '</i>'
302                                properties.append((status, None))
303                else: # no status message, show SHOW instead
304                        show = helpers.get_uf_show(contact.show)
305                        show = '<i>' + show + '</i>'
306                        properties.append((show, None))
307
308                if contact.jid.strip() != '':
309                        properties.append((_('Jabber ID: '), contact.jid))
310
311                if hasattr(contact, 'resource') and contact.resource.strip() != '':
312                        properties.append((_('Resource: '), 
313                                gtkgui_helpers.escape_for_pango_markup(contact.resource) ))
314                if contact.affiliation != 'none':
315                        uf_affiliation = helpers.get_uf_affiliation(contact.affiliation)
316                        affiliation_str = \
317                                _('%(owner_or_admin_or_member)s of this group chat') %\
318                                {'owner_or_admin_or_member': uf_affiliation}
319                        properties.append((affiliation_str, None))
320               
321                # Add avatar
322                puny_name = helpers.sanitize_filename(contact.name)
323                puny_room = helpers.sanitize_filename(contact.room_jid)
324                for type_ in ('jpeg', 'png'):
325                        file = os.path.join(gajim.AVATAR_PATH, puny_room,
326                                puny_name + '.' + type_)
327                        if os.path.exists(file):
328                                self.avatar_image.set_from_file(file)
329                                pix = self.avatar_image.get_pixbuf()
330                                pix = gtkgui_helpers.get_scaled_pixbuf(pix, 'tooltip')
331                                self.avatar_image.set_from_pixbuf(pix)
332                                break
333                else:
334                        self.avatar_image.set_from_pixbuf(None)
335                while properties:
336                        property = properties.pop(0)
337                        vcard_current_row += 1
338                        vertical_fill = gtk.FILL
339                        if not properties:
340                                vertical_fill |= gtk.EXPAND
341                        label = gtk.Label()
342                        label.set_alignment(0, 0)
343                        if property[1]:
344                                label.set_markup(property[0])
345                                vcard_table.attach(label, 1, 2, vcard_current_row,
346                                        vcard_current_row + 1, gtk.FILL, vertical_fill, 0, 0)
347                                label = gtk.Label()
348                                label.set_alignment(0, 0)
349                                label.set_markup(property[1])
350                                label.set_line_wrap(True)
351                                vcard_table.attach(label, 2, 3, vcard_current_row,
352                                        vcard_current_row + 1, gtk.EXPAND | gtk.FILL,
353                                        vertical_fill, 0, 0)
354                        else:
355                                label.set_markup(property[0])
356                                vcard_table.attach(label, 1, 3, vcard_current_row,
357                                        vcard_current_row + 1, gtk.FILL, vertical_fill, 0)
358               
359                self.avatar_image.set_alignment(0, 0)
360                vcard_table.attach(self.avatar_image, 3, 4, 2, vcard_current_row + 1, 
361                        gtk.FILL, gtk.FILL | gtk.EXPAND, 3, 3)
362                self.win.add(vcard_table)
363
364class RosterTooltip(NotificationAreaTooltip):
365        ''' Tooltip that is shown in the roster treeview '''
366        def __init__(self):
367                self.account = None
368                self.image = gtk.Image()
369                self.image.set_alignment(0, 0)
370                # padding is independent of the total length and better than alignment
371                self.image.set_padding(1, 2) 
372                self.avatar_image = gtk.Image()
373                NotificationAreaTooltip.__init__(self)
374
375        def populate(self, contacts):
376                self.create_window()
377               
378                self.create_table()
379                if not contacts or len(contacts) == 0:
380                        # Tooltip for merged accounts row
381                        accounts = helpers.get_accounts_info()
382                        self.table.resize(2, 1)
383                        self.spacer_label = ''
384                        self.fill_table_with_accounts(accounts)
385                        self.win.add(self.table)
386                        return
387               
388               
389                # primary contact
390                prim_contact = gajim.contacts.get_highest_prio_contact_from_contacts(
391                        contacts)
392               
393                puny_jid = helpers.sanitize_filename(prim_contact.jid)
394                table_size = 3
395               
396                for type_ in ('jpeg', 'png'):
397                        file = os.path.join(gajim.AVATAR_PATH, puny_jid + '.' + type_)
398                        if os.path.exists(file):
399                                self.avatar_image.set_from_file(file)
400                                pix = self.avatar_image.get_pixbuf()
401                                pix = gtkgui_helpers.get_scaled_pixbuf(pix, 'tooltip')
402                                self.avatar_image.set_from_pixbuf(pix)
403                                table_size = 4
404                                break
405                else:
406                        self.avatar_image.set_from_pixbuf(None)
407                vcard_table = gtk.Table(table_size, 1)
408                vcard_table.set_property('column-spacing', 2)
409                vcard_table.set_homogeneous(False)
410                vcard_current_row = 1
411                properties = []
412
413                name_markup = u'<span weight="bold">' + \
414                        gtkgui_helpers.escape_for_pango_markup(prim_contact.get_shown_name())\
415                        + '</span>'
416                properties.append((name_markup, None))
417               
418                num_resources = 0
419                # put contacts in dict, where key is priority
420                contacts_dict = {}
421                for contact in contacts:
422                        if contact.resource:
423                                num_resources += 1
424                                if contact.priority in contacts_dict:
425                                        contacts_dict[contact.priority].append(contact)
426                                else:
427                                        contacts_dict[contact.priority] = [contact]
428
429                if num_resources > 1:
430                        properties.append((_('Status: '),       ' '))
431                        transport = gajim.get_transport_name_from_jid(
432                                prim_contact.jid)
433                        if transport:
434                                file_path = os.path.join(gajim.DATA_DIR, 'iconsets', 
435                                        'transports', transport , '16x16')
436                        else:
437                                iconset = gajim.config.get('iconset')
438                                if not iconset:
439                                        iconset = 'dcraven'
440                                file_path = os.path.join(gajim.DATA_DIR,
441                                        'iconsets', iconset, '16x16')
442
443                        contact_keys = contacts_dict.keys()
444                        contact_keys.sort()
445                        contact_keys.reverse()
446                        for priority in contact_keys:
447                                for contact in contacts_dict[priority]:
448                                        status_line = self.get_status_info(contact.resource,
449                                                contact.priority, contact.show, contact.status)
450                                       
451                                        icon_name = self._get_icon_name_for_tooltip(contact)
452                                        self.add_status_row(file_path, icon_name, status_line,
453                                                contact.last_status_time)
454                        properties.append((self.table,  None))
455                else: # only one resource
456                        if contact.show:
457                                show = helpers.get_uf_show(contact.show) 
458                                if contact.last_status_time:
459                                        vcard_current_row += 1
460                                        if contact.show == 'offline':
461                                                text = ' - ' + _('Last status: %s')
462                                        else:
463                                                text = _(' since %s')
464                                       
465                                        if time.strftime('%j', time.localtime())== \
466                                                        time.strftime('%j', contact.last_status_time):
467                                        # it's today, show only the locale hour representation
468                                                local_time = time.strftime('%X',
469                                                        contact.last_status_time)
470                                        else:
471                                                # time.strftime returns locale encoded string
472                                                local_time = time.strftime('%c',
473                                                        contact.last_status_time)
474                                        local_time = local_time.decode(
475                                                locale.getpreferredencoding())
476                                        text = text % local_time
477                                        show += text
478                                show = '<i>' + show + '</i>'
479                                # we append show below
480                               
481                                if contact.status:
482                                        status = contact.status.strip()
483                                        if status:
484                                                # reduce long status
485                                                # (no more than 100 chars on line and no more than 5 lines)
486                                                status = helpers.reduce_chars_newlines(status, 100, 5)
487                                                # escape markup entities.
488                                                status = gtkgui_helpers.escape_for_pango_markup(status)
489                                                properties.append(('<i>%s</i>' % status, None))
490                                properties.append((show, None))
491               
492                properties.append((_('Jabber ID: '), prim_contact.jid ))
493
494                # contact has only one ressource
495                if num_resources == 1 and contact.resource:
496                        properties.append((_('Resource: '),
497                                gtkgui_helpers.escape_for_pango_markup(contact.resource) +\
498                                ' (' + unicode(contact.priority) + ')'))
499               
500                if prim_contact.sub and prim_contact.sub != 'both':
501                        # ('both' is the normal sub so we don't show it)
502                        properties.append(( _('Subscription: '), 
503                                gtkgui_helpers.escape_for_pango_markup(helpers.get_uf_sub(prim_contact.sub))))
504       
505                if prim_contact.keyID:
506                        keyID = None
507                        if len(prim_contact.keyID) == 8:
508                                keyID = prim_contact.keyID
509                        elif len(prim_contact.keyID) == 16:
510                                keyID = prim_contact.keyID[8:]
511                        if keyID:
512                                properties.append((_('OpenPGP: '),
513                                        gtkgui_helpers.escape_for_pango_markup(keyID)))
514               
515                while properties:
516                        property = properties.pop(0)
517                        vcard_current_row += 1
518                        vertical_fill = gtk.FILL
519                        if not properties and table_size == 4:
520                                vertical_fill |= gtk.EXPAND
521                        label = gtk.Label()
522                        label.set_alignment(0, 0)
523                        if property[1]:
524                                label.set_markup(property[0])
525                                vcard_table.attach(label, 1, 2, vcard_current_row,
526                                        vcard_current_row + 1, gtk.FILL,  vertical_fill, 0, 0)
527                                label = gtk.Label()
528                                label.set_alignment(0, 0)
529                                label.set_markup(property[1])
530                                label.set_line_wrap(True)
531                                vcard_table.attach(label, 2, 3, vcard_current_row,
532                                        vcard_current_row + 1, gtk.EXPAND | gtk.FILL,
533                                                vertical_fill, 0, 0)
534                        else:
535                                if isinstance(property[0], (unicode, str)): #FIXME: rm unicode?
536                                        label.set_markup(property[0])
537                                else:
538                                        label = property[0]
539                                vcard_table.attach(label, 1, 3, vcard_current_row,
540                                        vcard_current_row + 1, gtk.FILL, vertical_fill, 0)
541                self.avatar_image.set_alignment(0, 0)
542                if table_size == 4:
543                        vcard_table.attach(self.avatar_image, 3, 4, 2,
544                                vcard_current_row + 1, gtk.FILL, gtk.FILL | gtk.EXPAND, 3, 3)
545                self.win.add(vcard_table)
546
547class FileTransfersTooltip(BaseTooltip):
548        ''' Tooltip that is shown in the notification area '''
549        def __init__(self):
550                BaseTooltip.__init__(self)
551
552        def populate(self, file_props):
553                ft_table = gtk.Table(2, 1)
554                ft_table.set_property('column-spacing', 2)
555                current_row = 1
556                self.create_window()
557                properties = []
558                name = file_