## roster_window.py ## ## Contributors for this file: ## - Yann Le Boulanger ## - Nikos Kouremenos ## - Dimitur Kirov ## ## Copyright (C) 2003-2004 Yann Le Boulanger ## Vincent Hanquez ## Copyright (C) 2005 Yann Le Boulanger ## Vincent Hanquez ## Nikos Kouremenos ## Dimitur Kirov ## Travis Shirk ## Norman Rasmussen ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published ## by the Free Software Foundation; version 2 only. ## ## This program is distributed in the hope that it will be useful, ## 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. ## import gtk import gtk.glade import gobject import os import time import common.sleepy import tabbed_chat_window import groupchat_window import history_window import dialogs import vcard import config import disco import gtkgui_helpers import cell_renderer_image import tooltips from gajim import Contact from common import gajim from common import helpers from common import i18n _ = i18n._ APP = i18n.APP gtk.glade.bindtextdomain(APP, i18n.DIR) gtk.glade.textdomain(APP) #(icon, name, type, jid, account, editable, second pixbuf) ( C_IMG, # image to show state (online, new message etc) C_NAME, # cellrenderer text that holds contact nickame C_TYPE, # account, group or contact? C_JID, # the jid of the row C_ACCOUNT, # cellrenderer text that holds account name C_EDITABLE, # cellrenderer text that holds name editable or not? C_SECPIXBUF, # secondary_pixbuf (holds avatar or padlock) ) = range(7) GTKGUI_GLADE = 'gtkgui.glade' class RosterWindow: '''Class for main window of gtkgui interface''' def get_account_iter(self, name): model = self.tree.get_model() if model is None: return account_iter = model.get_iter_root() if self.regroup: return account_iter while account_iter: account_name = model[account_iter][C_NAME].decode('utf-8') if name == account_name: break account_iter = model.iter_next(account_iter) return account_iter def get_group_iter(self, name, account): model = self.tree.get_model() root = self.get_account_iter(account) group_iter = model.iter_children(root) while group_iter: group_name = model[group_iter][C_NAME].decode('utf-8') if name == group_name: break group_iter = model.iter_next(group_iter) return group_iter def get_contact_iter(self, jid, account): model = self.tree.get_model() acct = self.get_account_iter(account) found = [] if model is None: # when closing Gajim model can be none (async pbs?) return found group_iter = model.iter_children(acct) while group_iter: user_iter = model.iter_children(group_iter) while user_iter: if jid == model[user_iter][C_JID].decode('utf-8') and \ account == model[user_iter][C_ACCOUNT].decode('utf-8'): found.append(user_iter) user_iter = model.iter_next(user_iter) group_iter = model.iter_next(group_iter) return found def add_account_to_roster(self, account): model = self.tree.get_model() if self.get_account_iter(account): return if self.regroup: show = helpers.get_global_show() model.append(None, [self.jabber_state_images[show], _('Merged accounts'), 'account', '', 'all', False, None]) return show = gajim.SHOW_LIST[gajim.connections[account].connected] tls_pixbuf = None if gajim.con_types.has_key(account) and \ gajim.con_types[account] in ('tls', 'ssl'): tls_pixbuf = self.window.render_icon(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU) # the only way to create a pixbuf from stock our_jid = gajim.get_jid_from_account(account) model.append(None, [self.jabber_state_images[show], gtkgui_helpers.escape_for_pango_markup(account), 'account', our_jid, account, False, tls_pixbuf]) def remove_newly_added(self, jid, account): if jid in gajim.newly_added[account]: gajim.newly_added[account].remove(jid) self.draw_contact(jid, account) def add_contact_to_roster(self, jid, account): '''Add a contact to the roster and add groups if they aren't in roster''' showOffline = gajim.config.get('showoffline') if not gajim.contacts[account].has_key(jid): return users = gajim.contacts[account][jid] user = users[0] if user.jid.find('@') <= 0: # if not '@' or '@' starts the jid ==> agent user.groups = [_('Transports')] elif user.groups == []: user.groups.append(_('General')) hide = True if user.sub in ('both', 'to'): hide = False elif user.ask == 'subscribe': hide = False # FIXME: uncomment when we support contacts in no group # elif user.name or len(user.groups): elif user.name: hide = False # JEP-0162 if hide: return if user.show in ('offline', 'error') and \ not showOffline and (not _('Transports') in user.groups or \ gajim.connections[account].connected < 2) and \ not gajim.awaiting_events[account].has_key(user.jid): return model = self.tree.get_model() for g in user.groups: iterG = self.get_group_iter(g, account) if not iterG: IterAcct = self.get_account_iter(account) iterG = model.append(IterAcct, [self.jabber_state_images['closed'], gtkgui_helpers.escape_for_pango_markup(g), 'group', g, account, False, None]) if not gajim.groups[account].has_key(g): #It can probably never append if account + g in self.collapsed_rows: ishidden = False else: ishidden = True gajim.groups[account][g] = { 'expand': ishidden } if not account in self.collapsed_rows: self.tree.expand_row((model.get_path(iterG)[0]), False) typestr = 'contact' if g == _('Transports'): typestr = 'agent' # we add some values here. see draw_contact for more model.append(iterG, (None, user.name, typestr, user.jid, account, False, None)) if gajim.groups[account][g]['expand']: self.tree.expand_row(model.get_path(iterG), False) self.draw_contact(jid, account) self.draw_avatar(jid, account) def really_remove_contact(self, user, account): if user.jid in gajim.newly_added[account]: return if user.jid.find('@') < 1 and gajim.connections[account].connected > 1: # It's an agent return if user.jid in gajim.to_be_removed[account]: gajim.to_be_removed[account].remove(user.jid) if gajim.config.get('showoffline'): self.draw_contact(user.jid, account) return self.remove_contact(user, account) def remove_contact(self, user, account): '''Remove a user from the roster''' if user.jid in gajim.to_be_removed[account]: return model = self.tree.get_model() for i in self.get_contact_iter(user.jid, account): parent_i = model.iter_parent(i) group = model.get_value(parent_i, 3).decode('utf-8') model.remove(i) if model.iter_n_children(parent_i) == 0: model.remove(parent_i) # We need to check all contacts, even offline contacts for jid in gajim.contacts[account]: if group in gajim.get_contact_instance_with_highest_priority(account, jid).groups: break else: if gajim.groups[account].has_key(group): del gajim.groups[account][group] def get_appropriate_state_images(self, jid): '''check jid and return the appropriate state images dict''' transport = gajim.get_transport_name_from_jid(jid) if transport: return self.transports_state_images[transport] return self.jabber_state_images def draw_contact(self, jid, account, selected = False, focus = False): '''draw the correct state image, name BUT not avatar''' # focus is about if the roster window has toplevel-focus or not model = self.tree.get_model() iters = self.get_contact_iter(jid, account) if len(iters) == 0: return contact_instances = gajim.get_contact_instances_from_jid(account, jid) contact = gajim.get_highest_prio_contact_from_contacts(contact_instances) name = gtkgui_helpers.escape_for_pango_markup(contact.name) if len(contact_instances) > 1: name += ' (' + unicode(len(contact_instances)) + ')' # FIXME: remove when we use metacontacts # shoz (account_name) if there are 2 contact with same jid in merged mode if self.regroup: add_acct = False # look through all contacts of all accounts for a in gajim.connections: for j in gajim.contacts[a]: # [0] cause it'fster than highest_prio c = gajim.contacts[a][j][0] if c.name == contact.name and (j, a) != (jid, account): add_acct = True break if add_acct: # No need to continue in other account if we already found one break if add_acct: name += ' (' + account + ')' # add status msg, if not empty, under contact name in the treeview if contact.status and gajim.config.get('show_status_msgs_in_roster'): status = contact.status.strip() if status != '': status = gtkgui_helpers.reduce_chars_newlines(status, max_lines = 1) # escape markup entities and make them small italic and fg color color = gtkgui_helpers._get_fade_color(self.tree, selected, focus) colorstring = "#%04x%04x%04x" % (color.red, color.green, color.blue) name += '\n%s'\ % (colorstring, gtkgui_helpers.escape_for_pango_markup(status)) state_images = self.get_appropriate_state_images(jid) icon_name = helpers.get_icon_name_to_show(contact, account) img = state_images[icon_name] for iter in iters: model[iter][C_IMG] = img model[iter][C_NAME] = name def draw_avatar(self, jid, account): '''draw the avatar''' model = self.tree.get_model() iters = self.get_contact_iter(jid, account) if gajim.config.get('show_avatars_in_roster'): pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid) if pixbuf in ('ask', None): scaled_pixbuf = None else: scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'roster') else: scaled_pixbuf = None for iter in iters: model[iter][C_SECPIXBUF] = scaled_pixbuf def join_gc_room(self, account, room_jid, nick, password): '''joins the room immediatelly''' if room_jid in gajim.interface.instances[account]['gc'] and \ gajim.gc_connected[account][room_jid]: dialogs.ErrorDialog(_('You are already in room %s') % room_jid ).get_response() return invisible_show = gajim.SHOW_LIST.index('invisible') if gajim.connections[account].connected == invisible_show: dialogs.ErrorDialog(_('You cannot join a room while you are invisible') ).get_response() return room, server = room_jid.split('@') if not room_jid in gajim.interface.instances[account]['gc']: self.new_room(room_jid, nick, account) gajim.interface.instances[account]['gc'][room_jid].set_active_tab(room_jid) gajim.interface.instances[account]['gc'][room_jid].window.present() gajim.connections[account].join_gc(nick, room, server, password) if password: gajim.gc_passwords[room_jid] = password def on_bookmark_menuitem_activate(self, widget, account, bookmark): self.join_gc_room(account, bookmark['jid'], bookmark['nick'], bookmark['password']) def on_bm_header_changed_state(self, widget, event): widget.set_state(gtk.STATE_NORMAL) #do not allow selected_state def on_send_server_message_menuitem_activate(self, widget, account): server = gajim.config.get_per('accounts', account, 'hostname') server += '/announce/online' dialogs.SingleMessageWindow(account, server, 'send') def on_xml_console_menuitem_activate(self, widget, account): if gajim.interface.instances[account].has_key('xml_console'): gajim.interface.instances[account]['xml_console'].window.present() else: gajim.interface.instances[account]['xml_console'].window.show_all() def on_set_motd_menuitem_activate(self, widget, account): server = gajim.config.get_per('accounts', account, 'hostname') server += '/announce/motd' dialogs.SingleMessageWindow(account, server, 'send') def on_update_motd_menuitem_activate(self, widget, account): server = gajim.config.get_per('accounts', account, 'hostname') server += '/announce/motd/update' dialogs.SingleMessageWindow(account, server, 'send') def on_delete_motd_menuitem_activate(self, widget, account): server = gajim.config.get_per('accounts', account, 'hostname') server += '/announce/motd/delete' gajim.connections[account].send_motd(server) def on_online_users_menuitem_activate(self, widget, account): pass #FIXME: impement disco in users for 0.9 def get_and_connect_advanced_menuitem_menu(self, account): xml = gtk.glade.XML(GTKGUI_GLADE, 'advanced_menuitem_menu', APP) advanced_menuitem_menu = xml.get_widget('advanced_menuitem_menu') send_single_message_menuitem = xml.get_widget( 'send_single_message_menuitem') xml_console_menuitem = xml.get_widget('xml_console_menuitem') administrator_menuitem = xml.get_widget('administrator_menuitem') online_users_menuitem = xml.get_widget('online_users_menuitem') send_server_message_menuitem = xml.get_widget( 'send_server_message_menuitem') set_motd_menuitem = xml.get_widget('set_motd_menuitem') update_motd_menuitem = xml.get_widget('update_motd_menuitem') delete_motd_menuitem = xml.get_widget('delete_motd_menuitem') send_single_message_menuitem.connect('activate', self.on_send_single_message_menuitem_activate, account) xml_console_menuitem.connect('activate', self.on_xml_console_menuitem_activate, account) #FIXME: 0.9 should have this: it does disco in the place where users are online_users_menuitem.set_no_show_all(True) online_users_menuitem.hide() online_users_menuitem.connect('activate', self.on_online_users_menuitem_activate, account) send_server_message_menuitem.connect('activate', self.on_send_server_message_menuitem_activate, account) set_motd_menuitem.connect('activate', self.on_set_motd_menuitem_activate, account) update_motd_menuitem.connect('activate', self.on_update_motd_menuitem_activate, account) delete_motd_menuitem.connect('activate', self.on_delete_motd_menuitem_activate, account) advanced_menuitem_menu.show_all() return advanced_menuitem_menu def make_menu(self): '''create the main window's menus''' new_message_menuitem = self.xml.get_widget('new_message_menuitem') join_gc_menuitem = self.xml.get_widget('join_gc_menuitem') add_new_contact_menuitem = self.xml.get_widget('add_new_contact_menuitem') service_disco_menuitem = self.xml.get_widget('service_disco_menuitem') advanced_menuitem = self.xml.get_widget('advanced_menuitem') show_offline_contacts_menuitem = self.xml.get_widget( 'show_offline_contacts_menuitem') profile_avatar_menuitem = self.xml.get_widget('profile_avatar_menuitem') # make it sensitive. it is insensitive only if no accounts are *available* advanced_menuitem.set_sensitive(True) if self.add_new_contact_handler_id: add_new_contact_menuitem.handler_disconnect( self.add_new_contact_handler_id) self.add_new_contact_handler_id = None if self.service_disco_handler_id: service_disco_menuitem.handler_disconnect( self.service_disco_handler_id) self.service_disco_handler_id = None if self.new_message_menuitem_handler_id: new_message_menuitem.handler_disconnect( self.new_message_menuitem_handler_id) self.new_message_menuitem_handler_id = None #remove the existing submenus add_new_contact_menuitem.remove_submenu() service_disco_menuitem.remove_submenu() join_gc_menuitem.remove_submenu() new_message_menuitem.remove_submenu() advanced_menuitem.remove_submenu() #remove the existing accelerator if self.have_new_message_accel: ag = gtk.accel_groups_from_object(self.window)[0] new_message_menuitem.remove_accelerator(ag, gtk.keysyms.n, gtk.gdk.CONTROL_MASK) self.have_new_message_accel = False #join gc sub_menu = gtk.Menu() join_gc_menuitem.set_submenu(sub_menu) at_least_one_account_connected = False multiple_accounts = len(gajim.connections) >= 2 #FIXME: stop using bool var here for account in gajim.connections: if gajim.connections[account].connected <= 1: #if offline or connecting continue if not at_least_one_account_connected: at_least_one_account_connected = True if multiple_accounts: label = gtk.Label() label.set_markup('' + account.upper() +'') item = gtk.MenuItem() item.add(label) item.connect('state-changed', self.on_bm_header_changed_state) sub_menu.append(item) item = gtk.MenuItem(_('_Join New Room')) item.connect('activate', self.on_join_gc_activate, account) sub_menu.append(item) for bookmark in gajim.connections[account].bookmarks: item = gtk.MenuItem(bookmark['name']) item.connect('activate', self.on_bookmark_menuitem_activate, account, bookmark) sub_menu.append(item) if at_least_one_account_connected: #FIXME: move this below where we do this check #and make sure it works newitem = gtk.SeparatorMenuItem() # seperator sub_menu.append(newitem) newitem = gtk.ImageMenuItem(_('Manage Bookmarks...')) img = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) newitem.set_image(img) newitem.connect('activate', self.on_manage_bookmarks_menuitem_activate) sub_menu.append(newitem) sub_menu.show_all() if multiple_accounts: # 2 or more accounts? make submenus #add sub_menu = gtk.Menu() for account in gajim.connections: if gajim.connections[account].connected <= 1: #if offline or connecting continue item = gtk.MenuItem(_('to %s account') % account) sub_menu.append(item) item.connect('activate', self.on_add_new_contact, account) add_new_contact_menuitem.set_submenu(sub_menu) sub_menu.show_all() #disco sub_menu = gtk.Menu() for account in gajim.connections: if gajim.connections[account].connected <= 1: #if offline or connecting continue item = gtk.MenuItem(_('using %s account') % account) sub_menu.append(item) item.connect('activate', self.on_service_disco_menuitem_activate, account) service_disco_menuitem.set_submenu(sub_menu) sub_menu.show_all() #new message sub_menu = gtk.Menu() for account in gajim.connections: if gajim.connections[account].connected <= 1: #if offline or connecting continue item = gtk.MenuItem(_('using account %s') % account) sub_menu.append(item) item.connect('activate', self.on_new_message_menuitem_activate, account) new_message_menuitem.set_submenu(sub_menu) sub_menu.show_all() #Advanced Actions sub_menu = gtk.Menu() for account in gajim.connections: item = gtk.MenuItem(_('for account %s') % account) sub_menu.append(item) advanced_menuitem_menu = self.get_and_connect_advanced_menuitem_menu( account) item.set_submenu(advanced_menuitem_menu) advanced_menuitem.set_submenu(sub_menu) sub_menu.show_all() else: if len(gajim.connections) == 1: # user has only one account #add if not self.add_new_contact_handler_id: self.add_new_contact_handler_id = add_new_contact_menuitem.connect( 'activate', self.on_add_new_contact, gajim.connections.keys()[0]) #disco if not self.service_disco_handler_id: self.service_disco_handler_id = service_disco_menuitem.connect( 'activate', self.on_service_disco_menuitem_activate, gajim.connections.keys()[0]) #new msg if not self.new_message_menuitem_handler_id: self.new_message_menuitem_handler_id = new_message_menuitem.\ connect('activate', self.on_new_message_menuitem_activate, gajim.connections.keys()[0]) #new msg accel if not self.have_new_message_accel: ag = gtk.accel_groups_from_object(self.window)[0] new_message_menuitem.add_accelerator('activate', ag, gtk.keysyms.n, gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) self.have_new_message_accel = True account = gajim.connections.keys()[0] advanced_menuitem_menu = self.get_and_connect_advanced_menuitem_menu( account) advanced_menuitem.set_submenu(advanced_menuitem_menu) elif len(gajim.connections) == 0: # user has no accounts advanced_menuitem.set_sensitive(False) #FIXME: Gajim 0.9 should have this visible profile_avatar_menuitem.set_no_show_all(True) profile_avatar_menuitem.hide() if at_least_one_account_connected: new_message_menuitem.set_sensitive(True) join_gc_menuitem.set_sensitive(True) add_new_contact_menuitem.set_sensitive(True) service_disco_menuitem.set_sensitive(True) show_offline_contacts_menuitem.set_sensitive(True) else: # make the menuitems insensitive new_message_menuitem.set_sensitive(False) join_gc_menuitem.set_sensitive(False) add_new_contact_menuitem.set_sensitive(False) service_disco_menuitem.set_sensitive(False) show_offline_contacts_menuitem.set_sensitive(False) profile_avatar_menuitem.set_sensitive(False) def _change_style(self, model, path, iter, option): if option is None: model[iter][C_NAME] = model[iter][C_NAME] elif model[iter][C_TYPE] == 'account': if option == 'account': model[iter][C_NAME] = model[iter][C_NAME] elif model[iter][C_TYPE] == 'group': if option == 'group': model[iter][C_NAME] = model[iter][C_NAME] elif model[iter][C_TYPE] == 'contact': if option == 'contact': model[iter][C_NAME] = model[iter][C_NAME] def change_roster_style(self, option): model = self.tree.get_model() model.foreach(self._change_style, option) # update gc's roster for account in gajim.connections: gcs = gajim.interface.instances[account]['gc'] if gcs.has_key('tabbed'): gcs['tabbed'].draw_all_roster() else: for room_jid in gcs: gcs[room_jid].draw_all_roster() def draw_roster(self): '''Clear and draw roster''' self.tree.get_model().clear() for acct in gajim.connections: self.add_account_to_roster(acct) for jid in gajim.contacts[acct].keys(): self.add_contact_to_roster(jid, acct) self.make_menu() # re-make menu in case an account was removed #FIXME: maybe move thie make_menu() in where we remove the account? def fill_contacts_and_groups_dicts(self, array, account): '''fill gajim.contacts and gajim.groups''' if not gajim.contacts.has_key(account): gajim.contacts[account] = {} if not gajim.groups.has_key(account): gajim.groups[account] = {} for jid in array.keys(): jids = jid.split('/') #get jid ji = jids[0] #get resource resource = '' if len(jids) > 1: resource = '/'.join(jids[1:]) #get name name = array[jid]['name'] if not name: if ji.find('@') <= 0: name = ji else: name = jid.split('@')[0] show = 'offline' # show is offline by default status = '' #no status message by default keyID = '' attached_keys = gajim.config.get_per('accounts', account, 'attached_gpg_keys').split() if jid in attached_keys: keyID = attached_keys[attached_keys.index(jid) + 1] contact1 = Contact(jid = ji, name = name, groups = array[jid]['groups'], show = show, status = status, sub = array[jid]['subscription'], ask = array[jid]['ask'], resource = resource, keyID = keyID) # when we draw the roster, we avoid having the same contact # more than once (f.e. we avoid showing it twice when 2 resources) gajim.contacts[account][ji] = [contact1] for g in array[jid]['groups']: if g in gajim.groups[account].keys(): continue if account + g in self.collapsed_rows: ishidden = False else: ishidden = True gajim.groups[account][g] = { 'expand': ishidden } if gajim.config.get('ask_avatars_on_startup'): pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(ji) if pixbuf == 'ask': jid_with_resource = contact1.jid if contact1.resource: jid_with_resource += '/' + contact1.resource gajim.connections[account].request_vcard(jid_with_resource) def chg_contact_status(self, contact, show, status, account): '''When a contact changes his or her status''' showOffline = gajim.config.get('showoffline') contact_instances = gajim.contacts[account][contact.jid] contact.show = show contact.status = status if show in ('offline', 'error') and \ not gajim.awaiting_events[account].has_key(contact.jid): if len(contact_instances) > 1: # if multiple resources contact_instances.remove(contact) self.draw_contact(contact.jid, account) elif not showOffline: self.remove_contact(contact, account) else: self.draw_contact(contact.jid, account) else: if not self.get_contact_iter(contact.jid, account): self.add_contact_to_roster(contact.jid, account) self.draw_contact(contact.jid, account) # print status in chat window and update status/GPG image if gajim.interface.instances[account]['chats'].has_key(contact.jid): jid = contact.jid gajim.interface.instances[account]['chats'][jid].set_state_image(jid) name = contact.name if contact.resource != '': name += '/' + contact.resource uf_show = helpers.get_uf_show(show) gajim.interface.instances[account]['chats'][jid].print_conversation( _('%s is now %s (%s)') % (name, uf_show, status), jid, 'status') if contact == gajim.get_contact_instance_with_highest_priority(\ account, contact.jid): gajim.interface.instances[account]['chats'][jid].draw_name_banner(contact) def on_info(self, widget, user, account): '''Call vcard_information_window class to display user's information''' info = gajim.interface.instances[account]['infos'] if info.has_key(user.jid): info[user.jid].window.present() else: info[user.jid] = vcard.VcardWindow(user, account) def show_tooltip(self, contact): pointer = self.tree.get_pointer() props = self.tree.get_path_at_pos(pointer[0], pointer[1]) if props and self.tooltip.id == props[0]: # check if the current pointer is at the same path # as it was before setting the timeout rect = self.tree.get_cell_area(props[0], props[1]) position = self.tree.window.get_origin() pointer = self.window.get_pointer() self.tooltip.show_tooltip(contact, (pointer[0], rect.height), (position[0], position[1] + rect.y)) else: self.tooltip.hide_tooltip() def on_roster_treeview_leave_notify_event(self, widget, event): model = widget.get_model() props = widget.get_path_at_pos(int(event.x), int(event.y)) if self.tooltip.timeout > 0: if not props or self.tooltip.id == props[0]: self.tooltip.hide_tooltip() def on_roster_treeview_motion_notify_event(self, widget, event): model = widget.get_model() props = widget.get_path_at_pos(int(event.x), int(event.y)) if self.tooltip.timeout > 0: if not props or self.tooltip.id != props[0]: self.tooltip.hide_tooltip() if props: [row, col, x, y] = props iter = None try: iter = model.get_iter(row) except: self.tooltip.hide_tooltip() return if model[iter][C_TYPE] == 'contact': # we're on a contact entry in the roster account = model[iter][C_ACCOUNT].decode('utf-8') jid = model[iter][C_JID].decode('utf-8') if self.tooltip.timeout == 0 or self.tooltip.id != props[0]: self.tooltip.id = row self.tooltip.timeout = gobject.timeout_add(500, self.show_tooltip, gajim.contacts[account][jid]) elif model[iter][C_TYPE] == 'account': # we're on an account entry in the roster account = model[iter][C_ACCOUNT].decode('utf-8') if account == 'all': if self.tooltip.timeout == 0 or self.tooltip.id != props[0]: self.tooltip.id = row self.tooltip.timeout = gobject.timeout_add(500, self.show_tooltip, []) return jid = gajim.get_jid_from_account(account) contacts = [] connection = gajim.connections[account] # get our current contact info contact = Contact(jid = jid, name = account, show = connection.get_status(), sub = 'both', status = connection.status, resource = gajim.config.get_per('accounts', connection.name, 'resource'), priority = gajim.config.get_per('accounts', connection.name, 'priority'), keyID = gajim.config.get_per('accounts', connection.name, 'keyid')) contacts.append(contact) # if we're online ... if connection.connection: roster = connection.connection.getRoster() if roster.getItem(jid): resources = roster.getResources(jid) # ...get the contact info for our other online resources for resource in resources: show = roster.getShow(jid+'/'+resource) if not show: show = 'online' contact = Contact(jid=jid, name=account, show=show, status=roster.getStatus(jid+'/'+resource), resource=resource, priority=roster.getPriority(jid+'/'+resource)) contacts.append(contact) if self.tooltip.timeout == 0 or self.tooltip.id != props[0]: self.tooltip.id = row self.tooltip.timeout = gobject.timeout_add(500, self.show_tooltip, contacts) def on_agent_logging(self, widget, jid, state, account): '''When an agent is requested to log in or off''' gajim.connections[account].send_agent_status(jid, state) def on_edit_agent(self, widget, contact, account): '''When we want to modify the agent registration''' gajim.connections[account].request_register_agent_info(contact.jid) def on_remove_agent(self, widget, contact, account): '''When an agent is requested to log in or off''' if gajim.config.get_per('accounts', account, 'hostname') == contact.jid: # We remove the server contact # remove it from treeview self.remove_contact(contact, account) del gajim.contacts[account][contact.jid] return window = dialogs.ConfirmationDialog(_('Transport "%s" will be removed') % contact.jid, _('You will no longer be able to send and receive messages to contacts from this transport.')) if window.get_response() == gtk.RESPONSE_OK: gajim.connections[account].unsubscribe_agent(contact.jid + '/' \ + contact.resource) # remove transport from treeview self.remove_contact(contact, account) # remove transport's contacts from treeview for jid, contacts in gajim.contacts[account].items(): contact = contacts[0] if jid.endswith('@' + contact.jid): gajim.log.debug( 'Removing contact %s due to unregistered transport %s'\ % (contact.jid, contact.name)) self.remove_contact(contact, account) del gajim.contacts[account][contact.jid] def on_rename(self, widget, iter, path): # this function is called either by F2 or by Rename menuitem # to display that menuitem we show a menu, that does focus-out # we then select Rename and focus-in # focus-in callback checks on this var and if is NOT None # it redraws the selected contact resulting in stopping our rename # procedure. So set this to None to stop that self._last_selected_contact = None model = self.tree.get_model() row_type = model[iter][C_TYPE] jid = model[iter][C_JID].decode('utf-8') account = model[iter][C_ACCOUNT].decode('utf-8') if row_type == 'contact': # it's jid #Remove resource indicator (Name (2)) contacts = gajim.contacts[account][jid] name = contacts[0].name model[iter][C_NAME] = name model[iter][C_EDITABLE] = True # set 'editable' to True self.tree.set_cursor(path, self.tree.get_column(0), True) def on_assign_pgp_key(self, widget, user, account): attached_keys = gajim.config.get_per('accounts', account, 'attached_gpg_keys').split() keys = {} keyID = 'None' for i in xrange(0, len(attached_keys)/2): keys[attached_keys[2*i]] = attached_keys[2*i+1] if attached_keys[2*i] == user.jid: keyID = attached_keys[2*i+1] public_keys = gajim.connections[account].ask_gpg_keys() public_keys['None'] = 'None' instance = dialogs.ChooseGPGKeyDialog(_('Assign OpenPGP Key'), _('Select a key to apply to the contact'), public_keys, keyID) keyID = instance.run() if keyID is None: return if keyID[0] == 'None': if user.jid in keys: del keys[user.jid] else: keys[user.jid] = keyID[0] for u in gajim.contacts[account][user.jid]: u.keyID = keyID[0] if gajim.interface.instances[account]['chats'].has_key(user.jid): gajim.interface.instances[account]['chats'][user.jid].draw_widgets(user) keys_str = '' for jid in keys: keys_str += jid + ' ' + keys[jid] + ' ' gajim.config.set_per('accounts', account, 'attached_gpg_keys', keys_str) def on_edit_groups(self, widget, user, account): dlg = dialogs.EditGroupsDialog(user, account) dlg.run() def on_history(self, widget, contact, account): '''When history menuitem is activated: call log window''' if gajim.interface.instances['logs'].has_key(contact.jid): gajim.interface.instances['logs'][contact.jid].window.present() else: gajim.interface.instances['logs'][contact.jid] = history_window.\ HistoryWindow(contact.jid, account) def on_send_single_message_menuitem_activate(self, wiget, account, contact = None): if contact is None: dialogs.SingleMessageWindow(account, action = 'send') else: dialogs.SingleMessageWindow(account, contact.jid, 'send') def on_send_file_menuitem_activate(self, widget, account, contact): gajim.interface.instances['file_transfers'].show_file_send_request( account, contact) def mk_menu_user(self, event, iter): '''Make contact's popup menu''' model = self.tree.get_model() jid = model[iter][C_JID].decode('utf-8') path = model.get_path(iter) account = model[iter][C_ACCOUNT].decode('utf-8') contact = gajim.get_highest_prio_contact_from_contacts( gajim.contacts[account][jid]) xml = gtk.glade.XML(GTKGUI_GLADE, 'roster_contact_context_menu', APP) roster_contact_context_menu = xml.get_widget( 'roster_contact_context_menu') childs = roster_contact_context_menu.get_children() start_chat_menuitem = childs[0] send_single_message_menuitem = childs[1] rename_menuitem = childs[2] edit_groups_menuitem = childs[3] # separator4 goes with assign_openpgp_key_menuitem assign_openpgp_separator = childs[4] send_file_menuitem = childs[5] assign_openpgp_key_menuitem = childs[6] #skip a seperator send_auth_menuitem, ask_auth_menuitem, revoke_auth_menuitem =\ childs[8].get_submenu().get_children() add_to_roster_menuitem = childs[9] remove_from_roster_menuitem = childs[10] #skip a seperator information_menuitem = childs[12] history_menuitem = childs[13] if contact.resource: send_file_menuitem.connect('activate', self.on_send_file_menuitem_activate, account, contact) else: # if we do not have resource we cannot send file send_file_menuitem.hide() send_file_menuitem.set_no_show_all(True) start_chat_menuitem.connect('activate', self.on_roster_treeview_row_activated, path) send_single_message_menuitem.connect('activate', self.on_send_single_message_menuitem_activate, account, contact) rename_menuitem.connect('activate', self.on_rename, iter, path) remove_from_roster_menuitem.connect('activate', self.on_req_usub, contact, account) information_menuitem.connect('activate', self.on_info, contact, account) history_menuitem.connect('activate', self.on_history, contact, account) if _('not in the roster') not in contact.groups: #contact is in normal group edit_groups_menuitem.set_no_show_all(False) assign_openpgp_key_menuitem.set_no_show_all(False) add_to_roster_menuitem.hide() add_to_roster_menuitem.set_no_show_all(True) edit_groups_menuitem.connect('activate', self.on_edit_groups, contact, account) if gajim.config.get('usegpg'): assign_openpgp_key_menuitem.connect('activate', self.on_assign_pgp_key, contact, account) if contact.sub in ('from', 'both'): send_auth_menuitem.set_sensitive(False) else: send_auth_menuitem.connect('activate', self.authorize, jid, account) if contact.sub in ('to', 'both'): ask_auth_menuitem.set_sensitive(False) else: ask_auth_menuitem.connect('activate', self.req_sub, jid, _('I would like to add you to my roster'), account) if contact.sub in ('to', 'none'): revoke_auth_menuitem.set_sensitive(False) else: revoke_auth_menuitem.connect('activate', self.revoke_auth, jid, account) else: # contact is in group 'not in the roster' add_to_roster_menuitem.set_no_show_all(False) edit_groups_menuitem.hide() edit_groups_menuitem.set_no_show_all(True) # hide first of the two consecutive separators assign_openpgp_separator.hide() assign_openpgp_separator.set_no_show_all(True) assign_openpgp_key_menuitem.hide() assign_openpgp_key_menuitem.set_no_show_all(True) add_to_roster_menuitem.connect('activate', self.on_add_to_roster, contact, account) event_button = self.get_possible_button_event(event) roster_contact_context_menu.popup(None, None, None, event_button, event.time) roster_contact_context_menu.show_all() def mk_menu_g(self, event, iter): '''Make group's popup menu''' model = self.tree.get_model() path = model.get_path(iter) menu = gtk.Menu() rename_item = gtk.ImageMenuItem(_('Re_name')) rename_icon = gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU) rename_item.set_image(rename_icon) menu.append(rename_item) rename_item.connect('activate', self.on_rename, iter, path) event_button = self.get_possible_button_event(event) menu.popup(None, None, None, event_button, event.time) menu.show_all() def mk_menu_agent(self, event, iter): '''Make agent's popup menu''' model = self.tree.get_model() jid = model[iter][C_JID].decode('utf-8') path = model.get_path(iter) account = model[iter][C_ACCOUNT].decode('utf-8') user = gajim.get_contact_instance_with_highest_priority(account, jid) menu = gtk.Menu() item = gtk.ImageMenuItem(_('_Log on')) icon = gtk.image_new_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU) item.set_image(icon) menu.append(item) show = gajim.get_contact_instance_with_highest_priority(account, jid).show if show != 'offline' and show != 'error': item.set_sensitive(False) item.connect('activate', self.on_agent_logging, jid, None, account) item = gtk.ImageMenuItem(_('Log _off')) icon = gtk.image_new_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) item.set_image(icon) menu.append(item) if show in ('offline', 'error'): item.set_sensitive(False) item.connect('activate', self.on_agent_logging, jid, 'unavailable', account) item = gtk.SeparatorMenuItem() # seperator menu.append(item) item = gtk.ImageMenuItem(_('_Edit')) icon = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) item.set_image(icon) menu.append(item) item.connect('activate', self.on_edit_agent, user, account) item = gtk.ImageMenuItem(_('_Remove from Roster')) icon = gtk.image_new_from_stock(gtk.STOCK_REMOVE, gtk.ICON_SIZE_MENU) item.set_image(icon) menu.append(item) item.connect('activate', self.on_remove_agent, user, account) event_button = self.get_possible_button_event(event) menu.popup(None, None, None, event_button, event.time) menu.show_all() def on_edit_account(self, widget, account): if gajim.interface.instances[account].has_key('account_modification'): gajim.interface.instances[account]['account_modification'].window.present() else: gajim.interface.instances[account]['account_modification'] = \ config.AccountModificationWindow(account) def get_possible_button_event(self, event): '''mouse or keyboard caused the event?''' if event.type == gtk.gdk.KEY_PRESS: event_button = 0 # no event.button so pass 0 else: # BUTTON_PRESS event, so pass event.button event_button = event.button return event_button def on_change_status_message_activate(self, widget, account): show = gajim.SHOW_LIST[gajim.connections[account].connected] dlg = dialogs.ChangeStatusMessageDialog(show) message = dlg.run() if message is not None: # None is if user pressed Cancel self.send_status(account, show, message) def build_account_menu(self, account): #FIXME: make most menuitems of this menu insensitive if account is offline # we have to create our own set of icons for the menu # using self.jabber_status_images is poopoo iconset = gajim.config.get('iconset') if not iconset: iconset = 'sun' path = os.path.join(gajim.DATA_DIR, 'iconsets/' + iconset + '/16x16/') state_images = self.load_iconset(path) xml = gtk.glade.XML(GTKGUI_GLADE, 'account_context_menu', APP) account_context_menu = xml.get_widget('account_context_menu') childs = account_context_menu.get_children() status_menuitem = childs[0] # we skip the seperator # skip advanced_actions_menuitem, childs[2] xml_console_menuitem = xml.get_widget('xml_console_menuitem') set_motd_menuitem = xml.get_widget('set_motd_menuitem') update_motd_menuitem = xml.get_widget('update_motd_menuitem') delete_motd_menuitem = xml.get_widget('delete_motd_menuitem') edit_account_menuitem = childs[3] service_discovery_menuitem = childs[4] add_contact_menuitem = childs[5] join_group_chat_menuitem = childs[6] new_message_menuitem = childs[7] sub_menu = gtk.Menu() status_menuitem.set_submenu(sub_menu) for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): uf_show = helpers.get_uf_show(show, use_mnemonic = True) item = gtk.ImageMenuItem(uf_show) icon = state_images[show] item.set_image(icon) sub_menu.append(item) item.connect('activate', self.change_status, account, show) item = gtk.SeparatorMenuItem() sub_menu.append(item) item = gtk.ImageMenuItem(_('_Change Status Message')) path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'rename.png') img = gtk.Image() img.set_from_file(path) item.set_image(img) sub_menu.append(item) item.connect('activate', self.on_change_status_message_activate, account) if gajim.connections[account].connected < 2: item.set_sensitive(False) item = gtk.SeparatorMenuItem() sub_menu.append(item) uf_show = helpers.get_uf_show('offline', use_mnemonic = True) item = gtk.ImageMenuItem(uf_show) icon = state_images['offline'] item.set_image(icon) sub_menu.append(item) item.connect('activate', self.change_status, account, 'offline') xml_console_menuitem.connect('activate', self.on_xml_console_menuitem_activate, account) set_motd_menuitem.connect('activate', self.on_set_motd_menuitem_activate, account) update_motd_menuitem.connect('activate', self.on_update_motd_menuitem_activate, account) delete_motd_menuitem.connect('activate', self.on_delete_motd_menuitem_activate, account) edit_account_menuitem.connect('activate', self.on_edit_account, account) service_discovery_menuitem.connect('activate', self.on_service_disco_menuitem_activate, account) add_contact_menuitem.connect('activate', self.on_add_new_contact, account) join_group_chat_menuitem.connect('activate', self.on_join_gc_activate, account) new_message_menuitem.connect('activate', self.on_new_message_menuitem_activate, account) return account_context_menu def mk_menu_account(self, event, iter): '''Make account's popup menu''' model = self.tree.get_model() account = model[iter][C_ACCOUNT].decode('utf-8') if account != 'all': menu = self.build_account_menu(account) else: menu = gtk.Menu() iconset = gajim.config.get('iconset') if not iconset: iconset = 'sun' path = os.path.join(gajim.DATA_DIR, 'iconsets/' + iconset + '/16x16/') for account in gajim.connections: state_images = self.load_iconset(path) item = gtk.ImageMenuItem(account) show = gajim.SHOW_LIST[gajim.connections[account].connected] icon = state_images[show] item.set_image(icon) account_menu = self.build_account_menu(account) item.set_submenu(account_menu) menu.append(item) event_button = self.get_possible_button_event(event) menu.popup(None, self.tree, None, event_button, event.time) menu.show_all() def on_add_to_roster(self, widget, user, account): dialogs.AddNewContactWindow(account, user.jid) def authorize(self, widget, jid, account): '''Authorize a user (by re-sending auth menuitem)''' gajim.connections[account].send_authorization(jid) dialogs.InformationDialog(_('Authorization has been sent'), _('Now "%s" will know your status.') %jid) def req_sub(self, widget, jid, txt, account, group=None, pseudo=None): '''Request subscription to a user''' if not pseudo: pseudo = jid gajim.connections[account].request_subscription(jid, txt) if not group: group = _('General') if not gajim.contacts[account].has_key(jid): keyID = '' attached_keys = gajim.config.get_per('accounts', account, 'attached_gpg_keys').split() if jid in attached_keys: keyID = attached_keys[attached_keys.index(jid) + 1] user1 = Contact(jid = jid, name = pseudo, groups = [group], show = 'requested', status = '', ask = 'none', sub = 'subscribe', keyID = keyID) gajim.contacts[account][jid] = [user1] else: user1 = gajim.get_contact_instance_with_highest_priority(account, jid) if not _('not in the roster') in user1.groups: dialogs.InformationDialog(_('Subscription request has been sent'), _('If "%s" accepts this request you will know his or her status.') %jid) return user1.groups = [group] user1.name = pseudo self.remove_contact(user1, account) self.add_contact_to_roster(jid, account) def revoke_auth(self, widget, jid, account): '''Revoke a contact's authorization''' gajim.connections[account].refuse_authorization(jid) dialogs.InformationDialog(_('Authorization has been removed'), _('Now "%s" will always see you as offline.') %jid) def on_roster_treeview_scroll_event(self, widget, event): self.tooltip.hide_tooltip() def on_roster_treeview_key_press_event(self, widget, event): '''when a key is pressed in the treeviews''' self.tooltip.hide_tooltip() if event.keyval == gtk.keysyms.Menu: self.show_treeview_menu(event) return True if event.keyval == gtk.keysyms.Escape: self.tree.get_selection().unselect_all() if event.keyval == gtk.keysyms.F2: treeselection = self.tree.get_selection() model, iter = treeselection.get_selected() if not iter: return type = model[iter][C_TYPE] if type in ('contact', 'group'): path = model.get_path(iter) self.on_rename(widget, iter, path) if event.keyval == gtk.keysyms.Delete: treeselection = self.tree.get_selection() model, iter = treeselection.get_selected() if not iter: return jid = model[iter][C_JID].decode('utf-8') account = model[iter][C_ACCOUNT].decode('utf-8') type = model[iter][C_TYPE] if type in ('account', 'group'): return user = gajim.get_contact_instance_with_highest_priority(account, jid) if type == 'contact': self.on_req_usub(widget, user, account) elif type == 'agent': self.on_remove_agent(widget, user, account) def show_appropriate_context_menu(self, event, iter): model = self.tree.get_model() type = model[iter][C_TYPE] if type == 'group': self.mk_menu_g(event, iter) elif type == 'agent': self.mk_menu_agent(event, iter) elif type == 'contact': self.mk_menu_user(event, iter) elif type == 'account': self.mk_menu_account(event, iter) def show_treeview_menu(self, event): try: store, iter = self.tree.get_selection().get_selected() except TypeError: self.tree.get_selection().unselect_all() return model = self.tree.get_model() path = model.get_path(iter) self.tree.get_selection().select_path(path) self.show_appropriate_context_menu(event, iter) return True def on_roster_treeview_button_press_event(self, widget, event): # hide tooltip, no matter the button is pressed self.tooltip.hide_tooltip() if event.button == 3: # Right click try: path, column, x, y = self.tree.get_path_at_pos(int(event.x), int(event.y)) except TypeError: self.tree.get_selection().unselect_all() return self.tree.get_selection().select_path(path) model = self.tree.get_model() iter = model.get_iter(path) self.show_appropriate_context_menu(event, iter) return True if event.button == 2: # Middle click try: path, column, x, y = self.tree.get_path_at_pos(int(event.x), int(event.y)) except TypeError: self.tree.get_selection().unselect_all() return self.tree.get_selection().select_path(path) model = self.tree.get_model() iter = model.get_iter(path) type = model[iter][C_TYPE] if type in ('agent', 'contact'): account = model[iter][C_ACCOUNT].decode('utf-8') jid = model[iter][C_JID].decode('utf-8') if gajim.interface.instances[account]['chats'].has_key(jid): gajim.interface.instances[account]['chats'][jid].set_active_tab(jid) elif gajim.contacts[account].has_key(jid): c = gajim.get_contact_instance_with_highest_priority(account, jid) self.new_chat(c, account) gajim.interface.instances[account]['chats'][jid].set_active_tab(jid) gajim.interface.instances[account]['chats'][jid].window.present() elif type == 'account': account = model[iter][C_ACCOUNT] if account != 'all': show = gajim.connections[account].connected if show > 1: # We are connected self.on_change_status_message_activate(widget, account) return True show = helpers.get_global_show() if show == 'offline': return True dlg = dialogs.ChangeStatusMessageDialog(show) message = dlg.run() if not message: return True for acct in gajim.connections: if not gajim.config.get_per('accounts', acct, 'sync_with_global_status'): continue current_show = gajim.SHOW_LIST[gajim.connections[acct].connected] self.send_status(acct, current_show, message) return True if event.button == 1: # Left click try: path, column, x, y = self.tree.get_path_at_pos(int(event.x), int(event.y)) except TypeError: self.tree.get_selection().unselect_all() return False model = self.tree.get_model() iter = model.get_iter(path) type = model[iter][C_TYPE] if type == 'group': if x < 20: # first cell in 1st column (the arrow SINGLE clicked) if (self.tree.row_expanded(path)): self.tree.collapse_row(path) else: self.tree.expand_row(path, False) def on_req_usub(self, widget, user, account): '''Remove a contact''' window = dialogs.ConfirmationDialogCheck( _('Contact "%s" will be removed from your roster') % (user.name), _('By removing this contact you also by default remove authorization resulting in him or her always seeing you as offline.'), _('I want this contact to know my status after removal')) # FIXME: # maybe use 2 optionboxes from which the user can select? (better) if window.get_response() == gtk.RESPONSE_OK: remove_auth = True if window.is_checked(): remove_auth = False gajim.connections[account].unsubscribe(user.jid, remove_auth) for u in gajim.contacts[account][user.jid]: self.remove_contact(u, account) del gajim.contacts[account][u.jid] if user.jid in gajim.interface.instances[account]['chats']: user1 = Contact(jid = user.jid, name = user.name, groups = [_('not in the roster')], show = 'not in the roster', status = '', ask = 'none', keyID = user.keyID) gajim.contacts[account][user.jid] = [user1] self.add_contact_to_roster(user.jid, account) def forget_gpg_passphrase(self, keyid): if self.gpg_passphrase.has_key(keyid): del self.gpg_passphrase[keyid] return False def set_connecting_state(self, account): model = self.tree.get_model() accountIter = self.get_account_iter(account) if accountIter: model[accountIter][0] = self.jabber_state_images['connecting'] if gajim.interface.systray_enabled: gajim.interface.systray.change_status('connecting') def send_status(self, account, status, txt, sync = False, auto = False): model = self.tree.get_model() accountIter = self.get_account_iter(account) if status != 'offline': if gajim.connections[account].connected < 2: self.set_connecting_state(account) if gajim.connections[account].connected < 2 and \ not gajim.connections[account].password: passphrase = '' w = dialogs.PassphraseDialog( _('Password Required'), _('Enter your password for account %s') % account, _('Save password')) passphrase, save = w.run() if passphrase == -1: if accountIter: model[accountIter][0] = self.jabber_state_images['offline'] if gajim.interface.systray_enabled: gajim.interface.systray.change_status('offline') self.update_status_combobox() return gajim.connections[account].password = passphrase if save: gajim.config.set_per('accounts', account, 'savepass', True) gajim.config.set_per('accounts', account, 'password', passphrase) keyid = None use_gpg_agent = gajim.config.get('use_gpg_agent') # we don't need to bother with the passphrase if we use the agent if use_gpg_agent: save_gpg_pass = False else: save_gpg_pass = gajim.config.get_per('accounts', account, 'savegpgpass') keyid = gajim.config.get_per('accounts', account, 'keyid') if keyid and gajim.connections[account].connected < 2 and \ gajim.config.get('usegpg'): if use_gpg_agent: self.gpg_passphrase[keyid] = None else: if save_gpg_pass: passphrase = gajim.config.get_per('accounts', account, 'gpgpassword') else: if self.gpg_passphrase.has_key(keyid): passphrase = self.gpg_passphrase[keyid] save = False else: w = dialogs.PassphraseDialog( _('Passphrase Required'), _('Enter GPG key passphrase for account %s') % account, _('Save passphrase')) passphrase, save = w.run() if passphrase == -1: passphrase = None else: self.gpg_passphrase[keyid] = passphrase gobject.timeout_add(30000, self.forget_gpg_passphrase, keyid) if save: gajim.config.set_per('accounts', account, 'savegpgpass', True) gajim.config.set_per('accounts', account, 'gpgpassword', passphrase) gajim.connections[account].gpg_passphrase(passphrase) for room_jid in gajim.interface.instances[account]['gc']: if room_jid != 'tabbed': nick = gajim.interface.instances[account]['gc'][room_jid].nicks[room_jid] gajim.connections[account].send_gc_status(nick, room_jid, status, txt) gajim.connections[account].change_status(status, txt, sync, auto) if status == 'online' and gajim.interface.sleeper.getState() != \ common.sleepy.STATE_UNKNOWN: gajim.sleeper_state[account] = 'online' else: gajim.sleeper_state[account] = 'off' def get_status_message(self, show): if (show == 'online' and not gajim.config.get('ask_online_status')) or \ (show == 'offline' and not gajim.config.get('ask_offline_status')) or \ show == 'invisible': return '' dlg = dialogs.ChangeStatusMessageDialog(show) message = dlg.run() return message def connected_rooms(self, account): if True in gajim.gc_connected[account].values(): return True return False def change_status(self, widget, account, status): if status == 'invisible': if self.connected_rooms(account): dialog = dialogs.ConfirmationDialog( _('You are participating in one or more group chats'), _('Changing your status to invisible will result in disconnection from those group chats. Are you sure you want to go invisible?')) if dialog.get_response() != gtk.RESPONSE_OK: return message = self.get_status_message(status) if message is None: # user pressed Cancel to change status message dialog return self.send_status(account, status, message) def on_status_combobox_changed(self, widget): '''When we change our status via the combobox''' model = self.status_combobox.get_model() active = self.status_combobox.get_active() if active == -1: # no active item return if not self.combobox_callback_active: self.previous_status_combobox_active = active return accounts = gajim.connections.keys() if len(accounts) == 0: dialogs.ErrorDialog(_('No account available'), _('You must create an account before you can chat with other contacts.') ).get_response() self.update_status_combobox() return status = model[active][2].decode('utf-8') one_connected = helpers.one_account_connected() if active == 7: # We choose change status message (7 is that) # do not change show, just show change status dialog status = model[self.previous_status_combobox_active][2].decode('utf-8') dlg = dialogs.ChangeStatusMessageDialog(status) message = dlg.run() if message is not None: # None if user pressed Cancel for acct in accounts: if not gajim.config.get_per('accounts', acct, 'sync_with_global_status'): continue current_show = gajim.SHOW_LIST[gajim.connections[acct].connected] self.send_status(acct, current_show, message) self.combobox_callback_active = False self.status_combobox.set_active(self.previous_status_combobox_active) self.combobox_callback_active = True return # we are about to change show, so save this new show so in case # after user chooses "Change status message" menuitem # we can return to this show self.previous_status_combobox_active = active if status == 'invisible': bug_user = False for acct in accounts: if not one_connected or gajim.connections[acct].connected > 1: if not gajim.config.get_per('accounts', acct, 'sync_with_global_status'): continue # We're going to change our status to invisible if self.connected_rooms(acct): bug_user = True break if bug_user: dialog = dialogs.ConfirmationDialog( _('You are participating in one or more group chats'), _('Changing your status to invisible will result in disconnection from those group chats. Are you sure you want to go invisible?')) if dialog.get_response() != gtk.RESPONSE_OK: self.update_status_combobox() return message = self.get_status_message(status) if message is None: # user pressed Cancel to change status message dialog self.update_status_combobox() return for acct in accounts: if not gajim.config.get_per('accounts', acct, 'sync_with_global_status'): continue # we are connected (so we wanna change show and status) # or no account is connected and we want to connect with new show and status if not one_connected or gajim.connections[acct].connected > 1: self.send_status(acct, status, message) self.update_status_combobox() def update_status_combobox(self): # table to change index in connection.connected to index in combobox table = {'offline':9, 'connecting':9, 'online':0, 'chat':1, 'away':2, 'xa':3, 'dnd':4, 'invisible':5} show = helpers.get_global_show() # temporarily block signal in order not to send status that we show # in the combobox self.combobox_callback_active = False self.status_combobox.set_active(table[show]) self.combobox_callback_active = True if gajim.interface.systray_enabled: gajim.interface.systray.change_status(show) def on_status_changed(self, account, status): '''the core tells us that our status has changed''' if not gajim.contacts.has_key(account): return model = self.tree.get_model() accountIter = self.get_account_iter(account) if accountIter: model[accountIter][0] = self.jabber_state_images[status] if status == 'offline': if accountIter: model[accountIter][6] = None for jid in gajim.contacts[account]: luser = gajim.contacts[account][jid] luser_copy = [] for user in luser: luser_copy.append(user) for user in luser_copy: self.chg_contact_status(user, 'offline', 'Disconnected', account) self.update_status_combobox() self.make_menu() def new_chat(self, contact, account): chats = gajim.interface.instances[account]['chats'] if gajim.config.get('usetabbedchat'): if not chats.has_key('tabbed'): chats['tabbed'] = tabbed_chat_window.TabbedChatWindow(contact, account) else: chats['tabbed'].new_tab(contact) chats[contact.jid] = chats['tabbed'] else: chats[contact.jid] = tabbed_chat_window.TabbedChatWindow(contact, account) def new_chat_from_jid(self, account, jid): if gajim.contacts[account].has_key(jid): contact = gajim.get_contact_instance_with_highest_priority(account, jid) else: keyID = '' attached_keys = gajim.config.get_per('accounts', account, 'attached_gpg_keys').split() if jid in attached_keys: keyID = attached_keys[attached_keys.index(jid) + 1] contact = Contact(jid = jid, name = jid.split('@')[0], groups = [_('not in the roster')], show = 'not in the roster', status = '', sub = 'none', keyID = keyID) gajim.contacts[account][jid] = [contact] self.add_contact_to_roster(contact.jid, account) if not gajim.interface.instances[account]['chats'].has_key(jid): self.new_chat(contact, account) gajim.interface.instances[account]['chats'][jid].set_active_tab(jid) gajim.interface.instances[account]['chats'][jid].window.present() def new_room(self, jid, nick, account): if gajim.config.get('usetabbedchat'): if not gajim.interface.instances[account]['gc'].has_key('tabbed'): gajim.interface.instances[account]['gc']['tabbed'] = \ groupchat_window.GroupchatWindow(jid, nick, account) else: gajim.interface.instances[account]['gc']['tabbed'].new_room(jid, nick) gajim.interface.instances[account]['gc'][jid] = \ gajim.interface.instances[account]['gc']['tabbed'] else: gajim.interface.instances[account]['gc'][jid] = \ groupchat_window.GroupchatWindow(jid, nick, account) def on_message(self, jid, msg, tim, account, encrypted = False, msg_type = '', subject = None, resource = ''): '''when we receive a message''' if not gajim.contacts[account].has_key(jid): keyID = '' attached_keys = gajim.config.get_per('accounts', account, 'attached_gpg_keys').split() if jid in attached_keys: keyID = attached_keys[attached_keys.index(jid) + 1] user1 = Contact(jid = jid, name = jid.split('@')[0], groups = [_('not in the roster')], show = 'not in the roster', status = '', ask = 'none', keyID = keyID, resource = resource) gajim.contacts[account][jid] = [user1] self.add_contact_to_roster(jid, account) iters = self.get_contact_iter(jid, account) if iters: path = self.tree.get_model().get_path(iters[0]) else: path = None autopopup = gajim.config.get('autopopup') autopopupaway = gajim.config.get('autopopupaway') # Do we have a queue? qs = gajim.awaiting_events[account] no_queue = True if qs.has_key(jid): no_queue = False popup = False if autopopup and (autopopupaway or gajim.connections[account].connected \ in (1, 2)): popup = True if msg_type == 'normal' and popup: # it's single message to be autopopuped contact = gajim.get_contact_instance_with_highest_priority(account, jid) dialogs.SingleMessageWindow(account, contact.jid, action = 'receive', from_whom = jid, subject = subject, message = msg, resource = resource) return # We print if window is opened and it's not a single message if gajim.interface.instances[account]['chats'].has_key(jid) and \ msg_type != 'normal': typ = '' if msg_type == 'error': typ = 'status' gajim.interface.instances[account]['chats'][jid].print_conversation( msg, jid, typ, tim = tim, encrypted = encrypted, subject = subject) return # We save it in a queue if no_queue: qs[jid] = [] kind = 'chat' if msg_type == 'normal': kind = 'normal' qs[jid].append((kind, (msg, subject, msg_type, tim, encrypted, resource))) self.nb_unread += 1 if popup: if not gajim.interface.instances[account]['chats'].has_key(jid): c = gajim.get_contact_instance_with_highest_priority(account, jid) self.new_chat(c, account) if path: self.tree.expand_row(path[0:1], False) self.tree.expand_row(path[0:2], False) self.tree.scroll_to_cell(path) self.tree.set_cursor(path) else: if no_queue: # We didn't have a queue: we change icons self.draw_contact(jid, account) if gajim.interface.systray_enabled: gajim.interface.systray.add_jid(jid, account, kind) self.show_title() # we show the * or [n] if not path: self.add_contact_to_roster(jid, account) iters = self.get_contact_iter(jid, account) path = self.tree.get_model().get_path(iters[0]) self.tree.expand_row(path[0:1], False) self.tree.expand_row(path[0:2], False) self.tree.scroll_to_cell(path) self.tree.set_cursor(path) def on_preferences_menuitem_activate(self, widget): if gajim.interface.instances['preferences'].window.get_property('visible'): gajim.interface.instances['preferences'].window.present() else: gajim.interface.instances['preferences'].window.show_all() def on_add_new_contact(self, widget, account): dialogs.AddNewContactWindow(account) def on_join_gc_activate(self, widget, account): '''when the join gc menuitem is clicked, show the join gc window''' invisible_show = gajim.SHOW_LIST.index('invisible') if gajim.connections[account].connected == invisible_show: dialogs.ErrorDialog(_('You cannot join a room while you are invisible') ).get_response() return if gajim.interface.instances[account].has_key('join_gc'): gajim.interface.instances[account]['join_gc'].window.present() else: # c http://nkour.blogspot.com/2005/05/pythons-init-return-none-doesnt-return.html try: gajim.interface.instances[account]['join_gc'] = \ dialogs.JoinGroupchatWindow(account) except RuntimeError: pass def on_new_message_menuitem_activate(self, widget, account): dialogs.NewMessageDialog(account) def on_contents_menuitem_activate(self, widget): helpers.launch_browser_mailer('url', 'http://trac.gajim.org/wiki') def on_faq_menuitem_activate(self, widget): helpers.launch_browser_mailer('url', 'http://trac.gajim.org/wiki/GajimFaq') def on_about_menuitem_activate(self, widget): dialogs.AboutDialog() def on_accounts_menuitem_activate(self, widget): if gajim.interface.instances.has_key('accounts'): gajim.interface.instances['accounts'].window.present() else: gajim.interface.instances['accounts'] = config.AccountsWindow() def on_file_transfers_menuitem_activate(self, widget): if gajim.interface.instances['file_transfers'].window.get_property('visible'): gajim.interface.instances['file_transfers'].window.present() else: gajim.interface.instances['file_transfers'].window.show_all() def on_manage_bookmarks_menuitem_activate(self, widget): config.ManageBookmarksWindow() def close_all(self, dic): '''close all the windows in the given dictionary''' for w in dic.values(): if type(w) == type({}): self.close_all(w) else: w.window.destroy() def on_roster_window_delete_event(self, widget, event): '''When we want to close the window''' if gajim.interface.systray_enabled and not gajim.config.get('quit_on_roster_x_button'): self.tooltip.hide_tooltip() self.window.hide() else: accounts = gajim.connections.keys() get_msg = False for acct in accounts: if gajim.connections[acct].connected: get_msg = True break if get_msg: message = self.get_status_message('offline') if message is None: # user pressed Cancel to change status message dialog message = '' for acct in accounts: if gajim.connections[acct].connected: self.send_status(acct, 'offline', message, True) self.quit_gtkgui_interface() return True # do NOT destory the window def on_roster_window_focus_in_event(self, widget, event): # roster received focus, so if we had urgency REMOVE IT # NOTE: we do not have to read the message to remove urgency # so this functions does that if gtk.gtk_version >= (2, 8, 0) and gtk.pygtk_version >= (2, 8, 0): if widget.props.urgency_hint: widget.props.urgency_hint = False # if a contact row is selected, update colors (eg. for status msg) # because gtk engines may differ in bg when window is selected # or not if self._last_selected_contact is not None: jid, account = self._last_selected_contact self.draw_contact(jid, account, selected = True, focus = True) def on_roster_window_focus_out_event(self, widget, event): # if a contact row is selected, update colors (eg. for status msg) # because gtk engines may differ in bg when window is selected # or not if self._last_selected_contact is not None: jid, account = self._last_selected_contact self.draw_contact(jid, account, selected = True, focus = False) def on_roster_window_key_press_event(self, widget, event): if event.keyval == gtk.keysyms.Escape: treeselection = self.tree.get_selection() model, iter = treeselection.get_selected() if not iter and gajim.interface.systray_enabled and not gajim.config.get('quit_on_roster_x_button'): self.tooltip.hide_tooltip() self.window.hide() def quit_gtkgui_interface(self): '''When we quit the gtk interface : tell that to the core and exit gtk''' if gajim.config.get('saveposition'): # in case show_roster_on_start is False and roster is never shown # window.window is None if self.window.window is not None: x, y = self.window.window.get_root_origin() gajim.config.set('roster_x-position', x) gajim.config.set('roster_y-position', y) width, height = self.window.get_size() gajim.config.set('roster_width', width) gajim.config.set('roster_height', height) gajim.config.set('collapsed_rows', '\t'.join(self.collapsed_rows)) gajim.interface.save_config() for account in gajim.connections: gajim.connections[account].quit(True) self.close_all(gajim.interface.instances) if gajim.interface.systray_enabled: gajim.interface.hide_systray() gtk.main_quit() def on_quit_menuitem_activate(self, widget): accounts = gajim.connections.keys() get_msg = False for acct in accounts: if gajim.connections[acct].connected: get_msg = True break if get_msg: message = self.get_status_message('offline') if message is None: # user pressed Cancel to change status message dialog return # check if we have unread or recent mesages unread = False recent = False if self.nb_unread > 0: unread = True for account in accounts: if gajim.interface.instances[account]['chats'].has_key('tabbed'): wins = [gajim.interface.instances[account]['chats']['tabbed']] else: wins = gajim.interface.instances[account]['chats'].values() for win in wins: unrd = 0 for jid in win.nb_unread: unrd += win.nb_unread[jid] if unrd: unread = True break for jid in win.contacts: if time.time() - gajim.last_message_time[account][jid] < 2: recent = True break if unread: dialog = dialogs.ConfirmationDialog(_('You have unread messages'), _('Messages will only be available for reading them later if you have history enabled.')) if dialog.get_response() != gtk.RESPONSE_OK: return if recent: dialog = dialogs.ConfirmationDialog(_('You have unread messages'), _('Messages will only be available for reading them later if you have history enabled.')) if dialog.get_response() != gtk.RESPONSE_OK: return for acct in accounts: if gajim.connections[acct].connected: # send status asynchronously self.send_status(acct, 'offline', message, True) self.quit_gtkgui_interface() def open_event(self, account, jid, event): '''If an event was handled, return True, else return False''' typ = event[0] data = event[1] ft = gajim.interface.instances['file_transfers'] if typ == 'normal': dialogs.SingleMessageWindow(account, jid, action = 'receive', from_whom = jid, subject = data[1], message = data[0], resource = data[5]) gajim.interface.remove_first_event(account, jid, typ) return True elif typ == 'file-request': contact = gajim.get_contact_instance_with_highest_priority(account, jid) ft.show_file_request(account, contact, data) gajim.interface.remove_first_event(account, jid, typ) return True elif typ in ('file-request-error', 'file-send-error'): ft.show_send_error(data) gajim.interface.remove_first_event(account, jid, typ) return True elif typ in ('file-error', 'file-stopped'): ft.show_stopped(jid, data) gajim.interface.remove_first_event(account, jid, typ) return True elif typ == 'file-completed': ft.show_completed(jid, data) gajim.interface.remove_first_event(account, jid, typ) return True return False def on_roster_treeview_row_activated(self, widget, path, col = 0): '''When an iter is double clicked: open the first event window''' model = self.tree.get_model() iter = model.get_iter(path) account = model[iter][C_ACCOUNT].decode('utf-8') type = model[iter][C_TYPE] jid = model[iter][C_JID].decode('utf-8') if type in ('group', 'account'): if self.tree.row_expanded(path): self.tree.collapse_row(path) else: self.tree.expand_row(path, False) else: first_ev = gajim.get_first_event(account, jid) if first_ev: if self.open_event(account, jid, first_ev): return chats = gajim.interface.instances[account]['chats'] if chats.has_key(jid): chats[jid].set_active_tab(jid) elif gajim.contacts[account].has_key(jid): c = gajim.get_contact_instance_with_highest_priority(account, jid) self.new_chat(c, account) chats[jid].set_active_tab(jid) chats[jid].window.present() def on_roster_treeview_row_expanded(self, widget, iter, path): '''When a row is expanded change the icon of the arrow''' model = self.tree.get_model() if gajim.config.get('mergeaccounts'): accounts = gajim.connections.keys() else: accounts = [model[iter][C_ACCOUNT].decode('utf-8')] type = model[iter][C_TYPE] if type == 'group': model.set_value(iter, 0, self.jabber_state_images['opened']) jid = model[iter][C_JID].decode('utf-8') for account in accounts: if gajim.groups[account].has_key(jid): # This account has this group gajim.groups[account][jid]['expand'] = True if account + jid in self.collapsed_rows: self.collapsed_rows.remove(account + jid) elif type == 'account': account = accounts[0] # There is only one cause we don't use merge if account in self.collapsed_rows: self.collapsed_rows.remove(account) for g in gajim.groups[account]: groupIter = self.get_group_iter(g, account) if groupIter and gajim.groups[account][g]['expand']: pathG = model.get_path(groupIter) self.tree.expand_row(pathG, False) def on_roster_treeview_row_collapsed(self, widget, iter, path): '''When a row is collapsed : change the icon of the arrow''' model = self.tree.get_model() if gajim.config.get('mergeaccounts'): accounts = gajim.connections.keys() else: accounts = [model[iter][C_ACCOUNT].decode('utf-8')] type = model[iter][C_TYPE] if type == 'group': model.set_value(iter, 0, self.jabber_state_images['closed']) jid = model[iter][C_JID].decode('utf-8') for account in accounts: if gajim.groups[account].has_key(jid): # This account has this group gajim.groups[account][jid]['expand'] = False if not account + jid in self.collapsed_rows: self.collapsed_rows.append(account + jid) elif type == 'account': account = accounts[0] # There is only one cause we don't use merge if not account in self.collapsed_rows: self.collapsed_rows.append(account) def on_editing_started(self, cell, event, row): ''' start editing a cell in the tree ''' path = self.tree.get_cursor()[0] self.editing_path = path def on_editing_canceled(self, cell): '''editing has been canceled''' path = self.tree.get_cursor()[0] # do not set new name if row order has changed if path != self.editing_path: self.editing_path = None return self.editing_path = None model = self.tree.get_model() iter = model.get_iter(path) account = model[iter][C_ACCOUNT].decode('utf-8') jid = model[iter][C_JID].decode('utf-8') type = model[iter][C_TYPE] # restore the number of resources string at the end of contact name if type == 'contact' and len(gajim.contacts[account][jid]) > 1: self.draw_contact(jid, account) # reset editable to False model[iter][C_EDITABLE] = False def on_cell_edited(self, cell, row, new_text): '''When an iter is edited: if text has changed, rename the contact''' model = self.tree.get_model() # if this is a last item in the group, row is invalid try: iter = model.get_iter_from_string(row) except: self.editing_path = None return path = model.get_path(iter) # do not set new name if row order has changed if path != self.editing_path: self.editing_path = None return self.editing_path = None new_text = new_text.decode('utf-8') account = model[iter][C_ACCOUNT].decode('utf-8') jid = model[iter][C_JID].decode('utf-8') type = model[iter][C_TYPE] if type == 'contact': old_text = gajim.get_contact_instance_with_highest_priority(account, jid).name if old_text != new_text: for u in gajim.contacts[account][jid]: u.name = new_text gajim.connections[account].update_contact(jid, new_text, u.groups) self.draw_contact(jid, account) elif type == 'group': old_name = model[iter][C_NAME].decode('utf-8') # Groups maynot change name from or to 'not in the roster' if _('not in the roster') in (new_text, old_name): return #get all users in that group for jid in gajim.contacts[account]: user = gajim.get_contact_instance_with_highest_priority(account, jid) if old_name in user.groups: #set them in the new one and remove it from the old self.remove_contact(user, account) user.groups.remove(old_name) user.groups.append(new_text) self.add_contact_to_roster(user.jid, account) gajim.connections[account].update_contact(user.jid, user.name, user.groups) model.set_value(iter, 5, False) def on_service_disco_menuitem_activate(self, widget, account): server_jid = gajim.config.get_per('accounts', account, 'hostname') if gajim.interface.instances[account]['disco'].has_key(server_jid): gajim.interface.instances[account]['disco'][server_jid].window.present() else: try: # Object will add itself to the window dict disco.ServiceDiscoveryWindow(account, address_entry=True) except RuntimeError: pass def load_iconset(self, path): imgs = {} for state in ('connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'offline', 'error', 'requested', 'message', 'opened', 'closed', 'not in the roster', 'muc_active'): # try to open a pixfile with the correct method state_file = state.replace(' ', '_') files = [] files.append(path + state_file + '.gif') files.append(path + state_file + '.png') image = gtk.Image() image.show() imgs[state] = image for file in files: # loop seeking for either gif or png if os.path.exists(file): image.set_from_file(file) break return imgs def make_jabber_state_images(self): '''initialise jabber_state_images dict''' iconset = gajim.config.get('iconset') if not iconset: iconset = 'sun' path = os.path.join(gajim.DATA_DIR, 'iconsets/' + iconset + '/16x16/') self.jabber_state_images = self.load_iconset(path) def reload_jabber_state_images(self): self.make_jabber_state_images() # Update the roster self.draw_roster() # Update the status combobox model = self.status_combobox.get_model() iter = model.get_iter_root() while iter: if model[iter][2] != '': # If it's not change status message iter # eg. if it has show parameter not '' model[iter][1] = self.jabber_state_images[model[iter][2]] iter = model.iter_next(iter) # Update the systray if gajim.interface.systray_enabled: gajim.interface.systray.set_img() for account in gajim.connections: # Update opened chat windows for jid in gajim.interface.instances[account]['chats']: if jid != 'tabbed': gajim.interface.instances[account]['chats'][jid].set_state_image( jid) # Update opened groupchat windows gcs = gajim.interface.instances[account]['gc'] if gcs.has_key('tabbed'): gcs['tabbed'].draw_all_roster() else: for room_jid in gcs: gcs[room_jid].draw_all_roster() self.update_status_combobox() def repaint_themed_widgets(self): '''Notify windows that contain themed widgets to repaint them''' for account in gajim.connections: # Update opened chat windows/tabs for jid in gajim.interface.instances[account]['chats']: gajim.interface.instances[account]['chats'][jid].repaint_colored_widgets() for jid in gajim.interface.instances[account]['gc']: gajim.interface.instances[account]['gc'][jid].repaint_colored_widgets() for addr in gajim.interface.instances[account]['disco']: gajim.interface.instances[account]['disco'][addr].paint_banner() def on_show_offline_contacts_menuitem_activate(self, widget): '''when show offline option is changed: redraw the treeview''' gajim.config.set('showoffline', not gajim.config.get('showoffline')) self.draw_roster() def iconCellDataFunc(self, column, renderer, model, iter, data = None): '''When a row is added, set properties for icon renderer''' theme = gajim.config.get('roster_theme') if model[iter][C_TYPE] == 'account': color = gajim.config.get_per('themes', theme, 'accountbgcolor') if color: renderer.set_property('cell-background', color) else: renderer.set_property('cell-background', None) renderer.set_property('xalign', 0) elif model[iter][C_TYPE] == 'group': color = gajim.config.get_per('themes', theme, 'groupbgcolor') if color: renderer.set_property('cell-background', color) else: renderer.set_property('cell-background', None) renderer.set_property('xalign', 0.5) else: jid = model[iter][C_JID].decode('utf-8') account = model[iter][C_ACCOUNT].decode('utf-8') if jid in gajim.newly_added[account]: renderer.set_property('cell-background', '#adc3c6') elif jid in gajim.to_be_removed[account]: renderer.set_property('cell-background', '#ab6161') else: color = gajim.config.get_per('themes', theme, 'contactbgcolor') if color: renderer.set_property('cell-background', color) else: renderer.set_property('cell-background', None) renderer.set_property('xalign', 1) renderer.set_property('width', 20) def nameCellDataFunc(self, column, renderer, model, iter, data = None): '''When a row is added, set properties for name renderer''' theme = gajim.config.get('roster_theme') if model[iter][C_TYPE] == 'account': color = gajim.config.get_per('themes', theme, 'accounttextcolor') if color: renderer.set_property('foreground', color) else: renderer.set_property('foreground', None) color = gajim.config.get_per('themes', theme, 'accountbgcolor') if color: renderer.set_property('cell-background', color) else: renderer.set_property('cell-background', None) renderer.set_property('font', gtkgui_helpers.get_theme_font_for_option(theme, 'accountfont')) renderer.set_property('xpad', 0) renderer.set_property('width', 3) elif model[iter][C_TYPE] == 'group': color = gajim.config.get_per('themes', theme, 'grouptextcolor') if color: renderer.set_property('foreground', color) else: renderer.set_property('foreground', None) color = gajim.config.get_per('themes', theme, 'groupbgcolor') if color: renderer.set_property('cell-background', color) else: renderer.set_property('cell-background', None) renderer.set_property('font', gtkgui_helpers.get_theme_font_for_option(theme, 'groupfont')) renderer.set_property('xpad', 4) else: jid = model[iter][C_JID].decode('utf-8') account = model[iter][C_ACCOUNT].decode('utf-8') color = gajim.config.get_per('themes', theme, 'contacttextcolor') if color: renderer.set_property('foreground', color) else: renderer.set_property('foreground', None) if jid in gajim.newly_added[account]: renderer.set_property('cell-background', '#adc3c6') elif jid in gajim.to_be_removed[account]: renderer.set_property('cell-background', '#ab6161') else: color = gajim.config.get_per('themes', theme, 'contactbgcolor') if color: renderer.set_property('cell-background', color) else: renderer.set_property('cell-background', None) renderer.set_property('font', gtkgui_helpers.get_theme_font_for_option(theme, 'contactfont')) renderer.set_property('xpad', 8) def fill_secondary_pixbuf_rederer(self, column, renderer, model, iter, data=None): '''When a row is added, set properties for secondary renderer (avatar or padlock)''' theme = gajim.config.get('roster_theme') if model[iter][C_TYPE] == 'account': color = gajim.config.get_per('themes', theme, 'accountbgcolor') if color: renderer.set_property('cell-background', color) else: renderer.set_property('cell-background', None) elif model[iter][C_TYPE] == 'group': color = gajim.config.get_per('themes', theme, 'groupbgcolor') if color: renderer.set_property('cell-background', color) else: renderer.set_property('cell-background', None) else: # contact jid = model[iter][C_JID].decode('utf-8') account = model[iter][C_ACCOUNT].decode('utf-8') if jid in gajim.newly_added[account]: renderer.set_property('cell-background', '#adc3c6') elif jid in gajim.to_be_removed[account]: renderer.set_property('cell-background', '#ab6161') else: color = gajim.config.get_per('themes', theme, 'contactbgcolor') if color: renderer.set_property('cell-background', color) else: renderer.set_property('cell-background', None) renderer.set_property('xalign', 1) # align pixbuf to the right def get_show(self, luser): prio = luser[0].priority show = luser[0].show for u in luser: if u.priority > prio: prio = u.priority show = u.show return show def compareIters(self, model, iter1, iter2, data = None): '''Compare two iters to sort them''' name1 = model[iter1][C_NAME] name2 = model[iter2][C_NAME] if not name1 or not name2: return 0 else: name1 = name1.decode('utf-8') name2 = name2.decode('utf-8') type1 = model[iter1][C_TYPE] type2 = model[iter2][C_TYPE] if type1 == 'group': if name1 == _('Transports'): return 1 if name2 == _('Transports'): return -1 if name1 == _('not in the roster'): return 1 if name2 == _('not in the roster'): return -1 account1 = model[iter1][C_ACCOUNT] account2 = model[iter2][C_ACCOUNT] if account1 and account2: account1 = account1.decode('utf-8') account2 = account2.decode('utf-8') jid1 = model[iter1][C_JID].decode('utf-8') jid2 = model[iter2][C_JID].decode('utf-8') # We first compare by show if sort_by_show is True if type1 == 'contact' and type2 == 'contact' and \ gajim.config.get('sort_by_show'): if account1 and account2: # We an have contact without account # during a short time ... why? luser1 = gajim.contacts[account1][jid1] luser2 = gajim.contacts[account2][jid2] cshow = {'online':0, 'chat': 1, 'away': 2, 'xa': 3, 'dnd': 4, 'invisible': 5, 'offline': 6, 'not in the roster': 7, 'error': 8} s = self.get_show(luser1) if s in cshow: show1 = cshow[s] else: show1 = 9 s = self.get_show(luser2) if s in cshow: show2 = cshow[s] else: show2 = 9 if show1 < show2: return -1 elif show1 > show2: return 1 # We compare names if name1.lower() < name2.lower(): return -1 if name2.lower() < name1.lower(): return 1 if type1 == 'contact' and type2 == 'contact' and account1 and account2: # We compare account names if account1.lower() < account2.lower(): return -1 if account2.lower() < account1.lower(): return 1 # We compare jids if jid1.lower() < jid2.lower(): return -1 if jid2.lower() < jid1.lower(): return 1 return 0 def drag_data_get_data(self, treeview, context, selection, target_id, etime): treeselection = treeview.get_selection() model, iter = treeselection.get_selected() path = model.get_path(iter) data = '' if len(path) == 3: data = model[iter][C_JID].decode('utf-8') selection.set(selection.target, 8, data) def drag_data_received_data(self, treeview, context, x, y, selection, info, etime): model = treeview.get_model() if not selection.data: return data = selection.data drop_info = treeview.get_dest_row_at_pos(x, y) if not drop_info: return path_dest, position = drop_info if position == gtk.TREE_VIEW_DROP_BEFORE and len(path_dest) == 2 \ and path_dest[1] == 0: # dropped before the first group return if position == gtk.TREE_VIEW_DROP_BEFORE and len(path_dest) == 2: # dropped before a group : we drop it in the previous group path_dest = (path_dest[1], path_dest[1]-1) iter_dest = model.get_iter(path_dest) iter_source = treeview.get_selection().get_selected()[1] path_source = model.get_path(iter_source) if len(path_dest) == 1: # dropped on an account return if path_dest[0] != path_source[0]: # dropped in another account return iter_group_source = model.iter_parent(iter_source) grp_source = model[iter_group_source][C_JID].decode('utf-8') if grp_source == _('Transports') or grp_source == _('not in the roster'): return account = model[iter_dest][C_ACCOUNT].decode('utf-8') type_dest = model.get_value(iter_dest, C_TYPE) if type_dest == 'group': grp_dest = model[iter_dest][C_JID].decode('utf-8') else: grp_dest = model[model.iter_parent(iter_dest)][C_JID].decode('utf-8') if grp_dest == _('Transports') or grp_dest == _('not in the roster'): return if grp_source == grp_dest: return # We upgrade only the first user because user2.groups is a pointer to # user1.groups u = gajim.contacts[account][data][0] if context.action != gtk.gdk.ACTION_COPY: u.groups.remove(grp_source) if model.iter_n_children(iter_group_source) == 1: # this was the only child model.remove(iter_group_source) # delete the group if it is empty (need to look for offline users too) for jid in gajim.contacts[account]: if grp_source in gajim.get_contact_instance_with_highest_priority(account, jid).groups: break else: del gajim.groups[account][grp_source] if not grp_dest in u.groups: u.groups.append(grp_dest) self.add_contact_to_roster(data, account) gajim.connections[account].update_contact(u.jid, u.name, u.groups) if context.action in (gtk.gdk.ACTION_MOVE, gtk.gdk.ACTION_COPY): context.finish(True, True, etime) return def show_title(self): change_title_allowed = gajim.config.get('change_roster_title') if change_title_allowed: start = '' if self.nb_unread > 1: start = '[' + str(self.nb_unread) + '] ' elif self.nb_unread == 1: start = '* ' self.window.set_title(start + 'Gajim') gtkgui_helpers.set_unset_urgency_hint(self.window, self.nb_unread) def iter_is_separator(self, model, iter): if model[iter][0] == 'SEPARATOR': return True return False def iter_contact_rows(self): '''iterate over all contact rows in the tree model''' model = self.tree.get_model() account_iter = model.get_iter_root() while account_iter: group_iter = model.iter_children(account_iter) while group_iter: contact_iter = model.iter_children(group_iter) while contact_iter: yield model[contact_iter] contact_iter = model.iter_next(contact_iter) group_iter = model.iter_next(group_iter) account_iter = model.iter_next(account_iter) def on_roster_treeview_style_set(self, treeview, style): '''When style (theme) changes, redraw all contacts''' for contact in self.iter_contact_rows(): self.draw_contact(contact[C_JID].decode('utf-8'), contact[C_ACCOUNT].decode('utf-8')) def _on_treeview_selection_changed(self, selection): model, selected_iter = selection.get_selected() if self._last_selected_contact is not None: # update unselected row jid, account = self._last_selected_contact self.draw_contact(jid, account) if selected_iter is None: self._last_selected_contact = None return contact = model[selected_iter] self._last_selected_contact = (contact[C_JID].decode('utf-8'), contact[C_ACCOUNT].decode('utf-8')) # FIXME: we first set last selected contact and then test if contact?? if contact[C_TYPE] != 'contact': return self.draw_contact(contact[C_JID].decode('utf-8'), contact[C_ACCOUNT].decode('utf-8'), selected = True) def __init__(self): self.xml = gtk.glade.XML(GTKGUI_GLADE, 'roster_window', APP) self.window = self.xml.get_widget('roster_window') self.tree = self.xml.get_widget('roster_treeview') self.tree.get_selection().connect('changed', self._on_treeview_selection_changed) self._last_selected_contact = None # None or holds jid, account tupple self.nb_unread = 0 self.last_save_dir = None self.editing_path = None # path of row with cell in edit mode self.add_new_contact_handler_id = False self.service_disco_handler_id = False self.new_message_menuitem_handler_id = False self.regroup = gajim.config.get('mergeaccounts') #FIXME: When list_accel_closures will be wrapped in pygtk # no need of this variable self.have_new_message_accel = False # Is the "Ctrl+N" shown ? if gajim.config.get('saveposition'): gtkgui_helpers.move_window(self.window, gajim.config.get('roster_x-position'), gajim.config.get('roster_y-position')) gtkgui_helpers.resize_window(self.window, gajim.config.get('roster_width'), gajim.config.get('roster_height')) self.popups_notification_height = 0 self.popup_notification_windows = [] self.gpg_passphrase = {} #(icon, name, type, jid, account, editable, secondary_pixbuf) model = gtk.TreeStore(gtk.Image, str, str, str, str, bool, gtk.gdk.Pixbuf) model.set_sort_func(1, self.compareIters) model.set_sort_column_id(1, gtk.SORT_ASCENDING) self.tree.set_model(model) self.make_jabber_state_images() #FIXME: why do we init this dict of dicts here? #for loop below does it. maybe so we're sure we always have the keys? #eventhough childs can be empty? I don't get it self.transports_state_images = { 'aim': {}, 'gadugadu': {}, 'irc': {}, 'icq': {}, 'msn': {}, 'sms': {}, 'tlen': {}, 'weather': {}, 'yahoo': {} } path = os.path.join(gajim.DATA_DIR, 'iconsets', 'transports') folders = os.listdir(path) for transport in folders: if transport == '.svn': continue folder = os.path.join(path, transport) self.transports_state_images[transport] = self.load_iconset( folder + '/16x16/') # uf_show, img, show, sensitive liststore = gtk.ListStore(str, gtk.Image, str, bool) self.status_combobox = self.xml.get_widget('status_combobox') cell = cell_renderer_image.CellRendererImage() self.status_combobox.pack_start(cell, False) # img to show is in in 2nd column of liststore self.status_combobox.add_attribute(cell, 'image', 1) # if it will be sensitive or not it is in the fourth column # all items in the 'row' must have sensitive to False # if we want False (so we add it for img_cell too) self.status_combobox.add_attribute(cell, 'sensitive', 3) cell = gtk.CellRendererText() cell.set_property('xpad', 5) # padding for status text self.status_combobox.pack_start(cell, True) # text to show is in in first column of liststore self.status_combobox.add_attribute(cell, 'text', 0) # if it will be sensitive or not it is in the fourth column self.status_combobox.add_attribute(cell, 'sensitive', 3) self.status_combobox.set_row_separator_func(self.iter_is_separator) for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): uf_show = helpers.get_uf_show(show) liststore.append([uf_show, self.jabber_state_images[show], show, True]) # Add a Separator (self.iter_is_separator() checks on string SEPARATOR) liststore.append(['SEPARATOR', None, '', True]) path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'rename.png') img = gtk.Image() img.set_from_file(path) # sensitivity to False because by default we're offline self.status_message_menuitem_iter = liststore.append( [_('Change Status Message...'), img, '', False]) # Add a Separator (self.iter_is_separator() checks on string SEPARATOR) liststore.append(['SEPARATOR', None, '', True]) uf_show = helpers.get_uf_show('offline') liststore.append([uf_show, self.jabber_state_images['offline'], 'offline', True]) status_combobox_items = ['online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'separator1', 'change_status_msg', 'separator2', 'offline'] self.status_combobox.set_model(liststore) # default to offline number_of_menuitem = status_combobox_items.index('offline') self.status_combobox.set_active(number_of_menuitem) # holds index to previously selected item so if "change status message..." # is selected we can fallback to previously selected item and not stay # with that item selected self.previous_status_combobox_active = number_of_menuitem showOffline = gajim.config.get('showoffline') self.xml.get_widget('show_offline_contacts_menuitem').set_active( showOffline) # columns # this col has two cells: first one img, second one text col = gtk.TreeViewColumn() render_image = cell_renderer_image.CellRendererImage() # show img or +- col.pack_start(render_image, expand = False) col.add_attribute(render_image, 'image', C_IMG) col.set_cell_data_func(render_image, self.iconCellDataFunc, None) render_text = gtk.CellRendererText() # contact or group or account name render_text.connect('edited', self.on_cell_edited) render_text.connect('editing-canceled', self.on_editing_canceled) render_text.connect('editing-started', self.on_editing_started) col.pack_start(render_text, expand = True) col.add_attribute(render_text, 'markup', C_NAME) # where we hold the name col.add_attribute(render_text, 'editable', C_EDITABLE) # where we hold if the row is editable col.set_cell_data_func(render_text, self.nameCellDataFunc, None) render_pixbuf = gtk.CellRendererPixbuf() # tls or avatar img col.pack_start(render_pixbuf, expand = False) col.add_attribute(render_pixbuf, 'pixbuf', C_SECPIXBUF) col.set_cell_data_func(render_pixbuf, self.fill_secondary_pixbuf_rederer, None) self.tree.append_column(col) #do not show gtk arrows workaround col = gtk.TreeViewColumn() render_pixbuf = gtk.CellRendererPixbuf() col.pack_start(render_pixbuf, expand = False) self.tree.append_column(col) col.set_visible(False) self.tree.set_expander_column(col) #signals TARGETS = [('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_WIDGET, 0)] self.tree.enable_model_drag_source( gtk.gdk.BUTTON1_MASK, TARGETS, gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE | gtk.gdk.ACTION_COPY) self.tree.enable_model_drag_dest(TARGETS, gtk.gdk.ACTION_DEFAULT) self.tree.connect('drag_data_get', self.drag_data_get_data) self.tree.connect('drag_data_received', self.drag_data_received_data) self.xml.signal_autoconnect(self) self.combobox_callback_active = True self.collapsed_rows = gajim.config.get('collapsed_rows').split('\t') self.tooltip = tooltips.RosterTooltip() self.make_menu() self.draw_roster() if gajim.config.get('show_roster_on_startup'): self.window.show_all() else: if not gajim.config.get('trayicon'): # cannot happen via GUI, but I put this incase user touches config self.window.show_all() # without trayicon, he or she should see the roster! gajim.config.set('show_roster_on_startup', True) if len(gajim.connections) == 0: # if we have no account gajim.interface.instances['account_creation_wizard'] = \ config.AccountCreationWizardWindow()