root/trunk/src/systray.py

Revision 10665, 15.3 kB (checked in by asterix, 13 days ago)

better GTK bug workarround. see #4310

  • Property svn:eol-style set to LF
Line 
1# -*- coding:utf-8 -*-
2## src/systray.py
3##
4## Copyright (C) 2003-2005 Vincent Hanquez <tab AT snarc.org>
5## Copyright (C) 2003-2008 Yann Leboulanger <asterix AT lagaule.org>
6## Copyright (C) 2005 Norman Rasmussen <norman AT rasmussen.co.za>
7## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
8##                         Travis Shirk <travis AT pobox.com>
9## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
10## Copyright (C) 2006 Stefan Bethge <stefan AT lanpartei.de>
11## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
12## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
13##                    Julien Pivotto <roidelapluie AT gmail.com>
14##
15## This file is part of Gajim.
16##
17## Gajim is free software; you can redistribute it and/or modify
18## it under the terms of the GNU General Public License as published
19## by the Free Software Foundation; version 3 only.
20##
21## Gajim is distributed in the hope that it will be useful,
22## but WITHOUT ANY WARRANTY; without even the implied warranty of
23## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24## GNU General Public License for more details.
25##
26## You should have received a copy of the GNU General Public License
27## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
28##
29
30import gtk
31import gobject
32import os
33
34import dialogs
35import config
36import tooltips
37import gtkgui_helpers
38
39from common import gajim
40from common import helpers
41
42HAS_SYSTRAY_CAPABILITIES = True
43
44try:
45        import egg.trayicon as trayicon # gnomepythonextras trayicon
46except Exception:
47        try:
48                import trayicon # our trayicon
49        except Exception:
50                gajim.log.debug('No trayicon module available')
51                HAS_SYSTRAY_CAPABILITIES = False
52
53
54class Systray:
55        '''Class for icon in the notification area
56        This class is both base class (for statusicon.py) and normal class
57        for trayicon in GNU/Linux'''
58
59        def __init__(self):
60                self.single_message_handler_id = None
61                self.new_chat_handler_id = None
62                self.t = None
63                # click somewhere else does not popdown menu. workaround this.
64                self.added_hide_menuitem = False 
65                self.img_tray = gtk.Image()
66                self.status = 'offline'
67                self.double_click = False
68                self.double_click_id = None
69                self.double_click_time = gtk.settings_get_default().get_property(
70                        'gtk-double-click-time')
71                self.xml = gtkgui_helpers.get_glade('systray_context_menu.glade')
72                self.systray_context_menu = self.xml.get_widget('systray_context_menu')
73                self.xml.signal_autoconnect(self)
74                self.popup_menus = []
75
76        def subscribe_events(self):
77                '''Register listeners to the events class'''
78                gajim.events.event_added_subscribe(self.on_event_added)
79                gajim.events.event_removed_subscribe(self.on_event_removed)
80
81        def unsubscribe_events(self):
82                '''Unregister listeners to the events class'''
83                gajim.events.event_added_unsubscribe(self.on_event_added)
84                gajim.events.event_removed_unsubscribe(self.on_event_removed)
85
86        def on_event_added(self, event):
87                '''Called when an event is added to the event list'''
88                if event.show_in_systray:
89                        self.set_img()
90
91        def on_event_removed(self, event_list):
92                '''Called when one or more events are removed from the event list'''
93                self.set_img()
94
95        def set_img(self):
96                if not gajim.interface.systray_enabled:
97                        return
98                if gajim.events.get_nb_systray_events():
99                        state = 'event'
100                else:
101                        state = self.status
102                image = gajim.interface.jabber_state_images['16'][state]
103                if image.get_storage_type() == gtk.IMAGE_ANIMATION:
104                        self.img_tray.set_from_animation(image.get_animation())
105                elif image.get_storage_type() == gtk.IMAGE_PIXBUF:
106                        self.img_tray.set_from_pixbuf(image.get_pixbuf())
107
108        def change_status(self, global_status):
109                ''' set tray image to 'global_status' '''
110                # change image and status, only if it is different
111                if global_status is not None and self.status != global_status:
112                        self.status = global_status
113                self.set_img()
114
115        def start_chat(self, widget, account, jid):
116                contact = gajim.contacts.get_first_contact_from_jid(account, jid)
117                if gajim.interface.msg_win_mgr.has_window(jid, account):
118                        gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab(
119                                jid, account)
120                elif contact:
121                        gajim.interface.new_chat(contact, account)
122                        gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab(
123                                jid, account)
124
125        def on_single_message_menuitem_activate(self, widget, account):
126                dialogs.SingleMessageWindow(account, action = 'send')
127
128        def on_new_chat(self, widget, account):
129                dialogs.NewChatDialog(account)
130
131        def make_menu(self, event_button, event_time):
132                '''create chat with and new message (sub) menus/menuitems'''
133                for m in self.popup_menus:
134                        m.destroy()
135
136                chat_with_menuitem = self.xml.get_widget('chat_with_menuitem')
137                single_message_menuitem = self.xml.get_widget(
138                        'single_message_menuitem')
139                status_menuitem = self.xml.get_widget('status_menu')
140                join_gc_menuitem = self.xml.get_widget('join_gc_menuitem')
141                sounds_mute_menuitem = self.xml.get_widget('sounds_mute_menuitem')
142
143                if self.single_message_handler_id:
144                        single_message_menuitem.handler_disconnect(
145                                self.single_message_handler_id)
146                        self.single_message_handler_id = None
147                if self.new_chat_handler_id:
148                        chat_with_menuitem.disconnect(self.new_chat_handler_id)
149                        self.new_chat_handler_id = None
150
151                sub_menu = gtk.Menu()
152                self.popup_menus.append(sub_menu)
153                status_menuitem.set_submenu(sub_menu)
154
155                gc_sub_menu = gtk.Menu() # gc is always a submenu
156                join_gc_menuitem.set_submenu(gc_sub_menu)
157
158                # We need our own set of status icons, let's make 'em!
159                iconset = gajim.config.get('iconset')
160                path = os.path.join(helpers.get_iconset_path(iconset), '16x16')
161                state_images = gtkgui_helpers.load_iconset(path)
162
163                if 'muc_active' in state_images:
164                        join_gc_menuitem.set_image(state_images['muc_active'])
165
166                for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'):
167                        uf_show = helpers.get_uf_show(show, use_mnemonic = True)
168                        item = gtk.ImageMenuItem(uf_show)
169                        item.set_image(state_images[show])
170                        sub_menu.append(item)
171                        item.connect('activate', self.on_show_menuitem_activate, show)
172
173                item = gtk.SeparatorMenuItem()
174                sub_menu.append(item)
175
176                item = gtk.ImageMenuItem(_('_Change Status Message...'))
177                path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'kbd_input.png')
178                img = gtk.Image()
179                img.set_from_file(path)
180                item.set_image(img)
181                sub_menu.append(item)
182                item.connect('activate', self.on_change_status_message_activate)
183
184                connected_accounts = gajim.get_number_of_connected_accounts()
185                if connected_accounts < 1:
186                        item.set_sensitive(False)
187
188                item = gtk.SeparatorMenuItem()
189                sub_menu.append(item)
190
191                uf_show = helpers.get_uf_show('offline', use_mnemonic = True)
192                item = gtk.ImageMenuItem(uf_show)
193                item.set_image(state_images['offline'])
194                sub_menu.append(item)
195                item.connect('activate', self.on_show_menuitem_activate, 'offline')
196
197                iskey = connected_accounts > 0 and not (connected_accounts == 1 and
198                                gajim.connections[gajim.connections.keys()[0]].is_zeroconf)
199                chat_with_menuitem.set_sensitive(iskey)
200                single_message_menuitem.set_sensitive(iskey)
201                join_gc_menuitem.set_sensitive(iskey)
202
203                if connected_accounts >= 2: # 2 or more connections? make submenus
204                        account_menu_for_chat_with = gtk.Menu()
205                        chat_with_menuitem.set_submenu(account_menu_for_chat_with)
206                        self.popup_menus.append(account_menu_for_chat_with)
207
208                        account_menu_for_single_message = gtk.Menu()
209                        single_message_menuitem.set_submenu(
210                                account_menu_for_single_message)
211                        self.popup_menus.append(account_menu_for_single_message)
212
213                        accounts_list = sorted(gajim.contacts.get_accounts())
214                        for account in accounts_list:
215                                if gajim.connections[account].is_zeroconf:
216                                        continue
217                                if gajim.connections[account].connected > 1:
218                                        # for chat_with
219                                        item = gtk.MenuItem(_('using account %s') % account)
220                                        account_menu_for_chat_with.append(item)
221                                        item.connect('activate', self.on_new_chat, account)
222
223                                        # for single message
224                                        item = gtk.MenuItem(_('using account %s') % account)
225                                        item.connect('activate',
226                                                self.on_single_message_menuitem_activate, account)
227                                        account_menu_for_single_message.append(item)
228
229                                        # join gc
230                                        gc_item = gtk.MenuItem(_('using account %s') % account, False)
231                                        gc_sub_menu.append(gc_item)
232                                        gc_menuitem_menu = gtk.Menu()
233                                        gajim.interface.roster.add_bookmarks_list(gc_menuitem_menu,
234                                                account)
235                                        gc_item.set_submenu(gc_menuitem_menu)
236                                        gc_sub_menu.show_all()
237
238                elif connected_accounts == 1: # one account
239                        # one account connected, no need to show 'as jid'
240                        for account in gajim.connections:
241                                if gajim.connections[account].connected > 1:
242                                        self.new_chat_handler_id = chat_with_menuitem.connect(
243                                                        'activate', self.on_new_chat, account)
244                                        # for single message
245                                        single_message_menuitem.remove_submenu()
246                                        self.single_message_handler_id = single_message_menuitem.\
247                                                connect('activate',
248                                                self.on_single_message_menuitem_activate, account)
249
250                                        # join gc
251                                        gajim.interface.roster.add_bookmarks_list(gc_sub_menu,
252                                                account)
253                                        break # No other connected account
254
255                newitem = gtk.SeparatorMenuItem() # separator
256                gc_sub_menu.append(newitem)
257                newitem = gtk.ImageMenuItem(_('_Manage Bookmarks...'))
258                img = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
259                newitem.set_image(img)
260                newitem.connect('activate',
261                        gajim.interface.roster.on_manage_bookmarks_menuitem_activate)
262                gc_sub_menu.append(newitem)
263
264                sounds_mute_menuitem.set_active(not gajim.config.get('sounds_on'))
265
266                if os.name == 'nt':
267                        if gtk.pygtk_version >= (2, 10, 0) and gtk.gtk_version >= (2, 10, 0):
268                                if self.added_hide_menuitem is False:
269                                        self.systray_context_menu.prepend(gtk.SeparatorMenuItem())
270                                        item = gtk.MenuItem(_('Hide this menu'))
271                                        self.systray_context_menu.prepend(item)
272                                        self.added_hide_menuitem = True
273
274                self.systray_context_menu.show_all()
275                self.systray_context_menu.popup(None, None, None, 0,
276                        event_time)
277
278        def on_show_all_events_menuitem_activate(self, widget):
279                events = gajim.events.get_systray_events()
280                for account in events:
281                        for jid in events[account]:
282                                for event in events[account][jid]:
283                                        gajim.interface.handle_event(account, jid, event.type_)
284
285        def on_sounds_mute_menuitem_activate(self, widget):
286                gajim.config.set('sounds_on', not widget.get_active()) 
287                gajim.interface.save_config()
288
289        def on_show_roster_menuitem_activate(self, widget):
290                win = gajim.interface.roster.window
291                win.present()
292
293        def on_preferences_menuitem_activate(self, widget):
294                if 'preferences' in gajim.interface.instances:
295                        gajim.interface.instances['preferences'].window.present()
296                else:
297                        gajim.interface.instances['preferences'] = config.PreferencesWindow()
298
299        def on_quit_menuitem_activate(self, widget):   
300                gajim.interface.roster.on_quit_request()
301
302        def on_left_click(self):
303                self.double_click_id = None
304                if self.double_click:
305                        self.double_click = False
306                        return
307                win = gajim.interface.roster.window
308                # toggle visible/hidden for roster window
309                if win.get_property('visible') and (win.get_property('has-toplevel-focus') or \
310                        os.name == 'nt'):
311                        # visible in ANY virtual desktop?
312
313                        # we could be in another VD right now. eg vd2
314                        # and we want to show it in vd2
315                        if not gtkgui_helpers.possibly_move_window_in_current_desktop(win):
316                                win.hide() # else we hide it from VD that was visible in
317                else:
318                        # in Windows (perhaps other Window Managers too) minimize state
319                        # is remembered, so make sure it's not minimized (iconified)
320                        # because user wants to see roster
321                        win.deiconify()
322                        win.present()
323
324        def handle_first_event(self):
325                account, jid, event = gajim.events.get_first_systray_event()
326                gajim.interface.handle_event(account, jid, event.type_)
327
328        def on_middle_click(self):
329                '''middle click raises window to have complete focus (fe. get kbd events)
330                but if already raised, it hides it'''
331                if gajim.events.get_nb_systray_events() == 0:
332                        return
333                self.handle_first_event()
334
335        def on_clicked(self, widget, event):
336                self.on_tray_leave_notify_event(widget, None)
337                if event.type == gtk.gdk._2BUTTON_PRESS:
338                        if len(gajim.events.get_systray_events()) == 0:
339                                return
340                        self.double_click = True
341                        self.on_middle_click()
342                if event.type != gtk.gdk.BUTTON_PRESS:
343                        return
344                if event.button == 1: # Left click
345                        if len(gajim.events.get_systray_events()) == 0:
346                                self.on_left_click()
347                        else:
348                                if self.double_click_id:
349                                        gobject.source_remove(self.double_click_id)
350                                self.double_click_id = gobject.timeout_add(
351                                        self.double_click_time, self.on_left_click)
352                elif event.button == 2: # middle click
353                        self.on_middle_click()
354                elif event.button == 3: # right click
355                        self.make_menu(event.button, event.time)
356
357        def on_show_menuitem_activate(self, widget, show):
358                # we all add some fake (we cannot select those nor have them as show)
359                # but this helps to align with roster's status_combobox index positions
360                l = ['online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'SEPARATOR',
361                        'CHANGE_STATUS_MSG_MENUITEM', 'SEPARATOR', 'offline']
362                index = l.index(show)
363                if not helpers.statuses_unified():
364                        gajim.interface.roster.status_combobox.set_active(index + 2)
365                        return
366                current = gajim.interface.roster.status_combobox.get_active()
367                if index != current:
368                        gajim.interface.roster.status_combobox.set_active(index)
369
370        def on_change_status_message_activate(self, widget):
371                model = gajim.interface.roster.status_combobox.get_model()
372                active = gajim.interface.roster.status_combobox.get_active()
373                status = model[active][2].decode('utf-8')
374                def on_response(message):
375                        if message is None: # None if user press Cancel
376                                return
377                        accounts = gajim.connections.keys()
378                        for acct in accounts:
379                                if not gajim.config.get_per('accounts', acct,
380                                        'sync_with_global_status'):
381                                        continue
382                                show = gajim.SHOW_LIST[gajim.connections[acct].connected]
383                                gajim.interface.roster.send_status(acct, show, message)
384                dlg = dialogs.ChangeStatusMessageDialog(on_response, status)
385                dlg.window.present()
386
387        def show_tooltip(self, widget):
388                position = widget.window.get_origin()
389                if self.tooltip.id == position:
390                        size = widget.window.get_size()
391                        self.tooltip.show_tooltip('', size[1], position[1])
392
393        def on_tray_motion_notify_event(self, widget, event):
394                position = widget.window.get_origin()
395                if self.tooltip.timeout > 0:
396                        if self.tooltip.id != position:
397                                self.tooltip.hide_tooltip()
398                if self.tooltip.timeout == 0 and \
399                        self.tooltip.id != position:
400                        self.tooltip.id = position
401                        self.tooltip.timeout = gobject.timeout_add(500,
402                                self.show_tooltip, widget)
403
404        def on_tray_leave_notify_event(self, widget, event):
405                position = widget.window.get_origin()
406                if self.tooltip.timeout > 0 and \
407                        self.tooltip.id == position:
408                        self.tooltip.hide_tooltip()
409
410        def on_tray_destroyed(self, widget):
411                '''re-add trayicon when systray is destroyed'''
412                self.t = None
413                if gajim.interface.systray_enabled:
414                        self.show_icon()
415
416        def show_icon(self):
417                if not self.t:
418                        self.t = trayicon.TrayIcon('Gajim')
419                        self.t.connect('destroy', self.on_tray_destroyed)
420                        eb = gtk.EventBox()
421                        # avoid draw seperate bg color in some gtk themes
422                        eb.set_visible_window(False)
423                        eb.set_events(gtk.gdk.POINTER_MOTION_MASK)
424                        eb.connect('button-press-event', self.on_clicked)
425                        eb.connect('motion-notify-event', self.on_tray_motion_notify_event)
426                        eb.connect('leave-notify-event', self.on_tray_leave_notify_event)
427                        self.tooltip = tooltips.NotificationAreaTooltip()
428
429                        self.img_tray = gtk.Image()
430                        eb.add(self.img_tray)
431                        self.t.add(eb)
432                        self.set_img()
433                        self.subscribe_events()
434                self.t.show_all()
435
436        def hide_icon(self):
437                if self.t:
438                        self.t.destroy()
439                        self.t = None
440                        self.unsubscribe_events()
441
442# vim: se ts=3:
Note: See TracBrowser for help on using the browser.