diff --git a/src/chat.py b/src/chat.py index 87532c370..c8b490144 100644 --- a/src/chat.py +++ b/src/chat.py @@ -60,6 +60,7 @@ class Chat: def __init__(self, account, widget_name): self.xml = gtk.glade.XML(GTKGUI_GLADE, widget_name, APP) self.window = self.xml.get_widget(widget_name) + print type(self.window) self.widget_name = widget_name diff --git a/src/chat_control.py b/src/chat_control.py index a81a939a0..5ad38e423 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -54,8 +54,8 @@ class ChatControlBase(MessageControl): def _update_banner_state_image(self): pass # Derived types MAY implement this - def __init__(self, widget_name, contact): - MessageControl.__init__(self, widget_name, contact); + def __init__(self, parent_win, widget_name, contact, acct): + MessageControl.__init__(self, parent_win, widget_name, contact, acct); # FIXME: These are hidden from 0.8 on, but IMO all these things need # to be shown optionally. Esp. the never-used Send button @@ -79,6 +79,14 @@ class ChatControlBase(MessageControl): self.msg_textview.connect('key_press_event', self.on_message_textview_key_press_event) + # the following vars are used to keep history of user's messages + self.sent_history = [] + self.sent_history_pos = -1 + self.typing_new = False + self.orig_msg = '' + + self.nb_unread = 0 + def _paint_banner(self): '''Repaint banner with theme color''' theme = gajim.config.get('roster_theme') @@ -117,13 +125,9 @@ class ChatControlBase(MessageControl): print "ChatControl.on_message_textview_key_press_event", event if event.keyval == gtk.keysyms.Page_Down: # PAGE DOWN - if event.state & gtk.gdk.CONTROL_MASK: # CTRL + PAGE DOWN - self.notebook.emit('key_press_event', event) if event.state & gtk.gdk.SHIFT_MASK: # SHIFT + PAGE DOWN self.conv_textview.emit('key_press_event', event) elif event.keyval == gtk.keysyms.Page_Up: # PAGE UP - if event.state & gtk.gdk.CONTROL_MASK: # CTRL + PAGE UP - self.notebook.emit('key_press_event', event) if event.state & gtk.gdk.SHIFT_MASK: # SHIFT + PAGE UP self.conv_textview.emit('key_press_event', event) @@ -132,8 +136,6 @@ class ChatControlBase(MessageControl): '''When a key is pressed: if enter is pressed without the shift key, message (if not empty) is sent and printed in the conversation''' - # FIXME: Need send_message - assert(False) # NOTE: handles mykeypress which is custom signal connected to this # CB in new_tab(). for this singal see message_textview.py @@ -185,17 +187,100 @@ class ChatControlBase(MessageControl): _('Your message can not be sent until you are connected.')).get_response() send_message = False - # FIXME: Define send_message (base class??) if send_message: self.send_message(message) # send the message - + + def _process_command(self, message): + if not message: + return False + if message == '/clear': + self.conv_textview.clear() # clear conversation + # FIXME: Need this function + self.clear(self.msg_textview) # clear message textview too + return True + elif message == '/compact': + # FIXME: Need this function + self.set_compact_view(not self.compact_view_current_state) + # FIXME: Need this function + self.clear(self.msg_textview) + return True + else: + return False + + def send_message(self, message, keyID = '', chatstate = None): + '''Send the given message to the active tab''' + if not message or message == '\n': + return + + if not self._process_command(message): + MessageControl.send_message(self, message, keyID, chatstate) + # Record message history + self.save_sent_message(message) + + # Clear msg input + message_buffer = self.msg_textview.get_buffer() + message_buffer.set_text('') # clear message buffer (and tv of course) + + def save_sent_message(self, message): + #save the message, so user can scroll though the list with key up/down + size = len(self.sent_history) + #we don't want size of the buffer to grow indefinately + max_size = gajim.config.get('key_up_lines') + if size >= max_size: + for i in xrange(0, size - 1): + self.sent_history[i] = self.sent_history[i + 1] + self.sent_history[max_size - 1] = message + else: + self.sent_history.append(message) + self.sent_history_pos = size + 1 + + self.typing_new = True + self.orig_msg = '' + + def print_conversation_line(self, text, kind, name, tim, + other_tags_for_name = [], other_tags_for_time = [], + other_tags_for_text = [], count_as_new = True, subject = None): + '''prints 'chat' type messages''' + jid = self.contact.jid + textview = self.conv_textview + end = False + if textview.at_the_end() or kind == 'outgoing': + end = True + textview.print_conversation_line(text, jid, kind, name, tim, + other_tags_for_name, other_tags_for_time, other_tags_for_text, subject) + + if not count_as_new: + return + if kind == 'incoming_queue': + gajim.last_message_time[self.account][jid] = time.time() + urgent = True + if (jid != self.parent_win.get_active_jid() or \ + not self.parent_win.is_active() or not end) and\ + kind in ('incoming', 'incoming_queue'): + self.nb_unread += 1 + if gajim.interface.systray_enabled and\ + gajim.config.get('trayicon_notification_on_new_messages'): + gajim.interface.systray.add_jid(jid, self.account, + self.get_message_type(jid)) + self.redraw_tab(jid) + self.show_title(urgent) class ChatControl(ChatControlBase): '''A control for standard 1-1 chat''' - def __init__(self, contact): - ChatControlBase.__init__(self, 'chat_child_vbox', contact); + def __init__(self, parent_win, contact, acct): + ChatControlBase.__init__(self, parent_win, 'chat_child_vbox', contact, acct); self.compact_view = gajim.config.get('always_compact_view_chat') + # chatstate timers and state + self._schedule_activity_timers() + self.reset_kbd_mouse_timeout_vars() + + def _schedule_activity_timers(self): + self.possible_paused_timeout_id = gobject.timeout_add(5000, + self.check_for_possible_paused_chatstate, None) + self.possible_inactive_timeout_id = gobject.timeout_add(30000, + self.check_for_possible_inactive_chatstate, None) + def draw_widgets(self): # The name banner is drawn here ChatControlBase.draw_widgets(self) @@ -293,5 +378,139 @@ class ChatControl(ChatControlBase): 'assigned one') % self.contact.name gtk.Tooltips().set_tip(self.xml.get_widget('gpg_eventbox'), tt) + def send_message(self, message, keyID = '', chatstate = None): + '''Send a message to contact''' + if not message or message == '\n' or self._process_command(message): + return + + contact = self.contact + jid = self.contact.jid + + keyID = '' + encrypted = False + if self.xml.get_widget('gpg_togglebutton').get_active(): + keyID = contact.keyID + encrypted = True + chatstates_on = gajim.config.get('chat_state_notifications') != 'disabled' + chatstate_to_send = None + if chatstates_on and contact is not None: + if contact.our_chatstate is None: + # no info about peer + # send active to discover chat state capabilities + # this is here (and not in send_chatstate) + # because we want it sent with REAL message + # (not standlone) eg. one that has body + chatstate_to_send = 'active' + contact.our_chatstate = 'ask' # pseudo state + # if peer supports jep85 and we are not 'ask', send 'active' + # NOTE: first active and 'ask' is set in gajim.py + elif contact.our_chatstate not in (False, 'ask'): + #send active chatstate on every message (as JEP says) + chatstate_to_send = 'active' + contact.our_chatstate = 'active' + + gobject.source_remove(self.possible_paused_timeout_id) + gobject.source_remove(self.possible_inactive_timeout_id) + self._schedule_activity_timers() + + ChatControlBase.send_message(self, message, keyID, chatstate_to_send) + self.print_conversation(message, self.contact.jid, encrypted = encrypted) + + def check_for_possible_paused_chatstate(self, arg): + ''' 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 ''' + contact = self.contact + jid = contact.jid + current_state = contact.our_chatstate + if current_state is False: # jid doesn't support chatstates + return False # stop looping + + message_buffer = self.msg_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 + # FIXME: Need send_chatstate + self.send_chatstate('composing') + elif self.mouse_over_in_last_5_secs and\ + jid == self.parent_win.get_active_jid(): + self.send_chatstate('active') + else: + if current_state == 'composing': + self.send_chatstate('paused') # pause composing + + # assume no activity and let the motion-notify or 'insert-text' make them True + # refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds! + self.reset_kbd_mouse_timeout_vars() + return True # loop forever + + def check_for_possible_inactive_chatstate(self, arg): + ''' 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 ''' + contact = self.contact + jid = contact.jid + + current_state = contact.our_chatstate + if current_state is False: # jid doesn't support chatstates + return False # stop looping + + if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs: + return True # loop forever + + if not self.mouse_over_in_last_30_secs or 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 + # refresh 30 seconds too or else it's 30 - 5 = 25 seconds! + self.reset_kbd_mouse_timeout_vars() + return True # loop forever + + def reset_kbd_mouse_timeout_vars(self): + self.kbd_activity_in_last_5_secs = False + self.mouse_over_in_last_5_secs = False + self.mouse_over_in_last_30_secs = False + self.kbd_activity_in_last_30_secs = False + + + def print_conversation(self, text, frm = '', tim = None, + encrypted = False, subject = None): + '''Print a line in the conversation: + if contact is set to status: it's a status message + if contact is set to another value: it's an outgoing message + if contact is set to print_queue: it is incomming from queue + if contact is not set: it's an incomming message''' + contact = self.contact + jid = contact.jid + + if frm == 'status': + kind = 'status' + name = '' + else: + ec = gajim.encrypted_chats[self.account] + if encrypted and jid not in ec: + msg = _('Encryption enabled') + ChatControlBase.print_conversation_line(self, msg, + 'status', '', tim) + ec.append(jid) + if not encrypted and jid in ec: + msg = _('Encryption disabled') + ChatControlBase.print_conversation_line(self, msg, + 'status', '', tim) + ec.remove(jid) + self.xml.get_widget('gpg_togglebutton').set_active(encrypted) + if not frm: + kind = 'incoming' + name = contact.name + elif frm == 'print_queue': # incoming message, but do not update time + kind = 'incoming_queue' + name = contact.name + else: + kind = 'outgoing' + name = gajim.nicks[self.account] + ChatControlBase.print_conversation_line(self, text, kind, name, tim, + subject = subject) + diff --git a/src/message_window.py b/src/message_window.py index 9df0467b8..6be780a1f 100644 --- a/src/message_window.py +++ b/src/message_window.py @@ -186,6 +186,24 @@ class MessageWindow: for ctl in self._controls.values(): ctl.repaint_themed_widgets() + def _widgetToControl(self, widget): + for ctl in self._controls.values(): + if ctl.widget == widget: + return ctl + return None + + def get_active_contact(self): + notebook = self.notebook + active_widget = notebook.get_nth_page(notebook.get_current_page()) + return self._widgetToControl(active_widget).contact + + def get_active_jid(self): + return self.get_active_contact().jid + + def is_active(self): + return self.window.is_active() + + class MessageWindowMgr: '''A manager and factory for MessageWindow objects''' @@ -257,11 +275,13 @@ class MessageWindowMgr: class MessageControl(gtk.VBox): '''An abstract base widget that can embed in the gtk.Notebook of a MessageWindow''' - def __init__(self, widget_name, contact): + def __init__(self, parent_win, widget_name, contact, account): gtk.VBox.__init__(self) + self.parent_win = parent_win self.widget_name = widget_name self.contact = contact + self.account = account self.compact_view = False self.xml = gtk.glade.XML(GTKGUI_GLADE, widget_name, APP) @@ -273,3 +293,17 @@ class MessageControl(gtk.VBox): pass # NOTE: Derived classes SHOULD implement this def update_state(self): pass # NOTE: Derived classes SHOULD implement this + + def send_message(self, message, keyID = '', chatstate = None): + '''Send the given message to the active tab''' + if not message or message == '\n': + return + + # refresh timers + self.reset_kbd_mouse_timeout_vars() + + jid = self.contact.jid + # Send and update history + gajim.connections[self.account].send_message(jid, message, keyID, chatstate) + + diff --git a/src/roster_window.py b/src/roster_window.py index 231b16a68..9a41ec86a 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1653,7 +1653,7 @@ _('If "%s" accepts this request you will know his or her status.') %jid) def new_chat(self, contact, account): # Get target window mw = self.msg_win_mgr.get_window(contact, account, None) # FIXME: type arg - chat_control = ChatControl(contact) + chat_control = ChatControl(mw, contact, account) mw.new_tab(chat_control) # REMOVE ME @@ -2225,7 +2225,7 @@ _('If "%s" accepts this request you will know his or her status.') %jid) def repaint_themed_widgets(self): '''Notify windows that contain themed widgets to repaint them''' - for win in self.msg_win_mgr.windows(): + for win in self.msg_win_mgr.windows.values(): win.repaint_themed_widgets() for account in gajim.connections: for addr in gajim.interface.instances[account]['disco']: