| 1 | ## tabbed_chat_window.py |
|---|
| 2 | ## |
|---|
| 3 | ## Contributors for this file: |
|---|
| 4 | ## - Yann Le Boulanger <asterix@lagaule.org> |
|---|
| 5 | ## - Nikos Kouremenos <kourem@gmail.com> |
|---|
| 6 | ## - Travis Shirk <travis@pobox.com> |
|---|
| 7 | ## |
|---|
| 8 | ## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> |
|---|
| 9 | ## Vincent Hanquez <tab@snarc.org> |
|---|
| 10 | ## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> |
|---|
| 11 | ## Vincent Hanquez <tab@snarc.org> |
|---|
| 12 | ## Nikos Kouremenos <nkour@jabber.org> |
|---|
| 13 | ## Dimitur Kirov <dkirov@gmail.com> |
|---|
| 14 | ## Travis Shirk <travis@pobox.com> |
|---|
| 15 | ## Norman Rasmussen <norman@rasmussen.co.za> |
|---|
| 16 | ## |
|---|
| 17 | ## This program 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 2 only. |
|---|
| 20 | ## |
|---|
| 21 | ## This program 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. |
|---|
| 25 | ## |
|---|
| 26 | |
|---|
| 27 | import gtk |
|---|
| 28 | import gtk.glade |
|---|
| 29 | import pango |
|---|
| 30 | import gobject |
|---|
| 31 | import time |
|---|
| 32 | import os |
|---|
| 33 | |
|---|
| 34 | import dialogs |
|---|
| 35 | import chat |
|---|
| 36 | import gtkgui_helpers |
|---|
| 37 | |
|---|
| 38 | from common import gajim |
|---|
| 39 | from common import helpers |
|---|
| 40 | from common.logger import Constants |
|---|
| 41 | constants = Constants() |
|---|
| 42 | |
|---|
| 43 | from common import i18n |
|---|
| 44 | |
|---|
| 45 | _ = i18n._ |
|---|
| 46 | APP = i18n.APP |
|---|
| 47 | gtk.glade.bindtextdomain(APP, i18n.DIR) |
|---|
| 48 | gtk.glade.textdomain(APP) |
|---|
| 49 | |
|---|
| 50 | GTKGUI_GLADE = 'gtkgui.glade' |
|---|
| 51 | |
|---|
| 52 | class TabbedChatWindow(chat.Chat): |
|---|
| 53 | '''Class for tabbed chat window''' |
|---|
| 54 | def __init__(self, contact, account): |
|---|
| 55 | # we check that on opening new windows |
|---|
| 56 | self.always_compact_view = gajim.config.get('always_compact_view_chat') |
|---|
| 57 | chat.Chat.__init__(self, account, 'tabbed_chat_window') |
|---|
| 58 | self.contacts = {} |
|---|
| 59 | # keep check for possible paused timeouts per jid |
|---|
| 60 | self.possible_paused_timeout_id = {} |
|---|
| 61 | # keep check for possible inactive timeouts per jid |
|---|
| 62 | self.possible_inactive_timeout_id = {} |
|---|
| 63 | |
|---|
| 64 | # keep timeout id and window obj for possible big avatar |
|---|
| 65 | # it is on enter-notify and leave-notify so no need to be per jid |
|---|
| 66 | self.show_bigger_avatar_timeout_id = None |
|---|
| 67 | self.bigger_avatar_window = None |
|---|
| 68 | |
|---|
| 69 | # list that holds all the jid we have asked vcard once |
|---|
| 70 | # (so we do not have to ask again) |
|---|
| 71 | self.jids_for_which_we_asked_vcard_already = list() |
|---|
| 72 | |
|---|
| 73 | self.TARGET_TYPE_URI_LIST = 80 |
|---|
| 74 | self.dnd_list = [ ( 'text/uri-list', 0, self.TARGET_TYPE_URI_LIST ) ] |
|---|
| 75 | self.new_tab(contact) |
|---|
| 76 | self.show_title() |
|---|
| 77 | |
|---|
| 78 | # NOTE: if it not a window event, do not connect here (new_tab() autoconnects) |
|---|
| 79 | signal_dict = { |
|---|
| 80 | 'on_tabbed_chat_window_destroy': self.on_tabbed_chat_window_destroy, |
|---|
| 81 | 'on_tabbed_chat_window_delete_event': self.on_tabbed_chat_window_delete_event, |
|---|
| 82 | 'on_tabbed_chat_window_focus_in_event': self.on_tabbed_chat_window_focus_in_event, |
|---|
| 83 | 'on_chat_notebook_key_press_event': self.on_chat_notebook_key_press_event, |
|---|
| 84 | 'on_chat_notebook_switch_page': self.on_chat_notebook_switch_page, # in chat.py |
|---|
| 85 | 'on_tabbed_chat_window_motion_notify_event': self.on_tabbed_chat_window_motion_notify_event, |
|---|
| 86 | } |
|---|
| 87 | |
|---|
| 88 | self.xml.signal_autoconnect(signal_dict) |
|---|
| 89 | |
|---|
| 90 | |
|---|
| 91 | if gajim.config.get('saveposition') and \ |
|---|
| 92 | not gtkgui_helpers.one_window_opened('chats'): |
|---|
| 93 | # get window position and size from config |
|---|
| 94 | gtkgui_helpers.move_window(self.window, gajim.config.get('chat-x-position'), |
|---|
| 95 | gajim.config.get('chat-y-position')) |
|---|
| 96 | gtkgui_helpers.resize_window(self.window, gajim.config.get('chat-width'), |
|---|
| 97 | gajim.config.get('chat-height')) |
|---|
| 98 | |
|---|
| 99 | # gtk+ doesn't make use of the motion notify on gtkwindow by default |
|---|
| 100 | # so this line adds that |
|---|
| 101 | self.window.set_events(gtk.gdk.POINTER_MOTION_MASK) |
|---|
| 102 | |
|---|
| 103 | self.window.show_all() |
|---|
| 104 | |
|---|
| 105 | def save_var(self, jid): |
|---|
| 106 | '''return the specific variable of a jid, like gpg_enabled |
|---|
| 107 | the return value have to be compatible with wthe one given to load_var''' |
|---|
| 108 | gpg_enabled = self.xmls[jid].get_widget('gpg_togglebutton').get_active() |
|---|
| 109 | return {'gpg_enabled': gpg_enabled} |
|---|
| 110 | |
|---|
| 111 | def load_var(self, jid, var): |
|---|
| 112 | if not self.xmls.has_key(jid): |
|---|
| 113 | return |
|---|
| 114 | self.xmls[jid].get_widget('gpg_togglebutton').set_active( |
|---|
| 115 | var['gpg_enabled']) |
|---|
| 116 | |
|---|
| 117 | def on_tabbed_chat_window_motion_notify_event(self, widget, event): |
|---|
| 118 | '''it gets called no matter if it is the active window or not''' |
|---|
| 119 | if widget.get_property('has-toplevel-focus'): |
|---|
| 120 | # change chatstate only if window is the active one |
|---|
| 121 | self.mouse_over_in_last_5_secs = True |
|---|
| 122 | self.mouse_over_in_last_30_secs = True |
|---|
| 123 | |
|---|
| 124 | def on_drag_data_received(self, widget, context, x, y, selection, |
|---|
| 125 | target_type, timestamp, contact): |
|---|
| 126 | # If not resource, we can't send file |
|---|
| 127 | if not contact.resource: |
|---|
| 128 | return |
|---|
| 129 | if target_type == self.TARGET_TYPE_URI_LIST: |
|---|
| 130 | uri = selection.data.strip() |
|---|
| 131 | uri_splitted = uri.split() # we may have more than one file dropped |
|---|
| 132 | for uri in uri_splitted: |
|---|
| 133 | path = helpers.get_file_path_from_dnd_dropped_uri(uri) |
|---|
| 134 | if os.path.isfile(path): # is it file? |
|---|
| 135 | gajim.interface.instances['file_transfers'].send_file(self.account, |
|---|
| 136 | contact, path) |
|---|
| 137 | |
|---|
| 138 | def on_avatar_eventbox_enter_notify_event(self, widget, event): |
|---|
| 139 | '''we enter the eventbox area so we under conditions add a timeout |
|---|
| 140 | to show a bigger avatar after 0.5 sec''' |
|---|
| 141 | jid = self.get_active_jid() |
|---|
| 142 | real_jid = gajim.get_real_jid_from_fjid(self.account, jid) |
|---|
| 143 | if not real_jid: # this can happend if we're in a moderate room |
|---|
| 144 | return |
|---|
| 145 | avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(real_jid) |
|---|
| 146 | if avatar_pixbuf in ('ask', None): |
|---|
| 147 | return |
|---|
| 148 | avatar_w = avatar_pixbuf.get_width() |
|---|
| 149 | avatar_h = avatar_pixbuf.get_height() |
|---|
| 150 | |
|---|
| 151 | scaled_buf = self.xmls[jid].get_widget('avatar_image').get_pixbuf() |
|---|
| 152 | scaled_buf_w = scaled_buf.get_width() |
|---|
| 153 | scaled_buf_h = scaled_buf.get_height() |
|---|
| 154 | |
|---|
| 155 | # do we have something bigger to show? |
|---|
| 156 | if avatar_w > scaled_buf_w or avatar_h > scaled_buf_h: |
|---|
| 157 | # wait for 0.5 sec in case we leave earlier |
|---|
| 158 | self.show_bigger_avatar_timeout_id = gobject.timeout_add(500, |
|---|
| 159 | self.show_bigger_avatar, widget) |
|---|
| 160 | |
|---|
| 161 | def on_avatar_eventbox_leave_notify_event(self, widget, event): |
|---|
| 162 | '''we left the eventbox area that holds the avatar img''' |
|---|
| 163 | # did we add a timeout? if yes remove it |
|---|
| 164 | if self.show_bigger_avatar_timeout_id is not None: |
|---|
| 165 | gobject.source_remove(self.show_bigger_avatar_timeout_id) |
|---|
| 166 | |
|---|
| 167 | def show_bigger_avatar(self, small_avatar): |
|---|
| 168 | '''resizes the avatar, if needed, so it has at max half the screen size |
|---|
| 169 | and shows it''' |
|---|
| 170 | jid = self.get_active_jid() |
|---|
| 171 | real_jid = gajim.get_real_jid_from_fjid(self.account, jid) |
|---|
| 172 | if not real_jid: # this can happend if we're in a moderate room |
|---|
| 173 | return |
|---|
| 174 | avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(real_jid) |
|---|
| 175 | screen_w = gtk.gdk.screen_width() |
|---|
| 176 | screen_h = gtk.gdk.screen_height() |
|---|
| 177 | avatar_w = avatar_pixbuf.get_width() |
|---|
| 178 | avatar_h = avatar_pixbuf.get_height() |
|---|
| 179 | half_scr_w = screen_w / 2 |
|---|
| 180 | half_scr_h = screen_h / 2 |
|---|
| 181 | if avatar_w > half_scr_w: |
|---|
| 182 | avatar_w = half_scr_w |
|---|
| 183 | if avatar_h > half_scr_h: |
|---|
| 184 | avatar_h = half_scr_h |
|---|
| 185 | window = gtk.Window(gtk.WINDOW_POPUP) |
|---|
| 186 | self.bigger_avatar_window = window |
|---|
| 187 | pixmap, mask = avatar_pixbuf.render_pixmap_and_mask() |
|---|
| 188 | window.set_size_request(avatar_w, avatar_h) |
|---|
| 189 | # we should make the cursor visible |
|---|
| 190 | # gtk+ doesn't make use of the motion notify on gtkwindow by default |
|---|
| 191 | # so this line adds that |
|---|
| 192 | window.set_events(gtk.gdk.POINTER_MOTION_MASK) |
|---|
| 193 | window.set_app_paintable(True) |
|---|
| 194 | |
|---|
| 195 | window.realize() |
|---|
| 196 | window.window.set_back_pixmap(pixmap, False) # make it transparent |
|---|
| 197 | window.window.shape_combine_mask(mask, 0, 0) |
|---|
| 198 | |
|---|
| 199 | # make the bigger avatar window show up centered |
|---|
| 200 | x0, y0 = small_avatar.window.get_origin() |
|---|
| 201 | x0 += small_avatar.allocation.x |
|---|
| 202 | y0 += small_avatar.allocation.y |
|---|
| 203 | center_x= x0 + (small_avatar.allocation.width / 2) |
|---|
| 204 | center_y = y0 + (small_avatar.allocation.height / 2) |
|---|
| 205 | pos_x, pos_y = center_x - (avatar_w / 2), center_y - (avatar_h / 2) |
|---|
| 206 | window.move(pos_x, pos_y) |
|---|
| 207 | # make the cursor invisible so we can see the image |
|---|
| 208 | invisible_cursor = gtkgui_helpers.get_invisible_cursor() |
|---|
| 209 | window.window.set_cursor(invisible_cursor) |
|---|
| 210 | |
|---|
| 211 | # we should hide the window |
|---|
| 212 | window.connect('leave_notify_event', |
|---|
| 213 | self.on_window_avatar_leave_notify_event) |
|---|
| 214 | window.connect('motion-notify-event', |
|---|
| 215 | self.on_window_motion_notify_event) |
|---|
| 216 | |
|---|
| 217 | window.show_all() |
|---|
| 218 | |
|---|
| 219 | def on_window_avatar_leave_notify_event(self, widget, event): |
|---|
| 220 | '''we just left the popup window that holds avatar''' |
|---|
| 221 | self.bigger_avatar_window.destroy() |
|---|
| 222 | |
|---|
| 223 | def on_window_motion_notify_event(self, widget, event): |
|---|
| 224 | '''we just moved the mouse so show the cursor''' |
|---|
| 225 | cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR) |
|---|
| 226 | self.bigger_avatar_window.window.set_cursor(cursor) |
|---|
| 227 | |
|---|
| 228 | def draw_widgets(self, contact): |
|---|
| 229 | '''draw the widgets in a tab (f.e. gpg togglebutton) |
|---|
| 230 | according to the the information in the contact variable''' |
|---|
| 231 | jid = contact.jid |
|---|
| 232 | self.set_state_image(jid) |
|---|
| 233 | tb = self.xmls[jid].get_widget('gpg_togglebutton') |
|---|
| 234 | if contact.keyID: # we can do gpg |
|---|
| 235 | tb.set_sensitive(True) |
|---|
| 236 | tt = _('OpenPGP Encryption') |
|---|
| 237 | else: |
|---|
| 238 | tb.set_sensitive(False) |
|---|
| 239 | #we talk about a contact here |
|---|
| 240 | tt = _('%s has not broadcasted an OpenPGP key nor you have assigned one') % contact.name |
|---|
| 241 | tip = gtk.Tooltips() |
|---|
| 242 | tip.set_tip(self.xmls[jid].get_widget('gpg_eventbox'), tt) |
|---|
| 243 | |
|---|
| 244 | # add the fat line at the top |
|---|
| 245 | self.draw_name_banner(contact) |
|---|
| 246 | |
|---|
| 247 | def draw_name_banner(self, contact, chatstate = None): |
|---|
| 248 | '''Draw the fat line at the top of the window that |
|---|
| 249 | houses the status icon, name, jid, and avatar''' |
|---|
| 250 | # this is the text for the big brown bar |
|---|
| 251 | # some chars need to be escaped.. |
|---|
| 252 | jid = contact.jid |
|---|
| 253 | banner_name_label = self.xmls[jid].get_widget('banner_name_label') |
|---|
| 254 | |
|---|
| 255 | name = gtkgui_helpers.escape_for_pango_markup(contact.name) |
|---|
| 256 | |
|---|
| 257 | status = contact.status |
|---|
| 258 | |
|---|
| 259 | if status is not None: |
|---|
| 260 | banner_name_label.set_ellipsize(pango.ELLIPSIZE_END) |
|---|
| 261 | status = gtkgui_helpers.reduce_chars_newlines(status, 0, 2) |
|---|
| 262 | |
|---|
| 263 | status = gtkgui_helpers.escape_for_pango_markup(status) |
|---|
| 264 | |
|---|
| 265 | #FIXME: uncomment me when we support sending messages to specific resource |
|---|
| 266 | # composing full jid |
|---|
| 267 | #fulljid = jid |
|---|
| 268 | #if self.contacts[jid].resource: |
|---|
| 269 | # fulljid += '/' + self.contacts[jid].resource |
|---|
| 270 | #label_text = '<span weight="heavy" size="x-large">%s</span>\n%s' \ |
|---|
| 271 | # % (name, fulljid) |
|---|
| 272 | |
|---|
| 273 | |
|---|
| 274 | st = gajim.config.get('chat_state_notifications') |
|---|
| 275 | if contact.chatstate and st in ('composing_only', 'all'): |
|---|
| 276 | if contact.show == 'offline': |
|---|
| 277 | chatstate = '' |
|---|
| 278 | elif st == 'all': |
|---|
| 279 | chatstate = helpers.get_uf_chatstate(contact.chatstate) |
|---|
| 280 | else: # 'composing_only' |
|---|
| 281 | if chatstate in ('composing', 'paused'): |
|---|
| 282 | # only print composing, paused |
|---|
| 283 | chatstate = helpers.get_uf_chatstate(contact.chatstate) |
|---|
| 284 | else: |
|---|
| 285 | chatstate = '' |
|---|
| 286 | label_text = \ |
|---|
| 287 | '<span weight="heavy" size="x-large">%s</span> %s' % (name, chatstate) |
|---|
| 288 | else: |
|---|
| 289 | label_text = '<span weight="heavy" size="x-large">%s</span>' % name |
|---|
| 290 | |
|---|
| 291 | if status is not None: |
|---|
| 292 | label_text += '\n%s' % status |
|---|
| 293 | |
|---|
| 294 | # setup the label that holds name and jid |
|---|
| 295 | banner_name_label.set_markup(label_text) |
|---|
| 296 | self.paint_banner(jid) |
|---|
| 297 | |
|---|
| 298 | def get_specific_unread(self, jid): |
|---|
| 299 | # return the number of unread (private) msgs with contacts in the room |
|---|
| 300 | # when gc, and that is 0 in tc |
|---|
| 301 | # FIXME: maybe refactor so this func is not called at all if TC? |
|---|
| 302 | return 0 |
|---|
| 303 | |
|---|
| 304 | def show_avatar(self, jid, resource): |
|---|
| 305 | # Get the XML instance |
|---|
| 306 | jid_with_resource = jid |
|---|
| 307 | if resource: |
|---|
| 308 | jid_with_resource += '/' + resource |
|---|
| 309 | |
|---|
| 310 | xml = None |
|---|
| 311 | if self.xmls.has_key(jid): |
|---|
| 312 | xml = self.xmls[jid] |
|---|
| 313 | else: |
|---|
| 314 | # it can be xmls[jid/resource] if it's a vcard from pm |
|---|
| 315 | if self.xmls.has_key(jid_with_resource): |
|---|
| 316 | xml = self.xmls[jid_with_resource] |
|---|
| 317 | if not xml: |
|---|
| 318 | return |
|---|
| 319 | |
|---|
| 320 | # we assume contact has no avatar |
|---|
| 321 | scaled_pixbuf = None |
|---|
| 322 | |
|---|
| 323 | real_jid = gajim.get_real_jid_from_fjid(self.account, jid) |
|---|
| 324 | pixbuf = None |
|---|
| 325 | if real_jid: |
|---|
| 326 | pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(real_jid) |
|---|
| 327 | if not real_jid or pixbuf == 'ask': |
|---|
| 328 | # we don't have the vcard or it's pm and we don't have the real jid |
|---|
| 329 | gajim.connections[self.account].request_vcard(jid_with_resource) |
|---|
| 330 | return |
|---|
| 331 | if pixbuf is not None: |
|---|
| 332 | scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'chat') |
|---|
| 333 | |
|---|
| 334 | |
|---|
| 335 | image = xml.get_widget('avatar_image') |
|---|
| 336 | image.set_from_pixbuf(scaled_pixbuf) |
|---|
| 337 | image.show_all() |
|---|
| 338 | |
|---|
| 339 | def set_state_image(self, jid): |
|---|
| 340 | prio = 0 |
|---|
| 341 | if gajim.contacts[self.account].has_key(jid): |
|---|
| 342 | contacts_list = gajim.contacts[self.account][jid] |
|---|
| 343 | else: |
|---|
| 344 | contacts_list = [self.contacts[jid]] |
|---|
| 345 | |
|---|
| 346 | contact = contacts_list[0] |
|---|
| 347 | show = contact.show |
|---|
| 348 | jid = contact.jid |
|---|
| 349 | keyID = contact.keyID |
|---|
| 350 | |
|---|
| 351 | for u in contacts_list: |
|---|
| 352 | if u.priority > prio: |
|---|
| 353 | prio = u.priority |
|---|
| 354 | show = u.show |
|---|
| 355 | keyID = u.keyID |
|---|
| 356 | child = self.childs[jid] |
|---|
| 357 | hb = self.notebook.get_tab_label(child).get_children()[0] |
|---|
| 358 | status_image = hb.get_children()[0] |
|---|
| 359 | |
|---|
| 360 | state_images_32 = gajim.interface.roster.get_appropriate_state_images(jid, |
|---|
| 361 | size = '32') |
|---|
| 362 | state_images_16 = gajim.interface.roster.get_appropriate_state_images(jid) |
|---|
| 363 | |
|---|
| 364 | # Set banner image |
|---|
| 365 | if state_images_32.has_key(show) and state_images_32[show].get_pixbuf(): |
|---|
| 366 | # we have 32x32! use it! |
|---|
| 367 | banner_image = state_images_32[show] |
|---|
| 368 | use_size_32 = True |
|---|
| 369 | else: |
|---|
| 370 | banner_image = state_images_16[show] |
|---|
| 371 | use_size_32 = False |
|---|
| 372 | |
|---|
| 373 | banner_status_image = self.xmls[jid].get_widget('banner_status_image') |
|---|
| 374 | if banner_image.get_storage_type() == gtk.IMAGE_ANIMATION: |
|---|
| 375 | banner_status_image.set_from_animation(banner_image.get_animation()) |
|---|
| 376 | else: |
|---|
| 377 | pix = banner_image.get_pixbuf() |
|---|
| 378 | if use_size_32: |
|---|
| 379 | banner_status_image.set_from_pixbuf(pix) |
|---|
| 380 | else: # we need to scale 16x16 to 32x32 |
|---|
| 381 | scaled_pix = pix.scale_simple(32, 32, gtk.gdk.INTERP_BILINEAR) |
|---|
| 382 | banner_status_image.set_from_pixbuf(scaled_pix) |
|---|
| 383 | |
|---|
| 384 | # Set tab image (always 16x16); unread messages show the 'message' image |
|---|
| 385 | if self.nb_unread[jid] and gajim.config.get('show_unread_tab_icon'): |
|---|
| 386 | tab_image = state_images_16['message'] |
|---|
| 387 | else: |
|---|
| 388 | tab_image = state_images_16[show] |
|---|
| 389 | if tab_image.get_storage_type() == gtk.IMAGE_ANIMATION: |
|---|
| 390 | status_image.set_from_animation(tab_image.get_animation()) |
|---|
| 391 | else: |
|---|
| 392 | status_image.set_from_pixbuf(tab_image.get_pixbuf()) |
|---|
| 393 | |
|---|
| 394 | if keyID: |
|---|
| 395 | self.xmls[jid].get_widget('gpg_togglebutton').set_sensitive(True) |
|---|
| 396 | else: |
|---|
| 397 | self.xmls[jid].get_widget('gpg_togglebutton').set_sensitive(False) |
|---|
| 398 | |
|---|
| 399 | def on_tabbed_chat_window_delete_event(self, widget, event): |
|---|
| 400 | '''close window''' |
|---|
| 401 | for jid in self.contacts: |
|---|
| 402 | if time.time() - gajim.last_message_time[self.account][jid] < 2: |
|---|
| 403 | # 2 seconds |
|---|
| 404 | dialog = dialogs.ConfirmationDialog( |
|---|
| 405 | #%s is being replaced in the code with JID |
|---|
| 406 | _('You just received a new message from "%s"' % jid), |
|---|
| 407 | _('If you close this tab and you have history disabled, this message will be lost.')) |
|---|
| 408 | if dialog.get_response() != gtk.RESPONSE_OK: |
|---|
| 409 | return True #stop the propagation of the event |
|---|
| 410 | |
|---|
| 411 | if gajim.config.get('saveposition'): |
|---|
| 412 | # save the window size and position |
|---|
| 413 | x, y = self.window.get_position() |
|---|
| 414 | gajim.config.set('chat-x-position', x) |
|---|
| 415 | gajim.config.set('chat-y-position', y) |
|---|
| 416 | width, height = self.window.get_size() |
|---|
| 417 | gajim.config.set('chat-width', width) |
|---|
| 418 | gajim.config.set('chat-height', height) |
|---|
| 419 | |
|---|
| 420 | def on_tabbed_chat_window_destroy(self, widget): |
|---|
| 421 | # Reset contact chatstates to all open tabs |
|---|
| 422 | for jid in self.xmls: |
|---|
| 423 | self.send_chatstate('gone', jid) |
|---|
| 424 | self.contacts[jid].chatstate = None |
|---|
| 425 | self.contacts[jid].our_chatstate = None |
|---|
| 426 | #clean gajim.interface.instances[self.account]['chats'] |
|---|
| 427 | chat.Chat.on_window_destroy(self, widget, 'chats') |
|---|
| 428 | |
|---|
| 429 | def on_tabbed_chat_window_focus_in_event(self, widget, event): |
|---|
| 430 | chat.Chat.on_chat_window_focus_in_event(self, widget, event) |
|---|
| 431 | # on focus in, send 'active' chatstate to current tab |
|---|
| 432 | self.send_chatstate('active') |
|---|
| 433 | |
|---|
| 434 | def on_chat_notebook_key_press_event(self, widget, event): |
|---|
| 435 | chat.Chat.on_chat_notebook_key_press_event(self, widget, event) |
|---|
| 436 | |
|---|
| 437 | def on_send_file_menuitem_activate(self, widget): |
|---|
| 438 | jid = self.get_active_jid() |
|---|
| 439 | contact = gajim.get_first_contact_instance_from_jid(self.account, jid) |
|---|
| 440 | gajim.interface.instances['file_transfers'].show_file_send_request( |
|---|
| 441 | self.account, contact) |
|---|
| 442 | |
|---|
| 443 | def on_add_to_roster_menuitem_activate(self, widget): |
|---|
| 444 | jid = self.get_active_jid() |
|---|
| 445 | dialogs.AddNewContactWindow(self.account, jid) |
|---|
| 446 | |
|---|
| 447 | def on_send_button_clicked(self, widget): |
|---|
| 448 | '''When send button is pressed: send the current message''' |
|---|
| 449 | jid = self.get_active_jid() |
|---|
| 450 | message_textview = self.message_textviews[jid] |
|---|
| 451 | message_buffer = message_textview.get_buffer() |
|---|
| 452 | start_iter = message_buffer.get_start_iter() |
|---|
| 453 | end_iter = message_buffer.get_end_iter() |
|---|
| 454 | message = message_buffer.get_text(start_iter, end_iter, 0).decode('utf-8') |
|---|
| 455 | |
|---|
| 456 | # send the message |
|---|
| 457 | self.send_message(message) |
|---|
| 458 | |
|---|
| 459 | def remove_tab(self, jid): |
|---|
| 460 | if time.time() - gajim.last_message_time[self.account][jid] < 2: |
|---|
| 461 | dialog = dialogs.ConfirmationDialog( |
|---|
| 462 | _('You just received a new message from "%s"' % jid), |
|---|
| 463 | _('If you close this tab and you have history disabled, the message will be lost.')) |
|---|
| 464 | if dialog.get_response() != gtk.RESPONSE_OK: |
|---|
| 465 | return |
|---|
| 466 | |
|---|
| 467 | # chatstates - tab is destroyed, send gone and reset |
|---|
| 468 | self.send_chatstate('gone', jid) |
|---|
| 469 | self.contacts[jid].chatstate = None |
|---|
| 470 | self.contacts[jid].our_chatstate = None |
|---|
| 471 | |
|---|
| 472 | chat.Chat.remove_tab(self, jid, 'chats') |
|---|
| 473 | del self.contacts[jid] |
|---|
| 474 | |
|---|
| 475 | def new_tab(self, contact): |
|---|
| 476 | '''when new tab is created''' |
|---|
| 477 | self.names[contact.jid] = contact.name |
|---|
| 478 | self.xmls[contact.jid] = gtk.glade.XML(GTKGUI_GLADE, 'chats_vbox', APP) |
|---|
| 479 | self.childs[contact.jid] = self.xmls[contact.jid].get_widget('chats_vbox') |
|---|
| 480 | self.contacts[contact.jid] = contact |
|---|
| 481 | |
|---|
| 482 | self.show_avatar(contact.jid, contact.resource) |
|---|
| 483 | |
|---|
| 484 | self.childs[contact.jid].connect('drag_data_received', |
|---|
| 485 | self.on_drag_data_received, contact) |
|---|
| 486 | self.childs[contact.jid].drag_dest_set( gtk.DEST_DEFAULT_MOTION | |
|---|
| 487 | gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_DROP, |
|---|
| 488 | self.dnd_list, gtk.gdk.ACTION_COPY) |
|---|
| 489 | |
|---|
| 490 | chat.Chat.new_tab(self, contact.jid) |
|---|
| 491 | |
|---|
| 492 | msg_textview = self.message_textviews[contact.jid] |
|---|
| 493 | message_tv_buffer = msg_textview.get_buffer() |
|---|
| 494 | message_tv_buffer.connect('changed', |
|---|
| 495 | self.on_message_tv_buffer_changed, contact) |
|---|
| 496 | |
|---|
| 497 | if contact.jid in gajim.encrypted_chats[self.account]: |
|---|
| 498 | self.xmls[contact.jid].get_widget('gpg_togglebutton').set_active(True) |
|---|
| 499 | |
|---|
| 500 | xm = gtk.glade.XML(GTKGUI_GLADE, 'tabbed_chat_popup_menu', APP) |
|---|
| 501 | xm.signal_autoconnect(self) |
|---|
| 502 | self.tabbed_chat_popup_menu = xm.get_widget('tabbed_chat_popup_menu') |
|---|
| 503 | |
|---|
| 504 | self.redraw_tab(contact.jid) |
|---|
| 505 | self.draw_widgets(contact) |
|---|
| 506 | |
|---|
| 507 | # restore previous conversation |
|---|
| 508 | self.restore_conversation(contact.jid) |
|---|
| 509 | |
|---|
| 510 | if gajim.awaiting_events[self.account].has_key(contact.jid): |
|---|
| 511 | self.read_queue(contact.jid) |
|---|
| 512 | |
|---|
| 513 | self.childs[contact.jid].show_all() |
|---|
| 514 | |
|---|
| 515 | # chatstates |
|---|
| 516 | self.reset_kbd_mouse_timeout_vars() |
|---|
| 517 | |
|---|
| 518 | self.possible_paused_timeout_id[contact.jid] = gobject.timeout_add( |
|---|
| 519 | 5000, self.check_for_possible_paused_chatstate, contact.jid) |
|---|
| 520 | self.possible_inactive_timeout_id[contact.jid] = gobject.timeout_add( |
|---|
| 521 | 30000, self.check_for_possible_inactive_chatstate, contact.jid) |
|---|
| 522 | |
|---|
| 523 | def handle_incoming_chatstate(self, account, contact): |
|---|
| 524 | ''' handle incoming chatstate that jid SENT TO us ''' |
|---|
| 525 | self.draw_name_banner(contact, contact.chatstate) |
|---|
| 526 | # update chatstate in tab for this chat |
|---|
| 527 | self.redraw_tab(contact.jid, contact.chatstate) |
|---|
| 528 | |
|---|
| 529 | def check_for_possible_paused_chatstate(self, jid): |
|---|
| 530 | ''' did we move mouse of that window or write something in message |
|---|
| 531 | textview |
|---|
| 532 | in the last 5 seconds? |
|---|
| 533 | if yes we go active for mouse, composing for kbd |
|---|
| 534 | if no we go paused if we were previously composing ''' |
|---|
| 535 | contact = gajim.get_first_contact_instance_from_jid(self.account, jid) |
|---|
| 536 | if jid not in self.xmls or contact is None: |
|---|
| 537 | # the tab with jid is no longer open or contact left |
|---|
| 538 | # stop timer |
|---|
| 539 | return False # stop looping |
|---|
| 540 | |
|---|
| 541 | current_state = contact.our_chatstate |
|---|
| 542 | if current_state is False: # jid doesn't support chatstates |
|---|
| 543 | return False # stop looping |
|---|
| 544 | |
|---|
| 545 | message_textview = self.message_textviews[jid] |
|---|
| 546 | message_buffer = message_textview.get_buffer() |
|---|
| 547 | if self.kbd_activity_in_last_5_secs and message_buffer.get_char_count(): |
|---|
| 548 | # Only composing if the keyboard activity was in text entry |
|---|
| 549 | self.send_chatstate('composing', jid) |
|---|
| 550 | elif self.mouse_over_in_last_5_secs and jid == self.get_active_jid(): |
|---|
| 551 | self.send_chatstate('active', jid) |
|---|
| 552 | else: |
|---|
| 553 | if current_state == 'composing': |
|---|
| 554 | self.send_chatstate('paused', jid) # pause composing |
|---|
| 555 | |
|---|
| 556 | # assume no activity and let the motion-notify or 'insert-text' make them True |
|---|
| 557 | # refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds! |
|---|
| 558 | self.reset_kbd_mouse_timeout_vars() |
|---|
| 559 | return True # loop forever |
|---|
| 560 | |
|---|
| 561 | def check_for_possible_inactive_chatstate(self, jid): |
|---|
| 562 | ''' did we move mouse over that window or wrote something in message |
|---|
| 563 | textview |
|---|
| 564 | in the last 30 seconds? |
|---|
| 565 | if yes we go active |
|---|
| 566 | if no we go inactive ''' |
|---|
| 567 | contact = gajim.get_first_contact_instance_from_jid(self.account, jid) |
|---|
| 568 | if jid not in self.xmls or contact is None: |
|---|
| 569 | # the tab with jid is no longer open or contact left |
|---|
| 570 | return False # stop looping |
|---|
| 571 | |
|---|
| 572 | current_state = contact.our_chatstate |
|---|
| 573 | if current_state is False: # jid doesn't support chatstates |
|---|
| 574 | return False # stop looping |
|---|
| 575 | |
|---|
| 576 | if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs: |
|---|
| 577 | return True # loop forever |
|---|
| 578 | |
|---|
| 579 | if not (self.mouse_over_in_last_30_secs or\ |
|---|
| 580 | self.kbd_activity_in_last_30_secs): |
|---|
| 581 | self.send_chatstate('inactive', jid) |
|---|
| 582 | |
|---|
| 583 | # assume no activity and let the motion-notify or 'insert-text' make them True |
|---|
| 584 | # refresh 30 seconds too or else it's 30 - 5 = 25 seconds! |
|---|
| 585 | self.reset_kbd_mouse_timeout_vars() |
|---|
| 586 | |
|---|
| 587 | return True # loop forever |
|---|
| 588 | |
|---|
| 589 | def on_message_tv_buffer_changed(self, textbuffer, contact): |
|---|
| 590 | self.kbd_activity_in_last_5_secs = True |
|---|
| 591 | self.kbd_activity_in_last_30_secs = True |
|---|
| 592 | if textbuffer.get_char_count(): |
|---|
| 593 | self.send_chatstate('composing', contact.jid) |
|---|
| 594 | else: |
|---|
| 595 | self.send_chatstate('active', contact.jid) |
|---|
| 596 | |
|---|
| 597 | def reset_kbd_mouse_timeout_vars(self): |
|---|
| 598 | self.kbd_activity_in_last_5_secs = False |
|---|
| 599 | self.mouse_over_in_last_5_secs = False |
|---|
| 600 | self.mouse_over_in_last_30_secs = False |
|---|
| 601 | self.kbd_activity_in_last_30_secs = False |
|---|
| 602 | |
|---|
| 603 | def on_message_textview_mykeypress_event(self, widget, event_keyval, |
|---|
| 604 | event_keymod): |
|---|
| 605 | '''When a key is pressed: |
|---|
| 606 | if enter is pressed without the shift key, message (if not empty) is sent |
|---|
| 607 | and printed in the conversation''' |
|---|
| 608 | # NOTE: handles mykeypress which is custom signal connected to this |
|---|
| 609 | # CB in new_tab(). for this singal see message_textview.py |
|---|
| 610 | jid = self.get_active_jid() |
|---|
| 611 | conv_textview = self.conversation_textviews[jid] |
|---|
| 612 | message_textview = widget |
|---|
| 613 | message_buffer = message_textview.get_buffer() |
|---|
| 614 | start_iter, end_iter = message_buffer.get_bounds() |
|---|
| 615 | message = message_buffer.get_text(start_iter, end_iter, False).decode( |
|---|
| 616 | 'utf-8') |
|---|
| 617 | |
|---|
| 618 | # construct event instance from binding |
|---|
| 619 | event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here |
|---|
| 620 | event.keyval = event_keyval |
|---|
| 621 | event.state = event_keymod |
|---|
| 622 | event.time = 0 # assign current time |
|---|
| 623 | |
|---|
| 624 | if event.keyval == gtk.keysyms.ISO_Left_Tab: # SHIFT + TAB |
|---|
| 625 | if event.state & gtk.gdk.CONTROL_MASK: # CTRL + SHIFT + TAB |
|---|
| 626 | self.notebook.emit('key_press_event', event) |
|---|
| 627 | if event.keyval == gtk< |
|---|