systray.py 14.1 KB
Newer Older
nkour's avatar
nkour committed
1
##	systray.py
2
##
Yann Leboulanger's avatar
Yann Leboulanger committed
3
## Copyright (C) 2003-2007 Yann Leboulanger <asterix@lagaule.org>
4
## Copyright (C) 2003-2004 Vincent Hanquez <tab@snarc.org>
nkour's avatar
nothing  
nkour committed
5
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
6 7 8
## Copyright (C) 2005 Dimitur Kirov <dkirov@gmail.com>
## Copyright (C) 2005-2006 Travis Shirk <travis@pobox.com>
## Copyright (C) 2005 Norman Rasmussen <norman@rasmussen.co.za>
9
## Copyright (C) 2007 Lukas Petrovicky <lukas@petrovicky.net>
10
##
11 12 13
## This file is part of Gajim.
##
## 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.
16
##
17
## Gajim is distributed in the hope that it will be useful,
18 19 20 21
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
22 23 24
## You should have received a copy of the GNU General Public License
## along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
##
25 26

import gtk
27
import gobject
28
import os
29

30 31
import dialogs
import config
32
import tooltips
33
import gtkgui_helpers
34

Yann Leboulanger's avatar
Yann Leboulanger committed
35
from common import gajim
36
from common import helpers
37

38 39
HAS_SYSTRAY_CAPABILITIES = True

40 41 42 43 44 45 46
try:
	import egg.trayicon as trayicon	# gnomepythonextras trayicon
except:
	try:
		import trayicon # our trayicon
	except:
		gajim.log.debug('No trayicon module available')
47
		HAS_SYSTRAY_CAPABILITIES = False
48

49

nkour's avatar
nkour committed
50
class Systray:
nkour's avatar
nkour committed
51
	'''Class for icon in the notification area
52
	This class is both base class (for statusicon.py) and normal class
nkour's avatar
nkour committed
53
	for trayicon in GNU/Linux'''
54

55
	def __init__(self):
56
		self.single_message_handler_id = None
57
		self.new_chat_handler_id = None
nkour's avatar
nkour committed
58
		self.t = None
59 60
		# click somewhere else does not popdown menu. workaround this.
		self.added_hide_menuitem = False 
nkour's avatar
nkour committed
61 62
		self.img_tray = gtk.Image()
		self.status = 'offline'
63
		self.xml = gtkgui_helpers.get_glade('systray_context_menu.glade')
nkour's avatar
nkour committed
64 65
		self.systray_context_menu = self.xml.get_widget('systray_context_menu')
		self.xml.signal_autoconnect(self)
66
		self.popup_menus = []
nkour's avatar
nkour committed
67

68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
	def subscribe_events(self):
		'''Register listeners to the events class'''
		gajim.events.event_added_subscribe(self.on_event_added)
		gajim.events.event_removed_subscribe(self.on_event_removed)

	def unsubscribe_events(self):
		'''Unregister listeners to the events class'''
		gajim.events.event_added_unsubscribe(self.on_event_added)
		gajim.events.event_removed_unsubscribe(self.on_event_removed)

	def on_event_added(self, event):
		'''Called when an event is added to the event list'''
		if event.show_in_systray:
			self.set_img()

83
	def on_event_removed(self, event_list):
84 85 86
		'''Called when one or more events are removed from the event list'''
		self.set_img()

87
	def set_img(self):
88 89
		if not gajim.interface.systray_enabled:
			return
90
		if gajim.events.get_nb_systray_events():
91
			state = 'event'
92
		else:
nkour's avatar
nkour committed
93
			state = self.status
94
		image = gajim.interface.roster.jabber_state_images['16'][state]
95 96 97 98 99
		if image.get_storage_type() == gtk.IMAGE_ANIMATION:
			self.img_tray.set_from_animation(image.get_animation())
		elif image.get_storage_type() == gtk.IMAGE_PIXBUF:
			self.img_tray.set_from_pixbuf(image.get_pixbuf())

dkirov's avatar
dkirov committed
100 101
	def change_status(self, global_status):
		''' set tray image to 'global_status' '''
dkirov's avatar
dkirov committed
102
		# change image and status, only if it is different 
103
		if global_status is not None and self.status != global_status:
dkirov's avatar
dkirov committed
104
			self.status = global_status
105
		self.set_img()
106

107
	def start_chat(self, widget, account, jid):
108
		contact = gajim.contacts.get_first_contact_from_jid(account, jid)
109
		if gajim.interface.msg_win_mgr.has_window(jid, account):
110 111
			gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab(
				jid, account)
112
			gajim.interface.msg_win_mgr.get_window(jid, account).window.present()
113
		elif contact:
114 115 116
			gajim.interface.roster.new_chat(contact, account)
			gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab(
				jid, account)
117

118 119
	def on_single_message_menuitem_activate(self, widget, account):
		dialogs.SingleMessageWindow(account, action = 'send')
120

121 122
	def on_new_chat(self, widget, account):
		dialogs.NewChatDialog(account)
123

124 125
	def make_menu(self, event_button, event_time):
		'''create chat with and new message (sub) menus/menuitems'''
126 127 128
		for m in self.popup_menus:
			m.destroy()

nkour's avatar
nkour committed
129
		chat_with_menuitem = self.xml.get_widget('chat_with_menuitem')
130 131
		single_message_menuitem = self.xml.get_widget(
			'single_message_menuitem')
132
		status_menuitem = self.xml.get_widget('status_menu')
jimpp's avatar
jimpp committed
133
		join_gc_menuitem = self.xml.get_widget('join_gc_menuitem')
134
		sounds_mute_menuitem = self.xml.get_widget('sounds_mute_menuitem')
135

136 137 138 139
		if self.single_message_handler_id:
			single_message_menuitem.handler_disconnect(
				self.single_message_handler_id)
			self.single_message_handler_id = None
140 141 142
		if self.new_chat_handler_id:
			chat_with_menuitem.disconnect(self.new_chat_handler_id)
			self.new_chat_handler_id = None
143 144

		sub_menu = gtk.Menu()
145
		self.popup_menus.append(sub_menu)
146
		status_menuitem.set_submenu(sub_menu)
147

jimpp's avatar
jimpp committed
148 149
		gc_sub_menu = gtk.Menu() # gc is always a submenu
		join_gc_menuitem.set_submenu(gc_sub_menu)
150

151 152
		# We need our own set of status icons, let's make 'em!
		iconset = gajim.config.get('iconset')
153
		path = os.path.join(helpers.get_iconset_path(iconset), '16x16')
154
		state_images = gajim.interface.roster.load_iconset(path)
155

156 157 158
		if state_images.has_key('muc_active'):
			join_gc_menuitem.set_image(state_images['muc_active'])

159
		for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'):
160
			uf_show = helpers.get_uf_show(show, use_mnemonic = True)
161 162
			item = gtk.ImageMenuItem(uf_show)
			item.set_image(state_images[show])
163 164 165
			sub_menu.append(item)
			item.connect('activate', self.on_show_menuitem_activate, show)

166 167 168
		item = gtk.SeparatorMenuItem()
		sub_menu.append(item)

169
		item = gtk.ImageMenuItem(_('_Change Status Message...'))
170
		path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'kbd_input.png')
171 172 173
		img = gtk.Image()
		img.set_from_file(path)
		item.set_image(img)
174 175
		sub_menu.append(item)
		item.connect('activate', self.on_change_status_message_activate)
176
		connected_accounts = gajim.get_number_of_connected_accounts()
177
		if connected_accounts < 1:
178 179 180 181 182
			item.set_sensitive(False)

		item = gtk.SeparatorMenuItem()
		sub_menu.append(item)

183
		uf_show = helpers.get_uf_show('offline', use_mnemonic = True)
184 185 186 187 188
		item = gtk.ImageMenuItem(uf_show)
		item.set_image(state_images['offline'])
		sub_menu.append(item)
		item.connect('activate', self.on_show_menuitem_activate, 'offline')

sb's avatar
sb committed
189 190
		iskey = connected_accounts > 0 and not (connected_accounts == 1 and
				gajim.connections[gajim.connections.keys()[0]].is_zeroconf)
Vincent Hanquez's avatar
Vincent Hanquez committed
191
		chat_with_menuitem.set_sensitive(iskey)
192
		single_message_menuitem.set_sensitive(iskey)
jimpp's avatar
jimpp committed
193
		join_gc_menuitem.set_sensitive(iskey)
194

195
		if connected_accounts >= 2: # 2 or more connections? make submenus
nkour's avatar
nkour committed
196 197
			account_menu_for_chat_with = gtk.Menu()
			chat_with_menuitem.set_submenu(account_menu_for_chat_with)
198
			self.popup_menus.append(account_menu_for_chat_with)
nkour's avatar
nkour committed
199

200
			account_menu_for_single_message = gtk.Menu()
201 202
			single_message_menuitem.set_submenu(
				account_menu_for_single_message)
203
			self.popup_menus.append(account_menu_for_single_message)
204

205
			accounts_list = gajim.contacts.get_accounts()
206
			accounts_list.sort()
207
			for account in accounts_list:
sb's avatar
sb committed
208 209
				if gajim.connections[account].is_zeroconf:
					continue
210
				if gajim.connections[account].connected > 1:
211 212 213
					#for chat_with
					item = gtk.MenuItem(_('using account %s') % account)
					account_menu_for_chat_with.append(item)
214
					item.connect('activate', self.on_new_chat, account)
jimpp's avatar
jimpp committed
215

216 217 218 219 220
					#for single message
					item = gtk.MenuItem(_('using account %s') % account)
					item.connect('activate',
						self.on_single_message_menuitem_activate, account)
					account_menu_for_single_message.append(item)
jimpp's avatar
jimpp committed
221 222

					# join gc 
223
					gc_item = gtk.MenuItem(_('using account %s') % account, False)
jimpp's avatar
jimpp committed
224
					gc_sub_menu.append(gc_item)
225 226
					gc_menuitem_menu = gtk.Menu()
					gajim.interface.roster.add_bookmarks_list(gc_menuitem_menu,
227
						account)
228 229
					gc_item.set_submenu(gc_menuitem_menu)
					gc_sub_menu.show_all()
230

231 232 233
		elif connected_accounts == 1: # one account
			# one account connected, no need to show 'as jid'
			for account in gajim.connections:
234
				if gajim.connections[account].connected > 1:
235 236
					self.new_chat_handler_id = chat_with_menuitem.connect(
							'activate', self.on_new_chat, account)
237 238
					# for single message
					single_message_menuitem.remove_submenu()
239 240 241
					self.single_message_handler_id = single_message_menuitem.\
						connect('activate',
						self.on_single_message_menuitem_activate, account)
242

jimpp's avatar
jimpp committed
243
					# join gc
244 245
					gajim.interface.roster.add_bookmarks_list(gc_sub_menu,
						account)
246
					break # No other connected account
247

248
		sounds_mute_menuitem.set_active(not gajim.config.get('sounds_on'))
249

250
		if os.name == 'nt':
251
			if gtk.pygtk_version >= (2, 10, 0) and gtk.gtk_version >= (2, 10, 0):
jimpp's avatar
jimpp committed
252
				if self.added_hide_menuitem is False:
253 254 255 256 257
					self.systray_context_menu.prepend(gtk.SeparatorMenuItem())
					item = gtk.MenuItem(_('Hide this menu'))
					self.systray_context_menu.prepend(item)
					self.added_hide_menuitem = True

nkour's avatar
nkour committed
258
		self.systray_context_menu.show_all()
259 260
		self.systray_context_menu.popup(None, None, None, event_button,
			event_time)
nkour's avatar
nkour committed
261

262
	def on_show_all_events_menuitem_activate(self, widget):
263 264 265 266 267
		events = gajim.events.get_systray_events()
		for account in events:
			for jid in events[account]:
				for event in events[account][jid]:
					gajim.interface.handle_event(account, jid, event.type_)
268

269 270 271 272
	def on_sounds_mute_menuitem_activate(self, widget):
		gajim.config.set('sounds_on', not widget.get_active()) 
		gajim.interface.save_config()

273 274 275 276
	def on_show_roster_menuitem_activate(self, widget):
		win = gajim.interface.roster.window
		win.present()

nkour's avatar
nkour committed
277
	def on_preferences_menuitem_activate(self, widget):
278
		if gajim.interface.instances.has_key('preferences'):
279
			gajim.interface.instances['preferences'].window.present()
nkour's avatar
nkour committed
280
		else:
281
			gajim.interface.instances['preferences'] = config.PreferencesWindow()
nkour's avatar
nkour committed
282

nkour's avatar
nkour committed
283
	def on_quit_menuitem_activate(self, widget):	
284
		gajim.interface.roster.on_quit_menuitem_activate(widget)
nkour's avatar
nkour committed
285

nkour's avatar
nkour committed
286
	def on_left_click(self):
287
		win = gajim.interface.roster.window
288
		# toggle visible/hidden for roster window
289 290
		if win.get_property('visible') and win.get_property('has-toplevel-focus'):
			# visible in ANY virtual desktop?
291 292 293 294 295

			# we could be in another VD right now. eg vd2
			# and we want to show it in vd2
			if not gtkgui_helpers.possibly_move_window_in_current_desktop(win):
				win.hide() # else we hide it from VD that was visible in
nkour's avatar
nkour committed
296
		else:
297 298 299 300 301
			# in Windows (perhaps other Window Managers too) minimize state
			# is remembered, so make sure it's not minimized (iconified)
			# because user wants to see roster
			win.deiconify()
			win.present()
302 303

	def handle_first_event(self):
304 305
		account, jid, event = gajim.events.get_first_systray_event()
		gajim.interface.handle_event(account, jid, event.type_)
306

nkour's avatar
nkour committed
307
	def on_middle_click(self):
nkour's avatar
nkour committed
308 309
		'''middle click raises window to have complete focus (fe. get kbd events)
		but if already raised, it hides it'''
310 311 312
		if len(gajim.events.get_systray_events()) == 0:
			return
		self.handle_first_event()
nkour's avatar
nkour committed
313 314 315

	def on_clicked(self, widget, event):
		self.on_tray_leave_notify_event(widget, None)
nkour's avatar
nkour committed
316 317 318
		if event.type != gtk.gdk.BUTTON_PRESS:
			return
		if event.button == 1: # Left click
nkour's avatar
nkour committed
319
			self.on_left_click()
320
		elif event.button == 2: # middle click
nkour's avatar
nkour committed
321
			self.on_middle_click()
322
		elif event.button == 3: # right click
323
			self.make_menu(event.button, event.time)
324

325
	def on_show_menuitem_activate(self, widget, show):
nkour's avatar
nkour committed
326 327 328 329
		# we all add some fake (we cannot select those nor have them as show)
		# but this helps to align with roster's status_combobox index positions
		l = ['online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'SEPARATOR',
			'CHANGE_STATUS_MSG_MENUITEM', 'SEPARATOR', 'offline']
330
		index = l.index(show)
331 332 333
		if not helpers.statuses_unified():
			gajim.interface.roster.status_combobox.set_active(index + 2)
			return
334 335 336
		current = gajim.interface.roster.status_combobox.get_active()
		if index != current:
			gajim.interface.roster.status_combobox.set_active(index)
337 338

	def on_change_status_message_activate(self, widget):
nkour's avatar
fix tb  
nkour committed
339
		model = gajim.interface.roster.status_combobox.get_model()
nkour's avatar
nkour committed
340
		active = gajim.interface.roster.status_combobox.get_active()
341 342
		status = model[active][2].decode('utf-8')
		dlg = dialogs.ChangeStatusMessageDialog(status)
343
		dlg.window.present()
344
		message = dlg.run()
345 346 347
		if message is not None: # None if user press Cancel
			accounts = gajim.connections.keys()
			for acct in accounts:
348 349 350
				if not gajim.config.get_per('accounts', acct,
					'sync_with_global_status'):
					continue
351
				show = gajim.SHOW_LIST[gajim.connections[acct].connected]
352
				gajim.interface.roster.send_status(acct, show, message)
353

354 355 356 357
	def show_tooltip(self, widget):
		position = widget.window.get_origin()
		if self.tooltip.id == position:
			size = widget.window.get_size()
358
			self.tooltip.show_tooltip('', size[1], position[1])
359

360 361 362 363 364 365 366 367 368 369
	def on_tray_motion_notify_event(self, widget, event):
		position = widget.window.get_origin()
		if self.tooltip.timeout > 0:
			if self.tooltip.id != position:
				self.tooltip.hide_tooltip()
		if self.tooltip.timeout == 0 and \
			self.tooltip.id != position:
			self.tooltip.id = position
			self.tooltip.timeout = gobject.timeout_add(500,
				self.show_tooltip, widget)
370

371 372 373 374 375
	def on_tray_leave_notify_event(self, widget, event):
		position = widget.window.get_origin()
		if self.tooltip.timeout > 0 and \
			self.tooltip.id == position:
			self.tooltip.hide_tooltip()
376 377 378 379

	def on_tray_destroyed(self, widget):
		'''re-add trayicon when systray is destroyed'''
		self.t = None
380 381
		if gajim.interface.systray_enabled:
			self.show_icon()
382

383 384
	def show_icon(self):
		if not self.t:
nkour's avatar
fixes  
nkour committed
385
			self.t = trayicon.TrayIcon('Gajim')
386
			self.t.connect('destroy', self.on_tray_destroyed)
387
			eb = gtk.EventBox()
388 389
			# avoid draw seperate bg color in some gtk themes
			eb.set_visible_window(False)
390
			eb.set_events(gtk.gdk.POINTER_MOTION_MASK)
nkour's avatar
fixes  
nkour committed
391
			eb.connect('button-press-event', self.on_clicked)
392 393
			eb.connect('motion-notify-event', self.on_tray_motion_notify_event)
			eb.connect('leave-notify-event', self.on_tray_leave_notify_event)
394
			self.tooltip = tooltips.NotificationAreaTooltip()
dkirov's avatar
dkirov committed
395

396 397 398 399
			self.img_tray = gtk.Image()
			eb.add(self.img_tray)
			self.t.add(eb)
			self.set_img()
400
			self.subscribe_events()
401
		self.t.show_all()
402

403 404 405 406
	def hide_icon(self):
		if self.t:
			self.t.destroy()
			self.t = None
407
			self.unsubscribe_events()