diff --git a/gajim/data/gui/message_window.ui b/gajim/data/gui/message_window.ui deleted file mode 100644 index a7e8bb1f56c9f5c65389e0732abf8d06be0f12cf..0000000000000000000000000000000000000000 --- a/gajim/data/gui/message_window.ui +++ /dev/null @@ -1,89 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.36.0 --> -<interface> - <requires lib="gtk+" version="3.22"/> - <object class="GtkEventBox" id="chat_tab_ebox"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="visible_window">False</property> - <child> - <object class="GtkBox" id="tab_hbox"> - <property name="width_request">170</property> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="spacing">4</property> - <child> - <object class="GtkImage" id="tab_image"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="tab_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - <property name="margin_start">3</property> - <property name="use_markup">True</property> - <property name="ellipsize">end</property> - <property name="single_line_mode">True</property> - <property name="xalign">0</property> - <style> - <class name="notebook-tab-label"/> - </style> - </object> - <packing> - <property name="expand">True</property> - <property name="fill">True</property> - <property name="position">1</property> - </packing> - </child> - <child> - <object class="GtkButton" id="tab_close_button"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="halign">end</property> - <property name="relief">none</property> - <child> - <object class="GtkImage"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="icon_name">window-close</property> - <property name="icon_size">1</property> - </object> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">2</property> - </packing> - </child> - </object> - </child> - </object> - <object class="GtkApplicationWindow" id="message_window"> - <property name="name">MessageWindow</property> - <property name="can_focus">False</property> - <property name="default_width">480</property> - <property name="default_height">440</property> - <property name="show_menubar">False</property> - <child> - <object class="GtkNotebook" id="notebook"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="scrollable">True</property> - </object> - </child> - <child type="titlebar"> - <placeholder/> - </child> - </object> -</interface> diff --git a/gajim/message_window.py b/gajim/message_window.py deleted file mode 100644 index fa5fca999018c2f32c516c06e71d5ca919dfbab1..0000000000000000000000000000000000000000 --- a/gajim/message_window.py +++ /dev/null @@ -1,1276 +0,0 @@ -# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org> -# Copyright (C) 2005-2008 Travis Shirk <travis AT pobox.com> -# Nikos Kouremenos <kourem AT gmail.com> -# Copyright (C) 2006 Geobert Quach <geobert AT gmail.com> -# Dimitur Kirov <dkirov AT gmail.com> -# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org> -# Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com> -# Stephan Erb <steve-e AT h3c.de> -# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com> -# Jonathan Schleifer <js-gajim AT webkeks.org> -# -# This file is part of Gajim. -# -# Gajim is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published -# by the Free Software Foundation; version 3 only. -# -# Gajim is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Gajim. If not, see <http://www.gnu.org/licenses/>. - -import logging - -from gi.repository import Gtk -from gi.repository import Gdk -from gi.repository import GObject -from gi.repository import GLib -from gi.repository import Gio - -from gajim.common import app -from gajim.common import ged -from gajim.common.i18n import Q_ -from gajim.common.i18n import _ -from gajim.common.nec import EventHelper - -from gajim import gtkgui_helpers -from gajim.chat_control_base import ChatControlBase -from gajim.chat_control import ChatControl - -from gajim.gui.dialogs import DialogButton -from gajim.gui.dialogs import ConfirmationCheckDialog -from gajim.gui.util import get_icon_name -from gajim.gui.util import resize_window -from gajim.gui.util import move_window -from gajim.gui.util import get_app_icon_list -from gajim.gui.util import get_builder -from gajim.gui.util import set_urgency_hint -from gajim.gui.util import get_app_window -from gajim.gui.const import ControlType - - -log = logging.getLogger('gajim.message_window') - - -WINDOW_TYPES = ['never', - 'always', - 'always_with_roster', - 'peracct', - 'pertype'] - - -class MessageWindow(EventHelper): - """ - Class for windows which contain message like things; chats, groupchats, etc - """ - - # DND_TARGETS is the targets needed by drag_source_set and drag_dest_set - DND_TARGETS = [('GAJIM_TAB', 0, 81)] - hid = 0 # drag_data_received handler id - ( - CLOSE_TAB_MIDDLE_CLICK, - CLOSE_ESC, - CLOSE_CLOSE_BUTTON, - CLOSE_COMMAND, - CLOSE_CTRL_KEY - ) = range(5) - - def __init__(self, acct, type_, parent_window=None, parent_paned=None): - EventHelper.__init__(self) - # A dictionary of dictionaries - # where _contacts[account][jid] == A MessageControl - self._controls = {} - - # If None, the window is not tied to any specific account - self.account = acct - # If None, the window is not tied to any specific type - self.type_ = type_ - # dict { handler id: widget}. Keeps callbacks, which - # lead to circular references - self.handlers = {} - # Don't show warning dialogs when we want to delete the window - self.dont_warn_on_delete = False - - self.widget_name = 'message_window' - self.xml = get_builder('%s.ui' % self.widget_name) - self.window = self.xml.get_object(self.widget_name) - self.window.set_application(app.app) - self.notebook = self.xml.get_object('notebook') - self.parent_paned = None - - if parent_window: - orig_window = self.window - self.window = parent_window - self.parent_paned = parent_paned - old_parent = self.notebook.get_parent() - old_parent.remove(self.notebook) - if app.settings.get('roster_on_the_right'): - child1 = self.parent_paned.get_child1() - self.parent_paned.remove(child1) - self.parent_paned.pack1(self.notebook, resize=False) - self.parent_paned.pack2(child1) - else: - self.parent_paned.pack2(self.notebook) - self.window.lookup_action('show-roster').set_enabled(True) - orig_window.destroy() - del orig_window - - # NOTE: we use 'connect_after' here because in - # MessageWindowMgr._new_window we register handler that saves window - # state when closing it, and it should be called before - # MessageWindow._on_window_delete, which manually destroys window - # through win.destroy() - this means no additional handlers for - # 'delete-event' are called. - id_ = self.window.connect_after('delete-event', self._on_window_delete) - self.handlers[id_] = self.window - id_ = self.window.connect('destroy', self._on_window_destroy) - self.handlers[id_] = self.window - id_ = self.window.connect('focus-in-event', self._on_window_focus) - self.handlers[id_] = self.window - - self._add_actions() - - # gtk+ doesn't make use of the motion notify on gtkwindow by default - # so this line adds that - self.window.add_events(Gdk.EventMask.POINTER_MOTION_MASK) - - id_ = self.notebook.connect('switch-page', - self._on_notebook_switch_page) - self.handlers[id_] = self.notebook - - # Tab customizations - pref_pos = app.settings.get('tabs_position') - if pref_pos == 'bottom': - nb_pos = Gtk.PositionType.BOTTOM - elif pref_pos == 'left': - nb_pos = Gtk.PositionType.LEFT - elif pref_pos == 'right': - nb_pos = Gtk.PositionType.RIGHT - else: - nb_pos = Gtk.PositionType.TOP - self.notebook.set_tab_pos(nb_pos) - window_mode = app.interface.msg_win_mgr.mode - if app.settings.get('tabs_always_visible') or \ - window_mode == MessageWindowMgr.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER: - self.notebook.set_show_tabs(True) - else: - self.notebook.set_show_tabs(False) - self.notebook.set_show_border(app.settings.get('tabs_border')) - self.show_icon() - - # self.register_events([ - # ('muc-disco-update', ged.GUI1, self._on_muc_disco_update), - # ]) - - def _add_actions(self): - actions = [ - 'change-nickname', - 'change-subject', - 'escape', - 'browse-history', - 'send-file', - 'show-contact-info', - 'show-emoji-chooser', - 'clear-chat', - 'delete-line', - 'close-tab', - 'move-tab-up', - 'move-tab-down', - 'switch-next-tab', - 'switch-prev-tab', - 'switch-next-unread-tab-right' - 'switch-next-unread-tab-left', - 'switch-tab-1', - 'switch-tab-2', - 'switch-tab-3', - 'switch-tab-4', - 'switch-tab-5', - 'switch-tab-6', - 'switch-tab-7', - 'switch-tab-8', - 'switch-tab-9', - ] - - disabled_for_emacs = ( - 'browse-history', - 'send-file', - 'close-tab' - ) - - key_theme = Gtk.Settings.get_default().get_property( - 'gtk-key-theme-name') - - for action in actions: - if key_theme == 'Emacs' and action in disabled_for_emacs: - continue - act = Gio.SimpleAction.new(action, None) - act.connect('activate', self._on_action) - self.window.add_action(act) - - def _on_action(self, action, _param): - control = self.get_active_control() - if not control: - # No more control in this window - return - - log.info('Activate action: %s, active control: %s', - action.get_name(), control.contact.jid) - - action = action.get_name() - - # Pass the event to the control - res = control.delegate_action(action) - if res != Gdk.EVENT_PROPAGATE: - return res - - if action == 'escape' and app.settings.get('escape_key_closes'): - self.remove_tab(control, self.CLOSE_ESC) - return - - if action == 'close-tab': - self.remove_tab(control, self.CLOSE_CTRL_KEY) - return - - if action == 'move-tab-up': - old_position = self.notebook.get_current_page() - self.notebook.reorder_child(control.widget, - old_position - 1) - return - - if action == 'move-tab-down': - old_position = self.notebook.get_current_page() - total_pages = self.notebook.get_n_pages() - if old_position == total_pages - 1: - self.notebook.reorder_child(control.widget, 0) - else: - self.notebook.reorder_child(control.widget, - old_position + 1) - return - - if action == 'switch-next-tab': - new = self.notebook.get_current_page() + 1 - if new >= self.notebook.get_n_pages(): - new = 0 - self.notebook.set_current_page(new) - return - - if action == 'switch-prev-tab': - new = self.notebook.get_current_page() - 1 - if new < 0: - new = self.notebook.get_n_pages() - 1 - self.notebook.set_current_page(new) - return - - if action == 'switch-next-unread-tab-right': - self.move_to_next_unread_tab(True) - return - - if action == 'switch-next-unread-tab-left': - self.move_to_next_unread_tab(False) - return - - if action.startswith('switch-tab-'): - number = int(action[-1]) - self.notebook.set_current_page(number - 1) - return - - def change_jid(self, account, old_jid, new_jid): - """ - Called when the full jid of the control is changed - """ - if account not in self._controls: - return - if old_jid not in self._controls[account]: - return - if old_jid == new_jid: - return - self._controls[account][new_jid] = self._controls[account][old_jid] - del self._controls[account][old_jid] - - def get_num_controls(self): - return sum(len(d) for d in self._controls.values()) - - def resize(self, width, height): - resize_window(self.window, width, height) - - def _on_muc_disco_update(self, event): - # If there is only one control in a window, - # the name is shown in the window title - if self.get_num_controls() != 1: - return - ctrl = self.get_active_control() - if ctrl.contact.jid != event.room_jid: - return - self.show_title() - - def _on_window_focus(self, widget, event): - # on destroy() the window that was last focused gets the focus - # again. if destroy() is called from the StartChat Dialog, this - # Window is not yet focused, because present() seems to be asynchronous - # at least on KDE, and takes time. - start_chat = get_app_window('StartChatDialog') - if start_chat is not None and start_chat.ready_to_destroy: - start_chat.destroy() - - # 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 - set_urgency_hint(self.window, False) - - ctrl = self.get_active_control() - if ctrl: - ctrl.set_control_active(True) - # Undo "unread" state display, etc. - if ctrl.is_groupchat: - self.redraw_tab(ctrl, 'active') - else: - # NOTE: we do not send any chatstate to preserve - # inactive, gone, etc. - self.redraw_tab(ctrl) - - def _on_window_delete(self, win, event): - if self.dont_warn_on_delete: - # Destroy the window - return False - - # Number of controls that will be closed and for which we'll loose data: - # chat, pm, gc that won't go in roster - number_of_closed_control = 0 - for ctrl in self.controls(): - if not ctrl.safe_shutdown(): - number_of_closed_control += 1 - - if number_of_closed_control > 1: - def _on_yes1(checked): - if checked: - app.settings.set('confirm_close_multiple_tabs', False) - self.dont_warn_on_delete = True - for ctrl in self.controls(): - if ctrl.minimizable(): - ctrl.minimize() - win.destroy() - - if not app.settings.get('confirm_close_multiple_tabs'): - for ctrl in self.controls(): - if ctrl.minimizable(): - ctrl.minimize() - # destroy window - return False - - ConfirmationCheckDialog( - _('Close Tabs'), - _('You are about to close several tabs'), - _('Do you really want to close all of them?'), - _('_Do not ask me again'), - [DialogButton.make('Cancel'), - DialogButton.make('Accept', - text=_('_Close'), - callback=_on_yes1)], - transient_for=self.window).show() - return True - - def on_yes(ctrl): - if self.on_delete_ok == 1: - self.dont_warn_on_delete = True - win.destroy() - self.on_delete_ok -= 1 - - def on_no(ctrl): - return - - def on_minimize(ctrl): - ctrl.minimize() - if self.on_delete_ok == 1: - self.dont_warn_on_delete = True - win.destroy() - self.on_delete_ok -= 1 - - # Make sure all controls are okay with being deleted - self.on_delete_ok = self.get_nb_controls() - for ctrl in self.controls(): - ctrl.allow_shutdown(self.CLOSE_CLOSE_BUTTON, on_yes, on_no, - on_minimize) - return True # halt the delete for the moment - - def _on_window_destroy(self, win): - for ctrl in self.remove_all_controls(): - ctrl.shutdown() - self._controls.clear() - # Clean up handlers connected to the parent window, this is important since - # self.window may be the RosterWindow - for i in list(self.handlers.keys()): - if self.handlers[i].handler_is_connected(i): - self.handlers[i].disconnect(i) - del self.handlers[i] - del self.handlers - - self.unregister_events() - - def new_tab(self, control): - fjid = control.get_full_jid() - - if control.account not in self._controls: - self._controls[control.account] = {} - - self._controls[control.account][fjid] = control - - if self.get_num_controls() == 2: - first_widget = self.notebook.get_nth_page(0) - ctrl = self._widget_to_control(first_widget) - self.notebook.set_show_tabs(True) - ctrl.scroll_to_end() - - # Add notebook page and connect up to the tab's close button - xml = get_builder('message_window.ui', ['chat_tab_ebox']) - tab_label_box = xml.get_object('chat_tab_ebox') - widget = xml.get_object('tab_close_button') - # this reduces the size of the button -# style = Gtk.RcStyle() -# style.xthickness = 0 -# style.ythickness = 0 -# widget.modify_style(style) - - id_ = widget.connect('clicked', self._on_close_button_clicked, control) - control.handlers[id_] = widget - - id_ = tab_label_box.connect('button-press-event', - self.on_tab_eventbox_button_press_event, control.widget) - control.handlers[id_] = tab_label_box - position = self.notebook.get_current_page() + 1 - self.notebook.insert_page(control.widget, tab_label_box, position) - - self.notebook.set_tab_reorderable(control.widget, True) - - self.redraw_tab(control) - if self.parent_paned: - self.notebook.show_all() - else: - 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. - self.show_title() - if self.get_num_controls() == 1: - GLib.timeout_add(500, control.focus) - - def on_tab_eventbox_button_press_event(self, widget, event, child): - if event.button == 3: # right click - n = self.notebook.page_num(child) - self.notebook.set_current_page(n) - self.popup_menu(event) - elif event.button == 2: # middle click - ctrl = self._widget_to_control(child) - self.remove_tab(ctrl, self.CLOSE_TAB_MIDDLE_CLICK) - else: - ctrl = self._widget_to_control(child) - GLib.idle_add(ctrl.focus) - - def _on_close_button_clicked(self, button, control): - """ - When close button is pressed: close a tab - """ - self.remove_tab(control, self.CLOSE_CLOSE_BUTTON) - - def show_icon(self): - window_mode = app.interface.msg_win_mgr.mode - if window_mode in (MessageWindowMgr.ONE_MSG_WINDOW_PERTYPE, - MessageWindowMgr.ONE_MSG_WINDOW_NEVER): - if self.type_ == 'gc': - icon = get_icon_name('muc-active') - self.window.set_icon_name(icon) - - def show_title(self, urgent=True, control=None): - """ - Redraw the window's title - """ - if not control: - control = self.get_active_control() - if not control: - # No more control in this window - return - unread = 0 - for ctrl in self.controls(): - if (ctrl.is_groupchat and - not ctrl.contact.can_notify() and - not ctrl.attention_flag): - # count only pm messages - unread += ctrl.get_nb_unread_pm() - continue - unread += ctrl.get_nb_unread() - - unread_str = '' - if unread > 1: - unread_str = '[' + str(unread) + '] ' - elif unread == 1: - unread_str = '* ' - else: - urgent = False - - if control.is_groupchat: - name = control.contact.get_shown_name() - urgent = (control.attention_flag or - control.contact.can_notify()) - else: - name = control.contact.get_shown_name() - if control.resource: - name += '/' + control.resource - - window_mode = app.interface.msg_win_mgr.mode - if window_mode == MessageWindowMgr.ONE_MSG_WINDOW_PERTYPE: - # Show the plural form since number of tabs > 1 - if self.type_ == 'chat': - label = Q_('?Noun:Chats') - if self.get_num_controls() == 1: - label = name - elif self.type_ == 'gc': - label = _('Group Chats') - if self.get_num_controls() == 1: - label = name - else: - label = _('Private Chats') - elif window_mode == MessageWindowMgr.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER: - label = None - elif self.get_num_controls() == 1: - label = name - else: - label = _('Messages') - - title = 'Gajim' - if label: - title = '%s - %s' % (label, title) - - if window_mode == MessageWindowMgr.ONE_MSG_WINDOW_PERACCT: - title = title + ": " + control.account - - self.window.set_title(unread_str + title) - set_urgency_hint(self.window, urgent and unread > 0) - - def set_active_tab(self, ctrl): - ctrl_page = self.notebook.page_num(ctrl.widget) - self.notebook.set_current_page(ctrl_page) - self.window.present() - GLib.idle_add(ctrl.focus) - - def remove_tab(self, ctrl, method, reason=None, force=False): - """ - Reason is only for gc (offline status message) if force is True, do not - ask any confirmation - """ - def close(ctrl): - # Update external state - app.events.remove_events( - ctrl.account, ctrl.get_full_jid, - types=['printed_msg', 'chat', 'gc_msg']) - - fjid = ctrl.get_full_jid() - jid = app.get_jid_without_resource(fjid) - - fctrl = self.get_control(fjid, ctrl.account) - bctrl = self.get_control(jid, ctrl.account) - - # keep last_message_time around unless this was our last control with - # that jid - if not fctrl and not bctrl and \ - fjid in app.last_message_time[ctrl.account]: - del app.last_message_time[ctrl.account][fjid] - - self.notebook.remove_page(self.notebook.page_num(ctrl.widget)) - - del self._controls[ctrl.account][fjid] - - if not self._controls[ctrl.account]: - del self._controls[ctrl.account] - - if reason is not None: # We are leaving gc with a status message - ctrl.shutdown(reason) - else: # We are leaving gc without status message or it's a chat - ctrl.shutdown() - - self.check_tabs() - self.show_title() - - def on_yes(ctrl): - close(ctrl) - - def on_no(ctrl): - return - - def on_minimize(ctrl): - if method != self.CLOSE_COMMAND: - ctrl.minimize() - self.check_tabs() - return - close(ctrl) - - # Shutdown the MessageControl - if force: - close(ctrl) - else: - ctrl.allow_shutdown(method, on_yes, on_no, on_minimize) - - def check_tabs(self): - if self.parent_paned: - # Do nothing in single window mode - pass - elif self.get_num_controls() == 0: - # These are not called when the window is destroyed like this, fake it - app.interface.msg_win_mgr._on_window_delete(self.window, None) - app.interface.msg_win_mgr._on_window_destroy(self.window) - # dnd clean up - self.notebook.drag_dest_unset() - if self.parent_paned: - # Don't close parent window, just remove the child - child = self.parent_paned.get_child2() - self.parent_paned.remove(child) - self.window.lookup_action('show-roster').set_enabled(False) - else: - self.window.destroy() - return # don't show_title, we are dead - elif self.get_num_controls() == 1: # we are going from two tabs to one - window_mode = app.interface.msg_win_mgr.mode - show_tabs_if_one_tab = app.settings.get('tabs_always_visible') or \ - window_mode == MessageWindowMgr.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER - self.notebook.set_show_tabs(show_tabs_if_one_tab) - - def redraw_tab(self, ctrl, chatstate=None): - tab = self.notebook.get_tab_label(ctrl.widget) - if not tab: - return - - hbox = tab.get_children()[0] - status_img = hbox.get_children()[0] - nick_label = hbox.get_children()[1] - - # Optionally hide close button - close_button = hbox.get_children()[2] - if app.settings.get('tabs_close_button'): - close_button.show() - else: - close_button.hide() - - # Update nick - if isinstance(ctrl, ChatControl): - tab_label_str = ctrl.get_tab_label() - # Set Label Color - if app.settings.get('show_chatstate_in_tabs'): - gtkgui_helpers.add_css_class( - nick_label, chatstate, 'gajim-state-') - else: - tab_label_str, color = ctrl.get_tab_label(chatstate) - # Set Label Color - if color == 'active': - gtkgui_helpers.add_css_class(nick_label, None, 'gajim-state-') - elif color is not None: - gtkgui_helpers.add_css_class(nick_label, color, 'gajim-state-') - - nick_label.set_markup(tab_label_str) - - tab_img = ctrl.get_tab_image() - if tab_img: - if isinstance(tab_img, Gtk.Image): - if tab_img.get_storage_type() == Gtk.ImageType.ANIMATION: - status_img.set_from_animation(tab_img.get_animation()) - else: - status_img.set_from_pixbuf(tab_img.get_pixbuf()) - elif isinstance(tab_img, str): - status_img.set_from_icon_name(tab_img, Gtk.IconSize.MENU) - else: - status_img.set_from_surface(tab_img) - - self.show_icon() - - def repaint_themed_widgets(self): - """ - Repaint controls in the window with theme color - """ - # iterate through controls and repaint - for ctrl in self.controls(): - ctrl.repaint_themed_widgets() - - def _widget_to_control(self, widget): - for ctrl in self.controls(): - if ctrl.widget == widget: - return ctrl - return None - - def get_active_control(self): - notebook = self.notebook - active_widget = notebook.get_nth_page(notebook.get_current_page()) - return self._widget_to_control(active_widget) - - def get_active_contact(self): - ctrl = self.get_active_control() - if ctrl: - return ctrl.contact - return None - - def get_active_jid(self): - contact = self.get_active_contact() - if contact: - return contact.jid - return None - - def is_active(self): - return self.window.is_active() - - def get_origin(self): - return self.window.get_window().get_origin() - - def get_control(self, jid, acct): - """ - Return the MessageControl for jid - """ - try: - return self._controls[acct][jid] - except Exception: - return None - - def has_control(self, jid, acct): - return acct in self._controls and jid in self._controls[acct] - - def change_key(self, old_jid, new_jid, acct): - """ - Change the JID key of a control - """ - try: - # Check if controls exists - ctrl = self._controls[acct][old_jid] - except KeyError: - return - - if new_jid in self._controls[acct]: - self.remove_tab(self._controls[acct][new_jid], - self.CLOSE_CLOSE_BUTTON, force=True) - - self._controls[acct][new_jid] = ctrl - del self._controls[acct][old_jid] - - if old_jid in app.last_message_time[acct]: - app.last_message_time[acct][new_jid] = \ - app.last_message_time[acct][old_jid] - del app.last_message_time[acct][old_jid] - - def controls(self): - for jid_dict in list(self._controls.values()): - for ctrl in list(jid_dict.values()): - yield ctrl - - def remove_all_controls(self): - for _account, control_dict in self._controls.items(): - for contact in list(control_dict.keys()): - yield control_dict[contact] - del control_dict[contact] - - def get_nb_controls(self): - return sum(len(jid_dict) for jid_dict in self._controls.values()) - - def move_to_next_unread_tab(self, forward): - ind = self.notebook.get_current_page() - current = ind - found = False - first_composing_ind = -1 # id of first composing ctrl to switch to - # if no others controls have awaiting events - # loop until finding an unread tab or having done a complete cycle - while True: - if forward is True: # look for the first unread tab on the right - ind = ind + 1 - if ind >= self.notebook.get_n_pages(): - ind = 0 - else: # look for the first unread tab on the right - ind = ind - 1 - if ind < 0: - ind = self.notebook.get_n_pages() - 1 - - nth_child = self.notebook.get_nth_page(ind) - ctrl = self._widget_to_control(nth_child) - if ctrl.get_nb_unread() > 0: - found = True - break # found - if app.settings.get('ctrl_tab_go_to_next_composing'): - # Search for a composing contact - contact = ctrl.contact - if first_composing_ind == -1 and contact.chatstate == 'composing': - # If no composing contact found yet, check if this one is composing - first_composing_ind = ind - if ind == current: - break # a complete cycle without finding an unread tab - if found: - self.notebook.set_current_page(ind) - elif first_composing_ind != -1: - self.notebook.set_current_page(first_composing_ind) - else: # not found and nobody composing - if forward: # CTRL + TAB - if current < (self.notebook.get_n_pages() - 1): - self.notebook.next_page() - else: # traverse for ever (eg. don't stop at last tab) - self.notebook.set_current_page(0) - else: # CTRL + SHIFT + TAB - if current > 0: - self.notebook.prev_page() - 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() - if menu is None: - return - # show the menu - menu.attach_to_widget(app.interface.roster.window, None) - menu.show_all() - menu.popup(None, None, None, None, event.button, event.time) - - def _on_notebook_switch_page(self, notebook, page, page_num): - old_no = notebook.get_current_page() - if old_no >= 0: - old_ctrl = self._widget_to_control(notebook.get_nth_page(old_no)) - old_ctrl.set_control_active(False) - - new_ctrl = self._widget_to_control(notebook.get_nth_page(page_num)) - new_ctrl.set_control_active(True) - self.show_title(control=new_ctrl) - - control = self.get_active_control() - if isinstance(control, ChatControlBase): - control.focus() - - def get_tab_at_xy(self, x, y): - """ - Return the tab under xy and if its nearer from left or right side of the - tab - """ - page_num = -1 - to_right = False - horiz = self.notebook.get_tab_pos() == Gtk.PositionType.TOP or \ - self.notebook.get_tab_pos() == Gtk.PositionType.BOTTOM - for i in range(self.notebook.get_n_pages()): - page = self.notebook.get_nth_page(i) - tab = self.notebook.get_tab_label(page) - tab_alloc = tab.get_allocation() - if horiz: - if tab_alloc.x <= x <= (tab_alloc.x + tab_alloc.width): - page_num = i - if x >= tab_alloc.x + (tab_alloc.width / 2.0): - to_right = True - break - else: - if tab_alloc.y <= y <= (tab_alloc.y + tab_alloc.height): - page_num = i - - if y > tab_alloc.y + (tab_alloc.height / 2.0): - to_right = True - break - return (page_num, to_right) - - def find_page_num_according_to_tab_label(self, tab_label): - """ - Find the page num of the tab label - """ - page_num = -1 - for i in range(self.notebook.get_n_pages()): - page = self.notebook.get_nth_page(i) - tab = self.notebook.get_tab_label(page) - if tab == tab_label: - page_num = i - break - return page_num - -################################################################################ -class MessageWindowMgr(GObject.GObject): - """ - A manager and factory for MessageWindow objects - """ - - __gsignals__ = { - 'window-delete': (GObject.SignalFlags.RUN_LAST, None, (object,)), - } - - # These constants map to WINDOW_TYPES indices - ( - ONE_MSG_WINDOW_NEVER, - ONE_MSG_WINDOW_ALWAYS, - ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER, - ONE_MSG_WINDOW_PERACCT, - ONE_MSG_WINDOW_PERTYPE, - ) = range(5) - - # A key constant for the main window in ONE_MSG_WINDOW_ALWAYS mode - MAIN_WIN = 'main' - # A key constant for the main window in ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER mode - ROSTER_MAIN_WIN = 'roster' - - def __init__(self, parent_window, parent_paned): - """ - A dictionary of windows; the key depends on the config: - ONE_MSG_WINDOW_NEVER: The key is the contact JID - ONE_MSG_WINDOW_ALWAYS: The key is MessageWindowMgr.MAIN_WIN - ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER: The key is MessageWindowMgr.MAIN_WIN - ONE_MSG_WINDOW_PERACCT: The key is the account name - ONE_MSG_WINDOW_PERTYPE: The key is a message type constant - """ - GObject.GObject.__init__(self) - self._windows = {} - - # Map the mode to a int constant for frequent compares - mode = app.settings.get('one_message_window') - self.mode = WINDOW_TYPES.index(mode) - - self.parent_win = parent_window - self.parent_paned = parent_paned - - Gtk.Window.set_default_icon_list(get_app_icon_list(parent_window)) - - def _new_window(self, acct, type_): - parent_win = None - parent_paned = None - if self.mode == self.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER: - parent_win = self.parent_win - parent_paned = self.parent_paned - win = MessageWindow(acct, type_, parent_win, parent_paned) - # we track the lifetime of this window - win.window.connect('delete-event', self._on_window_delete) - win.window.connect('destroy', self._on_window_destroy) - return win - - def _gtk_win_to_msg_win(self, gtk_win): - for w in self.windows(): - if w.window == gtk_win: - return w - return None - - def get_window(self, jid, acct): - for win in self.windows(): - if win.has_control(jid, acct): - return win - - return None - - def has_window(self, jid, acct): - return self.get_window(jid, acct) is not None - - def one_window_opened(self, contact=None, acct=None, type_=None): - try: - return \ - self._windows[self._mode_to_key(contact, acct, type_)] is not None - except KeyError: - return False - - def _resize_window(self, win, acct, type_): - """ - Resizes window according to config settings - """ - hpaned = app.settings.get('roster_hpaned_position') - if self.mode in (self.ONE_MSG_WINDOW_ALWAYS, - self.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER): - size = (app.settings.get('msgwin-width'), - app.settings.get('msgwin-height')) - if self.mode == self.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER: - # Add the hpaned position to our message window's size - size = (hpaned + size[0], size[1]) - elif self.mode == self.ONE_MSG_WINDOW_PERACCT: - size = (app.settings.get_account_setting(acct, 'msgwin-width'), - app.settings.get_account_setting(acct, 'msgwin-height')) - elif self.mode in (self.ONE_MSG_WINDOW_NEVER, self.ONE_MSG_WINDOW_PERTYPE): - opt_width = type_ + '-msgwin-width' - opt_height = type_ + '-msgwin-height' - size = (app.settings.get(opt_width), app.settings.get(opt_height)) - else: - return - win.resize(size[0], size[1]) - if win.parent_paned: - win.parent_paned.set_position(hpaned) - - def _position_window(self, win, acct, type_): - """ - Moves window according to config settings - """ - if (self.mode in [self.ONE_MSG_WINDOW_NEVER, - self.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER]): - return - - if self.mode == self.ONE_MSG_WINDOW_ALWAYS: - pos = (app.settings.get('msgwin-x-position'), - app.settings.get('msgwin-y-position')) - elif self.mode == self.ONE_MSG_WINDOW_PERACCT: - pos = (app.settings.get_account_setting(acct, 'msgwin-x-position'), - app.settings.get_account_setting(acct, 'msgwin-y-position')) - elif self.mode == self.ONE_MSG_WINDOW_PERTYPE: - pos = (app.settings.get(type_ + '-msgwin-x-position'), - app.settings.get(type_ + '-msgwin-y-position')) - else: - return - - move_window(win.window, pos[0], pos[1]) - - def _mode_to_key(self, contact, acct, type_, resource=None): - if self.mode == self.ONE_MSG_WINDOW_NEVER: - key = acct + contact.jid - if resource: - key += '/' + resource - return key - - if self.mode == self.ONE_MSG_WINDOW_ALWAYS: - return self.MAIN_WIN - - if self.mode == self.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER: - return self.ROSTER_MAIN_WIN - - if self.mode == self.ONE_MSG_WINDOW_PERACCT: - return acct - - if self.mode == self.ONE_MSG_WINDOW_PERTYPE: - return type_ - - def create_window(self, contact, acct, type_, resource=None): - type_ = str(type_) - win_acct = None - win_type = None - win_role = None # X11 window role - - win_key = self._mode_to_key(contact, acct, type_, resource) - if self.mode == self.ONE_MSG_WINDOW_PERACCT: - win_acct = acct - win_role = acct - elif self.mode == self.ONE_MSG_WINDOW_PERTYPE: - win_type = type_ - win_role = type_ - elif self.mode == self.ONE_MSG_WINDOW_NEVER: - win_type = type_ - win_role = contact.jid - elif self.mode == self.ONE_MSG_WINDOW_ALWAYS: - win_role = 'messages' - - win = None - try: - win = self._windows[win_key] - except KeyError: - win = self._new_window(win_acct, win_type) - - if win_role: - win.window.set_role(win_role) - - # Position and size window based on saved state and window mode - if not self.one_window_opened(contact, acct, type_): - if app.settings.get('msgwin-max-state'): - win.window.maximize() - else: - self._resize_window(win, acct, type_) - self._position_window(win, acct, type_) - - self._windows[win_key] = win - return win - - def change_key(self, old_jid, new_jid, acct): - win = self.get_window(old_jid, acct) - if self.mode == self.ONE_MSG_WINDOW_NEVER: - old_key = acct + old_jid - if old_jid not in self._windows: - return - new_key = acct + new_jid - self._windows[new_key] = self._windows[old_key] - del self._windows[old_key] - win.change_key(old_jid, new_jid, acct) - - def _on_window_delete(self, win, event): - self.save_state(self._gtk_win_to_msg_win(win)) - app.interface.save_config() - return False - - def _on_window_destroy(self, win): - for k in list(self._windows.keys()): - if self._windows[k].window == win: - self.emit('window-delete', self._windows[k]) - del self._windows[k] - return - - def get_control(self, jid, acct): - """ - Amongst all windows, return the MessageControl for jid - """ - win = self.get_window(jid, acct) - if win: - return win.get_control(jid, acct) - return None - - def search_control(self, jid, account, resource=None): - """ - Search windows with this policy: - 1. try to find already opened tab for resource - 2. find the tab for this jid with ctrl.resource not set - 3. there is none - """ - fjid = jid - if resource: - fjid += '/' + resource - ctrl = self.get_control(fjid, account) - if ctrl: - return ctrl - win = self.get_window(jid, account) - if win: - ctrl = win.get_control(jid, account) - if not ctrl.resource and not ctrl.is_groupchat: - return ctrl - return None - - def get_gc_control(self, jid, acct): - """ - Same as get_control. Was briefly required, is not any more. May be useful - some day in the future? - """ - ctrl = self.get_control(jid, acct) - if ctrl and ctrl.is_groupchat: - return ctrl - return None - - def get_controls(self, type_=None, acct=None): - ctrls = [] - for c in self.controls(): - if acct and c.account != acct: - continue - if not type_ or c.type == type_: - ctrls.append(c) - return ctrls - - def windows(self): - for w in list(self._windows.values()): - yield w - - def controls(self): - for w in self._windows.values(): - for c in w.controls(): - yield c - - def shutdown(self, width_adjust=0): - for w in self.windows(): - self.save_state(w, width_adjust) - if not w.parent_paned: - w.window.hide() - w.window.destroy() - - app.interface.save_config() - - def save_state(self, msg_win, width_adjust=0): - # Save window size and position - max_win_key = 'msgwin-max-state' - pos_x_key = 'msgwin-x-position' - pos_y_key = 'msgwin-y-position' - size_width_key = 'msgwin-width' - size_height_key = 'msgwin-height' - - acct = None - x, y = msg_win.window.get_position() - width, height = msg_win.window.get_size() - - # If any of these values seem bogus don't update. - if x < 0 or y < 0 or width < 0 or height < 0: - return - - if self.mode == self.ONE_MSG_WINDOW_PERACCT: - acct = msg_win.account - elif self.mode == self.ONE_MSG_WINDOW_PERTYPE: - type_ = msg_win.type_ - pos_x_key = type_ + '-msgwin-x-position' - pos_y_key = type_ + '-msgwin-y-position' - size_width_key = type_ + '-msgwin-width' - size_height_key = type_ + '-msgwin-height' - elif self.mode == self.ONE_MSG_WINDOW_NEVER: - type_ = msg_win.type_ - size_width_key = type_ + '-msgwin-width' - size_height_key = type_ + '-msgwin-height' - elif self.mode == self.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER: - # Ignore hpaned separator's width and calculate width ourselves - win_width = msg_win.window.get_allocation().width - hpaned_position = app.settings.get('roster_hpaned_position') - width = win_width - hpaned_position - - if acct: - app.settings.set_account_setting(acct, size_width_key, width) - app.settings.set_account_setting(acct, size_height_key, height) - - if self.mode != self.ONE_MSG_WINDOW_NEVER: - app.settings.set_account_setting(acct, pos_x_key, x) - app.settings.set_account_setting(acct, pos_y_key, y) - - else: - win_maximized = msg_win.window.get_window().get_state() == \ - Gdk.WindowState.MAXIMIZED - app.settings.set(max_win_key, win_maximized) - width += width_adjust - app.settings.set(size_width_key, width) - app.settings.set(size_height_key, height) - - if self.mode != self.ONE_MSG_WINDOW_NEVER: - app.settings.set(pos_x_key, x) - app.settings.set(pos_y_key, y) - - def reconfig(self): - for w in self.windows(): - self.save_state(w) - - mode = app.settings.get('one_message_window') - if self.mode == WINDOW_TYPES.index(mode): - # No change - return - self.mode = WINDOW_TYPES.index(mode) - - controls = [] - for w in self.windows(): - # Note, we are taking care not to hide/delete the roster window when the - # MessageWindow is embedded. - if not w.parent_paned: - w.window.hide() - else: - # Stash current size so it can be restored if the MessageWindow - # is not longer embedded - roster_width = w.parent_paned.get_position() - app.settings.set('roster_width', roster_width) - - while w.notebook.get_n_pages(): - page = w.notebook.get_nth_page(0) - ctrl = w._widget_to_control(page) - w.notebook.remove_page(0) - page.unparent() - controls.append(ctrl) - - # Must clear _controls to prevent MessageControl.shutdown calls - w._controls = {} - if not w.parent_paned: - w.window.destroy() - else: - # Don't close parent window, just remove the child - child = w.parent_paned.get_child2() - w.parent_paned.remove(child) - self.parent_win.lookup_action('show-roster').set_enabled(False) - resize_window(w.window, - app.settings.get('roster_width'), - app.settings.get('roster_height')) - - self._windows = {} - - for ctrl in controls: - mw = self.get_window(ctrl.contact.jid, ctrl.account) - if not mw: - mw = self.create_window(ctrl.contact, ctrl.account, ctrl.type) - ctrl.parent_win = mw - ctrl.add_actions() - ctrl.update_actions() - mw.new_tab(ctrl) - - def save_opened_controls(self): - if not app.settings.get('remember_opened_chat_controls'): - return - chat_controls = {} - for acct in app.connections: - chat_controls[acct] = [] - for ctrl in self.get_controls(type_=ControlType.CHAT): - acct = ctrl.account - if ctrl.contact.jid not in chat_controls[acct]: - chat_controls[acct].append(ctrl.contact.jid) - for acct in app.connections: - app.settings.set_account_setting(acct, - 'opened_chat_controls', - ','.join(chat_controls[acct])) diff --git a/gajim/roster_window.py b/gajim/roster_window.py index 6e7e8ced261d1cfe0d44b25e45be447cee27f6f7..113c8c5df5e20143ffea7a52f5404624d40c53db 100644 --- a/gajim/roster_window.py +++ b/gajim/roster_window.py @@ -59,7 +59,6 @@ from gajim.common.dbus import location from gajim.common import ged -from gajim.message_window import MessageWindowMgr from gajim.gui.dialogs import DialogButton from gajim.gui.dialogs import ConfirmationDialog