2 ## src/message_window.py
4 ## Copyright (C) 2003-2010 Yann Leboulanger <asterix AT lagaule.org>
5 ## Copyright (C) 2005-2008 Travis Shirk <travis AT pobox.com>
6 ## Nikos Kouremenos <kourem AT gmail.com>
7 ## Copyright (C) 2006 Geobert Quach <geobert AT gmail.com>
8 ## Dimitur Kirov <dkirov AT gmail.com>
9 ## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
10 ## Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
11 ## Stephan Erb <steve-e AT h3c.de>
12 ## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
13 ## Jonathan Schleifer <js-gajim AT webkeks.org>
15 ## This file is part of Gajim.
17 ## Gajim is free software; you can redistribute it and/or modify
18 ## it under the terms of the GNU General Public License as published
19 ## by the Free Software Foundation; version 3 only.
21 ## Gajim is distributed in the hope that it will be useful,
22 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
23 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 ## GNU General Public License for more details.
26 ## You should have received a copy of the GNU General Public License
27 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
36 import message_control
38 from chat_control
import ChatControlBase
40 from common
import gajim
44 class MessageWindow(object):
46 Class for windows which contain message like things; chats, groupchats, etc
49 # DND_TARGETS is the targets needed by drag_source_set and drag_dest_set
50 DND_TARGETS
= [('GAJIM_TAB', 0, 81)]
51 hid
= 0 # drag_data_received handler id
53 CLOSE_TAB_MIDDLE_CLICK
,
60 def __init__(self
, acct
, type_
, parent_window
=None, parent_paned
=None):
61 # A dictionary of dictionaries
62 # where _contacts[account][jid] == A MessageControl
65 # If None, the window is not tied to any specific account
67 # If None, the window is not tied to any specific type
69 # dict { handler id: widget}. Keeps callbacks, which
70 # lead to cylcular references
72 # Don't show warning dialogs when we want to delete the window
73 self
.dont_warn_on_delete
= False
75 self
.widget_name
= 'message_window'
76 self
.xml
= gtkgui_helpers
.get_gtk_builder('%s.ui' % self
.widget_name
)
77 self
.window
= self
.xml
.get_object(self
.widget_name
)
78 self
.notebook
= self
.xml
.get_object('notebook')
79 self
.parent_paned
= None
82 orig_window
= self
.window
83 self
.window
= parent_window
84 self
.parent_paned
= parent_paned
85 self
.notebook
.reparent(self
.parent_paned
)
86 self
.parent_paned
.pack2(self
.notebook
, resize
=True, shrink
=True)
90 # NOTE: we use 'connect_after' here because in
91 # MessageWindowMgr._new_window we register handler that saves window
92 # state when closing it, and it should be called before
93 # MessageWindow._on_window_delete, which manually destroys window
94 # through win.destroy() - this means no additional handlers for
95 # 'delete-event' are called.
96 id_
= self
.window
.connect_after('delete-event', self
._on
_window
_delete
)
97 self
.handlers
[id_
] = self
.window
98 id_
= self
.window
.connect('destroy', self
._on
_window
_destroy
)
99 self
.handlers
[id_
] = self
.window
100 id_
= self
.window
.connect('focus-in-event', self
._on
_window
_focus
)
101 self
.handlers
[id_
] = self
.window
103 keys
=['<Control>f', '<Control>g', '<Control>h', '<Control>i',
104 '<Control>l', '<Control>L', '<Control><Shift>n', '<Control>u',
105 '<Control>b', '<Control>F4',
106 '<Control>w', '<Control>Page_Up', '<Control>Page_Down', '<Alt>Right',
107 '<Alt>Left', '<Alt>d', '<Alt>c', '<Alt>m', '<Alt>t', 'Escape'] + \
108 ['<Alt>'+str(i
) for i
in xrange(10)]
109 accel_group
= gtk
.AccelGroup()
111 keyval
, mod
= gtk
.accelerator_parse(key
)
112 accel_group
.connect_group(keyval
, mod
, gtk
.ACCEL_VISIBLE
,
113 self
.accel_group_func
)
114 self
.window
.add_accel_group(accel_group
)
116 # gtk+ doesn't make use of the motion notify on gtkwindow by default
117 # so this line adds that
118 self
.window
.add_events(gtk
.gdk
.POINTER_MOTION_MASK
)
120 id_
= self
.notebook
.connect('switch-page',
121 self
._on
_notebook
_switch
_page
)
122 self
.handlers
[id_
] = self
.notebook
123 id_
= self
.notebook
.connect('key-press-event',
124 self
._on
_notebook
_key
_press
)
125 self
.handlers
[id_
] = self
.notebook
128 pref_pos
= gajim
.config
.get('tabs_position')
129 if pref_pos
== 'bottom':
130 nb_pos
= gtk
.POS_BOTTOM
131 elif pref_pos
== 'left':
132 nb_pos
= gtk
.POS_LEFT
133 elif pref_pos
== 'right':
134 nb_pos
= gtk
.POS_RIGHT
137 self
.notebook
.set_tab_pos(nb_pos
)
138 window_mode
= gajim
.interface
.msg_win_mgr
.mode
139 if gajim
.config
.get('tabs_always_visible') or \
140 window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
141 self
.notebook
.set_show_tabs(True)
143 self
.notebook
.set_show_tabs(False)
144 self
.notebook
.set_show_border(gajim
.config
.get('tabs_border'))
147 gobject
.idle_add(self
.notebook
.grab_focus
)
149 def change_account_name(self
, old_name
, new_name
):
150 if old_name
in self
._controls
:
151 self
._controls
[new_name
] = self
._controls
[old_name
]
152 del self
._controls
[old_name
]
154 for ctrl
in self
.controls():
155 if ctrl
.account
== old_name
:
156 ctrl
.account
= new_name
157 if self
.account
== old_name
:
158 self
.account
= new_name
160 def change_jid(self
, account
, old_jid
, new_jid
):
162 Called when the full jid of the control is changed
164 if account
not in self
._controls
:
166 if old_jid
not in self
._controls
[account
]:
168 if old_jid
== new_jid
:
170 self
._controls
[account
][new_jid
] = self
._controls
[account
][old_jid
]
171 del self
._controls
[account
][old_jid
]
173 def get_num_controls(self
):
174 return sum(len(d
) for d
in self
._controls
.values())
176 def resize(self
, width
, height
):
177 gtkgui_helpers
.resize_window(self
.window
, width
, height
)
179 def _on_window_focus(self
, widget
, event
):
180 # window received focus, so if we had urgency REMOVE IT
181 # NOTE: we do not have to read the message (it maybe in a bg tab)
182 # to remove urgency hint so this functions does that
183 gtkgui_helpers
.set_unset_urgency_hint(self
.window
, False)
185 ctrl
= self
.get_active_control()
187 ctrl
.set_control_active(True)
188 # Undo "unread" state display, etc.
189 if ctrl
.type_id
== message_control
.TYPE_GC
:
190 self
.redraw_tab(ctrl
, 'active')
192 # NOTE: we do not send any chatstate to preserve
193 # inactive, gone, etc.
194 self
.redraw_tab(ctrl
)
196 def _on_window_delete(self
, win
, event
):
197 if self
.dont_warn_on_delete
:
201 # Number of controls that will be closed and for which we'll loose data:
202 # chat, pm, gc that won't go in roster
203 number_of_closed_control
= 0
204 for ctrl
in self
.controls():
205 if not ctrl
.safe_shutdown():
206 number_of_closed_control
+= 1
208 if number_of_closed_control
> 1:
209 def on_yes1(checked
):
211 gajim
.config
.set('confirm_close_multiple_tabs', False)
212 self
.dont_warn_on_delete
= True
213 for ctrl
in self
.controls():
214 if ctrl
.minimizable():
218 if not gajim
.config
.get('confirm_close_multiple_tabs'):
222 _('You are going to close several tabs'),
223 _('Do you really want to close them all?'),
224 checktext
=_('_Do not ask me again'), on_response_yes
=on_yes1
)
228 if self
.on_delete_ok
== 1:
229 self
.dont_warn_on_delete
= True
231 self
.on_delete_ok
-= 1
236 def on_minimize(ctrl
):
238 if self
.on_delete_ok
== 1:
239 self
.dont_warn_on_delete
= True
241 self
.on_delete_ok
-= 1
243 # Make sure all controls are okay with being deleted
244 self
.on_delete_ok
= self
.get_nb_controls()
245 for ctrl
in self
.controls():
246 ctrl
.allow_shutdown(self
.CLOSE_CLOSE_BUTTON
, on_yes
, on_no
,
248 return True # halt the delete for the moment
250 def _on_window_destroy(self
, win
):
251 for ctrl
in self
.controls():
253 self
._controls
.clear()
254 # Clean up handlers connected to the parent window, this is important since
255 # self.window may be the RosterWindow
256 for i
in self
.handlers
.keys():
257 if self
.handlers
[i
].handler_is_connected(i
):
258 self
.handlers
[i
].disconnect(i
)
262 def new_tab(self
, control
):
263 fjid
= control
.get_full_jid()
265 if control
.account
not in self
._controls
:
266 self
._controls
[control
.account
] = {}
268 self
._controls
[control
.account
][fjid
] = control
270 if self
.get_num_controls() == 2:
271 # is first conversation_textview scrolled down ?
273 first_widget
= self
.notebook
.get_nth_page(0)
274 ctrl
= self
._widget
_to
_control
(first_widget
)
275 conv_textview
= ctrl
.conv_textview
276 if conv_textview
.at_the_end():
278 self
.notebook
.set_show_tabs(True)
280 gobject
.idle_add(conv_textview
.scroll_to_end_iter
)
282 # Add notebook page and connect up to the tab's close button
283 xml
= gtkgui_helpers
.get_gtk_builder('message_window.ui', 'chat_tab_ebox')
284 tab_label_box
= xml
.get_object('chat_tab_ebox')
285 widget
= xml
.get_object('tab_close_button')
286 #this reduces the size of the button
287 style
= gtk
.RcStyle()
290 widget
.modify_style(style
)
292 id_
= widget
.connect('clicked', self
._on
_close
_button
_clicked
, control
)
293 control
.handlers
[id_
] = widget
295 id_
= tab_label_box
.connect('button-press-event',
296 self
.on_tab_eventbox_button_press_event
, control
.widget
)
297 control
.handlers
[id_
] = tab_label_box
298 self
.notebook
.append_page(control
.widget
, tab_label_box
)
300 self
.notebook
.set_tab_reorderable(control
.widget
, True)
302 self
.redraw_tab(control
)
303 if self
.parent_paned
:
304 self
.notebook
.show_all()
306 self
.window
.show_all()
307 # NOTE: we do not call set_control_active(True) since we don't know
308 # whether the tab is the active one.
310 gobject
.idle_add(control
.msg_textview
.grab_focus
)
312 def on_tab_eventbox_button_press_event(self
, widget
, event
, child
):
313 if event
.button
== 3: # right click
314 n
= self
.notebook
.page_num(child
)
315 self
.notebook
.set_current_page(n
)
316 self
.popup_menu(event
)
317 elif event
.button
== 2: # middle click
318 ctrl
= self
._widget
_to
_control
(child
)
319 self
.remove_tab(ctrl
, self
.CLOSE_TAB_MIDDLE_CLICK
)
321 ctrl
= self
._widget
_to
_control
(child
)
322 gobject
.idle_add(ctrl
.msg_textview
.grab_focus
)
324 def _on_message_textview_mykeypress_event(self
, widget
, event_keyval
,
326 # NOTE: handles mykeypress which is custom signal; see message_textview.py
328 # construct event instance from binding
329 event
= gtk
.gdk
.Event(gtk
.gdk
.KEY_PRESS
) # it's always a key-press here
330 event
.keyval
= event_keyval
331 event
.state
= event_keymod
332 event
.time
= 0 # assign current time
334 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
335 # Tab switch bindings
336 if event
.keyval
== gtk
.keysyms
.Tab
: # CTRL + TAB
337 self
.move_to_next_unread_tab(True)
338 elif event
.keyval
== gtk
.keysyms
.ISO_Left_Tab
: # CTRL + SHIFT + TAB
339 self
.move_to_next_unread_tab(False)
340 elif event
.keyval
== gtk
.keysyms
.Page_Down
: # CTRL + PAGE DOWN
341 self
.notebook
.emit('key_press_event', event
)
342 elif event
.keyval
== gtk
.keysyms
.Page_Up
: # CTRL + PAGE UP
343 self
.notebook
.emit('key_press_event', event
)
345 def accel_group_func(self
, accel_group
, acceleratable
, keyval
, modifier
):
346 st
= '1234567890' # alt+1 means the first tab (tab 0)
347 control
= self
.get_active_control()
349 # No more control in this window
353 if modifier
& gtk
.gdk
.CONTROL_MASK
:
354 if keyval
== gtk
.keysyms
.h
: # CTRL + h
355 control
._on
_history
_menuitem
_activate
()
356 elif control
.type_id
== message_control
.TYPE_CHAT
and \
357 keyval
== gtk
.keysyms
.f
: # CTRL + f
358 control
._on
_send
_file
_menuitem
_activate
(None)
359 elif control
.type_id
== message_control
.TYPE_CHAT
and \
360 keyval
== gtk
.keysyms
.g
: # CTRL + g
361 control
._on
_convert
_to
_gc
_menuitem
_activate
(None)
362 elif control
.type_id
in (message_control
.TYPE_CHAT
,
363 message_control
.TYPE_PM
) and keyval
== gtk
.keysyms
.i
: # CTRL + i
364 control
._on
_contact
_information
_menuitem
_activate
(None)
365 elif keyval
== gtk
.keysyms
.l
or keyval
== gtk
.keysyms
.L
: # CTRL + l|L
366 control
.conv_textview
.clear()
367 elif keyval
== gtk
.keysyms
.u
: # CTRL + u: emacs style clear line
368 control
.clear(control
.msg_textview
)
369 elif control
.type_id
== message_control
.TYPE_GC
and \
370 keyval
== gtk
.keysyms
.b
: # CTRL + b
371 control
._on
_bookmark
_room
_menuitem
_activate
(None)
372 # Tab switch bindings
373 elif keyval
== gtk
.keysyms
.F4
: # CTRL + F4
374 self
.remove_tab(control
, self
.CLOSE_CTRL_KEY
)
375 elif keyval
== gtk
.keysyms
.w
: # CTRL + w
376 # CTRL + w removes latest word before sursor when User uses emacs
378 if not gtk
.settings_get_default().get_property(
379 'gtk-key-theme-name') == 'Emacs':
380 self
.remove_tab(control
, self
.CLOSE_CTRL_KEY
)
381 elif keyval
in (gtk
.keysyms
.Page_Up
, gtk
.keysyms
.Page_Down
):
382 # CTRL + PageUp | PageDown
383 # Create event and send it to notebook
384 event
= gtk
.gdk
.Event(gtk
.gdk
.KEY_PRESS
)
385 event
.window
= self
.window
.window
386 event
.time
= int(time
.time())
387 event
.state
= gtk
.gdk
.CONTROL_MASK
388 event
.keyval
= int(keyval
)
389 self
.notebook
.emit('key_press_event', event
)
391 if modifier
& gtk
.gdk
.SHIFT_MASK
:
393 if control
.type_id
== message_control
.TYPE_GC
and \
394 keyval
== gtk
.keysyms
.n
: # CTRL + SHIFT + n
395 control
._on
_change
_nick
_menuitem
_activate
(None)
397 elif modifier
& gtk
.gdk
.MOD1_MASK
:
398 # Tab switch bindings
399 if keyval
== gtk
.keysyms
.Right
: # ALT + RIGHT
400 new
= self
.notebook
.get_current_page() + 1
401 if new
>= self
.notebook
.get_n_pages():
403 self
.notebook
.set_current_page(new
)
404 elif keyval
== gtk
.keysyms
.Left
: # ALT + LEFT
405 new
= self
.notebook
.get_current_page() - 1
407 new
= self
.notebook
.get_n_pages() - 1
408 self
.notebook
.set_current_page(new
)
409 elif chr(keyval
) in st
: # ALT + 1,2,3..
410 self
.notebook
.set_current_page(st
.index(chr(keyval
)))
411 elif keyval
== gtk
.keysyms
.c
: # ALT + C toggles chat buttons
412 control
.chat_buttons_set_visible(not control
.hide_chat_buttons
)
413 elif keyval
== gtk
.keysyms
.m
: # ALT + M show emoticons menu
414 control
.show_emoticons_menu()
415 elif keyval
== gtk
.keysyms
.d
: # ALT + D show actions menu
416 control
.on_actions_button_clicked(control
.actions_button
)
417 elif control
.type_id
== message_control
.TYPE_GC
and \
418 keyval
== gtk
.keysyms
.t
: # ALT + t
419 control
._on
_change
_subject
_menuitem
_activate
(None)
421 elif keyval
== gtk
.keysyms
.Escape
and \
422 gajim
.config
.get('escape_key_closes'): # Escape
423 self
.remove_tab(control
, self
.CLOSE_ESC
)
426 def _on_close_button_clicked(self
, button
, control
):
428 When close button is pressed: close a tab
430 self
.remove_tab(control
, self
.CLOSE_CLOSE_BUTTON
)
433 window_mode
= gajim
.interface
.msg_win_mgr
.mode
435 if window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_NEVER
:
436 ctrl
= self
.get_active_control()
439 icon
= ctrl
.get_tab_image(count_unread
=False)
440 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS
:
441 pass # keep default icon
442 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
443 pass # keep default icon
444 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_PERACCT
:
445 pass # keep default icon
446 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_PERTYPE
:
447 if self
.type_
== 'gc':
448 icon
= gtkgui_helpers
.load_icon('muc_active')
451 icon
= gtkgui_helpers
.load_icon('online')
453 self
.window
.set_icon(icon
.get_pixbuf())
455 def show_title(self
, urgent
=True, control
=None):
457 Redraw the window's title
460 control
= self
.get_active_control()
462 # No more control in this window
465 for ctrl
in self
.controls():
466 if ctrl
.type_id
== message_control
.TYPE_GC
and not \
467 gajim
.config
.get('notify_on_all_muc_messages') and not \
469 # count only pm messages
470 unread
+= ctrl
.get_nb_unread_pm()
472 unread
+= ctrl
.get_nb_unread()
476 unread_str
= '[' + unicode(unread
) + '] '
482 if control
.type_id
== message_control
.TYPE_GC
:
483 name
= control
.room_jid
.split('@')[0]
484 urgent
= control
.attention_flag
486 name
= control
.contact
.get_shown_name()
488 name
+= '/' + control
.resource
490 window_mode
= gajim
.interface
.msg_win_mgr
.mode
491 if window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_PERTYPE
:
492 # Show the plural form since number of tabs > 1
493 if self
.type_
== 'chat':
495 elif self
.type_
== 'gc':
496 label
= _('Group Chats')
498 label
= _('Private Chats')
499 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
501 elif self
.get_num_controls() == 1:
504 label
= _('Messages')
508 title
= '%s - %s' % (label
, title
)
510 if window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_PERACCT
:
511 title
= title
+ ": " + control
.account
513 self
.window
.set_title(unread_str
+ title
)
516 gtkgui_helpers
.set_unset_urgency_hint(self
.window
, unread
)
518 gtkgui_helpers
.set_unset_urgency_hint(self
.window
, False)
520 def set_active_tab(self
, ctrl
):
521 ctrl_page
= self
.notebook
.page_num(ctrl
.widget
)
522 self
.notebook
.set_current_page(ctrl_page
)
523 self
.window
.present()
524 gobject
.idle_add(ctrl
.msg_textview
.grab_focus
)
526 def remove_tab(self
, ctrl
, method
, reason
= None, force
= False):
528 Reason is only for gc (offline status message) if force is True, do not
532 if reason
is not None: # We are leaving gc with a status message
533 ctrl
.shutdown(reason
)
534 else: # We are leaving gc without status message or it's a chat
536 # Update external state
537 gajim
.events
.remove_events(ctrl
.account
, ctrl
.get_full_jid
,
538 types
= ['printed_msg', 'chat', 'gc_msg'])
540 fjid
= ctrl
.get_full_jid()
541 jid
= gajim
.get_jid_without_resource(fjid
)
543 fctrl
= self
.get_control(fjid
, ctrl
.account
)
544 bctrl
= self
.get_control(jid
, ctrl
.account
)
545 # keep last_message_time around unless this was our last control with
547 if not fctrl
and not bctrl
and \
548 fjid
in gajim
.last_message_time
[ctrl
.account
]:
549 del gajim
.last_message_time
[ctrl
.account
][fjid
]
551 self
.notebook
.remove_page(self
.notebook
.page_num(ctrl
.widget
))
553 del self
._controls
[ctrl
.account
][fjid
]
555 if len(self
._controls
[ctrl
.account
]) == 0:
556 del self
._controls
[ctrl
.account
]
567 def on_minimize(ctrl
):
568 if method
!= self
.CLOSE_COMMAND
:
574 # Shutdown the MessageControl
578 ctrl
.allow_shutdown(method
, on_yes
, on_no
, on_minimize
)
580 def check_tabs(self
):
581 if self
.get_num_controls() == 0:
582 # These are not called when the window is destroyed like this, fake it
583 gajim
.interface
.msg_win_mgr
._on
_window
_delete
(self
.window
, None)
584 gajim
.interface
.msg_win_mgr
._on
_window
_destroy
(self
.window
)
586 self
.notebook
.drag_dest_unset()
587 if self
.parent_paned
:
588 # Don't close parent window, just remove the child
589 child
= self
.parent_paned
.get_child2()
590 self
.parent_paned
.remove(child
)
592 self
.window
.destroy()
593 return # don't show_title, we are dead
594 elif self
.get_num_controls() == 1: # we are going from two tabs to one
595 window_mode
= gajim
.interface
.msg_win_mgr
.mode
596 show_tabs_if_one_tab
= gajim
.config
.get('tabs_always_visible') or \
597 window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
598 self
.notebook
.set_show_tabs(show_tabs_if_one_tab
)
600 def redraw_tab(self
, ctrl
, chatstate
= None):
601 hbox
= self
.notebook
.get_tab_label(ctrl
.widget
).get_children()[0]
602 status_img
= hbox
.get_children()[0]
603 nick_label
= hbox
.get_children()[1]
605 # Optionally hide close button
606 close_button
= hbox
.get_children()[2]
607 if gajim
.config
.get('tabs_close_button'):
613 nick_label
.set_max_width_chars(10)
614 (tab_label_str
, tab_label_color
) = ctrl
.get_tab_label(chatstate
)
615 nick_label
.set_markup(tab_label_str
)
617 nick_label
.modify_fg(gtk
.STATE_NORMAL
, tab_label_color
)
618 nick_label
.modify_fg(gtk
.STATE_ACTIVE
, tab_label_color
)
620 tab_img
= ctrl
.get_tab_image()
622 if tab_img
.get_storage_type() == gtk
.IMAGE_ANIMATION
:
623 status_img
.set_from_animation(tab_img
.get_animation())
625 status_img
.set_from_pixbuf(tab_img
.get_pixbuf())
629 def repaint_themed_widgets(self
):
631 Repaint controls in the window with theme color
633 # iterate through controls and repaint
634 for ctrl
in self
.controls():
635 ctrl
.repaint_themed_widgets()
637 def _widget_to_control(self
, widget
):
638 for ctrl
in self
.controls():
639 if ctrl
.widget
== widget
:
643 def get_active_control(self
):
644 notebook
= self
.notebook
645 active_widget
= notebook
.get_nth_page(notebook
.get_current_page())
646 return self
._widget
_to
_control
(active_widget
)
648 def get_active_contact(self
):
649 ctrl
= self
.get_active_control()
654 def get_active_jid(self
):
655 contact
= self
.get_active_contact()
661 return self
.window
.is_active()
663 def get_origin(self
):
664 return self
.window
.window
.get_origin()
666 def get_control(self
, key
, acct
):
668 Return the MessageControl for jid or n, where n is a notebook page index.
669 When key is an int index acct may be None
671 if isinstance(key
, str):
672 key
= unicode(key
, 'utf-8')
674 if isinstance(key
, unicode):
677 return self
._controls
[acct
][jid
]
682 notebook
= self
.notebook
684 page_num
= notebook
.get_current_page()
685 nth_child
= notebook
.get_nth_page(page_num
)
686 return self
._widget
_to
_control
(nth_child
)
688 def has_control(self
, jid
, acct
):
689 return (acct
in self
._controls
and jid
in self
._controls
[acct
])
691 def change_key(self
, old_jid
, new_jid
, acct
):
693 Change the JID key of a control
696 # Check if controls exists
697 ctrl
= self
._controls
[acct
][old_jid
]
701 if new_jid
in self
._controls
[acct
]:
702 self
.remove_tab(self
._controls
[acct
][new_jid
],
703 self
.CLOSE_CLOSE_BUTTON
, force
=True)
705 self
._controls
[acct
][new_jid
] = ctrl
706 del self
._controls
[acct
][old_jid
]
708 if old_jid
in gajim
.last_message_time
[acct
]:
709 gajim
.last_message_time
[acct
][new_jid
] = \
710 gajim
.last_message_time
[acct
][old_jid
]
711 del gajim
.last_message_time
[acct
][old_jid
]
714 for jid_dict
in self
._controls
.values():
715 for ctrl
in jid_dict
.values():
718 def get_nb_controls(self
):
719 return sum(len(jid_dict
) for jid_dict
in self
._controls
.values())
721 def move_to_next_unread_tab(self
, forward
):
722 ind
= self
.notebook
.get_current_page()
725 first_composing_ind
= -1 # id of first composing ctrl to switch to
726 # if no others controls have awaiting events
727 # loop until finding an unread tab or having done a complete cycle
729 if forward
== True: # look for the first unread tab on the right
731 if ind
>= self
.notebook
.get_n_pages():
733 else: # look for the first unread tab on the right
736 ind
= self
.notebook
.get_n_pages() - 1
737 ctrl
= self
.get_control(ind
, None)
738 if ctrl
.get_nb_unread() > 0:
741 elif gajim
.config
.get('ctrl_tab_go_to_next_composing') :
742 # Search for a composing contact
743 contact
= ctrl
.contact
744 if first_composing_ind
== -1 and contact
.chatstate
== 'composing':
745 # If no composing contact found yet, check if this one is composing
746 first_composing_ind
= ind
748 break # a complete cycle without finding an unread tab
750 self
.notebook
.set_current_page(ind
)
751 elif first_composing_ind
!= -1:
752 self
.notebook
.set_current_page(first_composing_ind
)
753 else: # not found and nobody composing
754 if forward
: # CTRL + TAB
755 if current
< (self
.notebook
.get_n_pages() - 1):
756 self
.notebook
.next_page()
757 else: # traverse for ever (eg. don't stop at last tab)
758 self
.notebook
.set_current_page(0)
759 else: # CTRL + SHIFT + TAB
761 self
.notebook
.prev_page()
762 else: # traverse for ever (eg. don't stop at first tab)
763 self
.notebook
.set_current_page(
764 self
.notebook
.get_n_pages() - 1)
766 def popup_menu(self
, event
):
767 menu
= self
.get_active_control().prepare_context_menu()
769 menu
.popup(None, None, None, event
.button
, event
.time
)
772 def _on_notebook_switch_page(self
, notebook
, page
, page_num
):
773 old_no
= notebook
.get_current_page()
775 old_ctrl
= self
._widget
_to
_control
(notebook
.get_nth_page(old_no
))
776 old_ctrl
.set_control_active(False)
778 new_ctrl
= self
._widget
_to
_control
(notebook
.get_nth_page(page_num
))
779 new_ctrl
.set_control_active(True)
780 self
.show_title(control
= new_ctrl
)
782 control
= self
.get_active_control()
783 if isinstance(control
, ChatControlBase
):
784 control
.msg_textview
.grab_focus()
786 def _on_notebook_key_press(self
, widget
, event
):
787 # when tab itself is selected,
788 # make sure <- and -> are allowed for navigating between tabs
789 if event
.keyval
in (gtk
.keysyms
.Left
, gtk
.keysyms
.Right
):
792 control
= self
.get_active_control()
794 if event
.state
& gtk
.gdk
.SHIFT_MASK
:
796 if event
.state
& gtk
.gdk
.CONTROL_MASK
and \
797 event
.keyval
== gtk
.keysyms
.ISO_Left_Tab
:
798 self
.move_to_next_unread_tab(False)
800 # SHIFT + PAGE_[UP|DOWN]: send to conv_textview
801 elif event
.keyval
in (gtk
.keysyms
.Page_Down
, gtk
.keysyms
.Page_Up
):
802 control
.conv_textview
.tv
.emit('key_press_event', event
)
804 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
805 if event
.keyval
== gtk
.keysyms
.Tab
: # CTRL + TAB
806 self
.move_to_next_unread_tab(True)
808 # Ctrl+PageUP / DOWN has to be handled by notebook
809 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
810 self
.move_to_next_unread_tab(True)
812 elif event
.keyval
== gtk
.keysyms
.Page_Up
:
813 self
.move_to_next_unread_tab(False)
815 if event
.keyval
in (gtk
.keysyms
.Shift_L
, gtk
.keysyms
.Shift_R
,
816 gtk
.keysyms
.Control_L
, gtk
.keysyms
.Control_R
, gtk
.keysyms
.Caps_Lock
,
817 gtk
.keysyms
.Shift_Lock
, gtk
.keysyms
.Meta_L
, gtk
.keysyms
.Meta_R
,
818 gtk
.keysyms
.Alt_L
, gtk
.keysyms
.Alt_R
, gtk
.keysyms
.Super_L
,
819 gtk
.keysyms
.Super_R
, gtk
.keysyms
.Hyper_L
, gtk
.keysyms
.Hyper_R
):
822 if isinstance(control
, ChatControlBase
):
823 # we forwarded it to message textview
824 control
.msg_textview
.emit('key_press_event', event
)
825 control
.msg_textview
.grab_focus()
827 def get_tab_at_xy(self
, x
, y
):
829 Return the tab under xy and if its nearer from left or right side of the
834 horiz
= self
.notebook
.get_tab_pos() == gtk
.POS_TOP
or \
835 self
.notebook
.get_tab_pos() == gtk
.POS_BOTTOM
836 for i
in xrange(self
.notebook
.get_n_pages()):
837 page
= self
.notebook
.get_nth_page(i
)
838 tab
= self
.notebook
.get_tab_label(page
)
839 tab_alloc
= tab
.get_allocation()
841 if (x
>= tab_alloc
.x
) and \
842 (x
<= (tab_alloc
.x
+ tab_alloc
.width
)):
844 if x
>= tab_alloc
.x
+ (tab_alloc
.width
/ 2.0):
848 if (y
>= tab_alloc
.y
) and \
849 (y
<= (tab_alloc
.y
+ tab_alloc
.height
)):
852 if y
> tab_alloc
.y
+ (tab_alloc
.height
/ 2.0):
855 return (page_num
, to_right
)
857 def find_page_num_according_to_tab_label(self
, tab_label
):
859 Find the page num of the tab label
862 for i
in xrange(self
.notebook
.get_n_pages()):
863 page
= self
.notebook
.get_nth_page(i
)
864 tab
= self
.notebook
.get_tab_label(page
)
870 ################################################################################
871 class MessageWindowMgr(gobject
.GObject
):
873 A manager and factory for MessageWindow objects
877 'window-delete': (gobject
.SIGNAL_RUN_LAST
, None, (object,)),
880 # These constants map to common.config.opt_one_window_types indices
882 ONE_MSG_WINDOW_NEVER
,
883 ONE_MSG_WINDOW_ALWAYS
,
884 ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
,
885 ONE_MSG_WINDOW_PERACCT
,
886 ONE_MSG_WINDOW_PERTYPE
,
888 # A key constant for the main window in ONE_MSG_WINDOW_ALWAYS mode
890 # A key constant for the main window in ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER mode
891 ROSTER_MAIN_WIN
= 'roster'
893 def __init__(self
, parent_window
, parent_paned
):
895 A dictionary of windows; the key depends on the config:
896 ONE_MSG_WINDOW_NEVER: The key is the contact JID
897 ONE_MSG_WINDOW_ALWAYS: The key is MessageWindowMgr.MAIN_WIN
898 ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER: The key is MessageWindowMgr.MAIN_WIN
899 ONE_MSG_WINDOW_PERACCT: The key is the account name
900 ONE_MSG_WINDOW_PERTYPE: The key is a message type constant
902 gobject
.GObject
.__init
__(self
)
905 # Map the mode to a int constant for frequent compares
906 mode
= gajim
.config
.get('one_message_window')
907 self
.mode
= common
.config
.opt_one_window_types
.index(mode
)
909 self
.parent_win
= parent_window
910 self
.parent_paned
= parent_paned
912 def change_account_name(self
, old_name
, new_name
):
913 for win
in self
.windows():
914 win
.change_account_name(old_name
, new_name
)
916 def _new_window(self
, acct
, type_
):
919 if self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
920 parent_win
= self
.parent_win
921 parent_paned
= self
.parent_paned
922 win
= MessageWindow(acct
, type_
, parent_win
, parent_paned
)
923 # we track the lifetime of this window
924 win
.window
.connect('delete-event', self
._on
_window
_delete
)
925 win
.window
.connect('destroy', self
._on
_window
_destroy
)
928 def _gtk_win_to_msg_win(self
, gtk_win
):
929 for w
in self
.windows():
930 if w
.window
== gtk_win
:
934 def get_window(self
, jid
, acct
):
935 for win
in self
.windows():
936 if win
.has_control(jid
, acct
):
941 def has_window(self
, jid
, acct
):
942 return self
.get_window(jid
, acct
) is not None
944 def one_window_opened(self
, contact
=None, acct
=None, type_
=None):
947 self
._windows
[self
._mode
_to
_key
(contact
, acct
, type_
)] is not None
951 def _resize_window(self
, win
, acct
, type_
):
953 Resizes window according to config settings
955 if self
.mode
in (self
.ONE_MSG_WINDOW_ALWAYS
,
956 self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
):
957 size
= (gajim
.config
.get('msgwin-width'),
958 gajim
.config
.get('msgwin-height'))
959 if self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
960 parent_size
= win
.window
.get_size()
961 # Need to add the size of the now visible paned handle, otherwise
962 # the saved width of the message window decreases by this amount
963 handle_size
= win
.parent_paned
.style_get_property('handle-size')
964 size
= (parent_size
[0] + size
[0] + handle_size
, size
[1])
965 elif self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
966 size
= (gajim
.config
.get_per('accounts', acct
, 'msgwin-width'),
967 gajim
.config
.get_per('accounts', acct
, 'msgwin-height'))
968 elif self
.mode
in (self
.ONE_MSG_WINDOW_NEVER
, self
.ONE_MSG_WINDOW_PERTYPE
):
969 if type_
== message_control
.TYPE_PM
:
970 type_
= message_control
.TYPE_CHAT
971 opt_width
= type_
+ '-msgwin-width'
972 opt_height
= type_
+ '-msgwin-height'
973 size
= (gajim
.config
.get(opt_width
), gajim
.config
.get(opt_height
))
976 win
.resize(size
[0], size
[1])
978 win
.parent_paned
.set_position(parent_size
[0])
980 def _position_window(self
, win
, acct
, type_
):
982 Moves window according to config settings
984 if (self
.mode
in [self
.ONE_MSG_WINDOW_NEVER
,
985 self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
]):
988 if self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS
:
989 pos
= (gajim
.config
.get('msgwin-x-position'),
990 gajim
.config
.get('msgwin-y-position'))
991 elif self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
992 pos
= (gajim
.config
.get_per('accounts', acct
, 'msgwin-x-position'),
993 gajim
.config
.get_per('accounts', acct
, 'msgwin-y-position'))
994 elif self
.mode
== self
.ONE_MSG_WINDOW_PERTYPE
:
995 pos
= (gajim
.config
.get(type_
+ '-msgwin-x-position'),
996 gajim
.config
.get(type_
+ '-msgwin-y-position'))
1000 gtkgui_helpers
.move_window(win
.window
, pos
[0], pos
[1])
1002 def _mode_to_key(self
, contact
, acct
, type_
, resource
= None):
1003 if self
.mode
== self
.ONE_MSG_WINDOW_NEVER
:
1004 key
= acct
+ contact
.jid
1006 key
+= '/' + resource
1008 elif self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS
:
1009 return self
.MAIN_WIN
1010 elif self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
1011 return self
.ROSTER_MAIN_WIN
1012 elif self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
1014 elif self
.mode
== self
.ONE_MSG_WINDOW_PERTYPE
:
1017 def create_window(self
, contact
, acct
, type_
, resource
= None):
1020 win_role
= None # X11 window role
1022 win_key
= self
._mode
_to
_key
(contact
, acct
, type_
, resource
)
1023 if self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
1026 elif self
.mode
== self
.ONE_MSG_WINDOW_PERTYPE
:
1029 elif self
.mode
== self
.ONE_MSG_WINDOW_NEVER
:
1031 win_role
= contact
.jid
1032 elif self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS
:
1033 win_role
= 'messages'
1037 win
= self
._windows
[win_key
]
1039 win
= self
._new
_window
(win_acct
, win_type
)
1042 win
.window
.set_role(win_role
)
1044 # Position and size window based on saved state and window mode
1045 if not self
.one_window_opened(contact
, acct
, type_
):
1046 if gajim
.config
.get('msgwin-max-state'):
1047 win
.window
.maximize()
1049 self
._resize
_window
(win
, acct
, type_
)
1050 self
._position
_window
(win
, acct
, type_
)
1052 self
._windows
[win_key
] = win
1055 def change_key(self
, old_jid
, new_jid
, acct
):
1056 win
= self
.get_window(old_jid
, acct
)
1057 if self
.mode
== self
.ONE_MSG_WINDOW_NEVER
:
1058 old_key
= acct
+ old_jid
1059 if old_jid
not in self
._windows
:
1061 new_key
= acct
+ new_jid
1062 self
._windows
[new_key
] = self
._windows
[old_key
]
1063 del self
._windows
[old_key
]
1064 win
.change_key(old_jid
, new_jid
, acct
)
1066 def _on_window_delete(self
, win
, event
):
1067 self
.save_state(self
._gtk
_win
_to
_msg
_win
(win
))
1068 gajim
.interface
.save_config()
1071 def _on_window_destroy(self
, win
):
1072 for k
in self
._windows
.keys():
1073 if self
._windows
[k
].window
== win
:
1074 self
.emit('window-delete', self
._windows
[k
])
1075 del self
._windows
[k
]
1078 def get_control(self
, jid
, acct
):
1080 Amongst all windows, return the MessageControl for jid
1082 win
= self
.get_window(jid
, acct
)
1084 return win
.get_control(jid
, acct
)
1087 def get_gc_control(self
, jid
, acct
):
1089 Same as get_control. Was briefly required, is not any more. May be useful
1090 some day in the future?
1092 ctrl
= self
.get_control(jid
, acct
)
1093 if ctrl
and ctrl
.type_id
== message_control
.TYPE_GC
:
1097 def get_controls(self
, type_
=None, acct
=None):
1099 for c
in self
.controls():
1100 if acct
and c
.account
!= acct
:
1102 if not type_
or c
.type_id
== type_
:
1107 for w
in self
._windows
.values():
1111 for w
in self
._windows
.values():
1112 for c
in w
.controls():
1115 def shutdown(self
, width_adjust
=0):
1116 for w
in self
.windows():
1117 self
.save_state(w
, width_adjust
)
1118 if not w
.parent_paned
:
1122 gajim
.interface
.save_config()
1124 def save_state(self
, msg_win
, width_adjust
=0):
1125 # Save window size and position
1126 max_win_key
= 'msgwin-max-state'
1127 pos_x_key
= 'msgwin-x-position'
1128 pos_y_key
= 'msgwin-y-position'
1129 size_width_key
= 'msgwin-width'
1130 size_height_key
= 'msgwin-height'
1133 x
, y
= msg_win
.window
.get_position()
1134 width
, height
= msg_win
.window
.get_size()
1136 # If any of these values seem bogus don't update.
1137 if x
< 0 or y
< 0 or width
< 0 or height
< 0:
1140 elif self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
1141 acct
= msg_win
.account
1142 elif self
.mode
== self
.ONE_MSG_WINDOW_PERTYPE
:
1143 type_
= msg_win
.type_
1144 pos_x_key
= type_
+ '-msgwin-x-position'
1145 pos_y_key
= type_
+ '-msgwin-y-position'
1146 size_width_key
= type_
+ '-msgwin-width'
1147 size_height_key
= type_
+ '-msgwin-height'
1148 elif self
.mode
== self
.ONE_MSG_WINDOW_NEVER
:
1149 type_
= msg_win
.type_
1150 size_width_key
= type_
+ '-msgwin-width'
1151 size_height_key
= type_
+ '-msgwin-height'
1152 elif self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
1153 # Ignore any hpaned width
1154 width
= msg_win
.notebook
.allocation
.width
1157 gajim
.config
.set_per('accounts', acct
, size_width_key
, width
)
1158 gajim
.config
.set_per('accounts', acct
, size_height_key
, height
)
1160 if self
.mode
!= self
.ONE_MSG_WINDOW_NEVER
:
1161 gajim
.config
.set_per('accounts', acct
, pos_x_key
, x
)
1162 gajim
.config
.set_per('accounts', acct
, pos_y_key
, y
)
1165 win_maximized
= msg_win
.window
.window
.get_state() == \
1166 gtk
.gdk
.WINDOW_STATE_MAXIMIZED
1167 gajim
.config
.set(max_win_key
, win_maximized
)
1168 width
+= width_adjust
1169 gajim
.config
.set(size_width_key
, width
)
1170 gajim
.config
.set(size_height_key
, height
)
1172 if self
.mode
!= self
.ONE_MSG_WINDOW_NEVER
:
1173 gajim
.config
.set(pos_x_key
, x
)
1174 gajim
.config
.set(pos_y_key
, y
)
1177 for w
in self
.windows():
1179 gajim
.interface
.save_config()
1180 mode
= gajim
.config
.get('one_message_window')
1181 if self
.mode
== common
.config
.opt_one_window_types
.index(mode
):
1184 self
.mode
= common
.config
.opt_one_window_types
.index(mode
)
1187 for w
in self
.windows():
1188 # Note, we are taking care not to hide/delete the roster window when the
1189 # MessageWindow is embedded.
1190 if not w
.parent_paned
:
1193 # Stash current size so it can be restored if the MessageWindow
1194 # is not longer embedded
1195 roster_width
= w
.parent_paned
.get_child1().allocation
.width
1196 gajim
.config
.set('roster_width', roster_width
)
1198 while w
.notebook
.get_n_pages():
1199 page
= w
.notebook
.get_nth_page(0)
1200 ctrl
= w
._widget
_to
_control
(page
)
1201 w
.notebook
.remove_page(0)
1203 controls
.append(ctrl
)
1205 # Must clear _controls to prevent MessageControl.shutdown calls
1207 if not w
.parent_paned
:
1210 # Don't close parent window, just remove the child
1211 child
= w
.parent_paned
.get_child2()
1212 w
.parent_paned
.remove(child
)
1213 gtkgui_helpers
.resize_window(w
.window
,
1214 gajim
.config
.get('roster_width'),
1215 gajim
.config
.get('roster_height'))
1219 for ctrl
in controls
:
1220 mw
= self
.get_window(ctrl
.contact
.jid
, ctrl
.account
)
1222 mw
= self
.create_window(ctrl
.contact
, ctrl
.account
,
1224 ctrl
.parent_win
= mw