diff --git a/src/chat_control.py b/src/chat_control.py index 2a5060582..e468af6c5 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -79,18 +79,22 @@ class ChatControlBase(MessageControl): # Create textviews and connect signals self.conv_textview = ConversationTextview(None) # FIXME: remove account arg self.conv_textview.show_all() - scrolledwindow = self.xml.get_widget('conversation_scrolledwindow') - scrolledwindow.add(self.conv_textview) + self.conv_scrolledwindow = self.xml.get_widget('conversation_scrolledwindow') + self.conv_scrolledwindow.add(self.conv_textview) + self.conv_scrolledwindow.get_vadjustment().connect('value-changed', + self.on_conversation_vadjustment_value_changed) self.conv_textview.connect('key_press_event', self.on_conversation_textview_key_press_event) # add MessageTextView to UI and connect signals - message_scrolledwindow = self.xml.get_widget('message_scrolledwindow') + self.msg_scrolledwindow = self.xml.get_widget('message_scrolledwindow') self.msg_textview = MessageTextView() self.msg_textview.connect('mykeypress', self.on_message_textview_mykeypress_event) - message_scrolledwindow.add(self.msg_textview) + self.msg_scrolledwindow.add(self.msg_textview) self.msg_textview.connect('key_press_event', self.on_message_textview_key_press_event) + self.msg_textview.connect('size-request', self.size_request, self.xml) + self.update_font() # the following vars are used to keep history of user's messages self.sent_history = [] @@ -365,7 +369,6 @@ class ChatControlBase(MessageControl): def set_compact_view(self, state): '''Toggle compact view. state is bool''' MessageControl.set_compact_view(self, state) - # make the last message visible, when changing to "full view" if not state: gobject.idle_add(self.conv_textview.scroll_to_end_iter) @@ -403,6 +406,101 @@ class ChatControlBase(MessageControl): gajim.interface.instances['logs'][jid] = history_window.HistoryWindow( jid, self.account) + def set_control_active(self, state): + if state: + jid = self.contact.jid + if self.conv_textview.at_the_end(): + #we are at the end + if self.nb_unread > 0: + self.nb_unread = 0 + self.get_specific_unread() + self.parent_win.redraw_tab(jid) + self.parent_win.show_title() + if gajim.interface.systray_enabled: + gajim.interface.systray.remove_jid(jid, + self.account, + self.type) + self.conv_textview.grab_focus() + + def bring_scroll_to_end(self, textview, diff_y = 0): + ''' scrolls to the end of textview if end is not visible ''' + buffer = textview.get_buffer() + end_iter = buffer.get_end_iter() + end_rect = textview.get_iter_location(end_iter) + visible_rect = textview.get_visible_rect() + # scroll only if expected end is not visible + if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y): + gobject.idle_add(self.scroll_to_end_iter, textview) + + def scroll_to_end_iter(self, textview): + buffer = textview.get_buffer() + end_iter = buffer.get_end_iter() + textview.scroll_to_iter(end_iter, 0, False, 1, 1) + return False + + def size_request(self, msg_textview , requisition, xml_top): + ''' When message_textview changes its size. If the new height + will enlarge the window, enable the scrollbar automatic policy + Also enable scrollbar automatic policy for horizontal scrollbar + if message we have in message_textview is too big''' + if msg_textview.window is None: + return + + min_height = self.conv_scrolledwindow.get_property('height-request') + conversation_height = self.conv_textview.window.get_size()[1] + message_height = msg_textview.window.get_size()[1] + message_width = msg_textview.window.get_size()[0] + # new tab is not exposed yet + if conversation_height < 2: + return + + if conversation_height < min_height: + min_height = conversation_height + + # we don't want to always resize in height the message_textview + # so we have minimum on conversation_textview's scrolled window + # but we also want to avoid window resizing so if we reach that + # minimum for conversation_textview and maximum for message_textview + # we set to automatic the scrollbar policy + diff_y = message_height - requisition.height + if diff_y != 0: + if conversation_height + diff_y < min_height: + if message_height + conversation_height - min_height > min_height: + self.msg_scrolledwindow.set_property('vscrollbar-policy', + gtk.POLICY_AUTOMATIC) + self.msg_scrolledwindow.set_property('height-request', + message_height + conversation_height - min_height) + self.bring_scroll_to_end(msg_textview) + else: + self.msg_scrolledwindow.set_property('vscrollbar-policy', + gtk.POLICY_NEVER) + self.msg_scrolledwindow.set_property('height-request', -1) + + self.conv_textview.bring_scroll_to_end(diff_y - 18) + + # enable scrollbar automatic policy for horizontal scrollbar + # if message we have in message_textview is too big + if requisition.width > message_width: + self.msg_scrolledwindow.set_property('hscrollbar-policy', + gtk.POLICY_AUTOMATIC) + else: + self.msg_scrolledwindow.set_property('hscrollbar-policy', + gtk.POLICY_NEVER) + + return True + + def on_conversation_vadjustment_value_changed(self, widget): + if not self.nb_unread: + return + jid = self.contact.jid + if self.conv_textview.at_the_end() and self.window.is_active(): + #we are at the end + self.nb_unread = self.get_specific_unread() + self.parent_win.redraw_tab(jid) + self.parent_win.show_title() + if gajim.interface.systray_enabled: + gajim.interface.systray.remove_jid(jid, self.account, + self.type_id) + ################################################################################ class ChatControl(ChatControlBase): '''A control for standard 1-1 chat''' @@ -421,6 +519,8 @@ class ChatControl(ChatControlBase): 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) def _schedule_activity_timers(self): self.possible_paused_timeout_id = gobject.timeout_add(5000, @@ -742,12 +842,6 @@ class ChatControl(ChatControlBase): '''sets compact view menuitem active state sets active and sensitivity state for toggle_gpg_menuitem and remove possible 'Switch to' menuitems''' -# FIXME: GC only -# if self.widget_name == 'groupchat_window': -# menu = self.gc_popup_menu -# childs = menu.get_children() -# # compact_view_menuitem -# childs[5].set_active(self.compact_view_current) menu = self.popup_menu childs = menu.get_children() # check if gpg capabitlies or else make gpg toggle insensitive @@ -790,6 +884,7 @@ class ChatControl(ChatControlBase): else: widget.set_no_show_all(False) widget.show_all() + def on_compact_view_menuitem_activate(self, widget): isactive = widget.get_active() self.set_compact_view(isactive) @@ -886,7 +981,7 @@ class ChatControl(ChatControlBase): gajim.interface.systray.remove_jid(self.contact.jid, self.account, self.type) - def check_delete(self): + def allow_shutdown(self): jid = self.contact.jid if time.time() - gajim.last_message_time[self.account][jid] < 2: # 2 seconds @@ -896,8 +991,8 @@ class ChatControl(ChatControlBase): _('If you close this tab and you have history disabled, '\ 'this message will be lost.')) if dialog.get_response() != gtk.RESPONSE_OK: - return True #stop the propagation of the event - return False + return False #stop the propagation of the event + return True def handle_incoming_chatstate(self): ''' handle incoming chatstate that jid SENT TO us ''' @@ -905,4 +1000,16 @@ class ChatControl(ChatControlBase): # update chatstate in tab for this chat self.parent_win.redraw_tab(self.contact, self.contact.chatstate) + def on_banner_eventbox_button_press_event(self, widget, event): + '''If right-clicked, show popup''' + if event.button == 3: # right click + self.parent_win.popup_menu(event) + def set_control_active(self, state): + ChatControlBase.set_control_active(self, state) + # send chatstate inactive to the one we're leaving + # and active to the one we visit + if state: + self.send_chatstate('active', self.contact.jid) + else: + self.send_chatstate('inactive', self.contact.jid) diff --git a/src/groupchat_control.py b/src/groupchat_control.py index 9b7ea2709..e6f6f6007 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -44,6 +44,11 @@ class GroupchatControl(ChatControlBase): # if the room jid is in the list, the room has mentioned us self.muc_attentions = [] + # connect the menuitems to their respective functions + xm = gtk.glade.XML(GTKGUI_GLADE, 'gc_popup_menu', APP) + xm.signal_autoconnect(self) + self.gc_popup_menu = xm.get_widget('gc_popup_menu') + def markup_tab_label(self, label_str, chatstate): '''Markup the label if necessary. Returns a tuple such as: (new_label_str, color) @@ -82,3 +87,13 @@ class GroupchatControl(ChatControlBase): label_str = '' + str(num_unread) + label_str + '' return (label_str, color) + def prepare_context_menu(self): + '''sets compact view menuitem active state + sets active and sensitivity state for toggle_gpg_menuitem + and remove possible 'Switch to' menuitems''' + menu = self.gc_popup_menu + childs = menu.get_children() + # compact_view_menuitem + childs[5].set_active(self.compact_view_current_state) + menu = self.remove_possible_switch_to_menuitems(menu) + return menu diff --git a/src/message_control.py b/src/message_control.py index 61c3a623d..b5960c33d 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -50,12 +50,19 @@ 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 + gajim.last_message_time[self.account][contact.jid] = 0 self.xml = gtk.glade.XML(GTKGUI_GLADE, widget_name, APP) self.widget = self.xml.get_widget(widget_name) # Autoconnect glade signals self.xml.signal_autoconnect(self) + def set_control_active(self, state): + '''Called when the control becomes active (state is True) + or inactive (state is False)''' + pass # Derived types MUST implement this method + def shutdown(self): # NOTE: Derived classes MUST implement this assert(False) @@ -87,12 +94,12 @@ class MessageControl(gtk.VBox): def set_compact_view(self, state): # NOTE: Derived classes MAY implement this self.compact_view_current = state - def check_delete(self): - '''Called when a window has been asked to delete itself. If a control is - not in a suitable shutdown state this method should return True to halt - the delete''' + def allow_shutdown(self): + '''Called to check is a control is allowed to shutdown. + If a control is not in a suitable shutdown state this method + should return False''' # NOTE: Derived classes MAY implement this - return False + return True def send_message(self, message, keyID = '', type = 'chat', chatstate = None): '''Send the given message to the active tab''' diff --git a/src/message_window.py b/src/message_window.py index 606487e17..33edca09e 100644 --- a/src/message_window.py +++ b/src/message_window.py @@ -68,6 +68,8 @@ class MessageWindow: else: self.notebook.set_show_tabs(False) self.notebook.set_show_border(gajim.config.get('tabs_border')) + self.notebook.connect('switch-page', + self._on_notebook_switch_page) # Connect event handling for this Window self.window.connect('delete-event', self._on_window_delete) @@ -97,17 +99,20 @@ class MessageWindow: widget.props.urgency_hint = False ctl = self.get_active_control() - # Undo "unread" state display, etc. - if ctl.type_id == message_control.TYPE_GC: - self.redraw_tab(ctl.contact, 'active') - else: - # NOTE: we do not send any chatstate to preserve inactive, gone, etc. - self.redraw_tab(ctl.contact) + if ctl: + ctl.set_control_active(True) + # Undo "unread" state display, etc. + if ctl.type_id == message_control.TYPE_GC: + self.redraw_tab(ctl.contact, 'active') + else: + # NOTE: we do not send any chatstate to preserve + # inactive, gone, etc. + self.redraw_tab(ctl.contact) def _on_window_delete(self, win, event): # Make sure all controls are okay with being deleted for ctl in self._controls.values(): - if not ctl.check_delete(): + if not ctl.allow_shutdown(): return True # halt the delete # FIXME: Do based on type, main, never, peracct, pertype @@ -132,7 +137,11 @@ class MessageWindow: def new_tab(self, control): assert(not self._controls.has_key(control.contact.jid)) self._controls[control.contact.jid] = control + if len(self._controls) > 1: + self.notebook.set_show_tabs(True) + self.alignment.set_property('top-padding', 2) + # Connect to keyboard events control.widget.connect('key_press_event', self.on_conversation_textview_key_press_event) # FIXME: need to get this event without access to message_textvier @@ -146,11 +155,22 @@ class MessageWindow: tab_label_box = xml.get_widget('chat_tab_ebox') xml.signal_connect('on_close_button_clicked', self.on_close_button_clicked, control.contact) + xml.signal_connect('on_tab_eventbox_button_press_event', + self.on_tab_eventbox_button_press_event, control.widget) self.notebook.append_page(control.widget, tab_label_box) + self.redraw_tab(control.contact) self.show_title() self.window.show_all() + # NOTE: we do not call set_control_active(True) since we don't know whether + # the tab is the active one. + + def on_tab_eventbox_button_press_event(self, widget, event, child): + if event.button == 3: + n = self.notebook.page_num(child) + self.notebook.set_current_page(n) + self.popup_menu(event) def on_message_textview_mykeypress_event(self, widget, event_keyval, event_keymod): @@ -233,14 +253,27 @@ class MessageWindow: self.notebook.set_current_page(ctl_page) def remove_tab(self, contact): - print "MessageWindow.remove_tab" - if len(self._controls) == 1: - # There is only one tab - # FIXME: Should we assert on contact? - self.window.destroy() - else: - pass - # TODO + ctl = self.get_control(contact.jid) + if len(self._controls) == 1 or 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])) + + del self._controls[contact.jid] + del gajim.last_message_time[self.account][jid] + + if len(self.xmls) == 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: + self.alignment.set_property('top-padding', 0) + + self.show_title() def redraw_tab(self, contact, chatstate = None): ctl = self._controls[contact.jid] @@ -284,7 +317,7 @@ class MessageWindow: for ctl in self._controls.values(): ctl.repaint_themed_widgets() - def _widgetToControl(self, widget): + def _widget_to_control(self, widget): for ctl in self._controls.values(): if ctl.widget == widget: return ctl @@ -293,7 +326,7 @@ class MessageWindow: def get_active_control(self): notebook = self.notebook active_widget = notebook.get_nth_page(notebook.get_current_page()) - return self._widgetToControl(active_widget) + return self._widget_to_control(active_widget) def get_active_contact(self): return self.get_active_control().contact def get_active_jid(self): @@ -328,7 +361,7 @@ class MessageWindow: if page_num == None: page_num = notebook.get_current_page() nth_child = notebook.get_nth_page(page_num) - return self._widgetToControl(nth_child) + return self._widget_to_control(nth_child) def controls(self): for ctl in self._controls.values(): @@ -381,7 +414,33 @@ class MessageWindow: else: # traverse for ever (eg. don't stop at first tab) self.notebook.set_current_page( self.notebook.get_n_pages() - 1) + def popup_menu(self, event): + menu = self.get_active_control().prepare_context_menu() + # common menuitems (tab switches) + if len(self._controls) > 1: # if there is more than one tab + menu.append(gtk.SeparatorMenuItem()) # seperator + for ctl in self._controls.values(): + jid = ctl.contact.jid + if jid != self.get_active_jid(): + item = gtk.ImageMenuItem(_('Switch to %s') %\ + self.names[jid]) + img = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO, + gtk.ICON_SIZE_MENU) + item.set_image(img) + item.connect('activate', + lambda obj, jid:self.set_active_tab(jid), jid) + menu.append(item) + # show the menu + menu.popup(None, None, None, event.button, event.time) + menu.show_all() + def _on_notebook_switch_page(self, notebook, page, page_num): + old_no = notebook.get_current_page() + old_ctl = self._widget_to_control(notebook.get_nth_page(old_no)) + old_ctl.set_control_active(False) + + new_ctl = self._widget_to_control(notebook.get_nth_page(page_num)) + new_ctl.set_control_active(True) ################################################################################ class MessageWindowMgr: