diff --git a/src/chat_control.py b/src/chat_control.py index 468e7c2bf..91774f635 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -64,10 +64,13 @@ class ChatControlBase(MessageControl): # Derived types SHOULD implement this def repaint_themed_widgets(self): self.draw_banner() - # NOTE: Derived classes MAY implement this + # Derived classes MAY implement this def _update_banner_state_image(self): pass # Derived types MAY implement this + def handle_message_textview_mykey_press(self): + pass # Derived should implement this rather than connecting to the event itself. + def __init__(self, type_id, parent_win, widget_name, display_name, contact, acct): MessageControl.__init__(self, type_id, parent_win, widget_name, display_name, contact, acct); @@ -271,12 +274,16 @@ class ChatControlBase(MessageControl): send_message = True if gajim.connections[self.account].connected < 2: # we are not connected - dialogs.ErrorDialog(_('A connection is not available'), - _('Your message can not be sent until you are connected.')).get_response() + dialog = dialogs.ErrorDialog(_('A connection is not available'), + _('Your message can not be sent until you are connected.')) + dialog.get_response() send_message = False if send_message: self.send_message(message) # send the message + else: + # Give the control itself a chance to process + self.handle_message_textview_mykey_press(widget, event_keyval, event_keymod) def _process_command(self, message): if not message: @@ -584,16 +591,16 @@ class ChatControlBase(MessageControl): contact = self.parent_win.get_active_contact() jid = contact.jid - if _('not in the roster') in contact.groups: # for add_to_roster_menuitem - childs[5].show() - childs[5].set_no_show_all(False) - else: - childs[5].hide() - childs[5].set_no_show_all(True) if self.type_id == message_control.TYPE_GC: start_removing_from = 7 # # this is from the seperator and after else: + if _('not in the roster') in contact.groups: # for add_to_roster_menuitem + childs[5].show() + childs[5].set_no_show_all(False) + else: + childs[5].hide() + childs[5].set_no_show_all(True) start_removing_from = 6 # this is from the seperator and after for child in childs[start_removing_from:]: @@ -723,14 +730,11 @@ class ChatControl(ChatControlBase): def _on_window_motion_notify(self, widget, event): '''it gets called no matter if it is the active window or not''' - # FIXME NOT WORKING - print "_on_window_motion_notify" - if widget.get_property('has-toplevel-focus'): + if self.parent_win.get_active_jid() == self.contact.jid: # change chatstate only if window is the active one self.mouse_over_in_last_5_secs = True self.mouse_over_in_last_30_secs = True - def _schedule_activity_timers(self): self.possible_paused_timeout_id = gobject.timeout_add(5000, self.check_for_possible_paused_chatstate, None) diff --git a/src/common/contacts.py b/src/common/contacts.py index 73a45ecce..c76b391a4 100644 --- a/src/common/contacts.py +++ b/src/common/contacts.py @@ -103,7 +103,6 @@ class Contacts: def create_contact(self, jid='', name='', groups=[], show='', status='', sub='', ask='', resource='', priority=5, keyID='', our_chatstate=None, chatstate=None): - print "creating Contact:", jid return Contact(jid, name, groups, show, status, sub, ask, resource, priority, keyID, our_chatstate, chatstate) @@ -230,12 +229,10 @@ class Contacts: def create_gc_contact(self, room_jid='', name='', show='', status='', role='', affiliation='', jid='', resource=''): - print "creating GC_Contact:", jid return GC_Contact(room_jid, name, show, status, role, affiliation, jid, resource) def add_gc_contact(self, account, gc_contact): - print "add_gc_contact" # No such account before ? if not self._gc_contacts.has_key(account): self._contacts[account] = {gc_contact.room_jid : {gc_contact.name: \ diff --git a/src/gajim.py b/src/gajim.py index c9420852f..d9358b763 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -478,7 +478,7 @@ class Interface: fjid = array[0] jids = fjid.split('/', 1) jid = jids[0] - gcs = gajim.interface.msg_win_mgr.get_controls(message_window.TYPE_GC) + gcs = gajim.interface.msg_win_mgr.get_controls(message_control.TYPE_GC) for gc_control in gcs: if jid == gc_control.contact.jid: if len(jids) > 1: # it's a pm diff --git a/src/groupchat_control.py b/src/groupchat_control.py index 1ab8cf295..3b197fe7a 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -107,8 +107,8 @@ class GroupchatControl(ChatControlBase): # if True, the room has mentioned us self.attention_flag = False self.room_creation = time.time() - self.nick_hits = 0 - self.cmd_hits = 0 + self.nick_hits = [] + self.cmd_hits = [] self.last_key_tabs = False self.subject = '' @@ -126,6 +126,8 @@ class GroupchatControl(ChatControlBase): self.gc_popup_menu = xm.get_widget('gc_control_popup_menu') self.name_label = self.xml.get_widget('banner_name_label') + self.parent_win.window.connect('focus-in-event', + self._on_window_focus_in_event) # set the position of the current hpaned self.hpaned_position = gajim.config.get('gc-hpaned-position') @@ -174,6 +176,11 @@ class GroupchatControl(ChatControlBase): self.conv_textview.grab_focus() self.widget.show_all() + def _on_window_focus_in_event(self, widget, event): + '''When window gets focus''' + if self.parent_win.get_active_jid() == self.room_jid: + self.allow_focus_out_line = True + def tree_cell_data_func(self, column, renderer, model, iter, data=None): theme = gajim.config.get('roster_theme') if model.iter_parent(iter): @@ -1024,6 +1031,8 @@ class GroupchatControl(ChatControlBase): gajim.connections[self.account].change_gc_nick(self.room_jid, nick) def shutdown(self): + gajim.connections[self.account].send_gc_status(self.nick, self.room_jid, + show='offline', status='offline') # They can already be removed by the destroy function if self.room_jid in gajim.contacts.get_gc_list(self.account): gajim.contacts.remove_room(self.account, self.room_jid) @@ -1056,3 +1065,509 @@ class GroupchatControl(ChatControlBase): dialog.destroy() return retval + + def set_control_active(self, state): + ChatControlBase.set_control_active(self, state) + if not state: + # add the focus-out line to the tab we are leaving + self.check_and_possibly_add_focus_out_line() + + def get_specific_unread(self): + # returns the number of the number of unread msgs + # for room_jid & number of unread private msgs with each contact + # that we have + nb = 0 + for nick in gajim.contacts.get_nick_list(self.account, self.room_jid): + fjid = self.room_jid + '/' + nick + if gajim.awaiting_events[self.account].has_key(fjid): + # gc can only have messages as event + nb += len(gajim.awaiting_events[self.account][fjid]) + return nb + + def on_change_subject_menuitem_activate(self, widget): + instance = dialogs.InputDialog(_('Changing Subject'), + _('Please specify the new subject:'), self.subject) + response = instance.get_response() + if response == gtk.RESPONSE_OK: + # Note, we don't update self.subject since we don't know whether it will work yet + subject = instance.input_entry.get_text().decode('utf-8') + gajim.connections[self.account].send_gc_subject(self.room_jid, subject) + + def on_change_nick_menuitem_activate(self, widget): + title = _('Changing Nickname') + prompt = _('Please specify the new nickname you want to use:') + self.show_change_nick_input_dialog(title, prompt, self.nick) + + def on_configure_room_menuitem_activate(self, widget): + gajim.connections[self.account].request_gc_config(self.room_jid) + + def on_bookmark_room_menuitem_activate(self, widget): + bm = { + 'name': self.name, + 'jid': self.room_jid, + 'autojoin': '0', + 'password': '', + 'nick': self.nick + } + + for bookmark in gajim.connections[self.account].bookmarks: + if bookmark['jid'] == bm['jid']: + dialogs.ErrorDialog( + _('Bookmark already set'), + _('Room "%s" is already in your bookmarks.') % bm['jid']).\ + get_response() + return + + gajim.connections[self.account].bookmarks.append(bm) + gajim.connections[self.account].store_bookmarks() + + gajim.interface.roster.make_menu() + + dialogs.InformationDialog( + _('Bookmark has been added successfully'), + _('You can manage your bookmarks via Actions menu in your roster.')) + + def handle_message_textview_mykey_press(self, widget, event_keyval, event_keymod): + # NOTE: handles mykeypress which is custom signal connected to this + # CB in new_room(). for this singal see message_textview.py + + # construct event instance from binding + event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here + event.keyval = event_keyval + event.state = event_keymod + event.time = 0 # assign current time + + message_buffer = widget.get_buffer() + start_iter, end_iter = message_buffer.get_bounds() + message = message_buffer.get_text(start_iter, end_iter, False).decode('utf-8') + + if event.keyval == gtk.keysyms.Tab: # TAB + cursor_position = message_buffer.get_insert() + end_iter = message_buffer.get_iter_at_mark(cursor_position) + text = message_buffer.get_text(start_iter, end_iter, False).decode('utf-8') + if not text or text.endswith(' '): + # if we are nick cycling, last char will always be space + if not self.last_key_tabs: + return False + + splitted_text = text.split() + # command completion + if text.startswith('/') and len(splitted_text) == 1: + text = splitted_text[0] + if len(text) == 1: # user wants to cycle all commands + self.cmd_hits = self.muc_cmds + else: + # cycle possible commands depending on what the user typed + if self.last_key_tabs and len(self.cmd_hits) and \ + self.cmd_hits[0].startswith(text.lstrip('/')): + self.cmd_hits.append(self.cmd_hits[0]) + self.cmd_hits.pop(0) + else: # find possible commands + self.cmd_hits = [] + for cmd in self.muc_cmds: + if cmd.startswith(text.lstrip('/')): + self.cmd_hits.append(cmd) + if len(self.cmd_hits): + message_buffer.delete(start_iter, end_iter) + message_buffer.insert_at_cursor('/' + self.cmd_hits[0] + ' ') + self.last_key_tabs = True + return True + + # nick completion + # check if tab is pressed with empty message + if len(splitted_text): # if there are any words + begin = splitted_text[-1] # last word we typed + + if len(self.nick_hits) and \ + self.nick_hits[0].startswith(begin.replace( + self.gc_refer_to_nick_char, '')) and \ + self.last_key_tabs: # we should cycle + self.nick_hits.append(self.nick_hits[0]) + self.nick_hits.pop(0) + else: + self.nick_hits = [] # clear the hit list + list_nick = gajim.contacts.get_nick_list(self.account, + self.room_jid) + for nick in list_nick: + if nick.lower().startswith(begin.lower()): + # the word is the begining of a nick + self.nick_hits.append(nick) + if len(self.nick_hits): + if len(splitted_text) == 1: # This is the 1st word of the line + add = self.gc_refer_to_nick_char + ' ' + else: + add = ' ' + start_iter = end_iter.copy() + if self.last_key_tabs and begin.endswith(', '): + # have to accomodate for the added space from last + # completion + start_iter.backward_chars(len(begin) + 2) + elif self.last_key_tabs: + # have to accomodate for the added space from last + # completion + start_iter.backward_chars(len(begin) + 1) + else: + start_iter.backward_chars(len(begin)) + + message_buffer.delete(start_iter, end_iter) + message_buffer.insert_at_cursor(self.nick_hits[0] + add) + self.last_key_tabs = True + return True + self.last_key_tabs = False + + def on_list_treeview_key_press_event(self, widget, event): + if event.keyval == gtk.keysyms.Escape: + widget.get_selection().unselect_all() + + def on_list_treeview_row_expanded(self, widget, iter, path): + '''When a row is expanded: change the icon of the arrow''' + model = widget.get_model() + image = gajim.interface.roster.jabber_state_images['16']['opened'] + model[iter][C_IMG] = image + + def on_list_treeview_row_collapsed(self, widget, iter, path): + '''When a row is collapsed: change the icon of the arrow''' + model = widget.get_model() + image = gajim.interface.roster.jabber_state_images['16']['closed'] + model[iter][C_IMG] = image + + def kick(self, widget, nick): + '''kick a user''' + # ask for reason + instance = dialogs.InputDialog(_('Kicking %s') % nick, + _('You may specify a reason below:')) + response = instance.get_response() + if response == gtk.RESPONSE_OK: + reason = instance.input_entry.get_text().decode('utf-8') + else: + return # stop kicking procedure + gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'none', + reason) + + def mk_menu(self, event, iter): + '''Make contact's popup menu''' + model = self.list_treeview.get_model() + nick = model[iter][C_NICK].decode('utf-8') + c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) + jid = c.jid + target_affiliation = c.affiliation + target_role = c.role + + # looking for user's affiliation and role + user_nick = self.nick + user_affiliation = gajim.contacts.get_gc_contact(self.account, self.room_jid, + user_nick).affiliation + user_role = self.get_role(user_nick) + + # making menu from glade + xml = gtk.glade.XML(GTKGUI_GLADE, 'gc_occupants_menu', APP) + + # these conditions were taken from JEP 0045 + item = xml.get_widget('kick_menuitem') + if user_role != 'moderator' or \ + (user_affiliation == 'admin' and target_affiliation == 'owner') or \ + (user_affiliation == 'member' and target_affiliation in ('admin', 'owner')) or \ + (user_affiliation == 'none' and target_affiliation != 'none'): + item.set_sensitive(False) + item.connect('activate', self.kick, nick) + + item = xml.get_widget('voice_checkmenuitem') + item.set_active(target_role != 'visitor') + if user_role != 'moderator' or \ + user_affiliation == 'none' or \ + (user_affiliation=='member' and target_affiliation!='none') or \ + target_affiliation in ('admin', 'owner'): + item.set_sensitive(False) + item.connect('activate', self.on_voice_checkmenuitem_activate, nick) + + item = xml.get_widget('moderator_checkmenuitem') + item.set_active(target_role == 'moderator') + if not user_affiliation in ('admin', 'owner') or \ + target_affiliation in ('admin', 'owner'): + item.set_sensitive(False) + item.connect('activate', self.on_moderator_checkmenuitem_activate, nick) + + item = xml.get_widget('ban_menuitem') + if not user_affiliation in ('admin', 'owner') or \ + (target_affiliation in ('admin', 'owner') and\ + user_affiliation != 'owner'): + item.set_sensitive(False) + item.connect('activate', self.ban, jid) + + item = xml.get_widget('member_checkmenuitem') + item.set_active(target_affiliation != 'none') + if not user_affiliation in ('admin', 'owner') or \ + (user_affiliation != 'owner' and target_affiliation in ('admin','owner')): + item.set_sensitive(False) + item.connect('activate', self.on_member_checkmenuitem_activate, jid) + + item = xml.get_widget('admin_checkmenuitem') + item.set_active(target_affiliation in ('admin', 'owner')) + if not user_affiliation == 'owner': + item.set_sensitive(False) + item.connect('activate', self.on_admin_checkmenuitem_activate, jid) + + item = xml.get_widget('owner_checkmenuitem') + item.set_active(target_affiliation == 'owner') + if not user_affiliation == 'owner': + item.set_sensitive(False) + item.connect('activate', self.on_owner_checkmenuitem_activate, jid) + + item = xml.get_widget('information_menuitem') + item.connect('activate', self.on_info, nick) + + item = xml.get_widget('history_menuitem') + item.connect('activate', self.on_history, nick) + + item = xml.get_widget('add_to_roster_menuitem') + if not jid: + item.set_sensitive(False) + item.connect('activate', self.on_add_to_roster, jid) + + item = xml.get_widget('send_private_message_menuitem') + item.connect('activate', self.on_send_pm, model, iter) + + # show the popup now! + menu = xml.get_widget('gc_occupants_menu') + menu.popup(None, None, None, event.button, event.time) + menu.show_all() + + def _start_private_message(self, nick): + nick_jid = gajim.construct_fjid(self.room_jid, nick) + + win = gajim.interface.msg_win_mgr.get_window(nick_jid) + if not win: + gc_c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) + c = gajim.contacts.contact_from_gc_contact(gc_c) + gajim.interface.roster.new_chat(c, self.account) + win = gajim.interface.msg_win_mgr.get_window(nick_jid) + win.set_active_tab(nick_jid) + win.window.present() + + def on_list_treeview_row_activated(self, widget, path, col = 0): + '''When an iter is double clicked: open the chat window''' + model = widget.get_model() + iter = model.get_iter(path) + if len(path) == 1: # It's a group + if (widget.row_expanded(path)): + widget.collapse_row(path) + else: + widget.expand_row(path, False) + else: # We want to send a private message + nick = model[iter][C_NICK].decode('utf-8') + win = self._start_private_message(nick) + + def on_list_treeview_button_press_event(self, widget, event): + '''popup user's group's or agent menu''' + if event.button == 3: # right click + try: + path, column, x, y = widget.get_path_at_pos(int(event.x), + int(event.y)) + except TypeError: + widget.get_selection().unselect_all() + return + widget.get_selection().select_path(path) + model = widget.get_model() + iter = model.get_iter(path) + if len(path) == 2: + self.mk_menu(event, iter) + return True + + elif event.button == 2: # middle click + try: + path, column, x, y = widget.get_path_at_pos(int(event.x), + int(event.y)) + except TypeError: + widget.get_selection().unselect_all() + return + widget.get_selection().select_path(path) + model = widget.get_model() + iter = model.get_iter(path) + if len(path) == 2: + nick = model[iter][C_NICK].decode('utf-8') + fjid = gajim.construct_fjid(self.room_jid, nick) + win = gajim.interface.msg_win_mgr.get_window(fjid) + if not win: + gc_c = gajim.contacts.get_gc_contact(self.account, self.room_jid, + nick) + c = gajim.contacts.contact_from_gc_contact(gc_c) + gajim.interface.roster.new_chat(c, self.account, + private_chat = True) + win = gajim.interface.msg_win_mgr.get_window(fjid) + win.set_active_tab(fjid) + win.window.present() + return True + + elif event.button == 1: # left click + try: + path, column, x, y = widget.get_path_at_pos(int(event.x), + int(event.y)) + except TypeError: + widget.get_selection().unselect_all() + return + + model = widget.get_model() + iter = model.get_iter(path) + nick = model[iter][C_NICK].decode('utf-8') + if not nick in gajim.contacts.get_nick_list(self.account, self.room_jid): + #it's a group + if x < 20: # first cell in 1st column (the arrow SINGLE clicked) + if (widget.row_expanded(path)): + widget.collapse_row(path) + else: + widget.expand_row(path, False) + + def on_list_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 + typ = model[iter][C_TYPE].decode('utf-8') + if typ == 'contact': + account = self.account + + if self.tooltip.timeout == 0 or self.tooltip.id != props[0]: + self.tooltip.id = row + nick = model[iter][C_NICK].decode('utf-8') + self.tooltip.timeout = gobject.timeout_add(500, + self.show_tooltip, gajim.contacts.get_gc_contact(account, + self.room_jid, nick)) + + def on_list_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 show_tooltip(self, contact): + pointer = self.list_treeview.get_pointer() + props = self.list_treeview.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.list_treeview.get_cell_area(props[0],props[1]) + position = self.list_treeview.window.get_origin() + pointer = self.parent_win.window.get_pointer() + self.tooltip.show_tooltip(contact, (0, rect.height), + (self.parent_win.window.get_screen().get_display().get_pointer()[1], + position[1] + rect.y)) + else: + self.tooltip.hide_tooltip() + + + def grant_voice(self, widget, nick): + '''grant voice privilege to a user''' + gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'participant') + + def revoke_voice(self, widget, nick): + '''revoke voice privilege to a user''' + gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'visitor') + + def grant_moderator(self, widget, nick): + '''grant moderator privilege to a user''' + gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'moderator') + + def revoke_moderator(self, widget, nick): + '''revoke moderator privilege to a user''' + gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'participant') + + def ban(self, widget, jid): + '''ban a user''' + # to ban we know the real jid. so jid is not fakejid + nick = gajim.get_nick_from_jid(jid) + # ask for reason + instance = dialogs.InputDialog(_('Banning %s') % nick, + _('You may specify a reason below:')) + response = instance.get_response() + if response == gtk.RESPONSE_OK: + reason = instance.input_entry.get_text().decode('utf-8') + else: + return # stop banning procedure + gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, + 'outcast', reason) + + def grant_membership(self, widget, jid): + '''grant membership privilege to a user''' + gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, + 'member') + + def revoke_membership(self, widget, jid): + '''revoke membership privilege to a user''' + gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, + 'none') + + def grant_admin(self, widget, jid): + '''grant administrative privilege to a user''' + gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'admin') + + def revoke_admin(self, widget, jid): + '''revoke administrative privilege to a user''' + gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, + 'member') + + def grant_owner(self, widget, jid): + '''grant owner privilege to a user''' + gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'owner') + + def revoke_owner(self, widget, jid): + '''revoke owner privilege to a user''' + gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'admin') + + def on_info(self, widget, ck): + '''Call vcard_information_window class to display user's information''' + c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) + jid = c.get_full_jid() + if gajim.interface.instances[self.account]['infos'].has_key(jid): + gajim.interface.instances[self.account]['infos'][jid].window.present() + else: + # we create a Contact instance + c2 = gajim.contacts.contact_from_gc_contact(c) + gajim.interface.instances[self.account]['infos'][jid] = \ + vcard.VcardWindow(c2, self.account, False) + + def on_history(self, widget, ck): + jid = gajim.construct_fjid(self.room_jid, nick) + self.on_history_menuitem_clicked(jid = jid) + + def on_add_to_roster(self, widget, jid): + dialogs.AddNewContactWindow(self.account, jid) + + def on_voice_checkmenuitem_activate(self, widget, nick): + if widget.get_active(): + self.grant_voice(widget, nick) + else: + self.revoke_voice(widget, nick) + + def on_moderator_checkmenuitem_activate(self, widget, nick): + if widget.get_active(): + self.grant_moderator(widget, nick) + else: + self.revoke_moderator(widget, nick) + + def on_member_checkmenuitem_activate(self, widget, jid): + if widget.get_active(): + self.grant_membership(widget, jid) + else: + self.revoke_membership(widget, jid) + + def on_admin_checkmenuitem_activate(self, widget, jid): + if widget.get_active(): + self.grant_admin(widget, jid) + else: + self.revoke_admin(widget, jid) + + def on_owner_checkmenuitem_activate(self, widget, jid): + if widget.get_active(): + self.grant_owner(widget, jid) + else: + self.revoke_owner(widget, jid)