diff --git a/debian/patches/00_debian-group_bugfix.patch b/debian/patches/00_debian-group_bugfix.patch deleted file mode 100644 index 0a5ab2190..000000000 --- a/debian/patches/00_debian-group_bugfix.patch +++ /dev/null @@ -1,13 +0,0 @@ -Index: src/roster_window.py -=================================================================== ---- src/roster_window.py (revision 4923) -+++ src/roster_window.py (working copy) -@@ -87,6 +87,8 @@ - model = self.tree.get_model() - root = self.get_account_iter(account) - group_iter = model.iter_children(root) -+ # C_NAME column contacts the pango escaped group name -+ name = gtkgui_helpers.escape_for_pango_markup(name) - while group_iter: - group_name = model[group_iter][C_NAME].decode('utf-8') - if name == group_name: diff --git a/src/chat_control.py b/src/chat_control.py index e468af6c5..ab31ebc10 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -188,11 +188,11 @@ class ChatControlBase(MessageControl): if event.keyval == gtk.keysyms.Up: if event.state & gtk.gdk.CONTROL_MASK: # Ctrl+UP - self.sent_messages_scroll(jid, 'up', widget.get_buffer()) + self.sent_messages_scroll('up', widget.get_buffer()) return elif event.keyval == gtk.keysyms.Down: if event.state & gtk.gdk.CONTROL_MASK: # Ctrl+Down - self.sent_messages_scroll(jid, 'down', widget.get_buffer()) + self.sent_messages_scroll('down', widget.get_buffer()) return elif event.keyval == gtk.keysyms.Return or \ event.keyval == gtk.keysyms.KP_Enter: # ENTER @@ -501,6 +501,30 @@ class ChatControlBase(MessageControl): gajim.interface.systray.remove_jid(jid, self.account, self.type_id) + def sent_messages_scroll(self, direction, conv_buf): + size = len(self.sent_history) + if self.typing_new: + #user was typing something and then went into history, so save + #whatever is already typed + start_iter = conv_buf.get_start_iter() + end_iter = conv_buf.get_end_iter() + self.orig_msg = conv_buf.get_text(start_iter, end_iter, 0).decode('utf-8') + self.typing_new = False + if direction == 'up': + if self.sent_history_pos == 0: + return + self.sent_history_pos = self.sent_history_pos - 1 + conv_buf.set_text(self.sent_history[self.sent_history_pos]) + elif direction == 'down': + if self.sent_history_pos >= size - 1: + conv_buf.set_text(self.orig_msg); + self.typing_new = True + self.sent_history_pos = size + return + + self.sent_history_pos = self.sent_history_pos + 1 + conv_buf.set_text(self.sent_history[self.sent_history_pos]) + ################################################################################ class ChatControl(ChatControlBase): '''A control for standard 1-1 chat''' @@ -512,16 +536,50 @@ class ChatControl(ChatControlBase): self.compact_view_always = gajim.config.get('always_compact_view_chat') self.set_compact_view(self.compact_view_always) - # chatstate timers and state - self._schedule_activity_timers() - self.reset_kbd_mouse_timeout_vars() - xm = gtk.glade.XML(GTKGUI_GLADE, 'chat_control_popup_menu', APP) xm.signal_autoconnect(self) self.popup_menu = xm.get_widget('chat_control_popup_menu') xm = gtk.glade.XML(GTKGUI_GLADE, 'banner_eventbox', APP) xm.signal_autoconnect(self) + self.TARGET_TYPE_URI_LIST = 80 + self.dnd_list = [ ( 'text/uri-list', 0, self.TARGET_TYPE_URI_LIST ) ] + self.widget.connect('drag_data_received', self._on_drag_data_received) + self.widget.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT | + gtk.DEST_DEFAULT_DROP, + self.dnd_list, gtk.gdk.ACTION_COPY) + + # keep timeout id and window obj for possible big avatar + # it is on enter-notify and leave-notify so no need to be per jid + self.show_bigger_avatar_timeout_id = None + self.bigger_avatar_window = None + self.show_avatar(self.contact.resource) + + # chatstate timers and state + self.reset_kbd_mouse_timeout_vars() + self._schedule_activity_timers() + self.parent_win.window.connect('motion-notify-event', + self._on_window_motion_notify) + + def save_var(self, jid): + gpg_enabled = self.xmls[jid].get_widget('gpg_togglebutton').get_active() + return {'gpg_enabled': gpg_enabled} + + def load_var(self, jid, var): + if not self.xmls.has_key(jid): + return + self.xmls[jid].get_widget('gpg_togglebutton').set_active( + var['gpg_enabled']) + + def _on_window_motion_notify(self, widget, event): + '''it gets called no matter if it is the active window or not''' + print "_on_window_motion_notify" + if widget.get_property('has-toplevel-focus'): + # 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) @@ -1013,3 +1071,41 @@ class ChatControl(ChatControlBase): self.send_chatstate('active', self.contact.jid) else: self.send_chatstate('inactive', self.contact.jid) + + def show_avatar(self, resource = None): + jid = self.contact.jid + jid_with_resource = jid + if resource: + jid_with_resource += '/' + resource + + # we assume contact has no avatar + scaled_pixbuf = None + + real_jid = gajim.get_real_jid_from_fjid(self.account, jid) + pixbuf = None + if real_jid: + pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(real_jid) + if not real_jid or pixbuf == 'ask': + # we don't have the vcard or it's pm and we don't have the real jid + gajim.connections[self.account].request_vcard(jid_with_resource) + return + if pixbuf is not None: + scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'chat') + + image = self.xml.get_widget('avatar_image') + image.set_from_pixbuf(scaled_pixbuf) + image.show_all() + + def _on_drag_data_received(self, widget, context, x, y, selection, target_type, + timestamp): + # If not resource, we can't send file + if not self.contact.resource: + return + if target_type == self.TARGET_TYPE_URI_LIST: + uri = selection.data.strip() + uri_splitted = uri.split() # we may have more than one file dropped + for uri in uri_splitted: + path = helpers.get_file_path_from_dnd_dropped_uri(uri) + if os.path.isfile(path): # is it file? + ft = gajim.interface.instances['file_transfers'] + ft.send_file(self.account, self.contact, path) diff --git a/src/gajim.py b/src/gajim.py index b84a692af..2bc86fff6 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -650,13 +650,11 @@ class Interface: if gajim.interface.msg_win_mgr.has_window(jid): win = gajim.interface.msg_type.get_window(jid) ctl = win.get_control(jid) - # FIXME: Why is this needed elif resource and gajim.interface.msg_win_mgr.has_window(jid + '/' + resource): win = gajim.interface.msg_type.get_window(jid + '/' + resource) ctl = win.get_control(jid + '/' + resource) if win: - # FIXME: Are these args needed - ctl.show_avatar(jid, resource) + ctl.show_avatar() # Show avatar in roster self.roster.draw_avatar(jid, account) if self.remote_ctrl: diff --git a/src/groupchat_window.py b/src/groupchat_window.py index 5c7c8557b..e3e084380 100644 --- a/src/groupchat_window.py +++ b/src/groupchat_window.py @@ -1330,7 +1330,7 @@ current room topic.') % command, room_jid) room_jid, show='offline', status=reason) del self.nicks[room_jid] # They can already be removed by the destroy function - if room_jid in gajim.contacts.get_room_list(self.account): + if room_jid in gajim.contacts.get_gc_list(self.account): gajim.contacts.remove_room(self.account, room_jid) del gajim.gc_connected[self.account][room_jid] del self.list_treeview[room_jid] diff --git a/src/message_control.py b/src/message_control.py index b5960c33d..0d1d54809 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -34,12 +34,10 @@ APP = i18n.APP GTKGUI_GLADE = 'gtkgui.glade' #################### -class MessageControl(gtk.VBox): +class MessageControl: '''An abstract base widget that can embed in the gtk.Notebook of a MessageWindow''' def __init__(self, type_id, parent_win, widget_name, display_name, contact, account): - gtk.VBox.__init__(self) - self.type_id = type_id self.parent_win = parent_win self.widget_name = widget_name @@ -50,7 +48,7 @@ class MessageControl(gtk.VBox): self.compact_view_current = False self.nb_unread = 0 self.print_time_timeout_id = None - # FIXME: Make this a member like all the others + # FIXME: Make this a member gajim.last_message_time[self.account][contact.jid] = 0 self.xml = gtk.glade.XML(GTKGUI_GLADE, widget_name, APP) @@ -101,6 +99,14 @@ class MessageControl(gtk.VBox): # NOTE: Derived classes MAY implement this return True + def save_var(self, jid): + '''When called, the derived type should serialize it's state in the form of a + name/value (i.e. dict) result. + the return value must be compatible with wthe one given to load_var''' + pass # Derived classes SHOULD implement this + def load_var(self, jid, var): + pass # Derived classes SHOULD implement this + def send_message(self, message, keyID = '', type = 'chat', chatstate = None): '''Send the given message to the active tab''' # refresh timers diff --git a/src/message_window.py b/src/message_window.py index 33edca09e..887b4d748 100644 --- a/src/message_window.py +++ b/src/message_window.py @@ -42,6 +42,10 @@ class MessageWindow: self.widget_name = 'message_window' self.xml = gtk.glade.XML(GTKGUI_GLADE, self.widget_name, APP) self.window = self.xml.get_widget(self.widget_name) + # FIXME: assertion that !GTK_WIDGET_REALIZED fails + # gtk+ doesn't make use of the motion notify on gtkwindow by default + # so this line adds that + #self.window.set_events(gtk.gdk.POINTER_MOTION_MASK) self.alignment = self.xml.get_widget('alignment') self.notebook = self.xml.get_widget('notebook') @@ -70,6 +74,8 @@ class MessageWindow: self.notebook.set_show_border(gajim.config.get('tabs_border')) self.notebook.connect('switch-page', self._on_notebook_switch_page) + self.notebook.connect('key-press-event', + self._on_notebook_key_press) # Connect event handling for this Window self.window.connect('delete-event', self._on_window_delete) @@ -85,12 +91,8 @@ class MessageWindow: gtkgui_helpers.resize_window(self.window, gajim.config.get('msgwin-width'), gajim.config.get('msgwin-height')) - - self.window.show_all() def _on_window_focus(self, widget, event): - # FIXME: - print "_on_window_focus" # window received focus, so if we had urgency REMOVE IT # NOTE: we do not have to read the message (it maybe in a bg tab) # to remove urgency hint so this functions does that @@ -253,21 +255,26 @@ class MessageWindow: self.notebook.set_current_page(ctl_page) def remove_tab(self, contact): + # Shutdown the MessageControl ctl = self.get_control(contact.jid) - if len(self._controls) == 1 or not ctl.allow_shutdown(): + if not ctl.allow_shutdown(): return - - if gajim.interface.systray_enabled: - gajim.interface.systray.remove_jid(contact.jid, ctl.account, - ctl.type) ctl.shutdown() - self.notebook.remove_page(self.notebook.page_num(self.childs[jid])) + # Update external state + if gajim.interface.systray_enabled: + gajim.interface.systray.remove_jid(contact.jid, ctl.account, + ctl.type_id) + del gajim.last_message_time[ctl.account][ctl.contact.jid] + + if len(self._controls) == 1: + self.window.destroy() + return + + self.notebook.remove_page(self.notebook.page_num(ctl.widget)) del self._controls[contact.jid] - del gajim.last_message_time[self.account][jid] - - if len(self.xmls) == 1: # we now have only one tab + if len(self._controls) == 1: # we now have only one tab show_tabs_if_one_tab = gajim.config.get('tabs_always_visible') self.notebook.set_show_tabs(show_tabs_if_one_tab) if not show_tabs_if_one_tab: @@ -442,6 +449,112 @@ class MessageWindow: new_ctl = self._widget_to_control(notebook.get_nth_page(page_num)) new_ctl.set_control_active(True) + def _on_notebook_key_press(self, widget, event): + st = '1234567890' # alt+1 means the first tab (tab 0) + ctl = self.get_active_control() + jid = ctl.jid + if event.keyval == gtk.keysyms.Escape: # ESCAPE + if ctl.type == TYPE_CHAT: + self.remove_tab(jid) + elif event.keyval == gtk.keysyms.F4 and \ + (event.state & gtk.gdk.CONTROL_MASK): # CTRL + F4 + self.remove_tab(jid) + elif event.keyval == gtk.keysyms.w and \ + (event.state & gtk.gdk.CONTROL_MASK): # CTRL + W + self.remove_tab(jid) + elif event.string and event.string in st and \ + (event.state & gtk.gdk.MOD1_MASK): # alt + 1,2,3.. + self.notebook.set_current_page(st.index(event.string)) + elif event.keyval == gtk.keysyms.c and \ + (event.state & gtk.gdk.MOD1_MASK): # alt + C toggles compact view + ctl.set_compact_view(not self.compact_view_current_state) + # FIXME: Move this to ChatControlBase + elif event.keyval == gtk.keysyms.e and \ + (event.state & gtk.gdk.MOD1_MASK): # alt + E opens emoticons menu + if gajim.config.get('useemoticons'): + msg_tv = self.message_textviews[jid] + def set_emoticons_menu_position(w, msg_tv = msg_tv): + window = msg_tv.get_window(gtk.TEXT_WINDOW_WIDGET) + # get the window position + origin = window.get_origin() + size = window.get_size() + buf = msg_tv.get_buffer() + # get the cursor position + cursor = msg_tv.get_iter_location(buf.get_iter_at_mark( + buf.get_insert())) + cursor = msg_tv.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, + cursor.x, cursor.y) + x = origin[0] + cursor[0] + y = origin[1] + size[1] + menu_width, menu_height = self.emoticons_menu.size_request() + #FIXME: get_line_count is not so good + #get the iter of cursor, then tv.get_line_yrange + # so we know in which y we are typing (not how many lines we have + # then go show just above the current cursor line for up + # or just below the current cursor line for down + #TEST with having 3 lines and writing in the 2nd + if y + menu_height > gtk.gdk.screen_height(): + # move menu just above cursor + y -= menu_height + (msg_tv.allocation.height / buf.get_line_count()) + #else: # move menu just below cursor + # y -= (msg_tv.allocation.height / buf.get_line_count()) + return (x, y, True) # push_in True + self.emoticons_menu.popup(None, None, set_emoticons_menu_position, 1, 0) + # FIXME Move to ChatControlBase + elif event.keyval == gtk.keysyms.Page_Down: + if event.state & gtk.gdk.SHIFT_MASK: # SHIFT + PAGE DOWN + conv_textview = self.conversation_textviews[jid] + rect = conv_textview.get_visible_rect() + iter = conv_textview.get_iter_at_location(rect.x, + rect.y + rect.height) + conv_textview.scroll_to_iter(iter, 0.1, True, 0, 0) + # FIXME Move to ChatControlBase + elif event.keyval == gtk.keysyms.Page_Up: + if event.state & gtk.gdk.SHIFT_MASK: # SHIFT + PAGE UP + conv_textview = self.conversation_textviews[jid] + rect = conv_textview.get_visible_rect() + iter = conv_textview.get_iter_at_location(rect.x, rect.y) + conv_textview.scroll_to_iter(iter, 0.1, True, 0, 1) + # FIXME Move to ChatControlBase + elif event.keyval == gtk.keysyms.Up: + if event.state & gtk.gdk.SHIFT_MASK: # SHIFT + UP + conversation_scrolledwindow = self.xml.get_widget('conversation_scrolledwindow') + conversation_scrolledwindow.emit('scroll-child', + gtk.SCROLL_PAGE_BACKWARD, False) + elif event.keyval == gtk.keysyms.ISO_Left_Tab: # SHIFT + TAB + if event.state & gtk.gdk.CONTROL_MASK: # CTRL + SHIFT + TAB + self.move_to_next_unread_tab(False) + elif event.keyval == gtk.keysyms.Tab: # TAB + if event.state & gtk.gdk.CONTROL_MASK: # CTRL + TAB + self.move_to_next_unread_tab(True) + # FIXME Move to ChatControlBase + elif (event.keyval == gtk.keysyms.l or event.keyval == gtk.keysyms.L) \ + and event.state & gtk.gdk.CONTROL_MASK: # CTRL + L + conv_textview = self.conversation_textviews[jid] + conv_textview.get_buffer().set_text('') + # FIXME Move to ChatControlBase + elif event.keyval == gtk.keysyms.v and event.state & gtk.gdk.CONTROL_MASK: + # CTRL + V + msg_textview = self.message_textviews[jid] + if not msg_textview.is_focus(): + msg_textview.grab_focus() + msg_textview.emit('key_press_event', event) + elif event.state & gtk.gdk.CONTROL_MASK or \ + (event.keyval == gtk.keysyms.Control_L) or \ + (event.keyval == gtk.keysyms.Control_R): + # we pressed a control key or ctrl+sth: we don't block + # the event in order to let ctrl+c (copy text) and + # others do their default work + pass + # FIXME Move to ChatControlBase + else: # it's a normal key press make sure message_textview has focus + msg_textview = self.message_textviews[jid] + if msg_textview.get_property('sensitive'): + if not msg_textview.is_focus(): + msg_textview.grab_focus() + msg_textview.emit('key_press_event', event) + + ################################################################################ class MessageWindowMgr: '''A manager and factory for MessageWindow objects''' @@ -468,6 +581,7 @@ class MessageWindowMgr: def _new_window(self): win = MessageWindow() + win.window.show_all() # we track the lifetime of this window win.window.connect('destroy', self._on_window_destroy) return win diff --git a/src/systray.py b/src/systray.py index 06ea29dd4..e89073578 100644 --- a/src/systray.py +++ b/src/systray.py @@ -250,7 +250,8 @@ class Systray: contacts_menu = gtk.Menu() item.set_submenu(contacts_menu) for jid in gajim.contacts.get_jid_list(account): - contact = gajim.get_contact_with_highest_priority(account, jid) + contact = gajim.contacts.get_contact_with_highest_priority(account, + jid) if group in contact.groups and contact.show != 'offline' and \ contact.show != 'error': at_least_one = True