From bb6c0d6a26b394d21d05defd17624982c138da80 Mon Sep 17 00:00:00 2001 From: Travis Shirk Date: Sun, 4 Sep 2005 23:30:40 +0000 Subject: [PATCH] Chatstate notifications in tabs #672 --- src/chat.py | 64 ++++++++++++++++++++++++++++++++++----- src/common/config.py | 7 +++++ src/gajim.py | 7 ++++- src/groupchat_window.py | 5 +++ src/tabbed_chat_window.py | 63 +++++++++++++++++++++++--------------- 5 files changed, 112 insertions(+), 34 deletions(-) diff --git a/src/chat.py b/src/chat.py index 16169cc0a..677c90010 100644 --- a/src/chat.py +++ b/src/chat.py @@ -160,19 +160,59 @@ class Chat: self.window.set_title(title) gtkgui_helpers.set_unset_urgency_hint(self.window, unread) - def redraw_tab(self, jid): - """redraw the label of the tab""" - start = '' + def redraw_tab(self, contact, chatstate = None): + '''redraw the label of the tab + if chatstate is given that means we have HE SENT US a chatstate''' + if isinstance(contact, unicode): + jid = contact + else: + jid = contact.jid + + unread = '' if self.nb_unread[jid] > 1: - start = '[' + unicode(self.nb_unread[jid]) + '] ' - elif self.nb_unread[jid] == 1: - start = '* ' + unread = '[' + unicode(self.nb_unread[jid]) + '] ' + # Update status images + self.set_state_image(jid) child = self.childs[jid] hb = self.notebook.get_tab_label(child).get_children()[0] if self.widget_name == 'tabbed_chat_window': nickname = hb.get_children()[1] close_button = hb.get_children()[2] + + # Draw tab label using chatstate + theme = gajim.config.get('roster_theme') + color = None + if unread and chatstate == 'active': + color = gajim.config.get_per('themes', theme, + 'state_unread_color') + elif chatstate is not None: + if chatstate == 'composing': + color = gajim.config.get_per('themes', theme, + 'state_composing_color') + elif unread and self.has_focus: + color = gajim.config.get_per('themes', theme, + 'state_active_color') + elif unread: + color = gajim.config.get_per('themes', theme, + 'state_unread_color') + elif chatstate == 'inactive': + color = gajim.config.get_per('themes', theme, + 'state_inactive_color') + elif chatstate == 'gone': + color = gajim.config.get_per('themes', theme, + 'state_gone_color') + elif chatstate == 'paused': + color = gajim.config.get_per('themes', theme, + 'state_paused_color') + else: + color = gajim.config.get_per('themes', theme, + 'state_active_color') + if color: + # FIXME: When tabs are in the "background" the color change has + # no affect + #print jid, chatstate, color + nickname.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color)) elif self.widget_name == 'groupchat_window': nickname = hb.get_children()[0] close_button = hb.get_children()[1] @@ -183,7 +223,7 @@ class Chat: close_button.hide() nickname.set_max_width_chars(10) - nickname.set_text(start + self.names[jid]) + nickname.set_text(unread + self.names[jid]) def on_window_destroy(self, widget, kind): #kind is 'chats' or 'gc' @@ -227,9 +267,14 @@ class Chat: self.plugin.windows['logs'][jid] = history_window.HistoryWindow( self.plugin, jid, self.account) + def on_chat_window_focus_out_event(self, widget, event): + self.has_focus = False + def on_chat_window_focus_in_event(self, widget, event): """When window gets focus""" + self.has_focus = True jid = self.get_active_jid() + textview = self.xmls[jid].get_widget('conversation_textview') buffer = textview.get_buffer() end_iter = buffer.get_end_iter() @@ -239,7 +284,6 @@ class Chat: #we are at the end if self.nb_unread[jid] > 0: self.nb_unread[jid] = 0 - self.redraw_tab(jid) self.show_title() if self.plugin.systray_enabled: self.plugin.systray.remove_jid(jid, self.account) @@ -250,6 +294,8 @@ class Chat: if gtk.gtk_version >= (2, 8, 0) and gtk.pygtk_version >= (2, 8, 0): if widget.props.urgency_hint: widget.props.urgency_hint = False + # Undo "unread" state display, etc. But note, there is not chatstate past here + self.redraw_tab(jid) def on_compact_view_menuitem_activate(self, widget): isactive = widget.get_active() @@ -1108,6 +1154,8 @@ class Chat: # FIXME: who gives us text that is not a string? if not text: + # FIXME: Let's find out... + assert(False) text = '' if buffer.get_char_count() > 0: diff --git a/src/common/config.py b/src/common/config.py index b18b0e347..fc411b1ca 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -215,6 +215,13 @@ class Config: 'contactfont': [ opt_str, 'Sans 10' ], 'bannertextcolor': [ opt_color, '#ffffff' ], 'bannerbgcolor': [ opt_color, '#000000' ], + + 'state_unread_color': [ opt_color, '#000000' ], + 'state_active_color': [ opt_color, '#000000' ], + 'state_inactive_color': [ opt_color, '#9e9e9e' ], + 'state_composing_color': [ opt_color, '#008b00' ], + 'state_paused_color': [ opt_color, '#0000cd' ], + 'state_gone_color': [ opt_color, '#bebebe' ], }, {}), } diff --git a/src/gajim.py b/src/gajim.py index 1419ef3bb..587ef4cf1 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -382,9 +382,10 @@ class Interface: not gajim.contacts[account].has_key(jid): return + # Handle chat states + contact = gajim.get_first_contact_instance_from_jid(account, jid) if self.windows[account]['chats'].has_key(jid): chat_win = self.windows[account]['chats'][jid] - contact = gajim.get_first_contact_instance_from_jid(account, jid) if chatstate is not None: # he sent us reply, so he supports jep85 if contact.chatstate == 'ask': # we were jep85 disco? contact.chatstate = 'active' # no more @@ -393,6 +394,10 @@ class Interface: else: # got no valid jep85 answer, peer does not support it contact.chatstate = False + else: + # Brand new message, incoming. + if chatstate == 'active': + contact.chatstate = chatstate if not array[1]: #empty message text return diff --git a/src/groupchat_window.py b/src/groupchat_window.py index 338891797..c1eee9fd5 100644 --- a/src/groupchat_window.py +++ b/src/groupchat_window.py @@ -1073,6 +1073,11 @@ class GroupchatWindow(chat.Chat): self.got_disconnected(room_jid) #init some variables conversation_textview.grab_focus() self.childs[room_jid].show_all() + + def set_state_image(self, jid): + # FIXME: Tab notifications? + pass + 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)) diff --git a/src/tabbed_chat_window.py b/src/tabbed_chat_window.py index 1dfdac534..3c612e017 100644 --- a/src/tabbed_chat_window.py +++ b/src/tabbed_chat_window.py @@ -46,7 +46,6 @@ class TabbedChatWindow(chat.Chat): def __init__(self, user, plugin, account): chat.Chat.__init__(self, plugin, account, 'tabbed_chat_window') self.contacts = {} - self.chatstates = {} # keep check for possible paused timeouts per jid self.possible_paused_timeout_id = {} # keep check for possible inactive timeouts per jid @@ -245,6 +244,9 @@ timestamp, contact): hb = self.notebook.get_tab_label(child).get_children()[0] status_image = hb.get_children()[0] state_images = self.plugin.roster.get_appropriate_state_images(jid) + # If messages are unread show the 'message' image + if self.nb_unread[jid]: + show = 'message' image = state_images[show] banner_status_image = self.xmls[jid].get_widget('banner_status_image') @@ -296,18 +298,11 @@ timestamp, contact): def on_tabbed_chat_window_focus_out_event(self, widget, event): '''catch focus out and minimized and send inactive chatstate; minimize action also focuses out first so it's catched here''' + chat.Chat.on_chat_window_focus_out_event(self, widget, event) window_state = widget.window.get_state() if window_state is None: return - # focus-out is also emitted by showing context menu - # so check to see if we're really not paying attention to window/tab - # NOTE: if the user changes tab, switch-tab sends inactive to the tab - # we are leaving so we just send to active tab here - if self.popup_is_shown is False: # we are outside of the window - # so no context menu, so send 'inactive' to active tab - self.send_chatstate('inactive') - def on_chat_notebook_key_press_event(self, widget, event): chat.Chat.on_chat_notebook_key_press_event(self, widget, event) @@ -367,6 +362,8 @@ timestamp, contact): message_tv_buffer = message_textview.get_buffer() message_tv_buffer.connect('insert-text', self.on_message_tv_buffer_insert_text, contact.jid) + message_tv_buffer.connect('changed', + self.on_message_tv_buffer_changed, contact) if contact.jid in gajim.encrypted_chats[self.account]: self.xmls[contact.jid].get_widget('gpg_togglebutton').set_active(True) @@ -376,13 +373,12 @@ timestamp, contact): self.tabbed_chat_popup_menu = xm.get_widget('tabbed_chat_popup_menu') chat.Chat.new_tab(self, contact.jid) - self.redraw_tab(contact.jid) + self.redraw_tab(contact) self.draw_widgets(contact) # restore previous conversation self.restore_conversation(contact.jid) - # print queued messages if gajim.awaiting_messages[self.account].has_key(contact.jid): self.read_queue(contact.jid) @@ -392,7 +388,6 @@ timestamp, contact): # chatstates self.reset_kbd_mouse_timeout_vars() - self.chatstates[contact.jid] = None # OUR current chatstate with contact self.possible_paused_timeout_id[contact.jid] =\ gobject.timeout_add(5000, self.check_for_possible_paused_chatstate, contact.jid) @@ -404,25 +399,33 @@ timestamp, contact): ''' handle incoming chatstate that jid SENT TO us ''' contact = gajim.get_first_contact_instance_from_jid(account, jid) self.draw_name_banner(contact, chatstate) + # update chatstate in tab for this chat + self.redraw_tab(contact, chatstate) def check_for_possible_paused_chatstate(self, jid): - ''' did we move mouse of that window or wrote something in message textview + ''' did we move mouse of that window or write something in message + textview in the last 5 seconds? if yes we go active for mouse, composing for kbd if no we go paused if we were previously composing ''' if jid not in self.xmls: # the tab with jid is no longer open. stop timer return False # stop looping - current_state = self.chatstates[jid] + + # FIXME: Why don't we just pass contact? + contact = gajim.get_first_contact_instance_from_jid(self.account, jid) + current_state = contact.chatstate if current_state is False: # jid doesn't support chatstates return False # stop looping - if self.mouse_over_in_last_5_secs: - self.send_chatstate('active', jid) - elif self.kbd_activity_in_last_5_secs: + message_buffer = self.xmls[jid].get_widget('message_textview').get_buffer() + if self.kbd_activity_in_last_5_secs and message_buffer.get_char_count(): + # Only composing if the keyboard activity was in text entry self.send_chatstate('composing', jid) + elif self.mouse_over_in_last_5_secs: + self.send_chatstate('active', jid) else: - if self.chatstates[jid] == 'composing': + if current_state == 'composing': self.send_chatstate('paused', jid) # pause composing # assume no activity and let the motion-notify or 'insert-text' make them True @@ -431,7 +434,8 @@ timestamp, contact): return True # loop forever def check_for_possible_inactive_chatstate(self, jid): - ''' did we move mouse over that window or wrote something in message textview + ''' did we move mouse over that window or wrote something in message + textview in the last 30 seconds? if yes we go active if no we go inactive ''' @@ -439,7 +443,9 @@ timestamp, contact): # the tab with jid is no longer open. stop timer return False # stop looping - current_state = self.chatstates[jid] + # FIXME: Why don't we just pass contact? + contact = gajim.get_first_contact_instance_from_jid(self.account, jid) + current_state = contact.chatstate if current_state is False: # jid doesn't support chatstates return False # stop looping @@ -447,7 +453,7 @@ timestamp, contact): return True # loop forever if not (self.mouse_over_in_last_30_secs or\ - self.kbd_activity_in_last_30_secs): + self.kbd_activity_in_last_30_secs): self.send_chatstate('inactive', jid) # assume no activity and let the motion-notify or 'insert-text' make them True @@ -456,12 +462,19 @@ timestamp, contact): return True # loop forever - def on_message_tv_buffer_insert_text(self, textbuffer, textiter, text, - length, jid): + def on_message_tv_buffer_insert_text(self, textbuffer, textiter, text, length, jid): self.kbd_activity_in_last_5_secs = True self.kbd_activity_in_last_30_secs = True + # XXX: only send the event every N'th character after the first N... optimization self.send_chatstate('composing', jid) + def on_message_tv_buffer_changed(self, textbuffer, contact): + self.kbd_activity_in_last_5_secs = True + self.kbd_activity_in_last_30_secs = True + # All characters have been erased, undo composing + if textbuffer.get_char_count() == 0: + self.send_chatstate('active', contact.jid) + def reset_kbd_mouse_timeout_vars(self): self.kbd_activity_in_last_5_secs = False self.mouse_over_in_last_5_secs = False @@ -563,8 +576,8 @@ timestamp, contact): # NOTE: # send 'active', set current state to 'ask' and return is done # in self.send_message() because we need REAL message (with ) - # for that procedure so return to make sure we send only once 'active' - # until we know peer supports jep85 + # for that procedure so return to make sure we send only once + # 'active' until we know peer supports jep85 return if contact.chatstate == 'ask':