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 # CTRL + f moves cursor one char forward when user uses Emacs
360 if not gtk
.settings_get_default().get_property(
361 'gtk-key-theme-name') == 'Emacs':
362 control
._on
_send
_file
_menuitem
_activate
(None)
363 elif control
.type_id
== message_control
.TYPE_CHAT
and \
364 keyval
== gtk
.keysyms
.g
: # CTRL + g
365 control
._on
_convert
_to
_gc
_menuitem
_activate
(None)
366 elif control
.type_id
in (message_control
.TYPE_CHAT
,
367 message_control
.TYPE_PM
) and keyval
== gtk
.keysyms
.i
: # CTRL + i
368 control
._on
_contact
_information
_menuitem
_activate
(None)
369 elif keyval
== gtk
.keysyms
.l
or keyval
== gtk
.keysyms
.L
: # CTRL + l|L
370 control
.conv_textview
.clear()
371 elif keyval
== gtk
.keysyms
.u
: # CTRL + u: emacs style clear line
372 control
.clear(control
.msg_textview
)
373 elif control
.type_id
== message_control
.TYPE_GC
and \
374 keyval
== gtk
.keysyms
.b
: # CTRL + b
375 control
._on
_bookmark
_room
_menuitem
_activate
(None)
376 # Tab switch bindings
377 elif keyval
== gtk
.keysyms
.F4
: # CTRL + F4
378 self
.remove_tab(control
, self
.CLOSE_CTRL_KEY
)
379 elif keyval
== gtk
.keysyms
.w
: # CTRL + w
380 # CTRL + w removes latest word before sursor when User uses emacs
382 if not gtk
.settings_get_default().get_property(
383 'gtk-key-theme-name') == 'Emacs':
384 self
.remove_tab(control
, self
.CLOSE_CTRL_KEY
)
385 elif keyval
in (gtk
.keysyms
.Page_Up
, gtk
.keysyms
.Page_Down
):
386 # CTRL + PageUp | PageDown
387 # Create event and send it to notebook
388 event
= gtk
.gdk
.Event(gtk
.gdk
.KEY_PRESS
)
389 event
.window
= self
.window
.window
390 event
.time
= int(time
.time())
391 event
.state
= gtk
.gdk
.CONTROL_MASK
392 event
.keyval
= int(keyval
)
393 self
.notebook
.emit('key_press_event', event
)
395 if modifier
& gtk
.gdk
.SHIFT_MASK
:
397 if control
.type_id
== message_control
.TYPE_GC
and \
398 keyval
== gtk
.keysyms
.n
: # CTRL + SHIFT + n
399 control
._on
_change
_nick
_menuitem
_activate
(None)
401 elif modifier
& gtk
.gdk
.MOD1_MASK
:
402 # Tab switch bindings
403 if keyval
== gtk
.keysyms
.Right
: # ALT + RIGHT
404 new
= self
.notebook
.get_current_page() + 1
405 if new
>= self
.notebook
.get_n_pages():
407 self
.notebook
.set_current_page(new
)
408 elif keyval
== gtk
.keysyms
.Left
: # ALT + LEFT
409 new
= self
.notebook
.get_current_page() - 1
411 new
= self
.notebook
.get_n_pages() - 1
412 self
.notebook
.set_current_page(new
)
413 elif chr(keyval
) in st
: # ALT + 1,2,3..
414 self
.notebook
.set_current_page(st
.index(chr(keyval
)))
415 elif keyval
== gtk
.keysyms
.c
: # ALT + C toggles chat buttons
416 control
.chat_buttons_set_visible(not control
.hide_chat_buttons
)
417 elif keyval
== gtk
.keysyms
.m
: # ALT + M show emoticons menu
418 control
.show_emoticons_menu()
419 elif keyval
== gtk
.keysyms
.d
: # ALT + D show actions menu
420 control
.on_actions_button_clicked(control
.actions_button
)
421 elif control
.type_id
== message_control
.TYPE_GC
and \
422 keyval
== gtk
.keysyms
.t
: # ALT + t
423 control
._on
_change
_subject
_menuitem
_activate
(None)
425 elif keyval
== gtk
.keysyms
.Escape
and \
426 gajim
.config
.get('escape_key_closes'): # Escape
427 self
.remove_tab(control
, self
.CLOSE_ESC
)
429 def _on_close_button_clicked(self
, button
, control
):
431 When close button is pressed: close a tab
433 self
.remove_tab(control
, self
.CLOSE_CLOSE_BUTTON
)
436 window_mode
= gajim
.interface
.msg_win_mgr
.mode
438 if window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_NEVER
:
439 ctrl
= self
.get_active_control()
442 icon
= ctrl
.get_tab_image(count_unread
=False)
443 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS
:
444 pass # keep default icon
445 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
446 pass # keep default icon
447 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_PERACCT
:
448 pass # keep default icon
449 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_PERTYPE
:
450 if self
.type_
== 'gc':
451 icon
= gtkgui_helpers
.load_icon('muc_active')
454 icon
= gtkgui_helpers
.load_icon('online')
456 self
.window
.set_icon(icon
.get_pixbuf())
458 def show_title(self
, urgent
=True, control
=None):
460 Redraw the window's title
463 control
= self
.get_active_control()
465 # No more control in this window
468 for ctrl
in self
.controls():
469 if ctrl
.type_id
== message_control
.TYPE_GC
and not \
470 gajim
.config
.get('notify_on_all_muc_messages') and not \
472 # count only pm messages
473 unread
+= ctrl
.get_nb_unread_pm()
475 unread
+= ctrl
.get_nb_unread()
479 unread_str
= '[' + unicode(unread
) + '] '
485 if control
.type_id
== message_control
.TYPE_GC
:
486 name
= control
.room_jid
.split('@')[0]
487 urgent
= control
.attention_flag
489 name
= control
.contact
.get_shown_name()
491 name
+= '/' + control
.resource
493 window_mode
= gajim
.interface
.msg_win_mgr
.mode
494 if window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_PERTYPE
:
495 # Show the plural form since number of tabs > 1
496 if self
.type_
== 'chat':
498 elif self
.type_
== 'gc':
499 label
= _('Group Chats')
501 label
= _('Private Chats')
502 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
504 elif self
.get_num_controls() == 1:
507 label
= _('Messages')
511 title
= '%s - %s' % (label
, title
)
513 if window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_PERACCT
:
514 title
= title
+ ": " + control
.account
516 self
.window
.set_title(unread_str
+ title
)
519 gtkgui_helpers
.set_unset_urgency_hint(self
.window
, unread
)
521 gtkgui_helpers
.set_unset_urgency_hint(self
.window
, False)
523 def set_active_tab(self
, ctrl
):
524 ctrl_page
= self
.notebook
.page_num(ctrl
.widget
)
525 self
.notebook
.set_current_page(ctrl_page
)
526 self
.window
.present()
527 gobject
.idle_add(ctrl
.msg_textview
.grab_focus
)
529 def remove_tab(self
, ctrl
, method
, reason
= None, force
= False):
531 Reason is only for gc (offline status message) if force is True, do not
535 if reason
is not None: # We are leaving gc with a status message
536 ctrl
.shutdown(reason
)
537 else: # We are leaving gc without status message or it's a chat
539 # Update external state
540 gajim
.events
.remove_events(ctrl
.account
, ctrl
.get_full_jid
,
541 types
= ['printed_msg', 'chat', 'gc_msg'])
543 fjid
= ctrl
.get_full_jid()
544 jid
= gajim
.get_jid_without_resource(fjid
)
546 fctrl
= self
.get_control(fjid
, ctrl
.account
)
547 bctrl
= self
.get_control(jid
, ctrl
.account
)
548 # keep last_message_time around unless this was our last control with
550 if not fctrl
and not bctrl
and \
551 fjid
in gajim
.last_message_time
[ctrl
.account
]:
552 del gajim
.last_message_time
[ctrl
.account
][fjid
]
554 self
.notebook
.remove_page(self
.notebook
.page_num(ctrl
.widget
))
556 del self
._controls
[ctrl
.account
][fjid
]
558 if len(self
._controls
[ctrl
.account
]) == 0:
559 del self
._controls
[ctrl
.account
]
570 def on_minimize(ctrl
):
571 if method
!= self
.CLOSE_COMMAND
:
577 # Shutdown the MessageControl
581 ctrl
.allow_shutdown(method
, on_yes
, on_no
, on_minimize
)
583 def check_tabs(self
):
584 if self
.get_num_controls() == 0:
585 # These are not called when the window is destroyed like this, fake it
586 gajim
.interface
.msg_win_mgr
._on
_window
_delete
(self
.window
, None)
587 gajim
.interface
.msg_win_mgr
._on
_window
_destroy
(self
.window
)
589 self
.notebook
.drag_dest_unset()
590 if self
.parent_paned
:
591 # Don't close parent window, just remove the child
592 child
= self
.parent_paned
.get_child2()
593 self
.parent_paned
.remove(child
)
595 self
.window
.destroy()
596 return # don't show_title, we are dead
597 elif self
.get_num_controls() == 1: # we are going from two tabs to one
598 window_mode
= gajim
.interface
.msg_win_mgr
.mode
599 show_tabs_if_one_tab
= gajim
.config
.get('tabs_always_visible') or \
600 window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
601 self
.notebook
.set_show_tabs(show_tabs_if_one_tab
)
603 def redraw_tab(self
, ctrl
, chatstate
= None):
604 hbox
= self
.notebook
.get_tab_label(ctrl
.widget
).get_children()[0]
605 status_img
= hbox
.get_children()[0]
606 nick_label
= hbox
.get_children()[1]
608 # Optionally hide close button
609 close_button
= hbox
.get_children()[2]
610 if gajim
.config
.get('tabs_close_button'):
616 nick_label
.set_max_width_chars(10)
617 (tab_label_str
, tab_label_color
) = ctrl
.get_tab_label(chatstate
)
618 nick_label
.set_markup(tab_label_str
)
620 nick_label
.modify_fg(gtk
.STATE_NORMAL
, tab_label_color
)
621 nick_label
.modify_fg(gtk
.STATE_ACTIVE
, tab_label_color
)
623 tab_img
= ctrl
.get_tab_image()
625 if tab_img
.get_storage_type() == gtk
.IMAGE_ANIMATION
:
626 status_img
.set_from_animation(tab_img
.get_animation())
628 status_img
.set_from_pixbuf(tab_img
.get_pixbuf())
632 def repaint_themed_widgets(self
):
634 Repaint controls in the window with theme color
636 # iterate through controls and repaint
637 for ctrl
in self
.controls():
638 ctrl
.repaint_themed_widgets()
640 def _widget_to_control(self
, widget
):
641 for ctrl
in self
.controls():
642 if ctrl
.widget
== widget
:
646 def get_active_control(self
):
647 notebook
= self
.notebook
648 active_widget
= notebook
.get_nth_page(notebook
.get_current_page())
649 return self
._widget
_to
_control
(active_widget
)
651 def get_active_contact(self
):
652 ctrl
= self
.get_active_control()
657 def get_active_jid(self
):
658 contact
= self
.get_active_contact()
664 return self
.window
.is_active()
666 def get_origin(self
):
667 return self
.window
.window
.get_origin()
669 def get_control(self
, key
, acct
):
671 Return the MessageControl for jid or n, where n is a notebook page index.
672 When key is an int index acct may be None
674 if isinstance(key
, str):
675 key
= unicode(key
, 'utf-8')
677 if isinstance(key
, unicode):
680 return self
._controls
[acct
][jid
]
685 notebook
= self
.notebook
687 page_num
= notebook
.get_current_page()
688 nth_child
= notebook
.get_nth_page(page_num
)
689 return self
._widget
_to
_control
(nth_child
)
691 def has_control(self
, jid
, acct
):
692 return (acct
in self
._controls
and jid
in self
._controls
[acct
])
694 def change_key(self
, old_jid
, new_jid
, acct
):
696 Change the JID key of a control
699 # Check if controls exists
700 ctrl
= self
._controls
[acct
][old_jid
]
704 if new_jid
in self
._controls
[acct
]:
705 self
.remove_tab(self
._controls
[acct
][new_jid
],
706 self
.CLOSE_CLOSE_BUTTON
, force
=True)
708 self
._controls
[acct
][new_jid
] = ctrl
709 del self
._controls
[acct
][old_jid
]
711 if old_jid
in gajim
.last_message_time
[acct
]:
712 gajim
.last_message_time
[acct
][new_jid
] = \
713 gajim
.last_message_time
[acct
][old_jid
]
714 del gajim
.last_message_time
[acct
][old_jid
]
717 for jid_dict
in self
._controls
.values():
718 for ctrl
in jid_dict
.values():
721 def get_nb_controls(self
):
722 return sum(len(jid_dict
) for jid_dict
in self
._controls
.values())
724 def move_to_next_unread_tab(self
, forward
):
725 ind
= self
.notebook
.get_current_page()
728 first_composing_ind
= -1 # id of first composing ctrl to switch to
729 # if no others controls have awaiting events
730 # loop until finding an unread tab or having done a complete cycle
732 if forward
== True: # look for the first unread tab on the right
734 if ind
>= self
.notebook
.get_n_pages():
736 else: # look for the first unread tab on the right
739 ind
= self
.notebook
.get_n_pages() - 1
740 ctrl
= self
.get_control(ind
, None)
741 if ctrl
.get_nb_unread() > 0:
744 elif gajim
.config
.get('ctrl_tab_go_to_next_composing') :
745 # Search for a composing contact
746 contact
= ctrl
.contact
747 if first_composing_ind
== -1 and contact
.chatstate
== 'composing':
748 # If no composing contact found yet, check if this one is composing
749 first_composing_ind
= ind
751 break # a complete cycle without finding an unread tab
753 self
.notebook
.set_current_page(ind
)
754 elif first_composing_ind
!= -1:
755 self
.notebook
.set_current_page(first_composing_ind
)
756 else: # not found and nobody composing
757 if forward
: # CTRL + TAB
758 if current
< (self
.notebook
.get_n_pages() - 1):
759 self
.notebook
.next_page()
760 else: # traverse for ever (eg. don't stop at last tab)
761 self
.notebook
.set_current_page(0)
762 else: # CTRL + SHIFT + TAB
764 self
.notebook
.prev_page()
765 else: # traverse for ever (eg. don't stop at first tab)
766 self
.notebook
.set_current_page(
767 self
.notebook
.get_n_pages() - 1)
769 def popup_menu(self
, event
):
770 menu
= self
.get_active_control().prepare_context_menu()
772 menu
.popup(None, None, None, event
.button
, event
.time
)
775 def _on_notebook_switch_page(self
, notebook
, page
, page_num
):
776 old_no
= notebook
.get_current_page()
778 old_ctrl
= self
._widget
_to
_control
(notebook
.get_nth_page(old_no
))
779 old_ctrl
.set_control_active(False)
781 new_ctrl
= self
._widget
_to
_control
(notebook
.get_nth_page(page_num
))
782 new_ctrl
.set_control_active(True)
783 self
.show_title(control
= new_ctrl
)
785 control
= self
.get_active_control()
786 if isinstance(control
, ChatControlBase
):
787 control
.msg_textview
.grab_focus()
789 def _on_notebook_key_press(self
, widget
, event
):
790 # when tab itself is selected,
791 # make sure <- and -> are allowed for navigating between tabs
792 if event
.keyval
in (gtk
.keysyms
.Left
, gtk
.keysyms
.Right
):
795 control
= self
.get_active_control()
797 if event
.state
& gtk
.gdk
.SHIFT_MASK
:
799 if event
.state
& gtk
.gdk
.CONTROL_MASK
and \
800 event
.keyval
== gtk
.keysyms
.ISO_Left_Tab
:
801 self
.move_to_next_unread_tab(False)
803 # SHIFT + PAGE_[UP|DOWN]: send to conv_textview
804 elif event
.keyval
in (gtk
.keysyms
.Page_Down
, gtk
.keysyms
.Page_Up
):
805 control
.conv_textview
.tv
.emit('key_press_event', event
)
807 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
808 if event
.keyval
== gtk
.keysyms
.Tab
: # CTRL + TAB
809 self
.move_to_next_unread_tab(True)
811 # Ctrl+PageUP / DOWN has to be handled by notebook
812 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
813 self
.move_to_next_unread_tab(True)
815 elif event
.keyval
== gtk
.keysyms
.Page_Up
:
816 self
.move_to_next_unread_tab(False)
818 if event
.keyval
in (gtk
.keysyms
.Shift_L
, gtk
.keysyms
.Shift_R
,
819 gtk
.keysyms
.Control_L
, gtk
.keysyms
.Control_R
, gtk
.keysyms
.Caps_Lock
,
820 gtk
.keysyms
.Shift_Lock
, gtk
.keysyms
.Meta_L
, gtk
.keysyms
.Meta_R
,
821 gtk
.keysyms
.Alt_L
, gtk
.keysyms
.Alt_R
, gtk
.keysyms
.Super_L
,
822 gtk
.keysyms
.Super_R
, gtk
.keysyms
.Hyper_L
, gtk
.keysyms
.Hyper_R
):
825 if isinstance(control
, ChatControlBase
):
826 # we forwarded it to message textview
827 control
.msg_textview
.emit('key_press_event', event
)
828 control
.msg_textview
.grab_focus()
830 def get_tab_at_xy(self
, x
, y
):
832 Return the tab under xy and if its nearer from left or right side of the
837 horiz
= self
.notebook
.get_tab_pos() == gtk
.POS_TOP
or \
838 self
.notebook
.get_tab_pos() == gtk
.POS_BOTTOM
839 for i
in xrange(self
.notebook
.get_n_pages()):
840 page
= self
.notebook
.get_nth_page(i
)
841 tab
= self
.notebook
.get_tab_label(page
)
842 tab_alloc
= tab
.get_allocation()
844 if (x
>= tab_alloc
.x
) and \
845 (x
<= (tab_alloc
.x
+ tab_alloc
.width
)):
847 if x
>= tab_alloc
.x
+ (tab_alloc
.width
/ 2.0):
851 if (y
>= tab_alloc
.y
) and \
852 (y
<= (tab_alloc
.y
+ tab_alloc
.height
)):
855 if y
> tab_alloc
.y
+ (tab_alloc
.height
/ 2.0):
858 return (page_num
, to_right
)
860 def find_page_num_according_to_tab_label(self
, tab_label
):
862 Find the page num of the tab label
865 for i
in xrange(self
.notebook
.get_n_pages()):
866 page
= self
.notebook
.get_nth_page(i
)
867 tab
= self
.notebook
.get_tab_label(page
)
873 ################################################################################
874 class MessageWindowMgr(gobject
.GObject
):
876 A manager and factory for MessageWindow objects
880 'window-delete': (gobject
.SIGNAL_RUN_LAST
, None, (object,)),
883 # These constants map to common.config.opt_one_window_types indices
885 ONE_MSG_WINDOW_NEVER
,
886 ONE_MSG_WINDOW_ALWAYS
,
887 ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
,
888 ONE_MSG_WINDOW_PERACCT
,
889 ONE_MSG_WINDOW_PERTYPE
,
891 # A key constant for the main window in ONE_MSG_WINDOW_ALWAYS mode
893 # A key constant for the main window in ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER mode
894 ROSTER_MAIN_WIN
= 'roster'
896 def __init__(self
, parent_window
, parent_paned
):
898 A dictionary of windows; the key depends on the config:
899 ONE_MSG_WINDOW_NEVER: The key is the contact JID
900 ONE_MSG_WINDOW_ALWAYS: The key is MessageWindowMgr.MAIN_WIN
901 ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER: The key is MessageWindowMgr.MAIN_WIN
902 ONE_MSG_WINDOW_PERACCT: The key is the account name
903 ONE_MSG_WINDOW_PERTYPE: The key is a message type constant
905 gobject
.GObject
.__init
__(self
)
908 # Map the mode to a int constant for frequent compares
909 mode
= gajim
.config
.get('one_message_window')
910 self
.mode
= common
.config
.opt_one_window_types
.index(mode
)
912 self
.parent_win
= parent_window
913 self
.parent_paned
= parent_paned
915 def change_account_name(self
, old_name
, new_name
):
916 for win
in self
.windows():
917 win
.change_account_name(old_name
, new_name
)
919 def _new_window(self
, acct
, type_
):
922 if self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
923 parent_win
= self
.parent_win
924 parent_paned
= self
.parent_paned
925 win
= MessageWindow(acct
, type_
, parent_win
, parent_paned
)
926 # we track the lifetime of this window
927 win
.window
.connect('delete-event', self
._on
_window
_delete
)
928 win
.window
.connect('destroy', self
._on
_window
_destroy
)
931 def _gtk_win_to_msg_win(self
, gtk_win
):
932 for w
in self
.windows():
933 if w
.window
== gtk_win
:
937 def get_window(self
, jid
, acct
):
938 for win
in self
.windows():
939 if win
.has_control(jid
, acct
):
944 def has_window(self
, jid
, acct
):
945 return self
.get_window(jid
, acct
) is not None
947 def one_window_opened(self
, contact
=None, acct
=None, type_
=None):
950 self
._windows
[self
._mode
_to
_key
(contact
, acct
, type_
)] is not None
954 def _resize_window(self
, win
, acct
, type_
):
956 Resizes window according to config settings
958 if self
.mode
in (self
.ONE_MSG_WINDOW_ALWAYS
,
959 self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
):
960 size
= (gajim
.config
.get('msgwin-width'),
961 gajim
.config
.get('msgwin-height'))
962 if self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
963 parent_size
= win
.window
.get_size()
964 # Need to add the size of the now visible paned handle, otherwise
965 # the saved width of the message window decreases by this amount
966 handle_size
= win
.parent_paned
.style_get_property('handle-size')
967 size
= (parent_size
[0] + size
[0] + handle_size
, size
[1])
968 elif self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
969 size
= (gajim
.config
.get_per('accounts', acct
, 'msgwin-width'),
970 gajim
.config
.get_per('accounts', acct
, 'msgwin-height'))
971 elif self
.mode
in (self
.ONE_MSG_WINDOW_NEVER
, self
.ONE_MSG_WINDOW_PERTYPE
):
972 if type_
== message_control
.TYPE_PM
:
973 type_
= message_control
.TYPE_CHAT
974 opt_width
= type_
+ '-msgwin-width'
975 opt_height
= type_
+ '-msgwin-height'
976 size
= (gajim
.config
.get(opt_width
), gajim
.config
.get(opt_height
))
979 win
.resize(size
[0], size
[1])
981 win
.parent_paned
.set_position(parent_size
[0])
983 def _position_window(self
, win
, acct
, type_
):
985 Moves window according to config settings
987 if (self
.mode
in [self
.ONE_MSG_WINDOW_NEVER
,
988 self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
]):
991 if self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS
:
992 pos
= (gajim
.config
.get('msgwin-x-position'),
993 gajim
.config
.get('msgwin-y-position'))
994 elif self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
995 pos
= (gajim
.config
.get_per('accounts', acct
, 'msgwin-x-position'),
996 gajim
.config
.get_per('accounts', acct
, 'msgwin-y-position'))
997 elif self
.mode
== self
.ONE_MSG_WINDOW_PERTYPE
:
998 pos
= (gajim
.config
.get(type_
+ '-msgwin-x-position'),
999 gajim
.config
.get(type_
+ '-msgwin-y-position'))
1003 gtkgui_helpers
.move_window(win
.window
, pos
[0], pos
[1])
1005 def _mode_to_key(self
, contact
, acct
, type_
, resource
= None):
1006 if self
.mode
== self
.ONE_MSG_WINDOW_NEVER
:
1007 key
= acct
+ contact
.jid
1009 key
+= '/' + resource
1011 elif self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS
:
1012 return self
.MAIN_WIN
1013 elif self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
1014 return self
.ROSTER_MAIN_WIN
1015 elif self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
1017 elif self
.mode
== self
.ONE_MSG_WINDOW_PERTYPE
:
1020 def create_window(self
, contact
, acct
, type_
, resource
= None):
1023 win_role
= None # X11 window role
1025 win_key
= self
._mode
_to
_key
(contact
, acct
, type_
, resource
)
1026 if self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
1029 elif self
.mode
== self
.ONE_MSG_WINDOW_PERTYPE
:
1032 elif self
.mode
== self
.ONE_MSG_WINDOW_NEVER
:
1034 win_role
= contact
.jid
1035 elif self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS
:
1036 win_role
= 'messages'
1040 win
= self
._windows
[win_key
]
1042 win
= self
._new
_window
(win_acct
, win_type
)
1045 win
.window
.set_role(win_role
)
1047 # Position and size window based on saved state and window mode
1048 if not self
.one_window_opened(contact
, acct
, type_
):
1049 if gajim
.config
.get('msgwin-max-state'):
1050 win
.window
.maximize()
1052 self
._resize
_window
(win
, acct
, type_
)
1053 self
._position
_window
(win
, acct
, type_
)
1055 self
._windows
[win_key
] = win
1058 def change_key(self
, old_jid
, new_jid
, acct
):
1059 win
= self
.get_window(old_jid
, acct
)
1060 if self
.mode
== self
.ONE_MSG_WINDOW_NEVER
:
1061 old_key
= acct
+ old_jid
1062 if old_jid
not in self
._windows
:
1064 new_key
= acct
+ new_jid
1065 self
._windows
[new_key
] = self
._windows
[old_key
]
1066 del self
._windows
[old_key
]
1067 win
.change_key(old_jid
, new_jid
, acct
)
1069 def _on_window_delete(self
, win
, event
):
1070 self
.save_state(self
._gtk
_win
_to
_msg
_win
(win
))
1071 gajim
.interface
.save_config()
1074 def _on_window_destroy(self
, win
):
1075 for k
in self
._windows
.keys():
1076 if self
._windows
[k
].window
== win
:
1077 self
.emit('window-delete', self
._windows
[k
])
1078 del self
._windows
[k
]
1081 def get_control(self
, jid
, acct
):
1083 Amongst all windows, return the MessageControl for jid
1085 win
= self
.get_window(jid
, acct
)
1087 return win
.get_control(jid
, acct
)
1090 def get_gc_control(self
, jid
, acct
):
1092 Same as get_control. Was briefly required, is not any more. May be useful
1093 some day in the future?
1095 ctrl
= self
.get_control(jid
, acct
)
1096 if ctrl
and ctrl
.type_id
== message_control
.TYPE_GC
:
1100 def get_controls(self
, type_
=None, acct
=None):
1102 for c
in self
.controls():
1103 if acct
and c
.account
!= acct
:
1105 if not type_
or c
.type_id
== type_
:
1110 for w
in self
._windows
.values():
1114 for w
in self
._windows
.values():
1115 for c
in w
.controls():
1118 def shutdown(self
, width_adjust
=0):
1119 for w
in self
.windows():
1120 self
.save_state(w
, width_adjust
)
1121 if not w
.parent_paned
:
1125 gajim
.interface
.save_config()
1127 def save_state(self
, msg_win
, width_adjust
=0):
1128 # Save window size and position
1129 max_win_key
= 'msgwin-max-state'
1130 pos_x_key
= 'msgwin-x-position'
1131 pos_y_key
= 'msgwin-y-position'
1132 size_width_key
= 'msgwin-width'
1133 size_height_key
= 'msgwin-height'
1136 x
, y
= msg_win
.window
.get_position()
1137 width
, height
= msg_win
.window
.get_size()
1139 # If any of these values seem bogus don't update.
1140 if x
< 0 or y
< 0 or width
< 0 or height
< 0:
1143 elif self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
1144 acct
= msg_win
.account
1145 elif self
.mode
== self
.ONE_MSG_WINDOW_PERTYPE
:
1146 type_
= msg_win
.type_
1147 pos_x_key
= type_
+ '-msgwin-x-position'
1148 pos_y_key
= type_
+ '-msgwin-y-position'
1149 size_width_key
= type_
+ '-msgwin-width'
1150 size_height_key
= type_
+ '-msgwin-height'
1151 elif self
.mode
== self
.ONE_MSG_WINDOW_NEVER
:
1152 type_
= msg_win
.type_
1153 size_width_key
= type_
+ '-msgwin-width'
1154 size_height_key
= type_
+ '-msgwin-height'
1155 elif self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
1156 # Ignore any hpaned width
1157 width
= msg_win
.notebook
.allocation
.width
1160 gajim
.config
.set_per('accounts', acct
, size_width_key
, width
)
1161 gajim
.config
.set_per('accounts', acct
, size_height_key
, height
)
1163 if self
.mode
!= self
.ONE_MSG_WINDOW_NEVER
:
1164 gajim
.config
.set_per('accounts', acct
, pos_x_key
, x
)
1165 gajim
.config
.set_per('accounts', acct
, pos_y_key
, y
)
1168 win_maximized
= msg_win
.window
.window
.get_state() == \
1169 gtk
.gdk
.WINDOW_STATE_MAXIMIZED
1170 gajim
.config
.set(max_win_key
, win_maximized
)
1171 width
+= width_adjust
1172 gajim
.config
.set(size_width_key
, width
)
1173 gajim
.config
.set(size_height_key
, height
)
1175 if self
.mode
!= self
.ONE_MSG_WINDOW_NEVER
:
1176 gajim
.config
.set(pos_x_key
, x
)
1177 gajim
.config
.set(pos_y_key
, y
)
1180 for w
in self
.windows():
1182 gajim
.interface
.save_config()
1183 mode
= gajim
.config
.get('one_message_window')
1184 if self
.mode
== common
.config
.opt_one_window_types
.index(mode
):
1187 self
.mode
= common
.config
.opt_one_window_types
.index(mode
)
1190 for w
in self
.windows():
1191 # Note, we are taking care not to hide/delete the roster window when the
1192 # MessageWindow is embedded.
1193 if not w
.parent_paned
:
1196 # Stash current size so it can be restored if the MessageWindow
1197 # is not longer embedded
1198 roster_width
= w
.parent_paned
.get_child1().allocation
.width
1199 gajim
.config
.set('roster_width', roster_width
)
1201 while w
.notebook
.get_n_pages():
1202 page
= w
.notebook
.get_nth_page(0)
1203 ctrl
= w
._widget
_to
_control
(page
)
1204 w
.notebook
.remove_page(0)
1206 controls
.append(ctrl
)
1208 # Must clear _controls to prevent MessageControl.shutdown calls
1210 if not w
.parent_paned
:
1213 # Don't close parent window, just remove the child
1214 child
= w
.parent_paned
.get_child2()
1215 w
.parent_paned
.remove(child
)
1216 gtkgui_helpers
.resize_window(w
.window
,
1217 gajim
.config
.get('roster_width'),
1218 gajim
.config
.get('roster_height'))
1222 for ctrl
in controls
:
1223 mw
= self
.get_window(ctrl
.contact
.jid
, ctrl
.account
)
1225 mw
= self
.create_window(ctrl
.contact
, ctrl
.account
,
1227 ctrl
.parent_win
= mw