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 # Set window role to differ chats from MUCs
79 self
.window
.set_role(self
.type_
)
80 self
.notebook
= self
.xml
.get_object('notebook')
81 self
.parent_paned
= None
84 orig_window
= self
.window
85 self
.window
= parent_window
86 self
.parent_paned
= parent_paned
87 self
.notebook
.reparent(self
.parent_paned
)
88 self
.parent_paned
.pack2(self
.notebook
, resize
=True, shrink
=True)
92 # NOTE: we use 'connect_after' here because in
93 # MessageWindowMgr._new_window we register handler that saves window
94 # state when closing it, and it should be called before
95 # MessageWindow._on_window_delete, which manually destroys window
96 # through win.destroy() - this means no additional handlers for
97 # 'delete-event' are called.
98 id_
= self
.window
.connect_after('delete-event', self
._on
_window
_delete
)
99 self
.handlers
[id_
] = self
.window
100 id_
= self
.window
.connect('destroy', self
._on
_window
_destroy
)
101 self
.handlers
[id_
] = self
.window
102 id_
= self
.window
.connect('focus-in-event', self
._on
_window
_focus
)
103 self
.handlers
[id_
] = self
.window
105 keys
=['<Control>f', '<Control>g', '<Control>h', '<Control>i',
106 '<Control>l', '<Control>L', '<Control><Shift>n', '<Control>u',
107 '<Control>b', '<Control>F4',
108 '<Control>w', '<Control>Page_Up', '<Control>Page_Down', '<Alt>Right',
109 '<Alt>Left', '<Alt>d', '<Alt>c', '<Alt>m', '<Alt>t', 'Escape'] + \
110 ['<Alt>'+str(i
) for i
in xrange(10)]
111 accel_group
= gtk
.AccelGroup()
113 keyval
, mod
= gtk
.accelerator_parse(key
)
114 accel_group
.connect_group(keyval
, mod
, gtk
.ACCEL_VISIBLE
,
115 self
.accel_group_func
)
116 self
.window
.add_accel_group(accel_group
)
118 # gtk+ doesn't make use of the motion notify on gtkwindow by default
119 # so this line adds that
120 self
.window
.add_events(gtk
.gdk
.POINTER_MOTION_MASK
)
122 id_
= self
.notebook
.connect('switch-page',
123 self
._on
_notebook
_switch
_page
)
124 self
.handlers
[id_
] = self
.notebook
125 id_
= self
.notebook
.connect('key-press-event',
126 self
._on
_notebook
_key
_press
)
127 self
.handlers
[id_
] = self
.notebook
130 pref_pos
= gajim
.config
.get('tabs_position')
131 if pref_pos
== 'bottom':
132 nb_pos
= gtk
.POS_BOTTOM
133 elif pref_pos
== 'left':
134 nb_pos
= gtk
.POS_LEFT
135 elif pref_pos
== 'right':
136 nb_pos
= gtk
.POS_RIGHT
139 self
.notebook
.set_tab_pos(nb_pos
)
140 window_mode
= gajim
.interface
.msg_win_mgr
.mode
141 if gajim
.config
.get('tabs_always_visible') or \
142 window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
143 self
.notebook
.set_show_tabs(True)
145 self
.notebook
.set_show_tabs(False)
146 self
.notebook
.set_show_border(gajim
.config
.get('tabs_border'))
149 gobject
.idle_add(self
.notebook
.grab_focus
)
151 def change_account_name(self
, old_name
, new_name
):
152 if old_name
in self
._controls
:
153 self
._controls
[new_name
] = self
._controls
[old_name
]
154 del self
._controls
[old_name
]
156 for ctrl
in self
.controls():
157 if ctrl
.account
== old_name
:
158 ctrl
.account
= new_name
159 if self
.account
== old_name
:
160 self
.account
= new_name
162 def change_jid(self
, account
, old_jid
, new_jid
):
164 Called when the full jid of the control is changed
166 if account
not in self
._controls
:
168 if old_jid
not in self
._controls
[account
]:
170 if old_jid
== new_jid
:
172 self
._controls
[account
][new_jid
] = self
._controls
[account
][old_jid
]
173 del self
._controls
[account
][old_jid
]
175 def get_num_controls(self
):
176 return sum(len(d
) for d
in self
._controls
.values())
178 def resize(self
, width
, height
):
179 gtkgui_helpers
.resize_window(self
.window
, width
, height
)
181 def _on_window_focus(self
, widget
, event
):
182 # window received focus, so if we had urgency REMOVE IT
183 # NOTE: we do not have to read the message (it maybe in a bg tab)
184 # to remove urgency hint so this functions does that
185 gtkgui_helpers
.set_unset_urgency_hint(self
.window
, False)
187 ctrl
= self
.get_active_control()
189 ctrl
.set_control_active(True)
190 # Undo "unread" state display, etc.
191 if ctrl
.type_id
== message_control
.TYPE_GC
:
192 self
.redraw_tab(ctrl
, 'active')
194 # NOTE: we do not send any chatstate to preserve
195 # inactive, gone, etc.
196 self
.redraw_tab(ctrl
)
198 def _on_window_delete(self
, win
, event
):
199 if self
.dont_warn_on_delete
:
203 # Number of controls that will be closed and for which we'll loose data:
204 # chat, pm, gc that won't go in roster
205 number_of_closed_control
= 0
206 for ctrl
in self
.controls():
207 if not ctrl
.safe_shutdown():
208 number_of_closed_control
+= 1
210 if number_of_closed_control
> 1:
211 def on_yes1(checked
):
213 gajim
.config
.set('confirm_close_multiple_tabs', False)
214 self
.dont_warn_on_delete
= True
215 for ctrl
in self
.controls():
216 if ctrl
.minimizable():
220 if not gajim
.config
.get('confirm_close_multiple_tabs'):
224 _('You are going to close several tabs'),
225 _('Do you really want to close them all?'),
226 checktext
=_('_Do not ask me again'), on_response_yes
=on_yes1
)
230 if self
.on_delete_ok
== 1:
231 self
.dont_warn_on_delete
= True
233 self
.on_delete_ok
-= 1
238 def on_minimize(ctrl
):
240 if self
.on_delete_ok
== 1:
241 self
.dont_warn_on_delete
= True
243 self
.on_delete_ok
-= 1
245 # Make sure all controls are okay with being deleted
246 self
.on_delete_ok
= self
.get_nb_controls()
247 for ctrl
in self
.controls():
248 ctrl
.allow_shutdown(self
.CLOSE_CLOSE_BUTTON
, on_yes
, on_no
,
250 return True # halt the delete for the moment
252 def _on_window_destroy(self
, win
):
253 for ctrl
in self
.controls():
255 self
._controls
.clear()
256 # Clean up handlers connected to the parent window, this is important since
257 # self.window may be the RosterWindow
258 for i
in self
.handlers
.keys():
259 if self
.handlers
[i
].handler_is_connected(i
):
260 self
.handlers
[i
].disconnect(i
)
264 def new_tab(self
, control
):
265 fjid
= control
.get_full_jid()
267 if control
.account
not in self
._controls
:
268 self
._controls
[control
.account
] = {}
270 self
._controls
[control
.account
][fjid
] = control
272 if self
.get_num_controls() == 2:
273 # is first conversation_textview scrolled down ?
275 first_widget
= self
.notebook
.get_nth_page(0)
276 ctrl
= self
._widget
_to
_control
(first_widget
)
277 conv_textview
= ctrl
.conv_textview
278 if conv_textview
.at_the_end():
280 self
.notebook
.set_show_tabs(True)
282 gobject
.idle_add(conv_textview
.scroll_to_end_iter
)
284 # Add notebook page and connect up to the tab's close button
285 xml
= gtkgui_helpers
.get_gtk_builder('message_window.ui', 'chat_tab_ebox')
286 tab_label_box
= xml
.get_object('chat_tab_ebox')
287 widget
= xml
.get_object('tab_close_button')
288 #this reduces the size of the button
289 style
= gtk
.RcStyle()
292 widget
.modify_style(style
)
294 id_
= widget
.connect('clicked', self
._on
_close
_button
_clicked
, control
)
295 control
.handlers
[id_
] = widget
297 id_
= tab_label_box
.connect('button-press-event',
298 self
.on_tab_eventbox_button_press_event
, control
.widget
)
299 control
.handlers
[id_
] = tab_label_box
300 self
.notebook
.append_page(control
.widget
, tab_label_box
)
302 self
.notebook
.set_tab_reorderable(control
.widget
, True)
304 self
.redraw_tab(control
)
305 if self
.parent_paned
:
306 self
.notebook
.show_all()
308 self
.window
.show_all()
309 # NOTE: we do not call set_control_active(True) since we don't know
310 # whether the tab is the active one.
312 gobject
.idle_add(control
.msg_textview
.grab_focus
)
314 def on_tab_eventbox_button_press_event(self
, widget
, event
, child
):
315 if event
.button
== 3: # right click
316 n
= self
.notebook
.page_num(child
)
317 self
.notebook
.set_current_page(n
)
318 self
.popup_menu(event
)
319 elif event
.button
== 2: # middle click
320 ctrl
= self
._widget
_to
_control
(child
)
321 self
.remove_tab(ctrl
, self
.CLOSE_TAB_MIDDLE_CLICK
)
323 ctrl
= self
._widget
_to
_control
(child
)
324 gobject
.idle_add(ctrl
.msg_textview
.grab_focus
)
326 def _on_message_textview_mykeypress_event(self
, widget
, event_keyval
,
328 # NOTE: handles mykeypress which is custom signal; see message_textview.py
330 # construct event instance from binding
331 event
= gtk
.gdk
.Event(gtk
.gdk
.KEY_PRESS
) # it's always a key-press here
332 event
.keyval
= event_keyval
333 event
.state
= event_keymod
334 event
.time
= 0 # assign current time
336 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
337 # Tab switch bindings
338 if event
.keyval
== gtk
.keysyms
.Tab
: # CTRL + TAB
339 self
.move_to_next_unread_tab(True)
340 elif event
.keyval
== gtk
.keysyms
.ISO_Left_Tab
: # CTRL + SHIFT + TAB
341 self
.move_to_next_unread_tab(False)
342 elif event
.keyval
== gtk
.keysyms
.Page_Down
: # CTRL + PAGE DOWN
343 self
.notebook
.emit('key_press_event', event
)
344 elif event
.keyval
== gtk
.keysyms
.Page_Up
: # CTRL + PAGE UP
345 self
.notebook
.emit('key_press_event', event
)
347 def accel_group_func(self
, accel_group
, acceleratable
, keyval
, modifier
):
348 st
= '1234567890' # alt+1 means the first tab (tab 0)
349 control
= self
.get_active_control()
351 # No more control in this window
355 if modifier
& gtk
.gdk
.CONTROL_MASK
:
356 if keyval
== gtk
.keysyms
.h
: # CTRL + h
357 control
._on
_history
_menuitem
_activate
()
358 elif control
.type_id
== message_control
.TYPE_CHAT
and \
359 keyval
== gtk
.keysyms
.f
: # CTRL + f
360 control
._on
_send
_file
_menuitem
_activate
(None)
361 elif control
.type_id
== message_control
.TYPE_CHAT
and \
362 keyval
== gtk
.keysyms
.g
: # CTRL + g
363 control
._on
_convert
_to
_gc
_menuitem
_activate
(None)
364 elif control
.type_id
in (message_control
.TYPE_CHAT
,
365 message_control
.TYPE_PM
) and keyval
== gtk
.keysyms
.i
: # CTRL + i
366 control
._on
_contact
_information
_menuitem
_activate
(None)
367 elif keyval
== gtk
.keysyms
.l
or keyval
== gtk
.keysyms
.L
: # CTRL + l|L
368 control
.conv_textview
.clear()
369 elif keyval
== gtk
.keysyms
.u
: # CTRL + u: emacs style clear line
370 control
.clear(control
.msg_textview
)
371 elif control
.type_id
== message_control
.TYPE_GC
and \
372 keyval
== gtk
.keysyms
.b
: # CTRL + b
373 control
._on
_bookmark
_room
_menuitem
_activate
(None)
374 # Tab switch bindings
375 elif keyval
== gtk
.keysyms
.F4
: # CTRL + F4
376 self
.remove_tab(control
, self
.CLOSE_CTRL_KEY
)
377 elif keyval
== gtk
.keysyms
.w
: # CTRL + w
378 # CTRL + w removes latest word before sursor when User uses emacs
380 if not gtk
.settings_get_default().get_property(
381 'gtk-key-theme-name') == 'Emacs':
382 self
.remove_tab(control
, self
.CLOSE_CTRL_KEY
)
383 elif keyval
in (gtk
.keysyms
.Page_Up
, gtk
.keysyms
.Page_Down
):
384 # CTRL + PageUp | PageDown
385 # Create event and send it to notebook
386 event
= gtk
.gdk
.Event(gtk
.gdk
.KEY_PRESS
)
387 event
.window
= self
.window
.window
388 event
.time
= int(time
.time())
389 event
.state
= gtk
.gdk
.CONTROL_MASK
390 event
.keyval
= int(keyval
)
391 self
.notebook
.emit('key_press_event', event
)
393 if modifier
& gtk
.gdk
.SHIFT_MASK
:
395 if control
.type_id
== message_control
.TYPE_GC
and \
396 keyval
== gtk
.keysyms
.n
: # CTRL + SHIFT + n
397 control
._on
_change
_nick
_menuitem
_activate
(None)
399 elif modifier
& gtk
.gdk
.MOD1_MASK
:
400 # Tab switch bindings
401 if keyval
== gtk
.keysyms
.Right
: # ALT + RIGHT
402 new
= self
.notebook
.get_current_page() + 1
403 if new
>= self
.notebook
.get_n_pages():
405 self
.notebook
.set_current_page(new
)
406 elif keyval
== gtk
.keysyms
.Left
: # ALT + LEFT
407 new
= self
.notebook
.get_current_page() - 1
409 new
= self
.notebook
.get_n_pages() - 1
410 self
.notebook
.set_current_page(new
)
411 elif chr(keyval
) in st
: # ALT + 1,2,3..
412 self
.notebook
.set_current_page(st
.index(chr(keyval
)))
413 elif keyval
== gtk
.keysyms
.c
: # ALT + C toggles chat buttons
414 control
.chat_buttons_set_visible(not control
.hide_chat_buttons
)
415 elif keyval
== gtk
.keysyms
.m
: # ALT + M show emoticons menu
416 control
.show_emoticons_menu()
417 elif keyval
== gtk
.keysyms
.d
: # ALT + D show actions menu
418 control
.on_actions_button_clicked(control
.actions_button
)
419 elif control
.type_id
== message_control
.TYPE_GC
and \
420 keyval
== gtk
.keysyms
.t
: # ALT + t
421 control
._on
_change
_subject
_menuitem
_activate
(None)
423 elif keyval
== gtk
.keysyms
.Escape
and \
424 gajim
.config
.get('escape_key_closes'): # Escape
425 self
.remove_tab(control
, self
.CLOSE_ESC
)
428 def _on_close_button_clicked(self
, button
, control
):
430 When close button is pressed: close a tab
432 self
.remove_tab(control
, self
.CLOSE_CLOSE_BUTTON
)
435 window_mode
= gajim
.interface
.msg_win_mgr
.mode
437 if window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_NEVER
:
438 ctrl
= self
.get_active_control()
441 icon
= ctrl
.get_tab_image(count_unread
=False)
442 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS
:
443 pass # keep default icon
444 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
445 pass # keep default icon
446 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_PERACCT
:
447 pass # keep default icon
448 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_PERTYPE
:
449 if self
.type_
== 'gc':
450 icon
= gtkgui_helpers
.load_icon('muc_active')
453 icon
= gtkgui_helpers
.load_icon('online')
455 self
.window
.set_icon(icon
.get_pixbuf())
457 def show_title(self
, urgent
=True, control
=None):
459 Redraw the window's title
462 control
= self
.get_active_control()
464 # No more control in this window
467 for ctrl
in self
.controls():
468 if ctrl
.type_id
== message_control
.TYPE_GC
and not \
469 gajim
.config
.get('notify_on_all_muc_messages') and not \
471 # count only pm messages
472 unread
+= ctrl
.get_nb_unread_pm()
474 unread
+= ctrl
.get_nb_unread()
478 unread_str
= '[' + unicode(unread
) + '] '
484 if control
.type_id
== message_control
.TYPE_GC
:
485 name
= control
.room_jid
.split('@')[0]
486 urgent
= control
.attention_flag
488 name
= control
.contact
.get_shown_name()
490 name
+= '/' + control
.resource
492 window_mode
= gajim
.interface
.msg_win_mgr
.mode
493 if window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_PERTYPE
:
494 # Show the plural form since number of tabs > 1
495 if self
.type_
== 'chat':
497 elif self
.type_
== 'gc':
498 label
= _('Group Chats')
500 label
= _('Private Chats')
501 elif window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
503 elif self
.get_num_controls() == 1:
506 label
= _('Messages')
510 title
= '%s - %s' % (label
, title
)
512 if window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_PERACCT
:
513 title
= title
+ ": " + control
.account
515 self
.window
.set_title(unread_str
+ title
)
518 gtkgui_helpers
.set_unset_urgency_hint(self
.window
, unread
)
520 gtkgui_helpers
.set_unset_urgency_hint(self
.window
, False)
522 def set_active_tab(self
, ctrl
):
523 ctrl_page
= self
.notebook
.page_num(ctrl
.widget
)
524 self
.notebook
.set_current_page(ctrl_page
)
525 self
.window
.present()
526 gobject
.idle_add(ctrl
.msg_textview
.grab_focus
)
528 def remove_tab(self
, ctrl
, method
, reason
= None, force
= False):
530 Reason is only for gc (offline status message) if force is True, do not
534 if reason
is not None: # We are leaving gc with a status message
535 ctrl
.shutdown(reason
)
536 else: # We are leaving gc without status message or it's a chat
538 # Update external state
539 gajim
.events
.remove_events(ctrl
.account
, ctrl
.get_full_jid
,
540 types
= ['printed_msg', 'chat', 'gc_msg'])
542 fjid
= ctrl
.get_full_jid()
543 jid
= gajim
.get_jid_without_resource(fjid
)
545 fctrl
= self
.get_control(fjid
, ctrl
.account
)
546 bctrl
= self
.get_control(jid
, ctrl
.account
)
547 # keep last_message_time around unless this was our last control with
549 if not fctrl
and not bctrl
and \
550 fjid
in gajim
.last_message_time
[ctrl
.account
]:
551 del gajim
.last_message_time
[ctrl
.account
][fjid
]
553 self
.notebook
.remove_page(self
.notebook
.page_num(ctrl
.widget
))
555 del self
._controls
[ctrl
.account
][fjid
]
557 if len(self
._controls
[ctrl
.account
]) == 0:
558 del self
._controls
[ctrl
.account
]
569 def on_minimize(ctrl
):
570 if method
!= self
.CLOSE_COMMAND
:
576 # Shutdown the MessageControl
580 ctrl
.allow_shutdown(method
, on_yes
, on_no
, on_minimize
)
582 def check_tabs(self
):
583 if self
.get_num_controls() == 0:
584 # These are not called when the window is destroyed like this, fake it
585 gajim
.interface
.msg_win_mgr
._on
_window
_delete
(self
.window
, None)
586 gajim
.interface
.msg_win_mgr
._on
_window
_destroy
(self
.window
)
588 self
.notebook
.drag_dest_unset()
589 if self
.parent_paned
:
590 # Don't close parent window, just remove the child
591 child
= self
.parent_paned
.get_child2()
592 self
.parent_paned
.remove(child
)
594 self
.window
.destroy()
595 return # don't show_title, we are dead
596 elif self
.get_num_controls() == 1: # we are going from two tabs to one
597 window_mode
= gajim
.interface
.msg_win_mgr
.mode
598 show_tabs_if_one_tab
= gajim
.config
.get('tabs_always_visible') or \
599 window_mode
== MessageWindowMgr
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
600 self
.notebook
.set_show_tabs(show_tabs_if_one_tab
)
602 def redraw_tab(self
, ctrl
, chatstate
= None):
603 hbox
= self
.notebook
.get_tab_label(ctrl
.widget
).get_children()[0]
604 status_img
= hbox
.get_children()[0]
605 nick_label
= hbox
.get_children()[1]
607 # Optionally hide close button
608 close_button
= hbox
.get_children()[2]
609 if gajim
.config
.get('tabs_close_button'):
615 nick_label
.set_max_width_chars(10)
616 (tab_label_str
, tab_label_color
) = ctrl
.get_tab_label(chatstate
)
617 nick_label
.set_markup(tab_label_str
)
619 nick_label
.modify_fg(gtk
.STATE_NORMAL
, tab_label_color
)
620 nick_label
.modify_fg(gtk
.STATE_ACTIVE
, tab_label_color
)
622 tab_img
= ctrl
.get_tab_image()
624 if tab_img
.get_storage_type() == gtk
.IMAGE_ANIMATION
:
625 status_img
.set_from_animation(tab_img
.get_animation())
627 status_img
.set_from_pixbuf(tab_img
.get_pixbuf())
631 def repaint_themed_widgets(self
):
633 Repaint controls in the window with theme color
635 # iterate through controls and repaint
636 for ctrl
in self
.controls():
637 ctrl
.repaint_themed_widgets()
639 def _widget_to_control(self
, widget
):
640 for ctrl
in self
.controls():
641 if ctrl
.widget
== widget
:
645 def get_active_control(self
):
646 notebook
= self
.notebook
647 active_widget
= notebook
.get_nth_page(notebook
.get_current_page())
648 return self
._widget
_to
_control
(active_widget
)
650 def get_active_contact(self
):
651 ctrl
= self
.get_active_control()
656 def get_active_jid(self
):
657 contact
= self
.get_active_contact()
663 return self
.window
.is_active()
665 def get_origin(self
):
666 return self
.window
.window
.get_origin()
668 def get_control(self
, key
, acct
):
670 Return the MessageControl for jid or n, where n is a notebook page index.
671 When key is an int index acct may be None
673 if isinstance(key
, str):
674 key
= unicode(key
, 'utf-8')
676 if isinstance(key
, unicode):
679 return self
._controls
[acct
][jid
]
684 notebook
= self
.notebook
686 page_num
= notebook
.get_current_page()
687 nth_child
= notebook
.get_nth_page(page_num
)
688 return self
._widget
_to
_control
(nth_child
)
690 def has_control(self
, jid
, acct
):
691 return (acct
in self
._controls
and jid
in self
._controls
[acct
])
693 def change_key(self
, old_jid
, new_jid
, acct
):
695 Change the JID key of a control
698 # Check if controls exists
699 ctrl
= self
._controls
[acct
][old_jid
]
703 if new_jid
in self
._controls
[acct
]:
704 self
.remove_tab(self
._controls
[acct
][new_jid
],
705 self
.CLOSE_CLOSE_BUTTON
, force
=True)
707 self
._controls
[acct
][new_jid
] = ctrl
708 del self
._controls
[acct
][old_jid
]
710 if old_jid
in gajim
.last_message_time
[acct
]:
711 gajim
.last_message_time
[acct
][new_jid
] = \
712 gajim
.last_message_time
[acct
][old_jid
]
713 del gajim
.last_message_time
[acct
][old_jid
]
716 for jid_dict
in self
._controls
.values():
717 for ctrl
in jid_dict
.values():
720 def get_nb_controls(self
):
721 return sum(len(jid_dict
) for jid_dict
in self
._controls
.values())
723 def move_to_next_unread_tab(self
, forward
):
724 ind
= self
.notebook
.get_current_page()
727 first_composing_ind
= -1 # id of first composing ctrl to switch to
728 # if no others controls have awaiting events
729 # loop until finding an unread tab or having done a complete cycle
731 if forward
== True: # look for the first unread tab on the right
733 if ind
>= self
.notebook
.get_n_pages():
735 else: # look for the first unread tab on the right
738 ind
= self
.notebook
.get_n_pages() - 1
739 ctrl
= self
.get_control(ind
, None)
740 if ctrl
.get_nb_unread() > 0:
743 elif gajim
.config
.get('ctrl_tab_go_to_next_composing') :
744 # Search for a composing contact
745 contact
= ctrl
.contact
746 if first_composing_ind
== -1 and contact
.chatstate
== 'composing':
747 # If no composing contact found yet, check if this one is composing
748 first_composing_ind
= ind
750 break # a complete cycle without finding an unread tab
752 self
.notebook
.set_current_page(ind
)
753 elif first_composing_ind
!= -1:
754 self
.notebook
.set_current_page(first_composing_ind
)
755 else: # not found and nobody composing
756 if forward
: # CTRL + TAB
757 if current
< (self
.notebook
.get_n_pages() - 1):
758 self
.notebook
.next_page()
759 else: # traverse for ever (eg. don't stop at last tab)
760 self
.notebook
.set_current_page(0)
761 else: # CTRL + SHIFT + TAB
763 self
.notebook
.prev_page()
764 else: # traverse for ever (eg. don't stop at first tab)
765 self
.notebook
.set_current_page(
766 self
.notebook
.get_n_pages() - 1)
768 def popup_menu(self
, event
):
769 menu
= self
.get_active_control().prepare_context_menu()
771 menu
.popup(None, None, None, event
.button
, event
.time
)
774 def _on_notebook_switch_page(self
, notebook
, page
, page_num
):
775 old_no
= notebook
.get_current_page()
777 old_ctrl
= self
._widget
_to
_control
(notebook
.get_nth_page(old_no
))
778 old_ctrl
.set_control_active(False)
780 new_ctrl
= self
._widget
_to
_control
(notebook
.get_nth_page(page_num
))
781 new_ctrl
.set_control_active(True)
782 self
.show_title(control
= new_ctrl
)
784 control
= self
.get_active_control()
785 if isinstance(control
, ChatControlBase
):
786 control
.msg_textview
.grab_focus()
788 def _on_notebook_key_press(self
, widget
, event
):
789 # when tab itself is selected,
790 # make sure <- and -> are allowed for navigating between tabs
791 if event
.keyval
in (gtk
.keysyms
.Left
, gtk
.keysyms
.Right
):
794 control
= self
.get_active_control()
796 if event
.state
& gtk
.gdk
.SHIFT_MASK
:
798 if event
.state
& gtk
.gdk
.CONTROL_MASK
and \
799 event
.keyval
== gtk
.keysyms
.ISO_Left_Tab
:
800 self
.move_to_next_unread_tab(False)
802 # SHIFT + PAGE_[UP|DOWN]: send to conv_textview
803 elif event
.keyval
in (gtk
.keysyms
.Page_Down
, gtk
.keysyms
.Page_Up
):
804 control
.conv_textview
.tv
.emit('key_press_event', event
)
806 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
807 if event
.keyval
== gtk
.keysyms
.Tab
: # CTRL + TAB
808 self
.move_to_next_unread_tab(True)
810 # Ctrl+PageUP / DOWN has to be handled by notebook
811 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
812 self
.move_to_next_unread_tab(True)
814 elif event
.keyval
== gtk
.keysyms
.Page_Up
:
815 self
.move_to_next_unread_tab(False)
817 if event
.keyval
in (gtk
.keysyms
.Shift_L
, gtk
.keysyms
.Shift_R
,
818 gtk
.keysyms
.Control_L
, gtk
.keysyms
.Control_R
, gtk
.keysyms
.Caps_Lock
,
819 gtk
.keysyms
.Shift_Lock
, gtk
.keysyms
.Meta_L
, gtk
.keysyms
.Meta_R
,
820 gtk
.keysyms
.Alt_L
, gtk
.keysyms
.Alt_R
, gtk
.keysyms
.Super_L
,
821 gtk
.keysyms
.Super_R
, gtk
.keysyms
.Hyper_L
, gtk
.keysyms
.Hyper_R
):
824 if isinstance(control
, ChatControlBase
):
825 # we forwarded it to message textview
826 control
.msg_textview
.emit('key_press_event', event
)
827 control
.msg_textview
.grab_focus()
829 def get_tab_at_xy(self
, x
, y
):
831 Return the tab under xy and if its nearer from left or right side of the
836 horiz
= self
.notebook
.get_tab_pos() == gtk
.POS_TOP
or \
837 self
.notebook
.get_tab_pos() == gtk
.POS_BOTTOM
838 for i
in xrange(self
.notebook
.get_n_pages()):
839 page
= self
.notebook
.get_nth_page(i
)
840 tab
= self
.notebook
.get_tab_label(page
)
841 tab_alloc
= tab
.get_allocation()
843 if (x
>= tab_alloc
.x
) and \
844 (x
<= (tab_alloc
.x
+ tab_alloc
.width
)):
846 if x
>= tab_alloc
.x
+ (tab_alloc
.width
/ 2.0):
850 if (y
>= tab_alloc
.y
) and \
851 (y
<= (tab_alloc
.y
+ tab_alloc
.height
)):
854 if y
> tab_alloc
.y
+ (tab_alloc
.height
/ 2.0):
857 return (page_num
, to_right
)
859 def find_page_num_according_to_tab_label(self
, tab_label
):
861 Find the page num of the tab label
864 for i
in xrange(self
.notebook
.get_n_pages()):
865 page
= self
.notebook
.get_nth_page(i
)
866 tab
= self
.notebook
.get_tab_label(page
)
872 ################################################################################
873 class MessageWindowMgr(gobject
.GObject
):
875 A manager and factory for MessageWindow objects
879 'window-delete': (gobject
.SIGNAL_RUN_LAST
, None, (object,)),
882 # These constants map to common.config.opt_one_window_types indices
884 ONE_MSG_WINDOW_NEVER
,
885 ONE_MSG_WINDOW_ALWAYS
,
886 ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
,
887 ONE_MSG_WINDOW_PERACCT
,
888 ONE_MSG_WINDOW_PERTYPE
,
890 # A key constant for the main window in ONE_MSG_WINDOW_ALWAYS mode
892 # A key constant for the main window in ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER mode
893 ROSTER_MAIN_WIN
= 'roster'
895 def __init__(self
, parent_window
, parent_paned
):
897 A dictionary of windows; the key depends on the config:
898 ONE_MSG_WINDOW_NEVER: The key is the contact JID
899 ONE_MSG_WINDOW_ALWAYS: The key is MessageWindowMgr.MAIN_WIN
900 ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER: The key is MessageWindowMgr.MAIN_WIN
901 ONE_MSG_WINDOW_PERACCT: The key is the account name
902 ONE_MSG_WINDOW_PERTYPE: The key is a message type constant
904 gobject
.GObject
.__init
__(self
)
907 # Map the mode to a int constant for frequent compares
908 mode
= gajim
.config
.get('one_message_window')
909 self
.mode
= common
.config
.opt_one_window_types
.index(mode
)
911 self
.parent_win
= parent_window
912 self
.parent_paned
= parent_paned
914 def change_account_name(self
, old_name
, new_name
):
915 for win
in self
.windows():
916 win
.change_account_name(old_name
, new_name
)
918 def _new_window(self
, acct
, type_
):
921 if self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
922 parent_win
= self
.parent_win
923 parent_paned
= self
.parent_paned
924 win
= MessageWindow(acct
, type_
, parent_win
, parent_paned
)
925 # we track the lifetime of this window
926 win
.window
.connect('delete-event', self
._on
_window
_delete
)
927 win
.window
.connect('destroy', self
._on
_window
_destroy
)
930 def _gtk_win_to_msg_win(self
, gtk_win
):
931 for w
in self
.windows():
932 if w
.window
== gtk_win
:
936 def get_window(self
, jid
, acct
):
937 for win
in self
.windows():
938 if win
.has_control(jid
, acct
):
943 def has_window(self
, jid
, acct
):
944 return self
.get_window(jid
, acct
) is not None
946 def one_window_opened(self
, contact
=None, acct
=None, type_
=None):
949 self
._windows
[self
._mode
_to
_key
(contact
, acct
, type_
)] is not None
953 def _resize_window(self
, win
, acct
, type_
):
955 Resizes window according to config settings
957 if self
.mode
in (self
.ONE_MSG_WINDOW_ALWAYS
,
958 self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
):
959 size
= (gajim
.config
.get('msgwin-width'),
960 gajim
.config
.get('msgwin-height'))
961 if self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
962 parent_size
= win
.window
.get_size()
963 # Need to add the size of the now visible paned handle, otherwise
964 # the saved width of the message window decreases by this amount
965 handle_size
= win
.parent_paned
.style_get_property('handle-size')
966 size
= (parent_size
[0] + size
[0] + handle_size
, size
[1])
967 elif self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
968 size
= (gajim
.config
.get_per('accounts', acct
, 'msgwin-width'),
969 gajim
.config
.get_per('accounts', acct
, 'msgwin-height'))
970 elif self
.mode
in (self
.ONE_MSG_WINDOW_NEVER
, self
.ONE_MSG_WINDOW_PERTYPE
):
971 if type_
== message_control
.TYPE_PM
:
972 type_
= message_control
.TYPE_CHAT
973 opt_width
= type_
+ '-msgwin-width'
974 opt_height
= type_
+ '-msgwin-height'
975 size
= (gajim
.config
.get(opt_width
), gajim
.config
.get(opt_height
))
978 win
.resize(size
[0], size
[1])
980 win
.parent_paned
.set_position(parent_size
[0])
982 def _position_window(self
, win
, acct
, type_
):
984 Moves window according to config settings
986 if (self
.mode
in [self
.ONE_MSG_WINDOW_NEVER
,
987 self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
]):
990 if self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS
:
991 pos
= (gajim
.config
.get('msgwin-x-position'),
992 gajim
.config
.get('msgwin-y-position'))
993 elif self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
994 pos
= (gajim
.config
.get_per('accounts', acct
, 'msgwin-x-position'),
995 gajim
.config
.get_per('accounts', acct
, 'msgwin-y-position'))
996 elif self
.mode
== self
.ONE_MSG_WINDOW_PERTYPE
:
997 pos
= (gajim
.config
.get(type_
+ '-msgwin-x-position'),
998 gajim
.config
.get(type_
+ '-msgwin-y-position'))
1002 gtkgui_helpers
.move_window(win
.window
, pos
[0], pos
[1])
1004 def _mode_to_key(self
, contact
, acct
, type_
, resource
= None):
1005 if self
.mode
== self
.ONE_MSG_WINDOW_NEVER
:
1006 key
= acct
+ contact
.jid
1008 key
+= '/' + resource
1010 elif self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS
:
1011 return self
.MAIN_WIN
1012 elif self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
1013 return self
.ROSTER_MAIN_WIN
1014 elif self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
1016 elif self
.mode
== self
.ONE_MSG_WINDOW_PERTYPE
:
1019 def create_window(self
, contact
, acct
, type_
, resource
= None):
1022 win_role
= None # X11 window role
1024 win_key
= self
._mode
_to
_key
(contact
, acct
, type_
, resource
)
1025 if self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
1028 elif self
.mode
== self
.ONE_MSG_WINDOW_PERTYPE
:
1031 elif self
.mode
== self
.ONE_MSG_WINDOW_NEVER
:
1033 win_role
= contact
.jid
1034 elif self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS
:
1035 win_role
= 'messages'
1039 win
= self
._windows
[win_key
]
1041 win
= self
._new
_window
(win_acct
, win_type
)
1044 win
.window
.set_role(win_role
)
1046 # Position and size window based on saved state and window mode
1047 if not self
.one_window_opened(contact
, acct
, type_
):
1048 if gajim
.config
.get('msgwin-max-state'):
1049 win
.window
.maximize()
1051 self
._resize
_window
(win
, acct
, type_
)
1052 self
._position
_window
(win
, acct
, type_
)
1054 self
._windows
[win_key
] = win
1057 def change_key(self
, old_jid
, new_jid
, acct
):
1058 win
= self
.get_window(old_jid
, acct
)
1059 if self
.mode
== self
.ONE_MSG_WINDOW_NEVER
:
1060 old_key
= acct
+ old_jid
1061 if old_jid
not in self
._windows
:
1063 new_key
= acct
+ new_jid
1064 self
._windows
[new_key
] = self
._windows
[old_key
]
1065 del self
._windows
[old_key
]
1066 win
.change_key(old_jid
, new_jid
, acct
)
1068 def _on_window_delete(self
, win
, event
):
1069 self
.save_state(self
._gtk
_win
_to
_msg
_win
(win
))
1070 gajim
.interface
.save_config()
1073 def _on_window_destroy(self
, win
):
1074 for k
in self
._windows
.keys():
1075 if self
._windows
[k
].window
== win
:
1076 self
.emit('window-delete', self
._windows
[k
])
1077 del self
._windows
[k
]
1080 def get_control(self
, jid
, acct
):
1082 Amongst all windows, return the MessageControl for jid
1084 win
= self
.get_window(jid
, acct
)
1086 return win
.get_control(jid
, acct
)
1089 def get_gc_control(self
, jid
, acct
):
1091 Same as get_control. Was briefly required, is not any more. May be useful
1092 some day in the future?
1094 ctrl
= self
.get_control(jid
, acct
)
1095 if ctrl
and ctrl
.type_id
== message_control
.TYPE_GC
:
1099 def get_controls(self
, type_
=None, acct
=None):
1101 for c
in self
.controls():
1102 if acct
and c
.account
!= acct
:
1104 if not type_
or c
.type_id
== type_
:
1109 for w
in self
._windows
.values():
1113 for w
in self
._windows
.values():
1114 for c
in w
.controls():
1117 def shutdown(self
, width_adjust
=0):
1118 for w
in self
.windows():
1119 self
.save_state(w
, width_adjust
)
1120 if not w
.parent_paned
:
1124 gajim
.interface
.save_config()
1126 def save_state(self
, msg_win
, width_adjust
=0):
1127 # Save window size and position
1128 max_win_key
= 'msgwin-max-state'
1129 pos_x_key
= 'msgwin-x-position'
1130 pos_y_key
= 'msgwin-y-position'
1131 size_width_key
= 'msgwin-width'
1132 size_height_key
= 'msgwin-height'
1135 x
, y
= msg_win
.window
.get_position()
1136 width
, height
= msg_win
.window
.get_size()
1138 # If any of these values seem bogus don't update.
1139 if x
< 0 or y
< 0 or width
< 0 or height
< 0:
1142 elif self
.mode
== self
.ONE_MSG_WINDOW_PERACCT
:
1143 acct
= msg_win
.account
1144 elif self
.mode
== self
.ONE_MSG_WINDOW_PERTYPE
:
1145 type_
= msg_win
.type_
1146 pos_x_key
= type_
+ '-msgwin-x-position'
1147 pos_y_key
= type_
+ '-msgwin-y-position'
1148 size_width_key
= type_
+ '-msgwin-width'
1149 size_height_key
= type_
+ '-msgwin-height'
1150 elif self
.mode
== self
.ONE_MSG_WINDOW_NEVER
:
1151 type_
= msg_win
.type_
1152 size_width_key
= type_
+ '-msgwin-width'
1153 size_height_key
= type_
+ '-msgwin-height'
1154 elif self
.mode
== self
.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER
:
1155 # Ignore any hpaned width
1156 width
= msg_win
.notebook
.allocation
.width
1159 gajim
.config
.set_per('accounts', acct
, size_width_key
, width
)
1160 gajim
.config
.set_per('accounts', acct
, size_height_key
, height
)
1162 if self
.mode
!= self
.ONE_MSG_WINDOW_NEVER
:
1163 gajim
.config
.set_per('accounts', acct
, pos_x_key
, x
)
1164 gajim
.config
.set_per('accounts', acct
, pos_y_key
, y
)
1167 win_maximized
= msg_win
.window
.window
.get_state() == \
1168 gtk
.gdk
.WINDOW_STATE_MAXIMIZED
1169 gajim
.config
.set(max_win_key
, win_maximized
)
1170 width
+= width_adjust
1171 gajim
.config
.set(size_width_key
, width
)
1172 gajim
.config
.set(size_height_key
, height
)
1174 if self
.mode
!= self
.ONE_MSG_WINDOW_NEVER
:
1175 gajim
.config
.set(pos_x_key
, x
)
1176 gajim
.config
.set(pos_y_key
, y
)
1179 for w
in self
.windows():
1181 gajim
.interface
.save_config()
1182 mode
= gajim
.config
.get('one_message_window')
1183 if self
.mode
== common
.config
.opt_one_window_types
.index(mode
):
1186 self
.mode
= common
.config
.opt_one_window_types
.index(mode
)
1189 for w
in self
.windows():
1190 # Note, we are taking care not to hide/delete the roster window when the
1191 # MessageWindow is embedded.
1192 if not w
.parent_paned
:
1195 # Stash current size so it can be restored if the MessageWindow
1196 # is not longer embedded
1197 roster_width
= w
.parent_paned
.get_child1().allocation
.width
1198 gajim
.config
.set('roster_width', roster_width
)
1200 while w
.notebook
.get_n_pages():
1201 page
= w
.notebook
.get_nth_page(0)
1202 ctrl
= w
._widget
_to
_control
(page
)
1203 w
.notebook
.remove_page(0)
1205 controls
.append(ctrl
)
1207 # Must clear _controls to prevent MessageControl.shutdown calls
1209 if not w
.parent_paned
:
1212 # Don't close parent window, just remove the child
1213 child
= w
.parent_paned
.get_child2()
1214 w
.parent_paned
.remove(child
)
1215 gtkgui_helpers
.resize_window(w
.window
,
1216 gajim
.config
.get('roster_width'),
1217 gajim
.config
.get('roster_height'))
1221 for ctrl
in controls
:
1222 mw
= self
.get_window(ctrl
.contact
.jid
, ctrl
.account
)
1224 mw
= self
.create_window(ctrl
.contact
, ctrl
.account
,
1226 ctrl
.parent_win
= mw