root/trunk/src/tooltips.py

Revision 10549, 24.9 kB (checked in by asterix, 6 weeks ago)

revert thorstenp patches for now. They introduce bugs.

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