4 ## Copyright (C) 2006 Nikos Kouremenos <kourem AT gmail.com>
5 ## Copyright (C) 2006-2007 Jean-Marie Traissard <jim AT lapin.org>
6 ## Copyright (C) 2006-2010 Yann Leboulanger <asterix AT lagaule.org>
7 ## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
8 ## Julien Pivotto <roidelapluie AT gmail.com>
9 ## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
11 ## This file is part of Gajim.
13 ## Gajim is free software; you can redistribute it and/or modify
14 ## it under the terms of the GNU General Public License as published
15 ## by the Free Software Foundation; version 3 only.
17 ## Gajim is distributed in the hope that it will be useful,
18 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ## GNU General Public License for more details.
22 ## You should have received a copy of the GNU General Public License
23 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
37 from common
import gajim
38 from common
import helpers
39 from common
import pep
43 Class for the notification area icon
47 self
.single_message_handler_id
= None
48 self
.show_roster_handler_id
= None
49 self
.new_chat_handler_id
= None
50 # click somewhere else does not popdown menu. workaround this.
51 self
.added_hide_menuitem
= False
52 self
.status
= 'offline'
53 self
.xml
= gtkgui_helpers
.get_gtk_builder('systray_context_menu.ui')
54 self
.systray_context_menu
= self
.xml
.get_object('systray_context_menu')
55 self
.xml
.connect_signals(self
)
57 self
.status_icon
= None
58 self
.tooltip
= tooltips
.NotificationAreaTooltip()
60 def subscribe_events(self
):
62 Register listeners to the events class
64 gajim
.events
.event_added_subscribe(self
.on_event_added
)
65 gajim
.events
.event_removed_subscribe(self
.on_event_removed
)
67 def unsubscribe_events(self
):
69 Unregister listeners to the events class
71 gajim
.events
.event_added_unsubscribe(self
.on_event_added
)
72 gajim
.events
.event_removed_unsubscribe(self
.on_event_removed
)
74 def on_event_added(self
, event
):
76 Called when an event is added to the event list
78 if event
.show_in_systray
:
81 def on_event_removed(self
, event_list
):
83 Called when one or more events are removed from the event list
88 if not self
.status_icon
:
89 self
.status_icon
= gtk
.StatusIcon()
90 self
.statusicon_size
= '16'
91 self
.status_icon
.set_property('has-tooltip', True)
92 self
.status_icon
.connect('activate', self
.on_status_icon_left_clicked
)
93 self
.status_icon
.connect('popup-menu',
94 self
.on_status_icon_right_clicked
)
95 self
.status_icon
.connect('query-tooltip',
96 self
.on_status_icon_query_tooltip
)
97 self
.status_icon
.connect('size-changed',
98 self
.on_status_icon_size_changed
)
101 self
.subscribe_events()
103 def on_status_icon_right_clicked(self
, widget
, event_button
, event_time
):
104 self
.make_menu(event_button
, event_time
)
106 def on_status_icon_query_tooltip(self
, widget
, x
, y
, keyboard_mode
, tooltip
):
107 self
.tooltip
.populate()
108 tooltip
.set_custom(self
.tooltip
.hbox
)
112 self
.status_icon
.set_visible(False)
113 self
.unsubscribe_events()
115 def on_status_icon_left_clicked(self
, widget
):
118 def on_status_icon_size_changed(self
, statusicon
, size
):
120 self
.statusicon_size
= '32'
122 self
.statusicon_size
= '16'
123 if os
.environ
.get('KDE_FULL_SESSION') == 'true':
124 # detect KDE session. see #5476
125 self
.statusicon_size
= '32'
130 Apart from image, we also update tooltip text here
132 if not gajim
.interface
.systray_enabled
:
134 if gajim
.config
.get('trayicon') == 'always':
135 self
.status_icon
.set_visible(True)
136 if gajim
.events
.get_nb_systray_events():
137 self
.status_icon
.set_visible(True)
138 self
.status_icon
.set_blinking(True)
140 if gajim
.config
.get('trayicon') == 'on_event':
141 self
.status_icon
.set_visible(False)
142 self
.status_icon
.set_blinking(False)
144 image
= gajim
.interface
.jabber_state_images
[self
.statusicon_size
][
146 if image
.get_storage_type() == gtk
.IMAGE_PIXBUF
:
147 self
.status_icon
.set_from_pixbuf(image
.get_pixbuf())
148 # FIXME: oops they forgot to support GIF animation?
149 # or they were lazy to get it to work under Windows! WTF!
150 elif image
.get_storage_type() == gtk
.IMAGE_ANIMATION
:
151 self
.status_icon
.set_from_pixbuf(
152 image
.get_animation().get_static_image())
153 # self.img_tray.set_from_animation(image.get_animation())
155 def change_status(self
, global_status
):
157 Set tray image to 'global_status'
159 # change image and status, only if it is different
160 if global_status
is not None and self
.status
!= global_status
:
161 self
.status
= global_status
164 def start_chat(self
, widget
, account
, jid
):
165 contact
= gajim
.contacts
.get_first_contact_from_jid(account
, jid
)
166 if gajim
.interface
.msg_win_mgr
.has_window(jid
, account
):
167 gajim
.interface
.msg_win_mgr
.get_window(jid
, account
).set_active_tab(
170 gajim
.interface
.new_chat(contact
, account
)
171 gajim
.interface
.msg_win_mgr
.get_window(jid
, account
).set_active_tab(
174 def on_single_message_menuitem_activate(self
, widget
, account
):
175 dialogs
.SingleMessageWindow(account
, action
='send')
177 def on_new_chat(self
, widget
, account
):
178 dialogs
.NewChatDialog(account
)
180 def make_menu(self
, event_button
, event_time
):
182 Create chat with and new message (sub) menus/menuitems
184 for m
in self
.popup_menus
:
187 chat_with_menuitem
= self
.xml
.get_object('chat_with_menuitem')
188 single_message_menuitem
= self
.xml
.get_object(
189 'single_message_menuitem')
190 status_menuitem
= self
.xml
.get_object('status_menu')
191 join_gc_menuitem
= self
.xml
.get_object('join_gc_menuitem')
192 sounds_mute_menuitem
= self
.xml
.get_object('sounds_mute_menuitem')
193 show_roster_menuitem
= self
.xml
.get_object('show_roster_menuitem')
195 if self
.single_message_handler_id
:
196 single_message_menuitem
.handler_disconnect(
197 self
.single_message_handler_id
)
198 self
.single_message_handler_id
= None
199 if self
.new_chat_handler_id
:
200 chat_with_menuitem
.disconnect(self
.new_chat_handler_id
)
201 self
.new_chat_handler_id
= None
203 sub_menu
= gtk
.Menu()
204 self
.popup_menus
.append(sub_menu
)
205 status_menuitem
.set_submenu(sub_menu
)
207 gc_sub_menu
= gtk
.Menu() # gc is always a submenu
208 join_gc_menuitem
.set_submenu(gc_sub_menu
)
210 # We need our own set of status icons, let's make 'em!
211 iconset
= gajim
.config
.get('iconset')
212 path
= os
.path
.join(helpers
.get_iconset_path(iconset
), '16x16')
213 state_images
= gtkgui_helpers
.load_iconset(path
)
215 if 'muc_active' in state_images
:
216 join_gc_menuitem
.set_image(state_images
['muc_active'])
218 for show
in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'):
219 uf_show
= helpers
.get_uf_show(show
, use_mnemonic
= True)
220 item
= gtk
.ImageMenuItem(uf_show
)
221 item
.set_image(state_images
[show
])
222 sub_menu
.append(item
)
223 item
.connect('activate', self
.on_show_menuitem_activate
, show
)
225 item
= gtk
.SeparatorMenuItem()
226 sub_menu
.append(item
)
228 item
= gtk
.ImageMenuItem(_('_Change Status Message...'))
229 gtkgui_helpers
.add_image_to_menuitem(item
, 'gajim-kbd_input')
230 sub_menu
.append(item
)
231 item
.connect('activate', self
.on_change_status_message_activate
)
233 connected_accounts
= gajim
.get_number_of_connected_accounts()
234 if connected_accounts
< 1:
235 item
.set_sensitive(False)
237 connected_accounts_with_private_storage
= 0
239 item
= gtk
.SeparatorMenuItem()
240 sub_menu
.append(item
)
242 uf_show
= helpers
.get_uf_show('offline', use_mnemonic
= True)
243 item
= gtk
.ImageMenuItem(uf_show
)
244 item
.set_image(state_images
['offline'])
245 sub_menu
.append(item
)
246 item
.connect('activate', self
.on_show_menuitem_activate
, 'offline')
248 iskey
= connected_accounts
> 0 and not (connected_accounts
== 1 and
249 gajim
.connections
[gajim
.connections
.keys()[0]].is_zeroconf
)
250 chat_with_menuitem
.set_sensitive(iskey
)
251 single_message_menuitem
.set_sensitive(iskey
)
252 join_gc_menuitem
.set_sensitive(iskey
)
254 accounts_list
= sorted(gajim
.contacts
.get_accounts())
255 # items that get shown whether an account is zeroconf or not
256 if connected_accounts
> 1: # 2 or more connections? make submenus
257 account_menu_for_chat_with
= gtk
.Menu()
258 chat_with_menuitem
.set_submenu(account_menu_for_chat_with
)
259 self
.popup_menus
.append(account_menu_for_chat_with
)
261 for account
in accounts_list
:
262 if gajim
.account_is_connected(account
):
264 item
= gtk
.MenuItem(_('using account %s') % account
)
265 account_menu_for_chat_with
.append(item
)
266 item
.connect('activate', self
.on_new_chat
, account
)
268 elif connected_accounts
== 1: # one account
269 # one account connected, no need to show 'as jid'
270 for account
in gajim
.connections
:
271 if gajim
.connections
[account
].connected
> 1:
273 self
.new_chat_handler_id
= chat_with_menuitem
.connect(
274 'activate', self
.on_new_chat
, account
)
275 break # No other connected account
277 # menu items that don't apply to zeroconf connections
278 if connected_accounts
== 1 or (connected_accounts
== 2 and \
279 gajim
.zeroconf_is_connected()):
280 # only one 'real' (non-zeroconf) account is connected, don't need
282 for account
in gajim
.connections
:
283 if gajim
.account_is_connected(account
) and \
284 not gajim
.config
.get_per('accounts', account
, 'is_zeroconf'):
285 if gajim
.connections
[account
].private_storage_supported
:
286 connected_accounts_with_private_storage
+= 1
289 single_message_menuitem
.remove_submenu()
290 self
.single_message_handler_id
= single_message_menuitem
.\
292 self
.on_single_message_menuitem_activate
, account
)
294 gajim
.interface
.roster
.add_bookmarks_list(gc_sub_menu
,
296 break # No other account connected
298 # 2 or more 'real' accounts are connected, make submenus
299 account_menu_for_single_message
= gtk
.Menu()
300 single_message_menuitem
.set_submenu(
301 account_menu_for_single_message
)
302 self
.popup_menus
.append(account_menu_for_single_message
)
304 for account
in accounts_list
:
305 if gajim
.connections
[account
].is_zeroconf
or \
306 not gajim
.account_is_connected(account
):
308 if gajim
.connections
[account
].private_storage_supported
:
309 connected_accounts_with_private_storage
+= 1
311 item
= gtk
.MenuItem(_('using account %s') % account
)
312 item
.connect('activate',
313 self
.on_single_message_menuitem_activate
, account
)
314 account_menu_for_single_message
.append(item
)
317 gc_item
= gtk
.MenuItem(_('using account %s') % account
, False)
318 gc_sub_menu
.append(gc_item
)
319 gc_menuitem_menu
= gtk
.Menu()
320 gajim
.interface
.roster
.add_bookmarks_list(gc_menuitem_menu
,
322 gc_item
.set_submenu(gc_menuitem_menu
)
323 gc_sub_menu
.show_all()
325 newitem
= gtk
.SeparatorMenuItem() # separator
326 gc_sub_menu
.append(newitem
)
327 newitem
= gtk
.ImageMenuItem(_('_Manage Bookmarks...'))
328 img
= gtk
.image_new_from_stock(gtk
.STOCK_PREFERENCES
, gtk
.ICON_SIZE_MENU
)
329 newitem
.set_image(img
)
330 newitem
.connect('activate',
331 gajim
.interface
.roster
.on_manage_bookmarks_menuitem_activate
)
332 gc_sub_menu
.append(newitem
)
333 if connected_accounts_with_private_storage
== 0:
334 newitem
.set_sensitive(False)
336 sounds_mute_menuitem
.set_active(not gajim
.config
.get('sounds_on'))
338 win
= gajim
.interface
.roster
.window
339 if self
.show_roster_handler_id
:
340 show_roster_menuitem
.handler_disconnect(self
.show_roster_handler_id
)
341 if win
.get_property('has-toplevel-focus'):
342 show_roster_menuitem
.get_children()[0].set_label(_('Hide _Roster'))
343 self
.show_roster_handler_id
= show_roster_menuitem
.connect(
344 'activate', self
.on_hide_roster_menuitem_activate
)
346 show_roster_menuitem
.get_children()[0].set_label(_('Show _Roster'))
347 self
.show_roster_handler_id
= show_roster_menuitem
.connect(
348 'activate', self
.on_show_roster_menuitem_activate
)
351 if self
.added_hide_menuitem
is False:
352 self
.systray_context_menu
.prepend(gtk
.SeparatorMenuItem())
353 item
= gtk
.MenuItem(_('Hide this menu'))
354 self
.systray_context_menu
.prepend(item
)
355 self
.added_hide_menuitem
= True
357 self
.systray_context_menu
.show_all()
358 self
.systray_context_menu
.popup(None, None, None, 0,
361 def on_show_all_events_menuitem_activate(self
, widget
):
362 events
= gajim
.events
.get_systray_events()
363 for account
in events
:
364 for jid
in events
[account
]:
365 for event
in events
[account
][jid
]:
366 gajim
.interface
.handle_event(account
, jid
, event
.type_
)
368 def on_sounds_mute_menuitem_activate(self
, widget
):
369 gajim
.config
.set('sounds_on', not widget
.get_active())
370 gajim
.interface
.save_config()
372 def on_show_roster_menuitem_activate(self
, widget
):
373 win
= gajim
.interface
.roster
.window
376 def on_hide_roster_menuitem_activate(self
, widget
):
377 win
= gajim
.interface
.roster
.window
380 def on_preferences_menuitem_activate(self
, widget
):
381 if 'preferences' in gajim
.interface
.instances
:
382 gajim
.interface
.instances
['preferences'].window
.present()
384 gajim
.interface
.instances
['preferences'] = config
.PreferencesWindow()
386 def on_quit_menuitem_activate(self
, widget
):
387 gajim
.interface
.roster
.on_quit_request()
389 def on_left_click(self
):
390 win
= gajim
.interface
.roster
.window
391 if len(gajim
.events
.get_systray_events()) == 0:
392 # No pending events, so toggle visible/hidden for roster window
393 if win
.get_property('visible') and (win
.get_property(
394 'has-toplevel-focus') or os
.name
== 'nt'):
395 # visible in ANY virtual desktop?
397 # we could be in another VD right now. eg vd2
398 # and we want to show it in vd2
399 if not gtkgui_helpers
.possibly_move_window_in_current_desktop(
400 win
) and gajim
.config
.get('save-roster-position'):
401 x
, y
= win
.get_position()
402 gajim
.config
.set('roster_x-position', x
)
403 gajim
.config
.set('roster_y-position', y
)
404 win
.hide() # else we hide it from VD that was visible in
406 if not win
.get_property('visible'):
408 if gajim
.config
.get('save-roster-position'):
409 gtkgui_helpers
.move_window(win
,
410 gajim
.config
.get('roster_x-position'),
411 gajim
.config
.get('roster_y-position'))
412 if not gajim
.config
.get('roster_window_skip_taskbar'):
413 win
.set_property('skip-taskbar-hint', False)
414 win
.present_with_time(gtk
.get_current_event_time())
416 self
.handle_first_event()
418 def handle_first_event(self
):
419 account
, jid
, event
= gajim
.events
.get_first_systray_event()
422 win
= gajim
.interface
.roster
.window
423 if not win
.get_property('visible') and gajim
.config
.get(
424 'save-roster-position'):
425 gtkgui_helpers
.move_window(win
,
426 gajim
.config
.get('roster_x-position'),
427 gajim
.config
.get('roster_y-position'))
428 gajim
.interface
.handle_event(account
, jid
, event
.type_
)
430 def on_middle_click(self
):
432 Middle click raises window to have complete focus (fe. get kbd events)
433 but if already raised, it hides it
435 win
= gajim
.interface
.roster
.window
436 if win
.is_active(): # is it fully raised? (eg does it receive kbd events?)
441 def on_clicked(self
, widget
, event
):
442 self
.on_tray_leave_notify_event(widget
, None)
443 if event
.type != gtk
.gdk
.BUTTON_PRESS
:
445 if event
.button
== 1: # Left click
447 elif event
.button
== 2: # middle click
448 self
.on_middle_click()
449 elif event
.button
== 3: # right click
450 self
.make_menu(event
.button
, event
.time
)
452 def on_show_menuitem_activate(self
, widget
, show
):
453 # we all add some fake (we cannot select those nor have them as show)
454 # but this helps to align with roster's status_combobox index positions
455 l
= ['online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'SEPARATOR',
456 'CHANGE_STATUS_MSG_MENUITEM', 'SEPARATOR', 'offline']
457 index
= l
.index(show
)
458 if not helpers
.statuses_unified():
459 gajim
.interface
.roster
.status_combobox
.set_active(index
+ 2)
461 current
= gajim
.interface
.roster
.status_combobox
.get_active()
463 gajim
.interface
.roster
.status_combobox
.set_active(index
)
465 def on_change_status_message_activate(self
, widget
):
466 model
= gajim
.interface
.roster
.status_combobox
.get_model()
467 active
= gajim
.interface
.roster
.status_combobox
.get_active()
468 status
= model
[active
][2].decode('utf-8')
469 def on_response(message
, pep_dict
):
470 if message
is None: # None if user press Cancel
472 accounts
= gajim
.connections
.keys()
473 for acct
in accounts
:
474 if not gajim
.config
.get_per('accounts', acct
,
475 'sync_with_global_status'):
477 show
= gajim
.SHOW_LIST
[gajim
.connections
[acct
].connected
]
478 gajim
.interface
.roster
.send_status(acct
, show
, message
)
479 gajim
.interface
.roster
.send_pep(acct
, pep_dict
)
480 dlg
= dialogs
.ChangeStatusMessageDialog(on_response
, status
)