class ConversationTextview: '''Class for the conversation textview (where user reads already said messages) for chat/groupchat windows''' FOCUS_OUT_LINE_PIXBUF = gtk.gdk.pixbuf_new_from_file(os.path.join( gajim.DATA_DIR, 'pixmaps', 'muc_separator.png')) XEP0184_WARNING_PIXBUF = gtk.gdk.pixbuf_new_from_file(os.path.join( gajim.DATA_DIR, 'pixmaps', 'receipt_missing.png')) # smooth scroll constants MAX_SCROLL_TIME = 0.4 # seconds SCROLL_DELAY = 33 # milliseconds def __init__(self, account, used_in_history_window = False): '''if used_in_history_window is True, then we do not show Clear menuitem in context menu''' self.used_in_history_window = used_in_history_window # no need to inherit TextView, use it as atrribute is safer self.tv = HtmlTextView() self.tv.html_hyperlink_handler = self.html_hyperlink_handler # set properties self.tv.set_border_width(1) self.tv.set_accepts_tab(True) self.tv.set_editable(False) self.tv.set_cursor_visible(False) self.tv.set_wrap_mode(gtk.WRAP_WORD_CHAR) self.tv.set_left_margin(2) self.tv.set_right_margin(2) self.handlers = {} self.images = [] self.image_cache = {} self.xep0184_marks = {} self.xep0184_shown = {} # It's True when we scroll in the code, so we can detect scroll from user self.auto_scrolling = False # connect signals id = self.tv.connect('motion_notify_event', self.on_textview_motion_notify_event) self.handlers[id] = self.tv id = self.tv.connect('populate_popup', self.on_textview_populate_popup) self.handlers[id] = self.tv id = self.tv.connect('button_press_event', self.on_textview_button_press_event) self.handlers[id] = self.tv id = self.tv.connect('expose-event', self.on_textview_expose_event) self.handlers[id] = self.tv self.account = account self.change_cursor = None self.last_time_printout = 0 font = pango.FontDescription(gajim.config.get('conversation_font')) self.tv.modify_font(font) buffer = self.tv.get_buffer() end_iter = buffer.get_end_iter() buffer.create_mark('end', end_iter, False) self.tagIn = buffer.create_tag('incoming') color = gajim.config.get('inmsgcolor') self.tagIn.set_property('foreground', color) self.tagOut = buffer.create_tag('outgoing') color = gajim.config.get('outmsgcolor') self.tagOut.set_property('foreground', color) self.tagStatus = buffer.create_tag('status') color = gajim.config.get('statusmsgcolor') self.tagStatus.set_property('foreground', color) colors = gajim.config.get('gc_nicknames_colors') colors = colors.split(':') for i,color in enumerate(colors): tagname = 'gc_nickname_color_' + str(i) tag = buffer.create_tag(tagname) tag.set_property('foreground', color) tag = buffer.create_tag('marked') color = gajim.config.get('markedmsgcolor') tag.set_property('foreground', color) tag.set_property('weight', pango.WEIGHT_BOLD) tag = buffer.create_tag('time_sometimes') tag.set_property('foreground', 'darkgrey') tag.set_property('scale', pango.SCALE_SMALL) tag.set_property('justification', gtk.JUSTIFY_CENTER) tag = buffer.create_tag('small') tag.set_property('scale', pango.SCALE_SMALL) tag = buffer.create_tag('restored_message') color = gajim.config.get('restored_messages_color') tag.set_property('foreground', color) self.tagURL = buffer.create_tag('url') color = gajim.config.get('urlmsgcolor') self.tagURL.set_property('foreground', color) self.tagURL.set_property('underline', pango.UNDERLINE_SINGLE) id = self.tagURL.connect('event', self.hyperlink_handler, 'url') self.handlers[id] = self.tagURL self.tagMail = buffer.create_tag('mail') self.tagMail.set_property('foreground', color) self.tagMail.set_property('underline', pango.UNDERLINE_SINGLE) id = self.tagMail.connect('event', self.hyperlink_handler, 'mail') self.handlers[id] = self.tagMail tag = buffer.create_tag('bold') tag.set_property('weight', pango.WEIGHT_BOLD) tag = buffer.create_tag('italic') tag.set_property('style', pango.STYLE_ITALIC) tag = buffer.create_tag('underline') tag.set_property('underline', pango.UNDERLINE_SINGLE) buffer.create_tag('focus-out-line', justification = gtk.JUSTIFY_CENTER) tag = buffer.create_tag('xep0184-warning') # One mark at the begining then 2 marks between each lines size = gajim.config.get('max_conversation_lines') size = 2 * size - 1 self.marks_queue = Queue.Queue(size) self.allow_focus_out_line = True # holds a mark at the end of --- line self.focus_out_end_mark = None self.xep0184_warning_tooltip = tooltips.BaseTooltip() self.line_tooltip = tooltips.BaseTooltip() # use it for hr too self.tv.focus_out_line_pixbuf = ConversationTextview.FOCUS_OUT_LINE_PIXBUF self.smooth_id = None def del_handlers(self): for i in self.handlers.keys(): if self.handlers[i].handler_is_connected(i): self.handlers[i].disconnect(i) del self.handlers self.tv.destroy() #FIXME: # self.line_tooltip.destroy() def update_tags(self): self.tagIn.set_property('foreground', gajim.config.get('inmsgcolor')) self.tagOut.set_property('foreground', gajim.config.get('outmsgcolor')) self.tagStatus.set_property('foreground', gajim.config.get('statusmsgcolor')) self.tagURL.set_property('foreground', gajim.config.get('urlmsgcolor')) self.tagMail.set_property('foreground', gajim.config.get('urlmsgcolor')) def at_the_end(self): buffer = self.tv.get_buffer() end_iter = buffer.get_end_iter() end_rect = self.tv.get_iter_location(end_iter) visible_rect = self.tv.get_visible_rect() if end_rect.y <= (visible_rect.y + visible_rect.height): return True return False # Smooth scrolling inspired by Pidgin code def smooth_scroll(self): parent = self.tv.get_parent() if not parent: return False vadj = parent.get_vadjustment() max_val = vadj.upper - vadj.page_size + 1 cur_val = vadj.get_value() # scroll by 1/3rd of remaining distance onethird = cur_val + ((max_val - cur_val) / 3.0) self.auto_scrolling = True vadj.set_value(onethird) self.auto_scrolling = False if max_val - onethird < 0.01: self.smooth_id = None self.smooth_scroll_timer.cancel() return False return True def smooth_scroll_timeout(self): gobject.idle_add(self.do_smooth_scroll_timeout) return def do_smooth_scroll_timeout(self): if not self.smooth_id: # we finished scrolling return gobject.source_remove(self.smooth_id) self.smooth_id = None parent = self.tv.get_parent() if parent: vadj = parent.get_vadjustment() self.auto_scrolling = True vadj.set_value(vadj.upper - vadj.page_size + 1) self.auto_scrolling = False def smooth_scroll_to_end(self): if None != self.smooth_id: # already scrolling return False self.smooth_id = gobject.timeout_add(self.SCROLL_DELAY, self.smooth_scroll) self.smooth_scroll_timer = Timer(self.MAX_SCROLL_TIME, self.smooth_scroll_timeout) self.smooth_scroll_timer.start() return False def scroll_to_end(self): parent = self.tv.get_parent() buffer = self.tv.get_buffer() end_mark = buffer.get_mark('end') if not end_mark: return False self.auto_scrolling = True self.tv.scroll_to_mark(end_mark, 0, True, 0, 1) adjustment = parent.get_hadjustment() adjustment.set_value(0) self.auto_scrolling = False return False # when called in an idle_add, just do it once def bring_scroll_to_end(self, diff_y = 0, use_smooth=gajim.config.get('use_smooth_scrolling')): ''' scrolls to the end of textview if end is not visible ''' buffer = self.tv.get_buffer() end_iter = buffer.get_end_iter() end_rect = self.tv.get_iter_location(end_iter) visible_rect = self.tv.get_visible_rect() # scroll only if expected end is not visible if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y): if use_smooth: gobject.idle_add(self.smooth_scroll_to_end) else: gobject.idle_add(self.scroll_to_end_iter) def scroll_to_end_iter(self): buffer = self.tv.get_buffer() end_iter = buffer.get_end_iter() if not end_iter: return False self.tv.scroll_to_iter(end_iter, 0, False, 1, 1) return False # when called in an idle_add, just do it once def stop_scrolling(self): if self.smooth_id: gobject.source_remove(self.smooth_id) self.smooth_id = None self.smooth_scroll_timer.cancel() def show_xep0184_warning(self, id_): if id_ in self.xep0184_marks: return buffer = self.tv.get_buffer() buffer.begin_user_action() self.xep0184_marks[id_] = buffer.create_mark(None, buffer.get_end_iter(), left_gravity=True) self.xep0184_shown[id_] = NOT_SHOWN def show_it(): if (not id_ in self.xep0184_shown) or \ self.xep0184_shown[id_] == ALREADY_RECEIVED: return False end_iter = buffer.get_iter_at_mark( self.xep0184_marks[id_]) buffer.insert(end_iter, ' ') buffer.insert_pixbuf(end_iter, ConversationTextview.XEP0184_WARNING_PIXBUF) before_img_iter = buffer.get_iter_at_mark( self.xep0184_marks[id_]) before_img_iter.forward_char() post_img_iter = before_img_iter.copy() post_img_iter.forward_char() buffer.apply_tag_by_name('xep0184-warning', before_img_iter, post_img_iter) self.xep0184_shown[id_] = SHOWN return False gobject.timeout_add_seconds(2, show_it) buffer.end_user_action() def hide_xep0184_warning(self, id_): if id_ not in self.xep0184_marks: return if self.xep0184_shown[id_] == NOT_SHOWN: self.xep0184_shown[id_] = ALREADY_RECEIVED return buffer = self.tv.get_buffer() buffer.begin_user_action() begin_iter = buffer.get_iter_at_mark(self.xep0184_marks[id_]) end_iter = begin_iter.copy() # XXX: Is there a nicer way? end_iter.forward_char() end_iter.forward_char() buffer.delete(begin_iter, end_iter) buffer.delete_mark(self.xep0184_marks[id_]) buffer.end_user_action() del self.xep0184_marks[id_] del self.xep0184_shown[id_] def show_focus_out_line(self): if not self.allow_focus_out_line: # if room did not receive focus-in from the last time we added # --- line then do not readd return print_focus_out_line = False buffer = self.tv.get_buffer() if self.focus_out_end_mark is None: # this happens only first time we focus out on this room print_focus_out_line = True else: focus_out_end_iter = buffer.get_iter_at_mark(self.focus_out_end_mark) focus_out_end_iter_offset = focus_out_end_iter.get_offset() if focus_out_end_iter_offset != buffer.get_end_iter().get_offset(): # this means after last-focus something was printed # (else end_iter's offset is the same as before) # only then print ---- line (eg. we avoid printing many following # ---- lines) print_focus_out_line = True if print_focus_out_line and buffer.get_char_count() > 0: buffer.begin_user_action() # remove previous focus out line if such focus out line exists if self.focus_out_end_mark is not None: end_iter_for_previous_line = buffer.get_iter_at_mark( self.focus_out_end_mark) begin_iter_for_previous_line = end_iter_for_previous_line.copy() # img_char+1 (the '\n') begin_iter_for_previous_line.backward_chars(2) # remove focus out line buffer.delete(begin_iter_for_previous_line, end_iter_for_previous_line) buffer.delete_mark(self.focus_out_end_mark) # add the new focus out line end_iter = buffer.get_end_iter() buffer.insert(end_iter, '\n') buffer.insert_pixbuf(end_iter, ConversationTextview.FOCUS_OUT_LINE_PIXBUF) end_iter = buffer.get_end_iter() before_img_iter = end_iter.copy() # one char back (an image also takes one char) before_img_iter.backward_char() buffer.apply_tag_by_name('focus-out-line', before_img_iter, end_iter) self.allow_focus_out_line = False # update the iter we hold to make comparison the next time self.focus_out_end_mark = buffer.create_mark(None, buffer.get_end_iter(), left_gravity=True) buffer.end_user_action() # scroll to the end (via idle in case the scrollbar has appeared) gobject.idle_add(self.scroll_to_end) def show_xep0184_warning_tooltip(self): pointer = self.tv.get_pointer() x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer[0], pointer[1]) tags = self.tv.get_iter_at_location(x, y).get_tags() tag_table = self.tv.get_buffer().get_tag_table() xep0184_warning = False for tag in tags: if tag == tag_table.lookup('xep0184-warning'): xep0184_warning = True break if xep0184_warning and not self.xep0184_warning_tooltip.win: # check if the current pointer is still over the line position = self.tv.window.get_origin() self.xep0184_warning_tooltip.show_tooltip(_('This icon indicates that ' 'this message has not yet\nbeen received by the remote end. ' "If this icon stays\nfor a long time, it's likely the message got " 'lost.'), 8, position[1] + pointer[1]) def show_line_tooltip(self): pointer = self.tv.get_pointer() x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer[0], pointer[1]) tags = self.tv.get_iter_at_location(x, y).get_tags() tag_table = self.tv.get_buffer().get_tag_table() over_line = False for tag in tags: if tag == tag_table.lookup('focus-out-line'): over_line = True break if over_line and not self.line_tooltip.win: # check if the current pointer is still over the line position = self.tv.window.get_origin() self.line_tooltip.show_tooltip(_('Text below this line is what has ' 'been said since the\nlast time you paid attention to this group ' 'chat'), 8, position[1] + pointer[1]) def on_textview_expose_event(self, widget, event): expalloc = event.area exp_x0 = expalloc.x exp_y0 = expalloc.y exp_x1 = exp_x0 + expalloc.width exp_y1 = exp_y0 + expalloc.height try: tryfirst = [self.image_cache[(exp_x0, exp_y0)]] except KeyError: tryfirst = [] for image in tryfirst + self.images: imgalloc = image.allocation img_x0 = imgalloc.x img_y0 = imgalloc.y img_x1 = img_x0 + imgalloc.width img_y1 = img_y0 + imgalloc.height if img_x0 <= exp_x0 and img_y0 <= exp_y0 and \ exp_x1 <= img_x1 and exp_y1 <= img_y1: self.image_cache[(img_x0, img_y0)] = image widget.propagate_expose(image, event) return True return False def on_textview_motion_notify_event(self, widget, event): '''change the cursor to a hand when we are over a mail or an url''' pointer_x, pointer_y, spam = self.tv.window.get_pointer() x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer_x, pointer_y) tags = self.tv.get_iter_at_location(x, y).get_tags() if self.change_cursor: self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor( gtk.gdk.Cursor(gtk.gdk.XTERM)) self.change_cursor = None tag_table = self.tv.get_buffer().get_tag_table() over_line = False xep0184_warning = False for tag in tags: if tag in (tag_table.lookup('url'), tag_table.lookup('mail')): self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor( gtk.gdk.Cursor(gtk.gdk.HAND2)) self.change_cursor = tag elif tag == tag_table.lookup('focus-out-line'): over_line = True elif tag == tag_table.lookup('xep0184-warning'): xep0184_warning = True if self.line_tooltip.timeout != 0: # Check if we should hide the line tooltip if not over_line: self.line_tooltip.hide_tooltip() if over_line and not self.line_tooltip.win: self.line_tooltip.timeout = gobject.timeout_add(500, self.show_line_tooltip) self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor( gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)) self.change_cursor = tag if xep0184_warning and not self.xep0184_warning_tooltip.win: self.xep0184_warning_tooltip.timeout = \ gobject.timeout_add(500, self.show_xep0184_warning_tooltip) self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor( gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)) self.change_cursor = tag def clear(self, tv = None): '''clear text in the textview''' buffer = self.tv.get_buffer() start, end = buffer.get_bounds() buffer.delete(start, end) size = gajim.config.get('max_conversation_lines') size = 2 * size - 1 self.marks_queue = Queue.Queue(size) self.focus_out_end_mark = None def visit_url_from_menuitem(self, widget, link): '''basically it filters out the widget instance''' helpers.launch_browser_mailer('url', link) def on_textview_populate_popup(self, textview, menu): '''we override the default context menu and we prepend Clear (only if used_in_history_window is False) and if we have sth selected we show a submenu with actions on the phrase (see on_conversation_textview_button_press_event)''' separator_menuitem_was_added = False if not self.used_in_history_window: item = gtk.SeparatorMenuItem() menu.prepend(item) separator_menuitem_was_added = True item = gtk.ImageMenuItem(gtk.STOCK_CLEAR) menu.prepend(item) id = item.connect('activate', self.clear) self.handlers[id] = item if self.selected_phrase: if not separator_menuitem_was_added: item = gtk.SeparatorMenuItem() menu.prepend(item) self.selected_phrase = helpers.reduce_chars_newlines( self.selected_phrase, 25, 2) item = gtk.MenuItem(_('_Actions for "%s"') % self.selected_phrase) menu.prepend(item) submenu = gtk.Menu() item.set_submenu(submenu) always_use_en = gajim.config.get('always_english_wikipedia') if always_use_en: link = 'http://en.wikipedia.org/wiki/Special:Search?search=%s'\ % self.selected_phrase else: link = 'http://%s.wikipedia.org/wiki/Special:Search?search=%s'\ % (gajim.LANG, self.selected_phrase) item = gtk.MenuItem(_('Read _Wikipedia Article')) id = item.connect('activate', self.visit_url_from_menuitem, link) self.handlers[id] = item submenu.append(item) item = gtk.MenuItem(_('Look it up in _Dictionary')) dict_link = gajim.config.get('dictionary_url') if dict_link == 'WIKTIONARY': # special link (yeah undocumented but default) always_use_en = gajim.config.get('always_english_wiktionary') if always_use_en: link = 'http://en.wiktionary.org/wiki/Special:Search?search=%s'\ % self.selected_phrase else: link = 'http://%s.wiktionary.org/wiki/Special:Search?search=%s'\ % (gajim.LANG, self.selected_phrase) id = item.connect('activate', self.visit_url_from_menuitem, link) self.handlers[id] = item else: if dict_link.find('%s') == -1: # we must have %s in the url if not WIKTIONARY item = gtk.MenuItem(_( 'Dictionary URL is missing an "%s" and it is not WIKTIONARY')) item.set_property('sensitive', False) else: link = dict_link % self.selected_phrase id = item.connect('activate', self.visit_url_from_menuitem, link) self.handlers[id] = item submenu.append(item) search_link = gajim.config.get('search_engine') if search_link.find('%s') == -1: # we must have %s in the url item = gtk.MenuItem(_('Web Search URL is missing an "%s"')) item.set_property('sensitive', False) else: item = gtk.MenuItem(_('Web _Search for it')) link = search_link % self.selected_phrase id = item.connect('activate', self.visit_url_from_menuitem, link) self.handlers[id] = item submenu.append(item) item = gtk.MenuItem(_('Open as _Link')) id = item.connect('activate', self.visit_url_from_menuitem, link) self.handlers[id] = item submenu.append(item) menu.show_all() def on_textview_button_press_event(self, widget, event): # If we clicked on a taged text do NOT open the standard popup menu # if normal text check if we have sth selected self.selected_phrase = '' # do not move belove event button check! if event.button != 3: # if not right click return False x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, int(event.x), int(event.y)) iter = self.tv.get_iter_at_location(x, y) tags = iter.get_tags() if tags: # we clicked on sth special (it can be status message too) for tag in tags: tag_name = tag.get_property('name') if tag_name in ('url', 'mail'): return True # we block normal context menu # we check if sth was selected and if it was we assign # selected_phrase variable # so on_conversation_textview_populate_popup can use it buffer = self.tv.get_buffer() return_val = buffer.get_selection_bounds() if return_val: # if sth was selected when we right-clicked # get the selected text start_sel, finish_sel = return_val[0], return_val[1] self.selected_phrase = buffer.get_text(start_sel, finish_sel).decode( 'utf-8') elif ord(iter.get_char()) > 31: # we clicked on a word, do as if it's selected for context menu start_sel = iter.copy() if not start_sel.starts_word(): start_sel.backward_word_start() finish_sel = iter.copy() if not finish_sel.ends_word(): finish_sel.forward_word_end() self.selected_phrase = buffer.get_text(start_sel, finish_sel).decode( 'utf-8') def on_open_link_activate(self, widget, kind, text): helpers.launch_browser_mailer(kind, text) def on_copy_link_activate(self, widget, text): clip = gtk.clipboard_get() clip.set_text(text) def on_start_chat_activate(self, widget, jid): gajim.interface.new_chat_from_jid(self.account, jid) def on_join_group_chat_menuitem_activate(self, widget, room_jid): if 'join_gc' in gajim.interface.instances[self.account]: instance = gajim.interface.instances[self.account]['join_gc'] instance.xml.get_widget('room_jid_entry').set_text(room_jid) gajim.interface.instances[self.account]['join_gc'].window.present() else: try: gajim.interface.instances[self.account]['join_gc'] = \ dialogs.JoinGroupchatWindow(self.account, room_jid) except GajimGeneralException: pass def on_add_to_roster_activate(self, widget, jid): dialogs.AddNewContactWindow(self.account, jid) def make_link_menu(self, event, kind, text): xml = gtkgui_helpers.get_glade('chat_context_menu.glade') menu = xml.get_widget('chat_context_menu') childs = menu.get_children() if kind == 'url': id = childs[0].connect('activate', self.on_copy_link_activate, text) self.handlers[id] = childs[0] id = childs[1].connect('activate', self.on_open_link_activate, kind, text) self.handlers[id] = childs[1] childs[2].hide() # copy mail address childs[3].hide() # open mail composer childs[4].hide() # jid section separator childs[5].hide() # start chat childs[6].hide() # join group chat childs[7].hide() # add to roster else: # It's a mail or a JID # load muc icon join_group_chat_menuitem = xml.get_widget('join_group_chat_menuitem') muc_icon = gtkgui_helpers.load_icon('muc_active') if muc_icon: join_group_chat_menuitem.set_image(muc_icon) text = text.lower() id = childs[2].connect('activate', self.on_copy_link_activate, text) self.handlers[id] = childs[2] id = childs[3].connect('activate', self.on_open_link_activate, kind, text) self.handlers[id] = childs[3] id = childs[5].connect('activate', self.on_start_chat_activate, text) self.handlers[id] = childs[5] id = childs[6].connect('activate', self.on_join_group_chat_menuitem_activate, text) self.handlers[id] = childs[6] allow_add = False c = gajim.contacts.get_first_contact_from_jid(self.account, text) if c and not gajim.contacts.is_pm_from_contact(self.account, c): if _('Not in Roster') in c.groups: allow_add = True else: # he or she's not at all in the account contacts allow_add = True if allow_add: id = childs[7].connect('activate', self.on_add_to_roster_activate, text) self.handlers[id] = childs[7] childs[7].show() # show add to roster menuitem else: childs[7].hide() # hide add to roster menuitem childs[0].hide() # copy link location childs[1].hide() # open link in browser menu.popup(None, None, None, event.button, event.time) def hyperlink_handler(self, texttag, widget, event, iter_, kind): if event.type == gtk.gdk.BUTTON_PRESS: begin_iter = iter_.copy() # we get the begining of the tag while not begin_iter.begins_tag(texttag): begin_iter.backward_char() end_iter = iter_.copy() # we get the end of the tag while not end_iter.ends_tag(texttag): end_iter.forward_char() word = self.tv.get_buffer().get_text(begin_iter, end_iter).decode( 'utf-8') if event.button == 3: # right click self.make_link_menu(event, kind, word) else: # we launch the correct application helpers.launch_browser_mailer(kind, word) def html_hyperlink_handler(self, texttag, widget, event, iter_, kind, href): if event.type == gtk.gdk.BUTTON_PRESS: if event.button == 3: # right click self.make_link_menu(event, kind, href) return True else: # we launch the correct application helpers.launch_browser_mailer(kind, href) def detect_and_print_special_text(self, otext, other_tags): '''detects special text (emots & links & formatting) prints normal text before any special text it founts, then print special text (that happens many times until last special text is printed) and then returns the index after *last* special text, so we can print it in print_conversation_line()''' buffer = self.tv.get_buffer() start = 0 end = 0 index = 0 # basic: links + mail + formatting is always checked (we like that) if gajim.config.get('emoticons_theme'): # search for emoticons & urls iterator = gajim.interface.emot_and_basic_re.finditer(otext) else: # search for just urls + mail + formatting iterator = gajim.interface.basic_pattern_re.finditer(otext) for match in iterator: start, end = match.span() special_text = otext[start:end] if start != 0: text_before_special_text = otext[index:start] end_iter = buffer.get_end_iter() # we insert normal text buffer.insert_with_tags_by_name(end_iter, text_before_special_text, *other_tags) index = end # update index # now print it self.print_special_text(special_text, other_tags) return index # the position after *last* special text def latex_to_image(self, str_): result = None exitcode = 0 # some latex commands are really bad blacklist = ['\\def', '\\let', '\\futurelet', '\\newcommand', '\\renewcomment', '\\else', '\\fi', '\\write', '\\input', '\\include', '\\chardef', '\\catcode', '\\makeatletter', '\\noexpand', '\\toksdef', '\\every', '\\errhelp', '\\errorstopmode', '\\scrollmode', '\\nonstopmode', '\\batchmode', '\\read', '\\csname', '\\newhelp', '\\relax', '\\afterground', '\\afterassignment', '\\expandafter', '\\noexpand', '\\special', '\\command', '\\loop', '\\repeat', '\\toks', '\\output', '\\line', '\\mathcode', '\\name', '\\item', '\\section', '\\mbox', '\\DeclareRobustCommand', '\\[', '\\]'] str_ = str_[2:len(str_)-2] # filter latex code with bad commands for word in blacklist: if word in str_: exitcode = 1 break if exitcode == 0: random.seed() tmpfile = os.path.join(gettempdir(), 'gajimtex_' + random.randint(0, 100).__str__()) # build latex string texstr = '\\documentclass[12pt]{article}\\usepackage[dvips]{graphicx}' texstr += '\\usepackage{amsmath}\\usepackage{amssymb}' texstr += '\\pagestyle{empty}' texstr += '\\begin{document}\\begin{large}\\begin{gather*}' texstr += str_ texstr += '\\end{gather*}\\end{large}\\end{document}' file = open(os.path.join(tmpfile + '.tex'), 'w+') file.write(texstr) file.flush() file.close() p = Popen(['latex', '--interaction=nonstopmode', tmpfile + '.tex'], cwd=gettempdir()) exitcode = p.wait() if exitcode == 0: latex_png_dpi = gajim.config.get('latex_png_dpi') p = Popen(['dvipng', '-bg', 'white', '-T', 'tight', '-D', latex_png_dpi, tmpfile + '.dvi', '-o', tmpfile + '.png'], cwd=gettempdir()) exitcode = p.wait() extensions = ['.tex', '.log', '.aux', '.dvi'] for ext in extensions: try: os.remove(tmpfile + ext) except Exception: pass if exitcode == 0: result = tmpfile + '.png' return result def print_special_text(self, special_text, other_tags): '''is called by detect_and_print_special_text and prints special text (emots, links, formatting)''' tags = [] use_other_tags = True text_is_valid_uri = False show_ascii_formatting_chars = \ gajim.config.get('show_ascii_formatting_chars') buffer = self.tv.get_buffer() # Check if we accept this as an uri schemes = gajim.config.get('uri_schemes').split() for scheme in schemes: if special_text.startswith(scheme + ':'): text_is_valid_uri = True possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS if gajim.config.get('emoticons_theme') and \ possible_emot_ascii_caps in gajim.interface.emoticons.keys(): # it's an emoticon emot_ascii = possible_emot_ascii_caps end_iter = buffer.get_end_iter() anchor = buffer.create_child_anchor(end_iter) img = TextViewImage(anchor) animations = gajim.interface.emoticons_animations if not emot_ascii in animations: animations[emot_ascii] = gtk.gdk.PixbufAnimation( gajim.interface.emoticons[emot_ascii]) img.set_from_animation(animations[emot_ascii]) img.show() self.images.append(img) # add with possible animation self.tv.add_child_at_anchor(img, anchor) elif special_text.startswith('www.') or \ special_text.startswith('ftp.') or \ text_is_valid_uri: tags.append('url') use_other_tags = False elif special_text.startswith('mailto:') or \ gajim.interface.sth_at_sth_dot_sth_re.match(special_text): # it's a mail tags.append('mail') use_other_tags = False elif special_text.startswith('*'): # it's a bold text tags.append('bold') if special_text[1] == '/' and special_text[-2] == '/' and\ len(special_text) > 4: # it's also italic tags.append('italic') if not show_ascii_formatting_chars: special_text = special_text[2:-2] # remove */ /* elif special_text[1] == '_' and special_text[-2] == '_' and \ len(special_text) > 4: # it's also underlined tags.append('underline') if not show_ascii_formatting_chars: special_text = special_text[2:-2] # remove *_ _* else: if not show_ascii_formatting_chars: special_text = special_text[1:-1] # remove * * elif special_text.startswith('/'): # it's an italic text tags.append('italic') if special_text[1] == '*' and special_text[-2] == '*' and \ len(special_text) > 4: # it's also bold tags.append('bold') if not show_ascii_formatting_chars: special_text = special_text[2:-2] # remove /* */ elif special_text[1] == '_' and special_text[-2] == '_' and \ len(special_text) > 4: # it's also underlined tags.append('underline') if not show_ascii_formatting_chars: special_text = special_text[2:-2] # remove /_ _/ else: if not show_ascii_formatting_chars: special_text = special_text[1:-1] # remove / / elif special_text.startswith('_'): # it's an underlined text tags.append('underline') if special_text[1] == '*' and special_text[-2] == '*' and \ len(special_text) > 4: # it's also bold tags.append('bold') if not show_ascii_formatting_chars: special_text = special_text[2:-2] # remove _* *_ elif special_text[1] == '/' and special_text[-2] == '/' and \ len(special_text) > 4: # it's also italic tags.append('italic') if not show_ascii_formatting_chars: special_text = special_text[2:-2] # remove _/ /_ else: if not show_ascii_formatting_chars: special_text = special_text[1:-1] # remove _ _ elif special_text.startswith('$$') and special_text.endswith('$$'): imagepath = self.latex_to_image(special_text) end_iter = buffer.get_end_iter() anchor = buffer.create_child_anchor(end_iter) if imagepath is not None: img = gtk.Image() img.set_from_file(imagepath) img.show() # add self.tv.add_child_at_anchor(img, anchor) # delete old file try: os.remove(imagepath) except Exception: pass else: buffer.insert(end_iter, special_text) use_other_tags = False else: # It's nothing special if use_other_tags: end_iter = buffer.get_end_iter() buffer.insert_with_tags_by_name(end_iter, special_text, *other_tags) if len(tags) > 0: end_iter = buffer.get_end_iter() all_tags = tags[:] if use_other_tags: all_tags += other_tags buffer.insert_with_tags_by_name(end_iter, special_text, *all_tags) def print_empty_line(self): buffer = self.tv.get_buffer() end_iter = buffer.get_end_iter() buffer.insert_with_tags_by_name(end_iter, '\n', 'eol') def print_conversation_line(self, text, jid, kind, name, tim, other_tags_for_name=[], other_tags_for_time=[], other_tags_for_text=[], subject=None, old_kind=None, xhtml=None, simple=False): '''prints 'chat' type messages''' buffer = self.tv.get_buffer() buffer.begin_user_action() if self.marks_queue.full(): # remove oldest line m1 = self.marks_queue.get() m2 = self.marks_queue.get() i1 = buffer.get_iter_at_mark(m1) i2 = buffer.get_iter_at_mark(m2) buffer.delete(i1, i2) buffer.delete_mark(m1) end_iter = buffer.get_end_iter() at_the_end = False if self.at_the_end(): at_the_end = True # Create one mark and add it to queue once if it's the first line # else twice (one for end bound, one for start bound) mark = None if buffer.get_char_count() > 0: if not simple: buffer.insert_with_tags_by_name(end_iter, '\n', 'eol') mark = buffer.create_mark(None, end_iter, left_gravity=True) self.marks_queue.put(mark) if not mark: mark = buffer.create_mark(None, end_iter, left_gravity=True) self.marks_queue.put(mark) if kind == 'incoming_queue': kind = 'incoming' if old_kind == 'incoming_queue': old_kind = 'incoming' # print the time stamp if not tim: # We don't have tim for outgoing messages... tim = time.localtime() current_print_time = gajim.config.get('print_time') if current_print_time == 'always' and kind != 'info' and not simple: timestamp_str = self.get_time_to_show(tim) timestamp = time.strftime(timestamp_str, tim) buffer.insert_with_tags_by_name(end_iter, timestamp, *other_tags_for_time) elif current_print_time == 'sometimes' and kind != 'info' and not simple: every_foo_seconds = 60 * gajim.config.get( 'print_ichat_every_foo_minutes') seconds_passed = time.mktime(tim) - self.last_time_printout if seconds_passed > every_foo_seconds: self.last_time_printout = time.mktime(tim) end_iter = buffer.get_end_iter() if gajim.config.get('print_time_fuzzy') > 0: fc = FuzzyClock() fc.setTime(time.strftime('%H:%M', tim)) ft = fc.getFuzzyTime(gajim.config.get('print_time_fuzzy')) tim_format = ft.decode(locale.getpreferredencoding()) else: tim_format = self.get_time_to_show(tim) buffer.insert_with_tags_by_name(end_iter, tim_format + '\n', 'time_sometimes') # kind = info, we print things as if it was a status: same color, ... if kind == 'info': kind = 'status' other_text_tag = self.detect_other_text_tag(text, kind) text_tags = other_tags_for_text[:] # create a new list if other_text_tag: # note that color of /me may be overwritten in gc_control text_tags.append(other_text_tag) else: # not status nor /me if gajim.config.get( 'chat_merge_consecutive_nickname'): if kind != old_kind: self.print_name(name, kind, other_tags_for_name) else: self.print_real_text(gajim.config.get( 'chat_merge_consecutive_nickname_indent')) else: self.print_name(name, kind, other_tags_for_name) self.print_subject(subject) self.print_real_text(text, text_tags, name, xhtml) # scroll to the end of the textview if at_the_end or kind == 'outgoing': # we are at the end or we are sending something # scroll to the end (via idle in case the scrollbar has appeared) if gajim.config.get('use_smooth_scrolling'): gobject.idle_add(self.smooth_scroll_to_end) else: gobject.idle_add(self.scroll_to_end) buffer.end_user_action() def get_time_to_show(self, tim): '''Get the time, with the day before if needed and return it. It DOESN'T format a fuzzy time''' format = '' # get difference in days since epoch (86400 = 24*3600) # number of days since epoch for current time (in GMT) - # number of days since epoch for message (in GMT) diff_day = int(timegm(time.localtime())) / 86400 -\ int(timegm(tim)) / 86400 if diff_day == 0: day_str = '' elif diff_day == 1: day_str = _('Yesterday') else: #the number is >= 2 # %i is day in year (1-365), %d (1-31) we want %i day_str = _('%i days ago') % diff_day if day_str: format += day_str + ' ' timestamp_str = gajim.config.get('time_stamp') timestamp_str = helpers.from_one_line(timestamp_str) format += timestamp_str tim_format = time.strftime(format, tim) if locale.getpreferredencoding() != 'KOI8-R': # if tim_format comes as unicode because of day_str. # we convert it to the encoding that we want (and that is utf-8) tim_format = helpers.ensure_utf8_string(tim_format) return tim_format def detect_other_text_tag(self, text, kind): if kind == 'status': return kind elif text.startswith('/me ') or text.startswith('/me\n'): return kind def print_name(self, name, kind, other_tags_for_name): if name: buffer = self.tv.get_buffer() end_iter = buffer.get_end_iter() name_tags = other_tags_for_name[:] # create a new list name_tags.append(kind) before_str = gajim.config.get('before_nickname') before_str = helpers.from_one_line(before_str) after_str = gajim.config.get('after_nickname') after_str = helpers.from_one_line(after_str) format = before_str + name + after_str + ' ' buffer.insert_with_tags_by_name(end_iter, format, *name_tags) def print_subject(self, subject): if subject: # if we have subject, show it too! subject = _('Subject: %s\n') % subject buffer = self.tv.get_buffer() end_iter = buffer.get_end_iter() buffer.insert(end_iter, subject) self.print_empty_line() def print_real_text(self, text, text_tags = [], name = None, xhtml = None): '''this adds normal and special text. call this to add text''' if xhtml: try: if name and (text.startswith('/me ') or text.startswith('/me\n')): xhtml = xhtml.replace('/me', '<dfn>%s</dfn>'% (name,), 1) self.tv.display_html(xhtml.encode('utf-8')) return except Exception, e: gajim.log.debug(str('Error processing xhtml')+str(e)) gajim.log.debug(str('with |'+xhtml+'|')) buffer = self.tv.get_buffer() # /me is replaced by name if name is given if name and (text.startswith('/me ') or text.startswith('/me\n')): text = '* ' + name + text[3:] text_tags.append('italic') # detect urls formatting and if the user has it on emoticons index = self.detect_and_print_special_text(text, text_tags) # add the rest of text located in the index and after end_iter = buffer.get_end_iter() buffer.insert_with_tags_by_name(end_iter, text[index:], *text_tags)
class MainClass(Plugin.Plugin): '''Main plugin class''' def __init__(self, controller, msn): '''Contructor''' Plugin.Plugin.__init__(self, controller, msn, 1000) self.description = 'Log all events to a file' self.authors = {'Mariano Guerra' : 'luismarianoguerra at gmail dot com'} self.website = 'http://emesene.org' self.displayName = 'Logger' self.name = 'Logger' self.controller = controller self.config = controller.config self.config.readPluginConfig(self.name) self.path = self.config.getPluginValue(self.name, 'path', self.msn.cacheDir) self.file_name = os.path.join(self.path, self.msn.user + '.db') self.logger = Logger(self.file_name) self.ids = [] self.signals = {} self.signals['self-nick-changed'] = self._cb_self_nick_changed self.signals['personal-message-changed'] = \ self._cb_personal_message_changed self.signals['self-status-changed'] = self._cb_self_status_changed self.signals['self-personal-message-changed'] = \ self._cb_self_personal_message_changed self.signals['self-current-media-changed'] = \ self._cb_self_current_media_changed self.signals['nick-changed'] = self._cb_nick_changed self.signals['contact-status-change'] = \ self._cb_contact_status_change self.signals['initial-status-change'] = \ self._cb_initial_status_change self.signals['user-offline'] = self._cb_user_offline self.signals['display-picture-changed'] = \ self._cb_display_picture_changed self.signals['switchboard::message'] = self._cb_sb_message self.signals['switchboard::ink-message'] = self._cb_sb_ink_message self.signals['switchboard::nudge'] = self._cb_sb_nudge self.signals['switchboard::message-sent'] = self._cb_sb_message_sent self.signals['switchboard::ink-sent'] = self._cb_sb_ink_message_sent self.signals['switchboard::nudge-sent'] = self._cb_sb_nudge_sent self.signals['switchboard::action-message'] = \ self._cb_sb_action_message self.signals['switchboard::action-sent'] = \ self._cb_sb_action_message_sent self.signals['switchboard::custom-emoticon-received'] = \ self._cb_sb_custom_emoticon_received self.queued = [] self.queuedtag = 0 self.to_source = None all_events = ','.join(self.signals.keys()) self.menuItemId = 0 def start(self): '''start the plugin''' if sqlite is None: return self.enabled = True self.events_enabled = [] for signal in self.signals.keys(): if bool(int(self.config.getPluginValue( self.name, signal, True ))): self.events_enabled.append(signal) for (key, value) in self.signals.iteritems(): if key in self.events_enabled: self.ids.append(self.msn.connect(key, self.callback, value)) self.menuItemId = self.controller.connect("usermenu-item-add", self.add_usermenu_item) def stop(self): '''stop the plugin''' for identifier in self.ids: self.msn.disconnect(identifier) self.ids = [] self.controller.disconnect(self.menuItemId) self.enabled = False def callback(self, *args): # sorry, pylint, i know you don't like this '''Adds the function in the last argument to the queue, to be called later when the mainloop is idle. sqlite isn't exactly fast''' params, func = args[:-1], args[-1] params += (time.time(), ) self.queued.append((func, params)) if self.queuedtag == 0: self.queuedtag = gobject.idle_add(self.process_queue) def process_queue(self): '''Takes the signal queue and calls one function. It's called on a idle_add loop. If there are no function, it quits the loop returning false and removing the queuedtag''' if self.queued: func, params = self.queued.pop(0) func(*params) return True else: self.queuedtag = 0 return False def check(self): '''check if everything is OK to start the plugin return a tuple whith a boolean and a message if OK -> (True , 'some message') else -> (False , 'error message')''' if sqlite is None: return (False, 'sqlite not available, please install it.') else: return (True, 'Ok') def get_last_status(self, account, num = 1): '''return the last status of a contact, if num is > 1 then return the last num status, the format of the list is a list of lists with the timestamp and the status''' query = ''' select e.stamp, ue.data from event e, user_event ue, user u where e.id = ue.id_event and u.id = ue.id_user and e.name = "status-changed" and u.account = "%s" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def get_last_nick(self, account, num = 1): '''return the last nick of a contact, if num is > 1 then return the last num nicks, the format of the list is a list of lists with the timestamp and the nick''' query = ''' select e.stamp, ue.data from event e, user_event ue, user u where e.id = ue.id_event and u.id = ue.id_user and e.name = "nick-changed" and u.account = "%s" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def get_last_personal_message(self, account, num = 1): '''return the last pm of a contact, if num is > 1 then return the last num pms, the format of the list is a list of lists with the timestamp and the pm''' query = ''' select e.stamp, ue.data from event e, user_event ue, user u where e.id = ue.id_event and u.id = ue.id_user and e.name = "personal-message-changed" and u.account = "%s" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def get_just_last_personal_message(self, account): '''return the last personal message as a string or None rather than a list''' result = self.get_last_personal_message(account) if result: return result[0][1] return None def get_just_last_nick(self, account): '''return the last nick as a string or None rather than a list''' result = self.get_last_nick(account) if result: return result[0][1] return None def get_just_last_status(self, account): '''return the last status as a string or None rather than a list''' result = self.get_last_status(account) if result: return result[0][1] return None def get_last_message(self, account, num = 1): '''return the last message of a contact, if num is > 1 then return the last num messages, the format of the list is a list of lists with the timestamp and the message''' query = ''' select e.stamp, ce.data from event e, conversation_event ce, user u where e.id = ce.id_event and u.id = ce.id_user and e.name = "message" and u.account = "%s" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def get_last_display_picture(self, account, num = 1): '''return the last dp of a contact, if num is > 1 then return the last num dps, the format of the list is a list of lists with the timestamp and the dp''' query = ''' select e.stamp, ue.data from event e, user_event ue, user u where e.id = ue.id_event and u.id = ue.id_user and e.name = "display-picture" and u.account = "%s" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def get_last_custom_emoticon(self, account, num = 1): '''return the last ce of a contact, if num is > 1 then return the last num ces, the format of the list is a list of lists with the timestamp and the ce''' query = ''' select e.stamp, ce.data from event e, conversation_event ce, user u where e.id = ce.id_event and u.id = ce.id_user and e.name = "custom-emoticon" and u.account = "%s" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def get_last_conversation(self, account, num = 1): '''return the last message that was said on a conversation where account was present, the format of the list is a list od lists with the timestamp, email and message''' query = ''' select e.stamp, u.account, ce.data from event e, conversation_event ce, user u where e.id = ce.id_event and u.id = ce.id_user and ce.id_conversation in (select distinct c.id from conversation c, event e, conversation_event ce, user u where c.id = ce.id_conversation and e.id = ce.id_event and ce.id_user = u.id and u.account = "%s") and e.name = "message" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def add_usermenu_item(self, controller, userMenu): self.logMenuItem = userMenu.newImageMenuItem( _( "View conversation _log" ), gtk.STOCK_FILE ) userMenu.add( self.logMenuItem ) self.logMenuItem.show() self.logMenuItem.connect( 'activate', self.on_menuitem_activate, userMenu.user.email ) def on_menuitem_activate(self, widget, email): self.window = gtk.Window() self.window.set_border_width(5) self.window.set_title(_("Conversation log for %s" % email)) self.window.set_default_size(650,350) textview = gtk.TextView() self.textBuffer = textview.get_buffer() scroll = gtk.ScrolledWindow() scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.textview = HtmlTextView(self.controller, self.textBuffer, scroll) self.textview.set_wrap_mode(gtk.WRAP_WORD_CHAR) self.textview.set_left_margin(6) self.textview.set_right_margin(6) self.textview.set_editable(False) self.textview.set_cursor_visible(False) scroll.add_with_viewport(self.textview) hbox = gtk.HBox(False, 5) saveButton = gtk.Button(stock=gtk.STOCK_SAVE) saveButton.connect("clicked", self.on_save_logs) refreshButton = gtk.Button(stock=gtk.STOCK_REFRESH) refreshButton.connect("clicked", self.on_refresh_log, email) closeButton = gtk.Button(stock=gtk.STOCK_CLOSE) closeButton.connect("clicked", lambda w: self.window.hide()) hbox.pack_end(saveButton, False, False) hbox.pack_end(refreshButton, False, False) hbox.pack_end(closeButton, False, False) textRenderer = gtk.CellRendererText() column = gtk.TreeViewColumn(_("Date"), textRenderer, markup=0) self.datesListstore = gtk.ListStore(str) self.datesTree = gtk.TreeView(self.datesListstore) self.datesTree.connect("cursor-changed", self.on_dates_cursor_change) self.datesTree.append_column(column) self.datesTree.set_headers_visible(False) datesScroll = gtk.ScrolledWindow() datesScroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) datesScroll.add_with_viewport(self.datesTree) datesScroll.set_size_request(210, -1) hPaned = gtk.HPaned() hPaned.pack1(datesScroll) hPaned.pack2(scroll) vbox = gtk.VBox(spacing=5) vbox.pack_start(hPaned) vbox.pack_start(hbox, False, False) vbox.pack_start(gtk.Statusbar(), False, False) self.datesStamps = {} logsCant = len(self.fill_dates_tree(email)) if not logsCant > 0: dialog.information(_("No logs were found for %s" % (email) )) else: self.window.add(vbox) self.window.show_all() def fill_dates_tree(self, mail): '''fill the treeview with the logs dates''' for (id_conversation, stamp) in self.logger.get_conversation_ids(mail): ctime = str(time.ctime(float(stamp))) self.datesListstore.append([ctime]) self.datesStamps[ctime] = id_conversation return self.datesStamps def on_dates_cursor_change(self, *args): try: selected = self.datesListstore.get( self.datesTree.get_selection().get_selected()[1], 0)[0] except (TypeError, AttributeError): return id_conversation = self.datesStamps[selected] put_new_line = False nick_cache = {} self.textview.get_buffer().set_text("") for (stamp, mail, message) in \ self.logger.get_conversation(id_conversation, 10000): if mail in nick_cache: if nick_cache[mail]['next'] is not None and \ nick_cache[mail]['next'] <= stamp: nick = self.logger.get_user_nick(mail, stamp) next = self.logger.get_next_nick_stamp(mail, stamp) nick_cache[mail] = {'nick': nick, 'next' : next} else: nick = nick_cache[mail]['nick'] else: nick = self.logger.get_user_nick(mail, stamp) next = self.logger.get_next_nick_stamp(mail, stamp) nick_cache[mail] = {'nick': nick, 'next' : next} date = time.ctime(float(stamp)) (_format, encoding, text) = message.split('\r\n', 2) style = parse_format(_format) nick = self.controller.unifiedParser.getParser(nick).get() text = self.controller.unifiedParser.getParser(text).get() text = text.replace("\r\n", "\n").replace("\n", "<br />") try: self.textview.display_html('<body><b>%s: </b>' '<span style="%s">%s</span></body>' % (nick, style, text)) except: pass put_new_line = True def on_save_logs(self, button): title = (_("Save conversation log")) dialog = gtk.FileChooserDialog(title, None, \ gtk.FILE_CHOOSER_ACTION_SAVE, \ (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, \ gtk.STOCK_SAVE, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) dialog.show_all() response = dialog.run() while True: if response != gtk.RESPONSE_OK: break if dialog.get_filename(): path = dialog.get_filename() f = open(path, "w") f.write(self.textBuffer.get_text(self.textBuffer.get_start_iter(),\ self.textBuffer.get_end_iter() )) f.close() break dialog.hide() def on_refresh_log(self, button, email): self.on_dates_cursor_change() def _cb_self_nick_changed(self, msn, old, new, stamp=None): '''called when we change our own nick''' if new != self.get_last_nick(self.msn.user): self.logger.user_event_add('nick-changed', self.msn.user, new, stamp) def _cb_personal_message_changed(self, msn, email, personal_message, stamp=None): '''called when an user changes the personal message''' if personal_message != '' and \ self.get_just_last_personal_message(email) != personal_message: self.logger.user_event_add('personal-message-changed', email, personal_message, stamp) def _cb_self_status_changed(self, msn, status, stamp=None): '''called when we change the status''' if self.get_last_status(msn.user) != status: self.logger.user_event_add('status-changed', self.msn.user, status, stamp) def _cb_self_personal_message_changed(self, msn, ourmail, personal_message, stamp=None): '''called when we change our personal message''' if personal_message != '' and \ self.get_just_last_personal_message(ourmail) != personal_message: self.logger.user_event_add('personal-message-changed', ourmail, personal_message, stamp) def _cb_self_current_media_changed(self, msn, ourmail, personal_message, dict_, stamp=None): '''called when we change our current media''' if personal_message != '' and \ self.get_just_last_personal_message(ourmail) != personal_message: self.logger.user_event_add('personal-message-changed', ourmail, personal_message, stamp) def _cb_nick_changed(self, msn, email, nick, stamp=None): '''called when someone change his nick''' if nick != self.get_last_nick(email): self.logger.user_event_add('nick-changed', email, nick, stamp) def _cb_contact_status_change(self, msn, email, status, stamp=None): '''called when someone change his status''' if self.get_last_status(email) != status: self.logger.user_event_add('status-changed', email, status, stamp) def _cb_initial_status_change(self, msn, command, tid, params, stamp=None): '''called when someone change his status when we come online''' data = params.split(' ') status = data[0] email = data[1].lower() if self.get_last_status(email) != status: self.logger.user_event_add('status-changed', email, status, stamp) def _cb_user_offline(self, msn, email, stamp=None): '''called when someone goes offline''' self.logger.user_event_add('status-changed', email, 'FLN', stamp) def _cb_sb_user_join(self, msn, switchboard, signal, args, stamp=None): '''called when an user join to the conversation''' mail = args[0] self.logger.conversation_event_add('user-join', switchboard.started, mail, '', stamp) def _cb_sb_user_leave(self, msn, switchboard, signal, args, stamp=None): '''called when an user leaves the conversation''' mail = args[0] self.logger.conversation_event_add('user-leave', switchboard.started, mail, '', stamp) def _cb_sb_message(self, msn, switchboard, signal, args, stamp=None): '''called when we receive a message''' mail, nick, body, format, charset = args message = '%s\r\n%s\r\n%s' % (format, charset, body.replace('\\', '\\\\')) self.logger.conversation_event_add('message', switchboard.started, mail, message, stamp) def _cb_sb_action_message(self, msn, switchboard, signal, args, stamp=None): '''called when an action message is received''' mail, data = args self.logger.conversation_event_add('action-message', switchboard.started, mail, data, stamp) def _cb_sb_ink_message(self, msn, switchboard, signal, args, stamp=None): '''called when an ink message is received''' signal, filename = args self.logger.conversation_event_add('ink-message', switchboard.started, mail, filename, stamp) def _cb_sb_nudge(self, msn, switchboard, signal, args, stamp=None): '''called when a nudge is received''' mail = args[0] self.logger.conversation_event_add('nudge', switchboard.started, mail, '', stamp) def _cb_sb_message_sent(self, msn, switchboard, signal, args, stamp=None): '''called when we send a message''' body, format, charset = args try: format = format.split('X-MMS-IM-Format: ')[1] except IndexError: format = '' message = '%s\r\n%s\r\n%s' % (format, charset, body) self.logger.conversation_event_add('message', switchboard.started, self.msn.user, message, stamp) def _cb_sb_action_message_sent(self, msn, switchboard, signal, args, stamp=None): '''called when an action message is received''' data = args[0] self.logger.conversation_event_add('action-message', switchboard.started, self.msn.user, data, stamp) def _cb_sb_ink_message_sent(self, msn, switchboard, signal, args, stamp=None): '''called when an ink message is sent''' self.logger.conversation_event_add('ink-message', switchboard.started, self.msn.user, '', stamp) def _cb_sb_nudge_sent(self, msn, switchboard, signal, args, stamp=None): '''called when a nudge is sent''' self.logger.conversation_event_add('nudge', switchboard.started, self.msn.user, '', stamp) def _cb_sb_custom_emoticon_received(self, msn, switchboard, signal, args, stamp=None): '''called when a custom emoticon is received (du'h)''' shortcut, msnobj = args filename = shortcut + '_' + msnobj.sha1d + '.tmp' filename = urllib.quote(filename).replace('/', '_') complete_filename = self.msn.cacheDir + os.sep + filename self.logger.conversation_event_add('custom-emoticon', switchboard.started, self.msn.user, shortcut + ' ' + complete_filename, stamp) def _cb_display_picture_changed(self, msn, switchboard, msnobj, mail, stamp=None): '''called when a new display picture is received''' contact = self.msn.contactManager.getContact(mail) if contact is None: return filename = contact.displayPicturePath result = self.get_last_display_picture(mail) if result: old_filename = result[0][1] else: old_filename = None if old_filename != filename: self.logger.user_event_add('display-picture', mail, \ filename, stamp) def configure( self ): configuration = [] configuration.append(Plugin.Option('', gtk.Widget, '', '', gtk.Label("Enable/disable events"))) for signal in self.signals: configuration.append(Plugin.Option(signal, bool, signal.replace('-',' '), \ '', bool(int(self.config.getPluginValue(self.name, signal, True))))) configWindow = Plugin.ConfigWindow( 'Logger', configuration ) response = configWindow.run() if response != None: for signal in self.signals: if response.has_key(signal): self.config.setPluginValue( self.name, signal, str(int(response[signal].value)) ) self.stop() self.start() return True
class PluginsWindow(object): '''Class for Plugins window''' @log_calls('PluginsWindow') def __init__(self): '''Initialize Plugins window''' self.xml = gtkgui_helpers.get_gtk_builder('plugins_window.ui') self.window = self.xml.get_object('plugins_window') self.window.set_transient_for(gajim.interface.roster.window) widgets_to_extract = ('plugins_notebook', 'plugin_name_label', 'plugin_version_label', 'plugin_authors_label', 'plugin_homepage_linkbutton', 'uninstall_plugin_button', 'configure_plugin_button', 'installed_plugins_treeview') for widget_name in widgets_to_extract: setattr(self, widget_name, self.xml.get_object(widget_name)) self.plugin_description_textview = HtmlTextView() sw = self.xml.get_object('scrolledwindow2') sw.add(self.plugin_description_textview) self.installed_plugins_model = Gtk.ListStore(object, str, bool, bool, GdkPixbuf.Pixbuf) self.installed_plugins_treeview.set_model(self.installed_plugins_model) self.installed_plugins_treeview.set_rules_hint(True) renderer = Gtk.CellRendererText() col = Gtk.TreeViewColumn(_('Plugin'))#, renderer, text=NAME) cell = Gtk.CellRendererPixbuf() col.pack_start(cell, False) col.add_attribute(cell, 'pixbuf', ICON) col.pack_start(renderer, True) col.add_attribute(renderer, 'text', NAME) col.set_property('expand', True) self.installed_plugins_treeview.append_column(col) renderer = Gtk.CellRendererToggle() renderer.connect('toggled', self.installed_plugins_toggled_cb) col = Gtk.TreeViewColumn(_('Active'), renderer, active=ACTIVE, activatable=ACTIVATABLE) self.installed_plugins_treeview.append_column(col) icon = Gtk.Image() self.def_icon = icon.render_icon_pixbuf(Gtk.STOCK_PREFERENCES, Gtk.IconSize.MENU) # connect signal for selection change selection = self.installed_plugins_treeview.get_selection() selection.connect('changed', self.installed_plugins_treeview_selection_changed) selection.set_mode(Gtk.SelectionMode.SINGLE) self._clear_installed_plugin_info() self.fill_installed_plugins_model() root_iter = self.installed_plugins_model.get_iter_first() if root_iter: selection.select_iter(root_iter ) self.xml.connect_signals(self) self.plugins_notebook.set_current_page(0) self.xml.get_object('close_button').grab_focus() self.window.show_all() gtkgui_helpers.possibly_move_window_in_current_desktop(self.window) def on_plugins_notebook_switch_page(self, widget, page, page_num): GLib.idle_add(self.xml.get_object('close_button').grab_focus) @log_calls('PluginsWindow') def installed_plugins_treeview_selection_changed(self, treeview_selection): model, iter = treeview_selection.get_selected() if iter: plugin = model.get_value(iter, PLUGIN) plugin_name = model.get_value(iter, NAME) is_active = model.get_value(iter, ACTIVE) self._display_installed_plugin_info(plugin) else: self._clear_installed_plugin_info() def _display_installed_plugin_info(self, plugin): self.plugin_name_label.set_text(plugin.name) self.plugin_version_label.set_text(plugin.version) self.plugin_authors_label.set_text(plugin.authors) self.plugin_homepage_linkbutton.set_uri(plugin.homepage) self.plugin_homepage_linkbutton.set_label(plugin.homepage) label = self.plugin_homepage_linkbutton.get_children()[0] label.set_ellipsize(Pango.EllipsizeMode.END) self.plugin_homepage_linkbutton.set_property('sensitive', True) desc_textbuffer = self.plugin_description_textview.get_buffer() desc_textbuffer.set_text('') txt = plugin.description txt.replace('</body>', '') if plugin.available_text: txt += '<br/><br/>' + _('Warning: %s') % plugin.available_text if not txt.startswith('<body '): txt = '<body xmlns=\'http://www.w3.org/1999/xhtml\'>' + txt txt += ' </body>' self.plugin_description_textview.display_html(txt, self.plugin_description_textview, None) self.plugin_description_textview.set_property('sensitive', True) self.uninstall_plugin_button.set_property('sensitive', gajim.PLUGINS_DIRS[1] in plugin.__path__) self.configure_plugin_button.set_property( 'sensitive', not plugin.config_dialog is None) def _clear_installed_plugin_info(self): self.plugin_name_label.set_text('') self.plugin_version_label.set_text('') self.plugin_authors_label.set_text('') self.plugin_homepage_linkbutton.set_uri('') self.plugin_homepage_linkbutton.set_label('') self.plugin_homepage_linkbutton.set_property('sensitive', False) desc_textbuffer = self.plugin_description_textview.get_buffer() desc_textbuffer.set_text('') self.plugin_description_textview.set_property('sensitive', False) self.uninstall_plugin_button.set_property('sensitive', False) self.configure_plugin_button.set_property('sensitive', False) @log_calls('PluginsWindow') def fill_installed_plugins_model(self): pm = gajim.plugin_manager self.installed_plugins_model.clear() self.installed_plugins_model.set_sort_column_id(1, Gtk.SortType.ASCENDING) for plugin in pm.plugins: icon = self.get_plugin_icon(plugin) self.installed_plugins_model.append([plugin, plugin.name, plugin.active and plugin.activatable, plugin.activatable, icon]) def get_plugin_icon(self, plugin): icon_file = os.path.join(plugin.__path__, os.path.split( plugin.__path__)[1]) + '.png' icon = self.def_icon if os.path.isfile(icon_file): icon = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_file, 16, 16) return icon @log_calls('PluginsWindow') def installed_plugins_toggled_cb(self, cell, path): is_active = self.installed_plugins_model[path][ACTIVE] plugin = self.installed_plugins_model[path][PLUGIN] if is_active: gajim.plugin_manager.deactivate_plugin(plugin) else: try: gajim.plugin_manager.activate_plugin(plugin) except GajimPluginActivateException as e: WarningDialog(_('Plugin failed'), str(e), transient_for=self.window) return self.installed_plugins_model[path][ACTIVE] = not is_active @log_calls('PluginsWindow') def on_plugins_window_destroy(self, widget): '''Close window''' del gajim.interface.instances['plugins'] @log_calls('PluginsWindow') def on_close_button_clicked(self, widget): self.window.destroy() @log_calls('PluginsWindow') def on_configure_plugin_button_clicked(self, widget): #log.debug('widget: %s'%(widget)) selection = self.installed_plugins_treeview.get_selection() model, iter = selection.get_selected() if iter: plugin = model.get_value(iter, PLUGIN) plugin_name = model.get_value(iter, NAME) is_active = model.get_value(iter, ACTIVE) result = plugin.config_dialog.run(self.window) else: # No plugin selected. this should never be reached. As configure # plugin button should only be clickable when plugin is selected. # XXX: maybe throw exception here? pass @log_calls('PluginsWindow') def on_uninstall_plugin_button_clicked(self, widget): selection = self.installed_plugins_treeview.get_selection() model, iter = selection.get_selected() if iter: plugin = model.get_value(iter, PLUGIN) plugin_name = model.get_value(iter, NAME) is_active = model.get_value(iter, ACTIVE) try: gajim.plugin_manager.remove_plugin(plugin) except PluginsystemError as e: WarningDialog(_('Unable to properly remove the plugin'), str(e), self.window) return model.remove(iter) @log_calls('PluginsWindow') def on_install_plugin_button_clicked(self, widget): def show_warn_dialog(): text = _('Archive is malformed') dialog = WarningDialog(text, '', transient_for=self.window) dialog.set_modal(False) dialog.popup() def _on_plugin_exists(zip_filename): def on_yes(is_checked): plugin = gajim.plugin_manager.install_from_zip(zip_filename, True) if not plugin: show_warn_dialog() return model = self.installed_plugins_model for row in list(range(len(model))): if plugin == model[row][PLUGIN]: model.remove(model.get_iter((row, PLUGIN))) break iter_ = model.append([plugin, plugin.name, False, plugin.activatable, self.get_plugin_icon(plugin)]) sel = self.installed_plugins_treeview.get_selection() sel.select_iter(iter_) YesNoDialog(_('Plugin already exists'), sectext=_('Overwrite?'), on_response_yes=on_yes, transient_for=self.window) def _try_install(zip_filename): try: plugin = gajim.plugin_manager.install_from_zip(zip_filename) except PluginsystemError as er_type: error_text = str(er_type) if error_text == _('Plugin already exists'): _on_plugin_exists(zip_filename) return WarningDialog(error_text, '"%s"' % zip_filename, self.window) return if not plugin: show_warn_dialog() return model = self.installed_plugins_model iter_ = model.append([plugin, plugin.name, False, plugin.activatable], self.get_plugin_icon(plugin)) sel = self.installed_plugins_treeview.get_selection() sel.select_iter(iter_) self.dialog = ArchiveChooserDialog(on_response_ok=_try_install)
class PluginsWindow(object): '''Class for Plugins window''' @log_calls('PluginsWindow') def __init__(self): '''Initialize Plugins window''' self.xml = gtkgui_helpers.get_gtk_builder('plugins_window.ui') self.window = self.xml.get_object('plugins_window') self.window.set_transient_for(gajim.interface.roster.window) widgets_to_extract = ('plugins_notebook', 'plugin_name_label', 'plugin_version_label', 'plugin_authors_label', 'plugin_homepage_linkbutton', 'uninstall_plugin_button', 'configure_plugin_button', 'installed_plugins_treeview') for widget_name in widgets_to_extract: setattr(self, widget_name, self.xml.get_object(widget_name)) self.plugin_description_textview = HtmlTextView() sw = self.xml.get_object('scrolledwindow2') sw.add(self.plugin_description_textview) self.installed_plugins_model = Gtk.ListStore(object, str, bool, bool, GdkPixbuf.Pixbuf) self.installed_plugins_treeview.set_model(self.installed_plugins_model) self.installed_plugins_treeview.set_rules_hint(True) renderer = Gtk.CellRendererText() col = Gtk.TreeViewColumn(_('Plugin')) #, renderer, text=NAME) cell = Gtk.CellRendererPixbuf() col.pack_start(cell, False) col.add_attribute(cell, 'pixbuf', ICON) col.pack_start(renderer, True) col.add_attribute(renderer, 'text', NAME) col.set_property('expand', True) self.installed_plugins_treeview.append_column(col) renderer = Gtk.CellRendererToggle() renderer.connect('toggled', self.installed_plugins_toggled_cb) col = Gtk.TreeViewColumn(_('Active'), renderer, active=ACTIVE, activatable=ACTIVATABLE) self.installed_plugins_treeview.append_column(col) self.def_icon = gtkgui_helpers.get_icon_pixmap('preferences-desktop') # connect signal for selection change selection = self.installed_plugins_treeview.get_selection() selection.connect('changed', self.installed_plugins_treeview_selection_changed) selection.set_mode(Gtk.SelectionMode.SINGLE) self._clear_installed_plugin_info() self.fill_installed_plugins_model() root_iter = self.installed_plugins_model.get_iter_first() if root_iter: selection.select_iter(root_iter) self.xml.connect_signals(self) self.plugins_notebook.set_current_page(0) self.xml.get_object('close_button').grab_focus() self.window.show_all() gtkgui_helpers.possibly_move_window_in_current_desktop(self.window) def on_plugins_notebook_switch_page(self, widget, page, page_num): GLib.idle_add(self.xml.get_object('close_button').grab_focus) @log_calls('PluginsWindow') def installed_plugins_treeview_selection_changed(self, treeview_selection): model, iter = treeview_selection.get_selected() if iter: plugin = model.get_value(iter, PLUGIN) plugin_name = model.get_value(iter, NAME) is_active = model.get_value(iter, ACTIVE) self._display_installed_plugin_info(plugin) else: self._clear_installed_plugin_info() def _display_installed_plugin_info(self, plugin): self.plugin_name_label.set_text(plugin.name) self.plugin_version_label.set_text(plugin.version) self.plugin_authors_label.set_text(plugin.authors) self.plugin_homepage_linkbutton.set_uri(plugin.homepage) self.plugin_homepage_linkbutton.set_label(plugin.homepage) label = self.plugin_homepage_linkbutton.get_children()[0] label.set_ellipsize(Pango.EllipsizeMode.END) self.plugin_homepage_linkbutton.set_property('sensitive', True) desc_textbuffer = self.plugin_description_textview.get_buffer() desc_textbuffer.set_text('') txt = plugin.description txt.replace('</body>', '') if plugin.available_text: txt += '<br/><br/>' + _('Warning: %s') % plugin.available_text if not txt.startswith('<body '): txt = '<body xmlns=\'http://www.w3.org/1999/xhtml\'>' + txt txt += ' </body>' self.plugin_description_textview.display_html( txt, self.plugin_description_textview, None) self.plugin_description_textview.set_property('sensitive', True) self.uninstall_plugin_button.set_property( 'sensitive', gajim.PLUGINS_DIRS[1] in plugin.__path__) self.configure_plugin_button.set_property( 'sensitive', not plugin.config_dialog is None) def _clear_installed_plugin_info(self): self.plugin_name_label.set_text('') self.plugin_version_label.set_text('') self.plugin_authors_label.set_text('') self.plugin_homepage_linkbutton.set_uri('') self.plugin_homepage_linkbutton.set_label('') self.plugin_homepage_linkbutton.set_property('sensitive', False) desc_textbuffer = self.plugin_description_textview.get_buffer() desc_textbuffer.set_text('') self.plugin_description_textview.set_property('sensitive', False) self.uninstall_plugin_button.set_property('sensitive', False) self.configure_plugin_button.set_property('sensitive', False) @log_calls('PluginsWindow') def fill_installed_plugins_model(self): pm = gajim.plugin_manager self.installed_plugins_model.clear() self.installed_plugins_model.set_sort_column_id( 1, Gtk.SortType.ASCENDING) for plugin in pm.plugins: icon = self.get_plugin_icon(plugin) self.installed_plugins_model.append([ plugin, plugin.name, plugin.active and plugin.activatable, plugin.activatable, icon ]) def get_plugin_icon(self, plugin): icon_file = os.path.join(plugin.__path__, os.path.split(plugin.__path__)[1]) + '.png' icon = self.def_icon if os.path.isfile(icon_file): icon = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_file, 16, 16) return icon @log_calls('PluginsWindow') def installed_plugins_toggled_cb(self, cell, path): is_active = self.installed_plugins_model[path][ACTIVE] plugin = self.installed_plugins_model[path][PLUGIN] if is_active: gajim.plugin_manager.deactivate_plugin(plugin) else: try: gajim.plugin_manager.activate_plugin(plugin) except GajimPluginActivateException as e: WarningDialog(_('Plugin failed'), str(e), transient_for=self.window) return self.installed_plugins_model[path][ACTIVE] = not is_active @log_calls('PluginsWindow') def on_plugins_window_destroy(self, widget): '''Close window''' del gajim.interface.instances['plugins'] @log_calls('PluginsWindow') def on_close_button_clicked(self, widget): self.window.destroy() @log_calls('PluginsWindow') def on_configure_plugin_button_clicked(self, widget): #log.debug('widget: %s'%(widget)) selection = self.installed_plugins_treeview.get_selection() model, iter = selection.get_selected() if iter: plugin = model.get_value(iter, PLUGIN) plugin_name = model.get_value(iter, NAME) is_active = model.get_value(iter, ACTIVE) result = plugin.config_dialog.run(self.window) else: # No plugin selected. this should never be reached. As configure # plugin button should only be clickable when plugin is selected. # XXX: maybe throw exception here? pass @log_calls('PluginsWindow') def on_uninstall_plugin_button_clicked(self, widget): selection = self.installed_plugins_treeview.get_selection() model, iter = selection.get_selected() if iter: plugin = model.get_value(iter, PLUGIN) plugin_name = model.get_value(iter, NAME) is_active = model.get_value(iter, ACTIVE) try: gajim.plugin_manager.remove_plugin(plugin) except PluginsystemError as e: WarningDialog(_('Unable to properly remove the plugin'), str(e), self.window) return model.remove(iter) @log_calls('PluginsWindow') def on_install_plugin_button_clicked(self, widget): def show_warn_dialog(): text = _('Archive is malformed') dialog = WarningDialog(text, '', transient_for=self.window) dialog.set_modal(False) dialog.popup() def _on_plugin_exists(zip_filename): def on_yes(is_checked): plugin = gajim.plugin_manager.install_from_zip( zip_filename, True) if not plugin: show_warn_dialog() return model = self.installed_plugins_model for row in list(range(len(model))): if plugin == model[row][PLUGIN]: model.remove(model.get_iter((row, PLUGIN))) break iter_ = model.append([ plugin, plugin.name, False, plugin.activatable, self.get_plugin_icon(plugin) ]) sel = self.installed_plugins_treeview.get_selection() sel.select_iter(iter_) YesNoDialog(_('Plugin already exists'), sectext=_('Overwrite?'), on_response_yes=on_yes, transient_for=self.window) def _try_install(zip_filename): try: plugin = gajim.plugin_manager.install_from_zip(zip_filename) except PluginsystemError as er_type: error_text = str(er_type) if error_text == _('Plugin already exists'): _on_plugin_exists(zip_filename) return WarningDialog(error_text, '"%s"' % zip_filename, self.window) return if not plugin: show_warn_dialog() return model = self.installed_plugins_model iter_ = model.append( [plugin, plugin.name, False, plugin.activatable], self.get_plugin_icon(plugin)) sel = self.installed_plugins_treeview.get_selection() sel.select_iter(iter_) self.dialog = ArchiveChooserDialog(on_response_ok=_try_install)
class ConversationTextview(GObject.GObject): """ Class for the conversation textview (where user reads already said messages) for chat/groupchat windows """ __gsignals__ = dict(quote=( GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, None, # return value (str, ) # arguments )) # smooth scroll constants MAX_SCROLL_TIME = 0.4 # seconds SCROLL_DELAY = 33 # milliseconds def __init__(self, used_in_history_window=False): """ If used_in_history_window is True, then we do not show Clear menuitem in context menu """ GObject.GObject.__init__(self) self.used_in_history_window = used_in_history_window #self.fc = FuzzyClock() # no need to inherit TextView, use it as atrribute is safer self.tv = HtmlTextView() #self.tv.hyperlink_handler = self.hyperlink_handler # set properties self.tv.set_border_width(1) self.tv.set_accepts_tab(True) self.tv.set_editable(False) self.tv.set_cursor_visible(False) self.tv.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) self.tv.set_left_margin(2) self.tv.set_right_margin(2) self.handlers = {} self.images = [] self.image_cache = {} self.xep0184_marks = {} self.xep0184_shown = {} self.last_sent_message_marks = [None, None] # A pair per occupant. Key is '' in normal chat self.last_received_message_marks = {} # It's True when we scroll in the code, so we can detect scroll from user self.auto_scrolling = False # connect signals id_ = self.tv.connect('motion_notify_event', self.on_textview_motion_notify_event) self.handlers[id_] = self.tv id_ = self.tv.connect('populate_popup', self.on_textview_populate_popup) self.handlers[id_] = self.tv id_ = self.tv.connect('button_press_event', self.on_textview_button_press_event) self.handlers[id_] = self.tv id_ = self.tv.connect('draw', self.on_textview_draw) self.handlers[id_] = self.tv self.change_cursor = False self.last_time_printout = 0 #font = Pango.FontDescription(gajim.config.get('conversation_font')) #self.tv.override_font(font) buffer_ = self.tv.get_buffer() end_iter = buffer_.get_end_iter() buffer_.create_mark('end', end_iter, False) #self.tagIn = buffer_.create_tag('incoming') #color = gajim.config.get('inmsgcolor') #font = Pango.FontDescription(gajim.config.get('inmsgfont')) #self.tagIn.set_property('foreground', color) #self.tagIn.set_property('font-desc', font) #self.tagOut = buffer_.create_tag('outgoing') #color = gajim.config.get('outmsgcolor') #font = Pango.FontDescription(gajim.config.get('outmsgfont')) #self.tagOut.set_property('foreground', color) #self.tagOut.set_property('font-desc', font) #self.tagStatus = buffer_.create_tag('status') #color = gajim.config.get('statusmsgcolor') #font = Pango.FontDescription(gajim.config.get('satusmsgfont')) #self.tagStatus.set_property('foreground', color) #self.tagStatus.set_property('font-desc', font) #self.tagInText = buffer_.create_tag('incomingtxt') #color = gajim.config.get('inmsgtxtcolor') #font = Pango.FontDescription(gajim.config.get('inmsgtxtfont')) #if color: # self.tagInText.set_property('foreground', color) #self.tagInText.set_property('font-desc', font) #self.tagOutText = buffer_.create_tag('outgoingtxt') #color = gajim.config.get('outmsgtxtcolor') #if color: # font = Pango.FontDescription(gajim.config.get('outmsgtxtfont')) #self.tagOutText.set_property('foreground', color) #self.tagOutText.set_property('font-desc', font) #colors = gajim.config.get('gc_nicknames_colors') #colors = colors.split(':') #for i, color in enumerate(colors): # tagname = 'gc_nickname_color_' + str(i) # tag = buffer_.create_tag(tagname) # tag.set_property('foreground', color) #self.tagMarked = buffer_.create_tag('marked') #color = gajim.config.get('markedmsgcolor') #self.tagMarked.set_property('foreground', color) #self.tagMarked.set_property('weight', Pango.Weight.BOLD) #tag = buffer_.create_tag('time_sometimes') #tag.set_property('foreground', 'darkgrey') #Pango.SCALE_SMALL #tag.set_property('scale', 0.8333333333333) #tag.set_property('justification', Gtk.Justification.CENTER) #tag = buffer_.create_tag('small') #Pango.SCALE_SMALL #tag.set_property('scale', 0.8333333333333) #tag = buffer_.create_tag('restored_message') #color = gajim.config.get('restored_messages_color') #tag.set_property('foreground', color) #self.tv.create_tags() #tag = buffer_.create_tag('bold') #tag.set_property('weight', Pango.Weight.BOLD) #tag = buffer_.create_tag('italic') #tag.set_property('style', Pango.Style.ITALIC) #tag = buffer_.create_tag('underline') #tag.set_property('underline', Pango.Underline.SINGLE) #buffer_.create_tag('focus-out-line', justification = Gtk.Justification.CENTER) #self.displaymarking_tags = {} #tag = buffer_.create_tag('xep0184-warning') #tag.set_property('foreground', '#cc0000') #tag = buffer_.create_tag('xep0184-received') #tag.set_property('foreground', '#73d216') # One mark at the begining then 2 marks between each lines #size = gajim.config.get('max_conversation_lines') #size = 2 * size - 1 #self.marks_queue = queue.Queue(size) self.allow_focus_out_line = True # holds a mark at the end of --- line self.focus_out_end_mark = None #self.xep0184_warning_tooltip = tooltips.BaseTooltip() #self.line_tooltip = tooltips.BaseTooltip() self.smooth_id = None self.just_cleared = False size = 500 size = 2 * size - 1 self.marks_queue = queue.Queue(size) def print_conversation_line(self, text, jid, kind, name): """ Print 'chat' type messages """ buffer_ = self.tv.get_buffer() buffer_.begin_user_action() if self.marks_queue.full(): # remove oldest line m1 = self.marks_queue.get() m2 = self.marks_queue.get() i1 = buffer_.get_iter_at_mark(m1) i2 = buffer_.get_iter_at_mark(m2) buffer_.delete(i1, i2) buffer_.delete_mark(m1) end_iter = buffer_.get_end_iter() end_offset = end_iter.get_offset() at_the_end = self.at_the_end() move_selection = False if buffer_.get_has_selection() and buffer_.get_selection_bounds()[1].\ get_offset() == end_offset: move_selection = True # Create one mark and add it to queue once if it's the first line # else twice (one for end bound, one for start bound) mark = None if buffer_.get_char_count() > 0: mark = buffer_.create_mark(None, end_iter, left_gravity=True) self.marks_queue.put(mark) if not mark: mark = buffer_.create_mark(None, end_iter, left_gravity=True) self.marks_queue.put(mark) if kind == 'incoming_queue': kind = 'incoming' # print the time stamp # We don't have tim for outgoing messages... import time tim = time.localtime() direction_mark = '' # don't apply direction mark if it's status message timestamp_str = self.get_time_to_show(tim, direction_mark) timestamp = time.strftime(timestamp_str, tim) timestamp = timestamp + ' ' buffer_.insert(end_iter, timestamp) self.print_name(name, kind, direction_mark=direction_mark, iter_=end_iter) #if kind == 'incoming': # text_tags.append('incomingtxt') # mark1 = mark #elif kind == 'outgoing': # text_tags.append('outgoingtxt') # mark1 = mark #subject = None #self.print_subject(subject, iter_=end_iter) self.print_real_text(text, name, iter_=end_iter) # scroll to the end of the textview if at_the_end or kind == 'outgoing': # we are at the end or we are sending something # scroll to the end (via idle in case the scrollbar has appeared) if True: GLib.idle_add(self.smooth_scroll_to_end) else: GLib.idle_add(self.scroll_to_end) self.just_cleared = False buffer_.end_user_action() return end_iter # Smooth scrolling inspired by Pidgin code def smooth_scroll(self): parent = self.tv.get_parent() if not parent: return False vadj = parent.get_vadjustment() max_val = vadj.get_upper() - vadj.get_page_size() + 1 cur_val = vadj.get_value() # scroll by 1/3rd of remaining distance onethird = cur_val + ((max_val - cur_val) / 3.0) self.auto_scrolling = True vadj.set_value(onethird) self.auto_scrolling = False if max_val - onethird < 0.01: self.smooth_id = None self.smooth_scroll_timer.cancel() return False return True def smooth_scroll_timeout(self): GLib.idle_add(self.do_smooth_scroll_timeout) return def do_smooth_scroll_timeout(self): if not self.smooth_id: # we finished scrolling return GLib.source_remove(self.smooth_id) self.smooth_id = None parent = self.tv.get_parent() if parent: vadj = parent.get_vadjustment() self.auto_scrolling = True vadj.set_value(vadj.get_upper() - vadj.get_page_size() + 1) self.auto_scrolling = False def smooth_scroll_to_end(self): if None != self.smooth_id: # already scrolling return False self.smooth_id = GLib.timeout_add(self.SCROLL_DELAY, self.smooth_scroll) self.smooth_scroll_timer = Timer(self.MAX_SCROLL_TIME, self.smooth_scroll_timeout) self.smooth_scroll_timer.start() return False def print_name(self, name, kind, direction_mark='', iter_=None): if name: buffer_ = self.tv.get_buffer() if iter_: end_iter = iter_ else: end_iter = buffer_.get_end_iter() before_str = '' after_str = ':' format_ = before_str + name + direction_mark + after_str + ' ' buffer_.insert(end_iter, format_) def print_real_text(self, text, name, iter_=None): """ Add normal and special text. call this to add text """ # /me is replaced by name if name is given if name and (text.startswith('/me ') or text.startswith('/me\n')): text = '* ' + name + text[3:] #text_tags.append('italic') # detect urls formatting and if the user has it on emoticons buffer_ = self.tv.get_buffer() buffer_.insert(iter_, text + "\n") #return self.detect_and_print_special_text(text, iter_=iter_) def get_time_to_show(self, tim, direction_mark=''): from calendar import timegm import time """ Get the time, with the day before if needed and return it. It DOESN'T format a fuzzy time """ format_ = '' # get difference in days since epoch (86400 = 24*3600) # number of days since epoch for current time (in GMT) - # number of days since epoch for message (in GMT) diff_day = int(int(timegm(time.localtime())) / 86400 -\ int(timegm(tim)) / 86400) timestamp_str = '[%X]' format_ += timestamp_str tim_format = time.strftime(format_, tim) return tim_format def on_textview_motion_notify_event(self, widget, event): """ Change the cursor to a hand when we are over a mail or an url """ w = self.tv.get_window(Gtk.TextWindowType.TEXT) device = w.get_display().get_device_manager().get_client_pointer() pointer = w.get_device_position(device) x, y = self.tv.window_to_buffer_coords(Gtk.TextWindowType.TEXT, pointer[1], pointer[2]) tags = self.tv.get_iter_at_location(x, y).get_tags() if self.change_cursor: w.set_cursor(Gdk.Cursor.new(Gdk.CursorType.XTERM)) self.change_cursor = False tag_table = self.tv.get_buffer().get_tag_table() over_line = False xep0184_warning = False for tag in tags: if tag in (tag_table.lookup('url'), tag_table.lookup('mail'), \ tag_table.lookup('xmpp'), tag_table.lookup('sth_at_sth')): w.set_cursor(Gdk.Cursor.new(Gdk.CursorType.HAND2)) self.change_cursor = True elif tag == tag_table.lookup('focus-out-line'): over_line = True elif tag == tag_table.lookup('xep0184-warning'): xep0184_warning = True #if self.line_tooltip.timeout != 0: # Check if we should hide the line tooltip # if not over_line: # self.line_tooltip.hide_tooltip() #if self.xep0184_warning_tooltip.timeout != 0: # Check if we should hide the XEP-184 warning tooltip # if not xep0184_warning: # self.xep0184_warning_tooltip.hide_tooltip() if over_line and not self.line_tooltip.win: self.line_tooltip.timeout = GLib.timeout_add( 500, self.show_line_tooltip) w.set_cursor(Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR)) self.change_cursor = True if xep0184_warning and not self.xep0184_warning_tooltip.win: self.xep0184_warning_tooltip.timeout = GLib.timeout_add( 500, self.show_xep0184_warning_tooltip) w.set_cursor(Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR)) self.change_cursor = True def on_textview_populate_popup(self, textview, menu): """ Override the default context menu and we prepend Clear (only if used_in_history_window is False) and if we have sth selected we show a submenu with actions on the phrase (see on_conversation_textview_button_press_event) """ separator_menuitem_was_added = False menu.show_all() def on_textview_button_press_event(self, widget, event): # If we clicked on a taged text do NOT open the standard popup menu # if normal text check if we have sth selected self.selected_phrase = '' # do not move belove event button check! if event.button != 3: # if not right click return False x, y = self.tv.window_to_buffer_coords(Gtk.TextWindowType.TEXT, int(event.x), int(event.y)) iter_ = self.tv.get_iter_at_location(x, y) tags = iter_.get_tags() if tags: # we clicked on sth special (it can be status message too) for tag in tags: tag_name = tag.get_property('name') if tag_name in ('url', 'mail', 'xmpp', 'sth_at_sth'): return True # we block normal context menu # we check if sth was selected and if it was we assign # selected_phrase variable # so on_conversation_textview_populate_popup can use it buffer_ = self.tv.get_buffer() return_val = buffer_.get_selection_bounds() if return_val: # if sth was selected when we right-clicked # get the selected text start_sel, finish_sel = return_val[0], return_val[1] self.selected_phrase = buffer_.get_text(start_sel, finish_sel, True) elif iter_.get_char() and ord(iter_.get_char()) > 31: # we clicked on a word, do as if it's selected for context menu start_sel = iter_.copy() if not start_sel.starts_word(): start_sel.backward_word_start() finish_sel = iter_.copy() if not finish_sel.ends_word(): finish_sel.forward_word_end() self.selected_phrase = buffer_.get_text(start_sel, finish_sel, True) def on_textview_draw(self, widget, ctx): return #TODO expalloc = event.area exp_x0 = expalloc.x exp_y0 = expalloc.y exp_x1 = exp_x0 + expalloc.width exp_y1 = exp_y0 + expalloc.height try: tryfirst = [self.image_cache[(exp_x0, exp_y0)]] except KeyError: tryfirst = [] for image in tryfirst + self.images: imgalloc = image.allocation img_x0 = imgalloc.x img_y0 = imgalloc.y img_x1 = img_x0 + imgalloc.width img_y1 = img_y0 + imgalloc.height if img_x0 <= exp_x0 and img_y0 <= exp_y0 and \ exp_x1 <= img_x1 and exp_y1 <= img_y1: self.image_cache[(img_x0, img_y0)] = image widget.propagate_expose(image, event) return True return False def at_the_end(self): buffer_ = self.tv.get_buffer() end_iter = buffer_.get_end_iter() end_rect = self.tv.get_iter_location(end_iter) visible_rect = self.tv.get_visible_rect() if end_rect.y <= (visible_rect.y + visible_rect.height): return True return False def bring_scroll_to_end(self, diff_y=0, use_smooth=True): ''' scrolls to the end of textview if end is not visible ''' buffer_ = self.tv.get_buffer() end_iter = buffer_.get_end_iter() end_rect = self.tv.get_iter_location(end_iter) visible_rect = self.tv.get_visible_rect() # scroll only if expected end is not visible if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y): if use_smooth: GLib.idle_add(self.smooth_scroll_to_end) else: GLib.idle_add(self.scroll_to_end_iter)
class ConversationTextview(GObject.GObject): """ Class for the conversation textview (where user reads already said messages) for chat/groupchat windows """ __gsignals__ = dict( quote = (GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, None, # return value (str, ) # arguments ) ) # smooth scroll constants MAX_SCROLL_TIME = 0.4 # seconds SCROLL_DELAY = 33 # milliseconds def __init__(self, used_in_history_window = False): """ If used_in_history_window is True, then we do not show Clear menuitem in context menu """ GObject.GObject.__init__(self) self.used_in_history_window = used_in_history_window #self.fc = FuzzyClock() # no need to inherit TextView, use it as atrribute is safer self.tv = HtmlTextView() #self.tv.hyperlink_handler = self.hyperlink_handler # set properties self.tv.set_border_width(1) self.tv.set_accepts_tab(True) self.tv.set_editable(False) self.tv.set_cursor_visible(False) self.tv.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) self.tv.set_left_margin(2) self.tv.set_right_margin(2) self.handlers = {} self.images = [] self.image_cache = {} self.xep0184_marks = {} self.xep0184_shown = {} self.last_sent_message_marks = [None, None] # A pair per occupant. Key is '' in normal chat self.last_received_message_marks = {} # It's True when we scroll in the code, so we can detect scroll from user self.auto_scrolling = False # connect signals id_ = self.tv.connect('motion_notify_event', self.on_textview_motion_notify_event) self.handlers[id_] = self.tv id_ = self.tv.connect('populate_popup', self.on_textview_populate_popup) self.handlers[id_] = self.tv id_ = self.tv.connect('button_press_event', self.on_textview_button_press_event) self.handlers[id_] = self.tv id_ = self.tv.connect('draw', self.on_textview_draw) self.handlers[id_] = self.tv self.change_cursor = False self.last_time_printout = 0 #font = Pango.FontDescription(gajim.config.get('conversation_font')) #self.tv.override_font(font) buffer_ = self.tv.get_buffer() end_iter = buffer_.get_end_iter() buffer_.create_mark('end', end_iter, False) #self.tagIn = buffer_.create_tag('incoming') #color = gajim.config.get('inmsgcolor') #font = Pango.FontDescription(gajim.config.get('inmsgfont')) #self.tagIn.set_property('foreground', color) #self.tagIn.set_property('font-desc', font) #self.tagOut = buffer_.create_tag('outgoing') #color = gajim.config.get('outmsgcolor') #font = Pango.FontDescription(gajim.config.get('outmsgfont')) #self.tagOut.set_property('foreground', color) #self.tagOut.set_property('font-desc', font) #self.tagStatus = buffer_.create_tag('status') #color = gajim.config.get('statusmsgcolor') #font = Pango.FontDescription(gajim.config.get('satusmsgfont')) #self.tagStatus.set_property('foreground', color) #self.tagStatus.set_property('font-desc', font) #self.tagInText = buffer_.create_tag('incomingtxt') #color = gajim.config.get('inmsgtxtcolor') #font = Pango.FontDescription(gajim.config.get('inmsgtxtfont')) #if color: # self.tagInText.set_property('foreground', color) #self.tagInText.set_property('font-desc', font) #self.tagOutText = buffer_.create_tag('outgoingtxt') #color = gajim.config.get('outmsgtxtcolor') #if color: # font = Pango.FontDescription(gajim.config.get('outmsgtxtfont')) #self.tagOutText.set_property('foreground', color) #self.tagOutText.set_property('font-desc', font) #colors = gajim.config.get('gc_nicknames_colors') #colors = colors.split(':') #for i, color in enumerate(colors): # tagname = 'gc_nickname_color_' + str(i) # tag = buffer_.create_tag(tagname) # tag.set_property('foreground', color) #self.tagMarked = buffer_.create_tag('marked') #color = gajim.config.get('markedmsgcolor') #self.tagMarked.set_property('foreground', color) #self.tagMarked.set_property('weight', Pango.Weight.BOLD) #tag = buffer_.create_tag('time_sometimes') #tag.set_property('foreground', 'darkgrey') #Pango.SCALE_SMALL #tag.set_property('scale', 0.8333333333333) #tag.set_property('justification', Gtk.Justification.CENTER) #tag = buffer_.create_tag('small') #Pango.SCALE_SMALL #tag.set_property('scale', 0.8333333333333) #tag = buffer_.create_tag('restored_message') #color = gajim.config.get('restored_messages_color') #tag.set_property('foreground', color) #self.tv.create_tags() #tag = buffer_.create_tag('bold') #tag.set_property('weight', Pango.Weight.BOLD) #tag = buffer_.create_tag('italic') #tag.set_property('style', Pango.Style.ITALIC) #tag = buffer_.create_tag('underline') #tag.set_property('underline', Pango.Underline.SINGLE) #buffer_.create_tag('focus-out-line', justification = Gtk.Justification.CENTER) #self.displaymarking_tags = {} #tag = buffer_.create_tag('xep0184-warning') #tag.set_property('foreground', '#cc0000') #tag = buffer_.create_tag('xep0184-received') #tag.set_property('foreground', '#73d216') # One mark at the begining then 2 marks between each lines #size = gajim.config.get('max_conversation_lines') #size = 2 * size - 1 #self.marks_queue = queue.Queue(size) self.allow_focus_out_line = True # holds a mark at the end of --- line self.focus_out_end_mark = None #self.xep0184_warning_tooltip = tooltips.BaseTooltip() #self.line_tooltip = tooltips.BaseTooltip() self.smooth_id = None self.just_cleared = False size = 500 size = 2 * size - 1 self.marks_queue = queue.Queue(size) def print_conversation_line(self, text, jid, kind, name): """ Print 'chat' type messages """ buffer_ = self.tv.get_buffer() buffer_.begin_user_action() if self.marks_queue.full(): # remove oldest line m1 = self.marks_queue.get() m2 = self.marks_queue.get() i1 = buffer_.get_iter_at_mark(m1) i2 = buffer_.get_iter_at_mark(m2) buffer_.delete(i1, i2) buffer_.delete_mark(m1) end_iter = buffer_.get_end_iter() end_offset = end_iter.get_offset() at_the_end = self.at_the_end() move_selection = False if buffer_.get_has_selection() and buffer_.get_selection_bounds()[1].\ get_offset() == end_offset: move_selection = True # Create one mark and add it to queue once if it's the first line # else twice (one for end bound, one for start bound) mark = None if buffer_.get_char_count() > 0: mark = buffer_.create_mark(None, end_iter, left_gravity=True) self.marks_queue.put(mark) if not mark: mark = buffer_.create_mark(None, end_iter, left_gravity=True) self.marks_queue.put(mark) if kind == 'incoming_queue': kind = 'incoming' # print the time stamp # We don't have tim for outgoing messages... import time tim = time.localtime() direction_mark = '' # don't apply direction mark if it's status message timestamp_str = self.get_time_to_show(tim, direction_mark) timestamp = time.strftime(timestamp_str, tim) timestamp = timestamp + ' ' buffer_.insert (end_iter, timestamp) self.print_name(name, kind, direction_mark=direction_mark, iter_=end_iter) #if kind == 'incoming': # text_tags.append('incomingtxt') # mark1 = mark #elif kind == 'outgoing': # text_tags.append('outgoingtxt') # mark1 = mark #subject = None #self.print_subject(subject, iter_=end_iter) self.print_real_text(text, name, iter_=end_iter) # scroll to the end of the textview if at_the_end or kind == 'outgoing': # we are at the end or we are sending something # scroll to the end (via idle in case the scrollbar has appeared) if True: GLib.idle_add(self.smooth_scroll_to_end) else: GLib.idle_add(self.scroll_to_end) self.just_cleared = False buffer_.end_user_action() return end_iter # Smooth scrolling inspired by Pidgin code def smooth_scroll(self): parent = self.tv.get_parent() if not parent: return False vadj = parent.get_vadjustment() max_val = vadj.get_upper() - vadj.get_page_size() + 1 cur_val = vadj.get_value() # scroll by 1/3rd of remaining distance onethird = cur_val + ((max_val - cur_val) / 3.0) self.auto_scrolling = True vadj.set_value(onethird) self.auto_scrolling = False if max_val - onethird < 0.01: self.smooth_id = None self.smooth_scroll_timer.cancel() return False return True def smooth_scroll_timeout(self): GLib.idle_add(self.do_smooth_scroll_timeout) return def do_smooth_scroll_timeout(self): if not self.smooth_id: # we finished scrolling return GLib.source_remove(self.smooth_id) self.smooth_id = None parent = self.tv.get_parent() if parent: vadj = parent.get_vadjustment() self.auto_scrolling = True vadj.set_value(vadj.get_upper() - vadj.get_page_size() + 1) self.auto_scrolling = False def smooth_scroll_to_end(self): if None != self.smooth_id: # already scrolling return False self.smooth_id = GLib.timeout_add(self.SCROLL_DELAY, self.smooth_scroll) self.smooth_scroll_timer = Timer(self.MAX_SCROLL_TIME, self.smooth_scroll_timeout) self.smooth_scroll_timer.start() return False def print_name(self, name, kind, direction_mark='', iter_=None): if name: buffer_ = self.tv.get_buffer() if iter_: end_iter = iter_ else: end_iter = buffer_.get_end_iter() before_str = '' after_str = ':' format_ = before_str + name + direction_mark + after_str + ' ' buffer_.insert(end_iter, format_) def print_real_text(self, text, name, iter_=None): """ Add normal and special text. call this to add text """ # /me is replaced by name if name is given if name and (text.startswith('/me ') or text.startswith('/me\n')): text = '* ' + name + text[3:] #text_tags.append('italic') # detect urls formatting and if the user has it on emoticons buffer_ = self.tv.get_buffer() buffer_.insert (iter_, text + "\n") #return self.detect_and_print_special_text(text, iter_=iter_) def get_time_to_show(self, tim, direction_mark=''): from calendar import timegm import time """ Get the time, with the day before if needed and return it. It DOESN'T format a fuzzy time """ format_ = '' # get difference in days since epoch (86400 = 24*3600) # number of days since epoch for current time (in GMT) - # number of days since epoch for message (in GMT) diff_day = int(int(timegm(time.localtime())) / 86400 -\ int(timegm(tim)) / 86400) timestamp_str = '[%X]' format_ += timestamp_str tim_format = time.strftime(format_, tim) return tim_format def on_textview_motion_notify_event(self, widget, event): """ Change the cursor to a hand when we are over a mail or an url """ w = self.tv.get_window(Gtk.TextWindowType.TEXT) device = w.get_display().get_device_manager().get_client_pointer() pointer = w.get_device_position(device) x, y = self.tv.window_to_buffer_coords(Gtk.TextWindowType.TEXT, pointer[1], pointer[2]) tags = self.tv.get_iter_at_location(x, y).get_tags() if self.change_cursor: w.set_cursor(Gdk.Cursor.new(Gdk.CursorType.XTERM)) self.change_cursor = False tag_table = self.tv.get_buffer().get_tag_table() over_line = False xep0184_warning = False for tag in tags: if tag in (tag_table.lookup('url'), tag_table.lookup('mail'), \ tag_table.lookup('xmpp'), tag_table.lookup('sth_at_sth')): w.set_cursor(Gdk.Cursor.new(Gdk.CursorType.HAND2)) self.change_cursor = True elif tag == tag_table.lookup('focus-out-line'): over_line = True elif tag == tag_table.lookup('xep0184-warning'): xep0184_warning = True #if self.line_tooltip.timeout != 0: # Check if we should hide the line tooltip # if not over_line: # self.line_tooltip.hide_tooltip() #if self.xep0184_warning_tooltip.timeout != 0: # Check if we should hide the XEP-184 warning tooltip # if not xep0184_warning: # self.xep0184_warning_tooltip.hide_tooltip() if over_line and not self.line_tooltip.win: self.line_tooltip.timeout = GLib.timeout_add(500, self.show_line_tooltip) w.set_cursor(Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR)) self.change_cursor = True if xep0184_warning and not self.xep0184_warning_tooltip.win: self.xep0184_warning_tooltip.timeout = GLib.timeout_add(500, self.show_xep0184_warning_tooltip) w.set_cursor(Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR)) self.change_cursor = True def on_textview_populate_popup(self, textview, menu): """ Override the default context menu and we prepend Clear (only if used_in_history_window is False) and if we have sth selected we show a submenu with actions on the phrase (see on_conversation_textview_button_press_event) """ separator_menuitem_was_added = False menu.show_all() def on_textview_button_press_event(self, widget, event): # If we clicked on a taged text do NOT open the standard popup menu # if normal text check if we have sth selected self.selected_phrase = '' # do not move belove event button check! if event.button != 3: # if not right click return False x, y = self.tv.window_to_buffer_coords(Gtk.TextWindowType.TEXT, int(event.x), int(event.y)) iter_ = self.tv.get_iter_at_location(x, y) tags = iter_.get_tags() if tags: # we clicked on sth special (it can be status message too) for tag in tags: tag_name = tag.get_property('name') if tag_name in ('url', 'mail', 'xmpp', 'sth_at_sth'): return True # we block normal context menu # we check if sth was selected and if it was we assign # selected_phrase variable # so on_conversation_textview_populate_popup can use it buffer_ = self.tv.get_buffer() return_val = buffer_.get_selection_bounds() if return_val: # if sth was selected when we right-clicked # get the selected text start_sel, finish_sel = return_val[0], return_val[1] self.selected_phrase = buffer_.get_text(start_sel, finish_sel, True) elif iter_.get_char() and ord(iter_.get_char()) > 31: # we clicked on a word, do as if it's selected for context menu start_sel = iter_.copy() if not start_sel.starts_word(): start_sel.backward_word_start() finish_sel = iter_.copy() if not finish_sel.ends_word(): finish_sel.forward_word_end() self.selected_phrase = buffer_.get_text(start_sel, finish_sel, True) def on_textview_draw(self, widget, ctx): return #TODO expalloc = event.area exp_x0 = expalloc.x exp_y0 = expalloc.y exp_x1 = exp_x0 + expalloc.width exp_y1 = exp_y0 + expalloc.height try: tryfirst = [self.image_cache[(exp_x0, exp_y0)]] except KeyError: tryfirst = [] for image in tryfirst + self.images: imgalloc = image.allocation img_x0 = imgalloc.x img_y0 = imgalloc.y img_x1 = img_x0 + imgalloc.width img_y1 = img_y0 + imgalloc.height if img_x0 <= exp_x0 and img_y0 <= exp_y0 and \ exp_x1 <= img_x1 and exp_y1 <= img_y1: self.image_cache[(img_x0, img_y0)] = image widget.propagate_expose(image, event) return True return False def at_the_end(self): buffer_ = self.tv.get_buffer() end_iter = buffer_.get_end_iter() end_rect = self.tv.get_iter_location(end_iter) visible_rect = self.tv.get_visible_rect() if end_rect.y <= (visible_rect.y + visible_rect.height): return True return False def bring_scroll_to_end(self, diff_y = 0, use_smooth=True): ''' scrolls to the end of textview if end is not visible ''' buffer_ = self.tv.get_buffer() end_iter = buffer_.get_end_iter() end_rect = self.tv.get_iter_location(end_iter) visible_rect = self.tv.get_visible_rect() # scroll only if expected end is not visible if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y): if use_smooth: GLib.idle_add(self.smooth_scroll_to_end) else: GLib.idle_add(self.scroll_to_end_iter)
class MainClass(Plugin.Plugin): '''Main plugin class''' description = _('Log all events and conversations in a database') authors = {'Mariano Guerra' : 'luismarianoguerra at gmail dot com'} website = 'http://emesene.org' displayName = 'Logger' name = 'Logger' def __init__(self, controller, msn): '''Contructor''' Plugin.Plugin.__init__(self, controller, msn, 1000) self.description = _('Log all events and conversations in a database') self.authors = {'Mariano Guerra' : 'luismarianoguerra at gmail dot com'} self.website = 'http://emesene.org' self.displayName = 'Logger' self.name = 'Logger' self.controller = controller self.config = controller.config self.config.readPluginConfig(self.name) self.path = self.config.getPluginValue(self.name, 'path', self.msn.cacheDir) self.file_name = os.path.join(self.path, self.msn.user + '.db') self.logger = Logger(self.file_name) self.ids = [] self.signals = {} self.signals['self-nick-changed'] = self._cb_self_nick_changed self.signals['personal-message-changed'] = \ self._cb_personal_message_changed self.signals['self-status-changed'] = self._cb_self_status_changed self.signals['self-personal-message-changed'] = \ self._cb_self_personal_message_changed self.signals['self-current-media-changed'] = \ self._cb_self_current_media_changed self.signals['nick-changed'] = self._cb_nick_changed self.signals['contact-status-change'] = \ self._cb_contact_status_change self.signals['initial-status-change'] = \ self._cb_initial_status_change self.signals['user-offline'] = self._cb_user_offline self.signals['display-picture-changed'] = \ self._cb_display_picture_changed self.signals['switchboard::message'] = self._cb_sb_message self.signals['switchboard::ink-message'] = self._cb_sb_ink_message self.signals['switchboard::nudge'] = self._cb_sb_nudge self.signals['switchboard::message-sent'] = self._cb_sb_message_sent self.signals['switchboard::ink-sent'] = self._cb_sb_ink_message_sent self.signals['switchboard::nudge-sent'] = self._cb_sb_nudge_sent self.signals['switchboard::action-message'] = \ self._cb_sb_action_message self.signals['switchboard::action-sent'] = \ self._cb_sb_action_message_sent self.signals['switchboard::custom-emoticon-received'] = \ self._cb_sb_custom_emoticon_received self.signals['switchboard::user-join'] = self._cb_sb_user_join self.signals_labels = {} self.signals_labels['self-nick-changed'] = _('Self nick changed') self.signals_labels['personal-message-changed'] = _('PM changed') self.signals_labels['self-status-changed'] = _('Self status changed') self.signals_labels['self-personal-message-changed'] = _('Self PM changed') self.signals_labels['self-current-media-changed'] = _('Self current media changed') self.signals_labels['nick-changed'] = _('Nick changed') self.signals_labels['contact-status-change'] = _('Contact status changed') self.signals_labels['initial-status-change'] = _('Initial status changed') self.signals_labels['user-offline'] = _('User offline') self.signals_labels['display-picture-changed'] = _('Display picture changed') self.signals_labels['switchboard::message'] = _('Message received') self.signals_labels['switchboard::ink-message'] = _('Ink message received') self.signals_labels['switchboard::nudge'] = _('Nudge received') self.signals_labels['switchboard::message-sent'] = _('Message sent') self.signals_labels['switchboard::ink-sent'] = _('Ink message sent') self.signals_labels['switchboard::nudge-sent'] = _('Nudge sent') self.signals_labels['switchboard::action-message'] = _('Action message received') self.signals_labels['switchboard::action-sent'] = _('Action message sent') self.signals_labels['switchboard::custom-emoticon-received'] = _('Custom emoticon received') self.signals_labels['switchboard::user-join'] = _('User joined conversation') # functions to be called on mainloop idleness self.queued = [] self.queuedtag = 0 # timeout source to disconect it self.to_source = None all_events = ','.join(self.signals.keys()) self.menuItemId = 0 def start(self): '''start the plugin''' if sqlite is None: return self.enabled = True self.events_enabled = [] for signal in self.signals.keys(): if bool(int(self.config.getPluginValue( self.name, signal, True ))): self.events_enabled.append(signal) for (key, value) in self.signals.iteritems(): if key in self.events_enabled: self.ids.append(self.msn.connect(key, self.callback, value)) self.menuItemId = self.controller.connect("usermenu-item-add", self.add_usermenu_item) def stop(self): '''stop the plugin''' # disconnect msn signals for identifier in self.ids: self.msn.disconnect(identifier) self.ids = [] self.controller.disconnect(self.menuItemId) self.enabled = False def callback(self, *args): # sorry, pylint, i know you don't like this '''Adds the function in the last argument to the queue, to be called later when the mainloop is idle. sqlite isn't exactly fast''' params, func = args[:-1], args[-1] # add the stamp parameter params += (time.time(), ) self.queued.append((func, params)) # if the process_queue loop is dead, start it if self.queuedtag == 0: self.queuedtag = gobject.idle_add(self.process_queue) def process_queue(self): '''Takes the signal queue and calls one function. It's called on a idle_add loop. If there are no function, it quits the loop returning false and removing the queuedtag''' if self.queued: func, params = self.queued.pop(0) func(*params) return True else: self.queuedtag = 0 return False def check(self): '''check if everything is OK to start the plugin return a tuple whith a boolean and a message if OK -> (True , 'some message') else -> (False , 'error message')''' if sqlite is None: return (False, 'sqlite not available, please install it.') else: return (True, 'Ok') def get_last_status(self, account, num = 1): '''return the last status of a contact, if num is > 1 then return the last num status, the format of the list is a list of lists with the timestamp and the status''' query = ''' select e.stamp, ue.data from user u, event e, user_event ue where e.id = ue.id_event and u.id = ue.id_user and e.name = "status-changed" and u.account = "%s" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def get_last_nick(self, account, num = 1): '''return the last nick of a contact, if num is > 1 then return the last num nicks, the format of the list is a list of lists with the timestamp and the nick''' query = ''' select e.stamp, ue.data from user u, event e, user_event ue where e.id = ue.id_event and u.id = ue.id_user and e.name = "nick-changed" and u.account = "%s" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def get_last_personal_message(self, account, num = 1): '''return the last pm of a contact, if num is > 1 then return the last num pms, the format of the list is a list of lists with the timestamp and the pm''' query = ''' select e.stamp, ue.data from user u, event e, user_event ue where e.id = ue.id_event and u.id = ue.id_user and e.name = "personal-message-changed" and u.account = "%s" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def get_just_last_personal_message(self, account): '''return the last personal message as a string or None rather than a list''' result = self.get_last_personal_message(account) if result: return result[0][1] return None def get_just_last_nick(self, account): '''return the last nick as a string or None rather than a list''' result = self.get_last_nick(account) if result: return result[0][1] return None def get_just_last_status(self, account): '''return the last status as a string or None rather than a list''' result = self.get_last_status(account) if result: return result[0][1] return None def get_last_message(self, account, num = 1): '''return the last message of a contact, if num is > 1 then return the last num messages, the format of the list is a list of lists with the timestamp and the message''' query = ''' select e.stamp, ce.data from user u, event e, conversation_event ce where e.id = ce.id_event and u.id = ce.id_user and e.name = "message" and u.account = "%s" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def get_last_display_picture(self, account, num = 1): '''return the last dp of a contact, if num is > 1 then return the last num dps, the format of the list is a list of lists with the timestamp and the dp''' query = ''' select e.stamp, ue.data from user u, event e, user_event ue where e.id = ue.id_event and u.id = ue.id_user and e.name = "display-picture" and u.account = "%s" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def get_last_custom_emoticon(self, account, num = 1): '''return the last ce of a contact, if num is > 1 then return the last num ces, the format of the list is a list of lists with the timestamp and the ce''' query = ''' select e.stamp, ce.data from user u, event e, conversation_event ce where e.id = ce.id_event and u.id = ce.id_user and e.name = "custom-emoticon" and u.account = "%s" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def get_last_conversation(self, account, num = 1): '''return the last message that was said on a conversation where account was present, the format of the list is a list od lists with the timestamp, email and message''' # in the inner select we select all the id of conversations # where account was present, then on the outer conversation # we select all the messages from that conversations query = ''' select e.stamp, u.account, ce.data from event e, conversation_event ce, user u where e.id = ce.id_event and u.id = ce.id_user and ce.id_conversation in (select distinct c.id from conversation c, event e, conversation_event ce, user u where c.id = ce.id_conversation and e.id = ce.id_event and ce.id_user = u.id and u.account = "%s") and e.name = "message" order by e.stamp desc limit %s ''' return self.logger.query(query % (account, num)) def add_usermenu_item(self, controller, userMenu): self.logMenuItem = userMenu.newImageMenuItem( _( "Conversation _log" ), gtk.STOCK_FILE ) userMenu.viewMenu.add( self.logMenuItem ) self.logMenuItem.show() self.logMenuItem.connect( 'activate', self.on_menuitem_activate, userMenu.user.email ) def on_menuitem_activate(self, widget, email): self.window = gtk.Window() self.window.set_border_width(5) self.window.set_title(_("Conversation log for %s") % email) self.window.set_default_size(650,350) self.window.set_position(gtk.WIN_POS_CENTER) textview = gtk.TextView() self.textBuffer = textview.get_buffer() self.textBuffer.create_tag('highlight', background = 'yellow') scroll = gtk.ScrolledWindow() scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.textview = HtmlTextView(self.controller, self.textBuffer, scroll) self.textview.set_wrap_mode(gtk.WRAP_WORD_CHAR) self.textview.set_left_margin(6) self.textview.set_right_margin(6) self.textview.set_editable(False) self.textview.set_cursor_visible(False) scroll.add(self.textview) hbox = gtk.HBox(False, 5) saveButton = gtk.Button(stock=gtk.STOCK_SAVE) saveButton.connect("clicked", self.on_save_logs) refreshButton = gtk.Button(stock=gtk.STOCK_REFRESH) refreshButton.connect("clicked", self.on_refresh_log, email) closeButton = gtk.Button(stock=gtk.STOCK_CLOSE) closeButton.connect("clicked", lambda w: self.window.hide()) ############ Search TreeView ################### self.search_active = False self.searchStore = gtk.ListStore(str, str, str, str) self.searchTree = gtk.TreeView(self.searchStore) self.searchTree.connect("row-activated", self.set_cursor) self.searchTree.set_rules_hint(True) cell = gtk.CellRendererText() nameCol = gtk.TreeViewColumn(_("Date"), cell, text=0) dateCol = gtk.TreeViewColumn(_("Time"), cell, text=1) timeCol = gtk.TreeViewColumn(_("Name"), cell, text=2) msgCol = gtk.TreeViewColumn(_("Message"), cell, text=3) nameCol.set_resizable(True) dateCol.set_resizable(True) timeCol.set_resizable(True) msgCol.set_resizable(True) self.searchTree.append_column(nameCol) self.searchTree.append_column(dateCol) self.searchTree.append_column(timeCol) self.searchTree.append_column(msgCol) self.searchSw = gtk.ScrolledWindow() self.searchSw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.searchSw.add(self.searchTree) self.searchSw.set_size_request(100, 30) ################################################## #search box searchLabel = gtk.Label(_("Search:")) self.searchBox = gtk.Entry() self.searchBox.connect("key-press-event", self.enter_pressed) hbox.pack_end(saveButton, False, False) hbox.pack_end(refreshButton, False, False) hbox.pack_end(closeButton, False, False) hbox.pack_end(self.searchBox, False, False) hbox.pack_end(searchLabel, False, False) textRenderer = gtk.CellRendererText() column = gtk.TreeViewColumn(_("Date"), textRenderer, markup=0) self.datesListstore = gtk.ListStore(str) self.datesTree = gtk.TreeView(self.datesListstore) self.datesTree.connect("cursor-changed", self.on_dates_cursor_change) self.datesTree.append_column(column) self.datesTree.set_headers_visible(False) datesScroll = gtk.ScrolledWindow() datesScroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) datesScroll.add_with_viewport(self.datesTree) datesScroll.set_size_request(150, -1) hPaned = gtk.HPaned() hPaned.pack1(datesScroll) hPaned.pack2(scroll) vbox = gtk.VBox(spacing=5) vbox.pack_start(hPaned) vbox.pack_start(hbox, False, False) vbox.pack_start(self.searchSw) vbox.pack_start(gtk.Statusbar(), False, False) self.datesStamps = {} logsCant = len(self.fill_dates_tree(email)) if not logsCant > 0: dialog.information(_("No logs were found for %s") % (email)) else: self.window.add(vbox) self.window.show_all() self.searchSw.hide_all() def set_cursor(self, treeview, path, col): (model, iter) = self.searchTree.get_selection().get_selected() dateSearch = self.searchStore.get_value(iter, 0) timeSearch = self.searchStore.get_value(iter, 1) #Search in the datesTree self.datesTree.set_cursor_on_cell(0) (model, iter) = self.datesTree.get_selection().get_selected() while iter: date = self.datesListstore.get_value(iter, 0) if date == dateSearch: self.datesTree.set_cursor_on_cell(model.get_path(iter)) break iter = model.iter_next(iter) self.scroll_to_result(timeSearch) def scroll_to_result(self, timee): start_iter = self.textBuffer.get_start_iter() result = start_iter.forward_search(timee, gtk.TEXT_SEARCH_VISIBLE_ONLY, None) if result is not None: match_start_iter, match_end_iter = result match_start_iter.backward_char() #include '(' or other character before time match_end_iter.forward_line() #highlight all message not just time self.textBuffer.apply_tag_by_name('highlight', match_start_iter, match_end_iter) match_start_mark = self.textBuffer.create_mark('match_start', match_start_iter, True) self.textview.scroll_to_mark(match_start_mark, 0, True) def enter_pressed(self, button, event): if len(self.searchBox.get_text()) == 0: self.searchSw.hide_all() elif event.keyval == 65293 or event.keyval == 65421: #If enter key is pressed self.searchStore.clear() text = self.searchBox.get_text() self.searchSw.show_all() self.datesTree.set_cursor_on_cell(0) (model, iter) = self.datesTree.get_selection().get_selected() self.search_active = True while iter: self.searchIter = iter self.searchText = text self.datesTree.set_cursor_on_cell(model.get_path(iter)) iter = model.iter_next(iter) self.search_active = False self.textview.get_buffer().set_text("") def fill_dates_tree(self, mail): '''fill the treeview with the logs dates''' dates = [] for (id_conversation, stamp) in self.logger.get_conversation_ids(mail): ctime = str(time.ctime(float(stamp))) date = ctime.split() date = " ".join(date[0:3]) + " " + date[4] if date in self.datesStamps.keys(): self.datesStamps[date].append(id_conversation) else: self.datesStamps[date] = [id_conversation] dates.append(date) dates.reverse() for date in dates: self.datesListstore.append([date]) return self.datesStamps #self.datesListstore.append([ctime]) #self.datesStamps[ctime] = id_conversation return self.datesStamps def on_dates_cursor_change(self, *args): try: selected = self.datesListstore.get( self.datesTree.get_selection().get_selected()[1], 0)[0] except (TypeError, AttributeError): return string1 = (_('Conversation opened')) string2 = (_('Conversation closed')) ids_conversation = self.datesStamps[selected] nick_cache = {} self.textview.get_buffer().set_text("") for id_conversation in ids_conversation: listMessages = self.logger.get_conversation(id_conversation, 10000) if (len(listMessages)!= 0): self.textview.display_html('<body><span style=\"font-weight: bold;\"><b>*** '+string1+' ***</b>' '</span></body>') for (stamp, mail, message) in listMessages: if mail in nick_cache: if nick_cache[mail]['next'] is not None and \ nick_cache[mail]['next'] <= stamp: nick = self.logger.get_user_nick(mail, stamp) next = self.logger.get_next_nick_stamp(mail, stamp) nick_cache[mail] = {'nick': nick, 'next' : next} else: nick = nick_cache[mail]['nick'] else: nick = self.logger.get_user_nick(mail, stamp) next = self.logger.get_next_nick_stamp(mail, stamp) nick_cache[mail] = {'nick': nick, 'next' : next} date = time.ctime(float(stamp)).split()[3] (_format, encoding, text) = message.split('\r\n', 2) style = parse_format(_format) nick = self.controller.unifiedParser.getParser(nick).get() text = self.controller.unifiedParser.getParser(text).get() text = text.replace("\r\n", "\n").replace("\n", "<br />") if self.search_active: date2 = self.datesListstore.get_value(self.searchIter, 0) if text.find(self.searchText) != -1: self.searchStore.append([date2, date, nick, text]) try: self.textview.display_html('<body>(%s) <b>%s: </b>' '<span style="%s">%s</span></body>' % (date, nick, style, text)) except: # hide messages that we can't display pass self.textview.display_html('<body><span style=\"font-weight: bold;\"><b>*** '+string2+' ***</b>' '</span></body>') self.textview.display_html('<br/>') else: pass def on_save_logs(self, button): title = (_("Save conversation log")) dialog = gtk.FileChooserDialog(title, None, \ gtk.FILE_CHOOSER_ACTION_SAVE, \ (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, \ gtk.STOCK_SAVE, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) dialog.show_all() response = dialog.run() while True: if response != gtk.RESPONSE_OK: break if dialog.get_filename(): path = dialog.get_filename() f = open(path, "w") f.write(self.textBuffer.get_text(self.textBuffer.get_start_iter(),\ self.textBuffer.get_end_iter() )) f.close() break dialog.hide() def on_refresh_log(self, button, email): self.on_dates_cursor_change() def _cb_self_nick_changed(self, msn, old, new, stamp=None): '''called when we change our own nick''' if new != self.get_last_nick(self.msn.user): self.logger.user_event_add('nick-changed', self.msn.user, new, stamp) def _cb_personal_message_changed(self, msn, email, personal_message, stamp=None): '''called when an user changes the personal message''' if personal_message != '' and \ self.get_just_last_personal_message(email) != personal_message: self.logger.user_event_add('personal-message-changed', email, personal_message, stamp) def _cb_self_status_changed(self, msn, status, stamp=None): '''called when we change the status''' if self.get_last_status(msn.user) != status: self.logger.user_event_add('status-changed', self.msn.user, status, stamp) def _cb_self_personal_message_changed(self, msn, ourmail, personal_message, stamp=None): '''called when we change our personal message''' if personal_message != '' and \ self.get_just_last_personal_message(ourmail) != personal_message: self.logger.user_event_add('personal-message-changed', ourmail, personal_message, stamp) def _cb_self_current_media_changed(self, msn, ourmail, personal_message, dict_, stamp=None): '''called when we change our current media''' if personal_message != '' and \ self.get_just_last_personal_message(ourmail) != personal_message: self.logger.user_event_add('personal-message-changed', ourmail, personal_message, stamp) def _cb_nick_changed(self, msn, email, nick, stamp=None): '''called when someone change his nick''' if nick != self.get_last_nick(email): self.logger.user_event_add('nick-changed', email, nick, stamp) def _cb_contact_status_change(self, msn, email, status, stamp=None): '''called when someone change his status''' if self.get_last_status(email) != status: self.logger.user_event_add('status-changed', email, status, stamp) def _cb_initial_status_change(self, msn, command, tid, params, stamp=None): '''called when someone change his status when we come online''' data = params.split(' ') status = data[0] email = data[1].lower() if self.get_last_status(email) != status: self.logger.user_event_add('status-changed', email, status, stamp) def _cb_user_offline(self, msn, email, stamp=None): '''called when someone goes offline''' self.logger.user_event_add('status-changed', email, 'FLN', stamp) def _cb_sb_user_join(self, msn, switchboard, signal, args, stamp=None): '''called when an user join to the conversation''' mail = args[0] self.logger.conversation_event_add('user-join', switchboard.started, mail, '', stamp) def _cb_sb_user_leave(self, msn, switchboard, signal, args, stamp=None): '''called when an user leaves the conversation''' mail = args[0] self.logger.conversation_event_add('user-leave', switchboard.started, mail, '', stamp) def _cb_sb_message(self, msn, switchboard, signal, args, stamp=None): '''called when we receive a message''' mail, nick, body, format, charset, p4c = args message = '%s\r\n%s\r\n%s' % (format, charset, body.replace('\\', '\\\\')) self.logger.conversation_event_add('message', switchboard.started, mail, message, stamp) def _cb_sb_action_message(self, msn, switchboard, signal, args, stamp=None): '''called when an action message is received''' mail, data = args self.logger.conversation_event_add('action-message', switchboard.started, mail, data, stamp) def _cb_sb_ink_message(self, msn, switchboard, signal, args, stamp=None): '''called when an ink message is received''' signal, filename = args self.logger.conversation_event_add('ink-message', switchboard.started, mail, filename, stamp) def _cb_sb_nudge(self, msn, switchboard, signal, args, stamp=None): '''called when a nudge is received''' mail = args[0] self.logger.conversation_event_add('nudge', switchboard.started, mail, '', stamp) def _cb_sb_message_sent(self, msn, switchboard, signal, args, stamp=None): '''called when we send a message''' body, format, charset = args try: format = format.split('X-MMS-IM-Format: ')[1] except IndexError: format = '' message = '%s\r\n%s\r\n%s' % (format, charset, body) self.logger.conversation_event_add('message', switchboard.started, self.msn.user, message, stamp) def _cb_sb_action_message_sent(self, msn, switchboard, signal, args, stamp=None): '''called when an action message is received''' data = args[0] self.logger.conversation_event_add('action-message', switchboard.started, self.msn.user, data, stamp) def _cb_sb_ink_message_sent(self, msn, switchboard, signal, args, stamp=None): '''called when an ink message is sent''' self.logger.conversation_event_add('ink-message', switchboard.started, self.msn.user, '', stamp) def _cb_sb_nudge_sent(self, msn, switchboard, signal, args, stamp=None): '''called when a nudge is sent''' self.logger.conversation_event_add('nudge', switchboard.started, self.msn.user, '', stamp) def _cb_sb_custom_emoticon_received(self, msn, switchboard, signal, args, stamp=None): '''called when a custom emoticon is received (du'h)''' shortcut, msnobj = args filename = shortcut + '_' + msnobj.sha1d + '.tmp' filename = urllib.quote(filename).replace('/', '_') complete_filename = self.msn.cacheDir + os.sep + filename self.logger.conversation_event_add('custom-emoticon', switchboard.started, self.msn.user, shortcut + ' ' + complete_filename, stamp) def _cb_display_picture_changed(self, msn, switchboard, msnobj, mail, stamp=None): '''called when a new display picture is received''' contact = self.msn.contactManager.getContact(mail) if contact is None: return filename = contact.displayPicturePath result = self.get_last_display_picture(mail) if result: old_filename = result[0][1] else: old_filename = None if old_filename != filename: self.logger.user_event_add('display-picture', mail, \ filename, stamp) def configure( self ): configuration = [] configuration.append(Plugin.Option('', gtk.Widget, '', '', gtk.Label(_("Enable/disable events to be logged:")))) for signal in self.signals: configuration.append(Plugin.Option(signal, bool, self.signals_labels[signal], \ '', bool(int(self.config.getPluginValue(self.name, signal, True))))) configWindow = Plugin.ConfigWindow( 'Logger', configuration ) configWindow.vbox.set_spacing(0) response = configWindow.run() if response != None: for signal in self.signals: if response.has_key(signal): self.config.setPluginValue( self.name, signal, str(int(response[signal].value)) ) self.stop() self.start() return True