# -*- coding: utf-8 -*- # This file is part of emesene. # # Emesene is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # emesene is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with emesene; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import gtk import time import pango import gobject import webkitgtk import re import UserList import SmilieWindow from Theme import resizePixbuf import emesenelib.common import emesenelib.ContactData import Widgets class WebkitRenderer(webkitgtk.WebView): def __init__(self,themepath,header,footer,variant=False): webkitgtk.WebView.__init__(self) self.themepath=themepath self.header=header self.footer=footer if variant and variant != 'Default': varianttext = "\n\ \n" else: varianttext='' self.oldhtml="\n\ \n\ \n\ \n\ \n\ \n\ \n\ "+varianttext+"\n\ \n\ "+self.header+"\n\
" self.load_html_string(self.oldhtml+"
"+self.footer+"\ ", self.themepath) def display_html(self,html, consecutive=False): #TODO: Find a less ugly and more performant way to do this, maybe using DOM/JS? if consecutive: regexhax = re.compile( '<(?:span|div) id="insert"(?:\s+/)?.*>(?:.*)?' ) insertpos = regexhax.findall( self.oldhtml )[0] self.newcontent = self.oldhtml.rpartition( insertpos )[0] + html + self.oldhtml.rpartition( insertpos )[2] else: self.newcontent=self.oldhtml+html self.load_html_string(self.newcontent+""+self.footer+"", self.themepath) self.oldhtml=self.newcontent def get_buffer(self): return self.oldhtml class ConversationUI(gtk.VBox): '''this class represent all the widgets that are inside a tab also hold the tab widget because there is no better place than this to hold it...''' def __init__(self, controller, parentConversation): gtk.VBox.__init__(self) self.parentConversation = parentConversation self.controller = controller self.config = self.controller.config # the header that contain the nick, the PM, status etc self.header = Header(self, controller) # the widget that contain the tabs and the conversations self.tabWidget = TabWidget(self, controller) # the widget where you can put the text self.input = InputWidget(self, controller, \ parentConversation.isCurrent) # the status bar self.status = gtk.Statusbar() # the toolbar self.toolbar = ToolbarWidget(self, controller) self.toolbar.set_style(gtk.TOOLBAR_ICONS) self.toolbar.set_small(self.config.user['smallIcons']) self.toolbarinput = gtk.HBox() # the list of users when we are on a group chat self.listOfUsers = UserList.UserList(self.controller, \ self.controller.getTheme(), self.config, False) # the scroll for the list of users self.scrollList = gtk.ScrolledWindow() self.scrollList.set_shadow_type(gtk.SHADOW_IN) self.scrollList.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) self.scrollList.set_size_request(116, -1) self.scrollList.set_border_width(0) self.scrollList.add(self.listOfUsers) # the scroll for the textview below self.scroll = gtk.ScrolledWindow() self.scroll.set_shadow_type(gtk.SHADOW_IN) self.scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) # the widget where the conversation is shown self.textview=WebkitRenderer(self.controller.getConversationLayoutManager().theme['basepath'],self.controller.getConversationLayoutManager().theme['header'], self.controller.getConversationLayoutManager().theme['footer'], self.config.user['layoutvariant']) self.scroll.add(self.textview) # Friends avatar #self.imageFrame = gtk.AspectFrame() self.image = Widgets.avatarHolder(cellKeyPosition = gtk.ANCHOR_NORTH) contact = self.controller.getContact(\ self.parentConversation.switchboard.firstUser) self.image.set_from_pixbuf(\ self.controller.getTheme().getUserDisplayPicture(contact)) #self.imageFrame.add(self.image) self.controller.msn.connect('display-picture-changed', self.displayPictureChanged) self.controller.msn.connect('user-attr-changed', self.onUserAttrChanged) self.hidebutton = gtk.Button() self.hidebutton.connect('clicked', self.hideShowButton) self.hidebutton.set_relief(gtk.RELIEF_NONE) self.hidebuttonArrow = Widgets.TinyArrow(gtk.ARROW_RIGHT) self.hidebutton.add(self.hidebuttonArrow) # Vbox to pack both the userlist and friend display image self.vbox = gtk.VBox() self.vbox.pack_start(self.image, False,False,0) self.vbox.pack_start(self.scrollList, True, True) self.vbox.set_border_width(0) self.filler = gtk.VBox() self.filler.set_border_width(0) self.vbox2 = gtk.VBox() self.vbox2.set_border_width(0) self.vbox2.pack_start(self.hidebutton, False, False) self.vbox2.pack_start(self.filler, True, True) # HBox to pack the VBox above and the Hide/Show Friends Avatar button self.hboxFrame = gtk.HBox() self.hboxFrame.set_border_width(0) self.hboxFrame.pack_start(self.vbox, True, True) self.hboxFrame.pack_start(self.vbox2, False, True) self.hpaned = gtk.HBox() self.hpaned.pack_start(self.scroll, True, True,2) self.hpaned.pack_start(self.hboxFrame, False, False,2) # the hbox that pack the textview, the images of the other users # and the userlist self.hbox = gtk.HBox() self.hbox.set_border_width(2) self.hbox.pack_start(self.hpaned, True, True) self.toolbarinputVbox = gtk.VBox() self.toolbarinputVbox.pack_start(self.toolbar, False, True) self.toolbarinputVbox.pack_start(self.input, True, True) self.toolbarinput.pack_start(self.toolbarinputVbox, True, True) self.toolbarinput.connect('size-allocate', self.onToolbarinputResize) # the vertical panel that divide the textview and the input # (toolbar and input) self.vpaned = gtk.VPaned() self.vpaned.pack1(self.hbox, True, True) self.vpaned.pack2(self.toolbarinput, False, True) self.transfers = FtBarWidget(self.controller, self, parentConversation) self.pack_start(self.header, False, False) self.pack_start(self.vpaned, True, True) self.pack_start(self.transfers, False, False) self.pack_start(self.status, False, False) self.messageWaiting = {} self.contactTyping = {} self.typingTimeoutID = 0 self.closed = False self.last_mark = None #self.input.grabFocus() self.show_all() self.update() def onToolbarinputResize(self, *args): alloc = self.toolbarinput.get_allocation() self.config.user['convInputHeight'] = alloc.height def onTextviewKeyPress(self, widget, event): # don't focus inputbox if Control is pressed (Ctrl-C etc.) if event.keyval == gtk.keysyms.Control_L or \ event.keyval == gtk.keysyms.Control_R or \ event.state & gtk.gdk.CONTROL_MASK: return self.input.input.emit('key-press-event', event) self.input.grabFocus() def close(self): self.closed = True def setInputEnabled(self, enable): self.input.input.set_sensitive(enable) def onUserAttrChanged(self, msn, contact): if contact.email in self.parentConversation.getMembers(): self.update() def noneIsTyping(self): try: for contact in self.email_list: if self.contactTyping[contact] == True: return False return True except: return False def update(self): self.header.update() self.tabWidget.update() self.input.update() self.email_list = [mail.email for mail in self.parentConversation.getMembersDict().values()] # add new user in conversation for email in self.email_list: if email not in self.contactTyping.keys(): self.contactTyping[email] = False if email not in self.messageWaiting.keys(): self.messageWaiting[email] = False for email in self.contactTyping.keys(): if self.contactTyping[email]: self.setTyping(email) else: self.setDefault(email) isGroupChat = len(self.parentConversation.getMembers()) > 1 if isGroupChat: members = self.parentConversation.getMembersDict() d = {'members' : emesenelib.ContactData.Group('members')} for member in members.values(): d['members'].setUser(member.email, member) self.listOfUsers.fill(d) # if you want to show the avatar and hide the grouplist or # viceversa first do the hide and then the show, because this methods # also hide the container vbox to not let an empty widget there if isGroupChat: self.hideAvatar() self.hidebutton.hide() self.showUserList() elif self.config.user['showAvatars']: self.hidebutton.show() self.hideUserList() if self.config.user['showAvatarOther']: self.hidebuttonArrow.set(gtk.ARROW_RIGHT, gtk.SHADOW_NONE) self.showAvatar() else: self.hidebuttonArrow.set(gtk.ARROW_LEFT, gtk.SHADOW_NONE) self.hideAvatar() else: self.hideAvatar() self.hideUserList() self.hidebutton.hide() if not self.config.user['showHeader']: self.header.hide() else: self.header.show() if not self.config.user['showToolbar']: self.toolbar.hide() else: self.toolbar.show() if not self.config.user['showTabCloseButton']: self.tabWidget.closeButton.hide() else: self.tabWidget.closeButton.show() inputHeight = self.config.user['convInputHeight'] self.toolbarinput.set_size_request(-1, inputHeight) def scrollToBottom(self): '''scroll to the end of the conversation''' #not working yet, don't ask me why temp = self.scroll.get_vadjustment() temp.set_value(temp.upper-temp.page_size) print temp.upper, temp.page_size, temp.value self.scroll.set_vadjustment(temp) #textbuffer = self.textview.get_buffer() #mark = textbuffer.create_mark("end", textbuffer.get_end_iter(), False) #if self.last_mark: # textiter = textbuffer.get_iter_at_mark(self.last_mark) # rect = self.textview.get_visible_rect() # pos = rect.y + rect.height # if (pos + 25) < self.textview.get_iter_location(textiter).y: # return #self.last_mark = mark #self.textview.scroll_to_mark(mark, 0.05, True, 0.0, 1.0) #textbuffer.place_cursor(textbuffer.get_end_iter()) def showUserList(self): self.vbox.show() self.scrollList.show_all() self.listOfUsers.show() def hideUserList(self): self.vbox.hide() self.scrollList.hide() self.listOfUsers.hide() def showAvatar(self): self.vbox.show() self.image.show_all() def hideAvatar(self): self.vbox.hide() self.image.hide() def hideShowButton(self, *args): if self.hidebuttonArrow.arrow_type == gtk.ARROW_LEFT: self.hidebuttonArrow.set(gtk.ARROW_RIGHT, gtk.SHADOW_NONE) if len(self.parentConversation.getMembers()) > 1: self.showUserList() else: self.showAvatar() self.config.user['showAvatarOther'] = True else: self.hidebuttonArrow.set(gtk.ARROW_LEFT, gtk.SHADOW_NONE) if len(self.parentConversation.getMembers()) > 1: self.hideUserList() else: self.hideAvatar() self.config.user['showAvatarOther'] = False def rebuildStatusText(self): '''Builds the text displayed in the statusbar, based in self.contactTyping. The "output" is a comma separated list of (full) mails and "is/are typing a message..." ''' mails = [x for x in self.contactTyping.keys() \ if self.contactTyping[x] == True] string = '' if len(mails) > 0: comma = ', ' # TODO: gettext? for mail in mails: string += str(mail) + comma # yes, i know we don't need to deal with utf-8 here, but... string = str(unicode(string)[:-len(comma)]) if len(mails) == 1: string += ' ' + _('is typing a message...') else: string += ' ' + _('are typing a message...') #this is a hack to skip the statusbar stack self.status.get_children()[0].get_children()[0].set_text(string) def setMessageWaiting(self, mail): if self.parentConversation.getIsCurrent(): return self.setDefault(mail) self.tabWidget.setMessageWaiting() self.header.setDefault() if mail: self.contactTyping[mail] = False self.messageWaiting[mail] = True self.rebuildStatusText() def setTyping(self, mail): if self.messageWaiting.has_key(mail) and self.messageWaiting[mail]: return self.header.setTyping() self.tabWidget.setTyping() self.contactTyping[mail] = True self.rebuildStatusText() if self.typingTimeoutID > 0: gobject.source_remove(self.typingTimeoutID) self.typingTimeoutID = gobject.timeout_add(8000, \ self.clearTyping, mail) def clearTyping(self, mail): if self.messageWaiting[mail]: self.setMessageWaiting(mail) else: self.setDefault(mail) self.contactTyping[mail] = False return False def setDefault(self, mail): #if self.noneIsTyping(): self.tabWidget.setDefault() self.header.setDefault() if mail: self.contactTyping[mail] = False self.messageWaiting[mail] = False self.rebuildStatusText() def displayPictureChanged(self, msnp, switchboard, msnobj, email): if self.parentConversation.getSwitchboard().firstUser == email: contact = self.controller.msn.contactManager.getContact(email) self.image.set_from_pixbuf( self.controller.getTheme().getUserDisplayPicture(contact)) # TODO: getters def getTextView(self): return self.textview def getParentConversation(self): return self.parentConversation class Header(gtk.HBox): '''the header of the conversation''' def __init__(self, parentUI, controller): '''Contructor''' gtk.HBox.__init__(self) self.parentUI = parentUI self.controller = controller self.config = controller.config self.set_border_width(6) self.set_spacing(6) self.parser = controller.unifiedParser self.statusImage = gtk.Image() self.statusImage.set_from_pixbuf(self.controller.getTheme().getImage('groupChat')) self.label = gtk.Label() self.label.set_selectable(True) self.label.unset_flags(gtk.CAN_FOCUS) self.label.set_alignment(0.0, 0.5) self.label.set_line_wrap(True) self.label.set_ellipsize(pango.ELLIPSIZE_END) # when someone fin how to make that this label is selected all the time, # discoment the next line #self.label.set_selectable(True) self.label.set_markup('' + _('Connecting...') + '') self.reconnect = gtk.Button(_('Reconnect')) self.reconnect.connect('clicked', self.reconnectClicked) self.pack_start(self.statusImage, False, False) self.pack_start(self.label) self.pack_start(self.reconnect, False, False) self.statusImage.show() self.label.show() self.show() def reconnectClicked(self, button): self.parentUI.parentConversation.reconnect() self.reconnect.hide() def update(self): conversation = self.parentUI.parentConversation members = conversation.getMembersDict() if self.config.user['showTabImageStatus']: self.statusImage.hide() return if conversation.switchboard.status == 'error': self.setLabel() elif len(members) == 1: contact = members.values()[0] self.statusImage.set_from_pixbuf(self.controller.getTheme().statusToPixbuf(contact.status)) else: self.statusImage.set_from_pixbuf(self.controller.getTheme().getImage('userPanel')) def setTyping(self): typingColor = self.config.user['typingColor'] styleOpen = "" styleClose = "" self.setLabel(styleOpen, styleClose) def setLabel(self, styleOpen='', styleClose=''): conversation = self.parentUI.parentConversation members = conversation.getMembersDict() personalMessageColor = self.config.user['personalMessageColor'] switchboard = conversation.switchboard if switchboard.status == 'error': self.text = '%s %s (%d) %s' % \ (styleOpen, _('Can\'t connect'), switchboard.error, styleClose) self.label.set_markup(self.text) self.reconnect.show() return self.reconnect.hide() if len(members) == 1: contact = members.values()[0] self.text = styleOpen + emesenelib.common.escape(contact.nick) + styleClose if len(contact.personalMessage) > 0: self.text += "\n" self.text += emesenelib.common.escape(contact.personalMessage) + "" self.text += "\n" self.text += contact.email + "" elif len(members) > 1: self.text = styleOpen + _('Group chat') + styleClose self.text += "\n" self.text += str(len(members)) + ' ' + _('Members') + "" self.statusImage.set_from_pixbuf(self.controller.getTheme().getImage('groupChat')) else: self.text = styleOpen + '' + _('Connecting...') + '' + styleClose self.label.set_markup(self.text) def setDefault(self): self.setLabel() class TabWidget(gtk.HBox): '''this class represent the widget that is inside the tab it contains a label and a close button''' def __init__(self, parentUI, controller): '''Contructor''' gtk.HBox.__init__(self) self.parentUI = parentUI self.controller = controller self.config = controller.config self.text = self.parentUI.parentConversation.getTitle() self.label = gtk.Label() self.label.set_use_markup(True) self.label.set_markup(self.text) self.label.set_ellipsize(pango.ELLIPSIZE_END) img = gtk.Image() img.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU) self.closeButton = gtk.Button() self.closeButton.set_image(img) self.closeButton.set_relief(gtk.RELIEF_NONE) self.statusImage = gtk.Image() self.pack_start(self.statusImage, False, False) self.pack_start(self.label) self.pack_start(self.closeButton, False, False) self.show_all() self.closeButton.connect('clicked', self.onCloseClicked) def onCloseClicked(self, button): self.parentUI.parentConversation.close() def update(self): title = self.parentUI.parentConversation.getTitle() members = self.parentUI.parentConversation.getMembersDict() if not title == '': self.text = title if not self.config.user['showTabImageStatus']: return if len(members) == 1: contact = members.values()[0] basepixbuf = self.controller.theme.statusToPixbuf(contact.status) else: basepixbuf = self.controller.theme.getImage('userPanel') self.statusImage.set_from_pixbuf(resizePixbuf(basepixbuf, 16, 16)) def setDefault(self): self.label.set_markup(self.text) def setTyping(self): typingColor = self.config.user['typingColor'] self.label.set_markup('%s' % (typingColor, self.text)) def setMessageWaiting(self): messageWaitingColor = self.config.user['messageWaitingColor'] self.label.set_markup('%s' % (messageWaitingColor, self.text)) class LocalAvatar(gtk.HBox): def __init__(self, parentUI, controller): gtk.HBox.__init__(self) self.parentUI = parentUI self.controller = controller self.config = controller.config self.hidebutton = gtk.Button() self.hidebutton.connect('clicked', self.hideShowButton) self.hidebutton.set_relief(gtk.RELIEF_NONE) self.hidebuttonArrow = Widgets.TinyArrow(gtk.ARROW_RIGHT) self.hidebutton.add(self.hidebuttonArrow) self.localimage = Widgets.avatarHolder(cellKeyPosition = gtk.ANCHOR_SOUTH) self.imageEventBox = gtk.EventBox() self.imageEventBox.set_events(gtk.gdk.BUTTON_PRESS_MASK) self.imageEventBox.set_visible_window(False) self.imageEventBox.connect('button-press-event', self.changeAvatar) self.imageEventBox.add(self.localimage) if self.controller.avatar != None: self.localimage.set_from_pixbuf(self.controller.avatar.getImage()) else: self.pixbuf = self.controller.theme.getImage('icon96') self.localimage.set_from_pixbuf(self.pixbuf) self.controller.connect('avatar-changed', self.localDisplayPictureChanged) self.filler = gtk.VBox() self.vboxButtonFrame = gtk.VBox() self.vboxButtonFrame.pack_start(self.hidebutton, False, False) self.vboxButtonFrame.pack_start(self.filler, True, True) self.filler2 = gtk.VBox() self.vboxAvatarFrame = gtk.VBox() self.vboxAvatarFrame.pack_start(self.imageEventBox, False, False) self.vboxAvatarFrame.pack_start(self.filler2, True, True) self.pack_start(self.vboxAvatarFrame, False, False, 0) self.pack_start(self.vboxButtonFrame, False, False, 0) self.update() def update(self): if self.config.user['showAvatars']: self.hidebutton.show() if not self.config.user['showAvatarMine']: self.imageEventBox.hide() self.hidebuttonArrow.set(gtk.ARROW_LEFT, gtk.SHADOW_NONE) else: self.imageEventBox.show() self.hidebuttonArrow.set(gtk.ARROW_RIGHT, gtk.SHADOW_NONE) else: self.imageEventBox.hide() self.hidebutton.hide() def displayPictureChanged(self, msnp, email): if self.parentUI.parentConversation.getSwitchboard().firstUser == email: #self.image.set_from_pixbuf(self.controller.getTheme().getUserDisplayPicture(email)) print 'check here because self.image soesnt exist (ConversationUI 720)' def localDisplayPictureChanged(self, controller): self.localimage.set_from_pixbuf(self.controller.avatar.getImage()) def changeAvatar(self, widget, event): self.controller.changeAvatarDialog() def hideShowButton(self, *args): if self.config.user['showAvatars']: self.hidebutton.show() if self.config.user['showAvatarMine']: self.hidebuttonArrow.set(gtk.ARROW_LEFT, gtk.SHADOW_NONE) self.imageEventBox.hide() self.config.user['showAvatarMine'] = False else: self.hidebuttonArrow.set(gtk.ARROW_RIGHT, gtk.SHADOW_NONE) self.imageEventBox.show() self.config.user['showAvatarMine'] = True else: self.imageEventBox.hide() self.hidebutton.hide() class InputWidget(gtk.HBox): '''This class represents the input widgets (text entry) and avatar (if enabled)''' def __init__(self, parentUI, controller, setFocus=True): gtk.HBox.__init__(self) self.parentUI = parentUI self.controller = controller self.config = controller.config self.lastKeyPressed = 0 self.history = [] self.historyIndex = -1 self.set_border_width(2) # the local avatar self.localAvatar = LocalAvatar(self, controller) self.input = Widgets.inputBox() self.input.set_left_margin(6) self.input.set_right_margin(6) self.input.set_wrap_mode(gtk.WRAP_WORD_CHAR) self.inputBuffer = self.input.get_buffer() self.scrollInput = gtk.ScrolledWindow() self.scrollInput.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.scrollInput.add(self.input) frameInput = gtk.Frame() frameInput.add(self.scrollInput) self.pack_start(frameInput, True, True, 2) self.pack_start(self.localAvatar, False, False, 2) self.inputBuffer.connect('changed', self.on_changed_event) self.input.connect('key-press-event' , self.on_key_press_event) self.input.connect_after('message-send', self.message_send) self.input.connect_after('escape-pressed', self.on_escape_pressed_event) self.input.connect_after('map-event', self.on_input_map_event) if setFocus: self.input.grab_focus() self.tag = None self.id_timeout = None self.parse_off = 0 self.applyAttrsToInput() self.show_all() def update(self): '''update the inner componnents if needed''' self.localAvatar.update() def transformEmo(self, *args): ''' transform smiley shorcuts in pixbuf ''' if not self.controller.config.user['parseSmilies']: print "XXX HAI" return theme = self.controller.theme emos = [] customEmo = self.controller.customEmoticons for code in theme.getSmileysList(): iter_start = self.inputBuffer.get_start_iter() iter_start.set_offset(self.parse_off) while iter_start.forward_search(code, gtk.TEXT_SEARCH_VISIBLE_ONLY): iter_pos, iter_end = iter_start.forward_search(code, \ gtk.TEXT_SEARCH_VISIBLE_ONLY) pixbuf = theme.getSmiley(code, False) pixbuf.shortcut = code tag = gtk.TextTag() tag.set_property('invisible', True) tagtable = self.inputBuffer.get_tag_table() tagtable.add(tag) self.inputBuffer.apply_tag(tag, iter_pos, iter_end) mark1 = self.inputBuffer.create_mark(None, iter_pos) mark2 = self.inputBuffer.create_mark(None, iter_end) img = gtk.Image() img.set_from_pixbuf(pixbuf) img.show() img.shortcut = code emos.append((img, mark1, mark2)) iter_start = iter_end for code, filename in customEmo.list.iteritems(): iter_start = self.inputBuffer.get_start_iter() iter_start.set_offset(self.parse_off) while iter_start.forward_search(code, gtk.TEXT_SEARCH_VISIBLE_ONLY): iter_pos,iter_end = iter_start.forward_search(code, \ gtk.TEXT_SEARCH_VISIBLE_ONLY) pixbuf = gtk.gdk.pixbuf_new_from_file(filename) pixbuf = resizePixbuf(pixbuf, 24, 24) tag = gtk.TextTag() tag.set_property('invisible', True) tagtable = self.inputBuffer.get_tag_table() tagtable.add(tag) self.inputBuffer.apply_tag(tag, iter_pos, iter_end) mark1 = self.inputBuffer.create_mark(None, iter_pos) mark2 = self.inputBuffer.create_mark(None, iter_end) img = gtk.Image() img.set_from_pixbuf(pixbuf) img.show() img.shortcut = code emos.append((img, mark1, mark2)) iter_start = iter_end for emo in emos: iterStart = self.inputBuffer.get_iter_at_mark(emo[1]) iterEnd = self.inputBuffer.get_iter_at_mark(emo[2]) self.inputBuffer.delete(iterStart, iterEnd) iterStart = self.inputBuffer.get_iter_at_mark(emo[1]) #self.inputBuffer.insert_pixbuf(iterStart, emo[0]) anchor = self.inputBuffer.create_child_anchor(iterStart) self.input.add_child_at_anchor(emo[0], anchor) iter_off = self.inputBuffer.get_end_iter() if iter_off.inside_word() or iter_off.ends_word(): iter_off.backward_word_start() self.parse_off = iter_off.get_offset() def grabFocus(self): self.input.grab_focus() def getBuffer(self): return self.inputBuffer def clearMessageInput(self): self.setInputText('') return True def sendMessage(self): if self.getInputText() != '': # Message size is limited so we must truncate it to send # it in several parts message = self.getInputText() self.parentUI.parentConversation.sendMessage(message) self.setInputText('') self.history.append(message) self.historyIndex = -1 return True def setInputText(self , string , tag = None): if tag == None: self.inputBuffer.set_text(string) else: pass def getInputText(self): '''return the text in the input textview''' iterStart, iterEnd = self.inputBuffer.get_bounds() text = self.inputBuffer.get_slice(iterStart, iterEnd) emos = [] #TODO: magic string '\xef\xbf\xbc' while iterStart.forward_search('\xef\xbf\xbc', \ gtk.TEXT_SEARCH_VISIBLE_ONLY): iterPos,iterEnd = iterStart.forward_search('\xef\xbf\xbc', \ gtk.TEXT_SEARCH_VISIBLE_ONLY) if iterPos.get_child_anchor().get_widgets(): shortcut = iterPos.get_child_anchor().get_widgets()[0].shortcut emos.append(shortcut) #shortcut = iterPos.get_child_anchor().shortcut iterStart = iterEnd for emo in emos: text = text.replace('\xef\xbf\xbc', emo, 1) return text def appendInputText(self, text, tag = None): '''append the given text to the inputBuffer, if tag != None insert with the given tag''' self.inputBuffer.insert_at_cursor(text) def on_changed_event(self, *args): '''Method called when text is inserted in the textbuffer (typing) First, it checks if the message length is >= 5 or if the message is a slash command ("^\/[^\/]" is confusing) Then, checks the last key pressed time, and if it's more than 5 seconds, send a typing message''' if self.inputBuffer.get_start_iter().get_offset() < self.parse_off: self.parse_off = 0 if self.id_timeout != None: gobject.source_remove(self.id_timeout) if not self.getInputText().startswith('/'): self.id_timeout = gobject.timeout_add(200, self.transformEmo) text = self.getInputText() if len(text) < 5 or (text[0] == '/' and text[1] != '/'): return self.updateInputFormat() # sents user typing notification actualTime = int(time.time()) if (actualTime - self.lastKeyPressed) > 5 and \ self.config.user['sendTyping']: self.lastKeyPressed = actualTime try: self.parentUI.parentConversation.sendIsTyping() except Exception , e: print str(e) self.updateInputFormat() def applyAttrsToInput(self): '''apply the current attributes to the text in input''' self.tag = self.parentUI.parentConversation.getTextTag() self.inputBuffer.get_tag_table().add(self.tag) self.tag.set_property('left-margin', 6) self.tag.set_priority(self.inputBuffer.get_tag_table().get_size() - 1) self.inputBuffer.apply_tag(self.tag, self.inputBuffer.get_start_iter(), self.inputBuffer.get_end_iter()) self.controller.emit('input-format-changed', self.input) def updateInputFormat(self): if self.tag != None: self.inputBuffer.apply_tag(self.tag, self.inputBuffer.get_start_iter(), self.inputBuffer.get_end_iter()) def message_send(self, widget): if self.id_timeout != None: gobject.source_remove(self.id_timeout) self.id_timeout = None return self.sendMessage() def on_input_map_event(self, widget, event): if self.parentUI.getParentConversation().getIsCurrent(): self.input.grab_focus() def on_escape_pressed_event(self, widget): if not self.parentUI.closed: self.parentUI.parentConversation.close() def on_key_press_event(self , widget, event): if event.keyval == gtk.keysyms.space: if self.id_timeout != None: gobject.source_remove(self.id_timeout) self.id_timeout = None if not self.getInputText().startswith('/'): self.transformEmo() elif event.keyval in (gtk.keysyms.Up, gtk.keysyms.Down) and \ (event.state & gtk.gdk.CONTROL_MASK): up = event.keyval == gtk.keysyms.Up if up and self.historyIndex == -1 and len(self.history) > 0: self.historyIndex = len(self.history) - 1 if self.getInputText() != '': self.history.append(self.getInputText()) elif self.historyIndex > 0 and self.historyIndex < len(self.history): if up: self.historyIndex -= 1 else: self.historyIndex += 1 if self.historyIndex > -1 and self.historyIndex < len(self.history): self.setInputText(self.history[self.historyIndex]) elif self.historyIndex != -1: self.setInputText('') class ToolbarWidget(gtk.Toolbar): '''This represents a toolbar that contains text formatation, smilies buttom, do nudge button, and so on...''' def __init__(self, parentUI, controller): gtk.Toolbar.__init__(self) self.parentUI = parentUI self.controller = controller self.config = controller.config self.smilieWindow = SmilieWindow.SmilieWindow(self.controller, \ self.smilieSelected, \ None) self.fontFace = gtk.ToolButton() self.fontFace.set_stock_id(gtk.STOCK_SELECT_FONT) self.fontFace.connect('clicked', self.on_font_face_clicked) self.fontColor = gtk.ToolButton() self.fontColor.set_stock_id(gtk.STOCK_SELECT_COLOR) self.fontColor.connect('clicked', self.on_font_color_clicked) self.boldButton = gtk.ToggleToolButton() self.boldButton.set_stock_id(gtk.STOCK_BOLD) self.boldButton.connect('toggled', self.toggled, 'bold') self.boldButton.set_active(self.config.user['fontBold']) self.italicButton = gtk.ToggleToolButton() self.italicButton.set_stock_id(gtk.STOCK_ITALIC) self.italicButton.connect('toggled', self.toggled, 'italic') self.italicButton.set_active(self.config.user['fontItalic']) self.underlineButton = gtk.ToggleToolButton() self.underlineButton.set_stock_id(gtk.STOCK_UNDERLINE) self.underlineButton.connect('toggled', self.toggled, 'underline') self.underlineButton.set_active(self.config.user['fontUnderline']) self.strikeButton = gtk.ToggleToolButton() self.strikeButton.set_stock_id(gtk.STOCK_STRIKETHROUGH) self.strikeButton.connect('toggled', self.toggled, 'strike') self.strikeButton.set_active(self.config.user['fontStrike']) self.smilieButton = gtk.ToolButton() self.smilieButton.set_label(_('Smilie')) self.smilieButton.connect('clicked', self.showSmilieWindow) self.nudgeButton = gtk.ToolButton() self.nudgeButton.set_label(_('Nudge')) self.nudgeButton.connect('clicked', self.doNudge) self.inviteButton = gtk.ToolButton() self.inviteButton.set_label(_('Invite')) self.inviteButton.set_stock_id(gtk.STOCK_ADD) self.inviteButton.connect('clicked', self.showInviteDialog) #clear conversation toolbar button imgclear = gtk.Image() imgclear.set_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_LARGE_TOOLBAR) self.clearButton = gtk.ToolButton(imgclear, _('Clear Conversation')) self.clearButton.connect('clicked', self.clearOutputText) # Add buttons to the toolbar self.insert(self.fontFace, -1) self.insert(self.fontColor, -1) self.insert(self.boldButton, -1) self.insert(self.italicButton, -1) self.insert(self.underlineButton, -1) self.insert(self.strikeButton, -1) self.insert(gtk.SeparatorToolItem(), -1) self.insert(self.smilieButton, -1) self.insert(self.nudgeButton, -1) self.insert(gtk.SeparatorToolItem(), -1) self.insert(self.inviteButton, -1) self.insert(self.clearButton, -1) # Set tooltips to buttons self.tooltips = gtk.Tooltips() self.tooltips.enable() self.fontFace.set_tooltip(self.tooltips, _('Font selection')) self.fontColor.set_tooltip(self.tooltips, _('Font color selection')) self.boldButton.set_tooltip(self.tooltips, _('Bold')) self.italicButton.set_tooltip(self.tooltips, _('Italic')) self.underlineButton.set_tooltip(self.tooltips, _('Underline')) self.strikeButton.set_tooltip(self.tooltips, _('Strike')) self.smilieButton.set_tooltip(self.tooltips, _('Insert a smilie')) self.nudgeButton.set_tooltip(self.tooltips, _('Send nudge')) self.inviteButton.set_tooltip(self.tooltips, \ _('Invite a friend to the conversation')) self.clearButton.set_tooltip(self.tooltips, _('Clear conversation')) self.show_all() def on_font_face_clicked(self, *args): '''Called when user clicks on the font selection button in the toolbar''' self.parentUI.parentConversation.parentConversationWindow.changeFont() def on_font_color_clicked(self, *args): ''' Called when user clicks on the color selection button in the toolbar''' self.parentUI.parentConversation.parentConversationWindow.changeColor() def toggled(self, button, option): '''callback for the toggled signal''' if option == 'bold': self.config.user['fontBold'] = button.get_active() elif option == 'italic': self.config.user['fontItalic'] = button.get_active() elif option == 'underline': self.config.user['fontUnderline'] = button.get_active() elif option == 'strike': self.config.user['fontStrike'] = button.get_active() self.parentUI.input.applyAttrsToInput() def set_small(self, value): '''sets the icons size to small if value is True''' if value: size = gtk.ICON_SIZE_MENU else: size = gtk.ICON_SIZE_LARGE_TOOLBAR imgSmilie = gtk.Image() imgNudge = gtk.Image() nudge = self.controller.theme.getImage('nudge') grin = self.controller.theme.getSmiley(':D') if isinstance(grin, gtk.gdk.PixbufAnimation): grin = grin.get_static_image() grin = resizePixbuf(grin, *gtk.icon_size_lookup(size)) nudge = resizePixbuf(nudge, *gtk.icon_size_lookup(size)) imgSmilie.set_from_pixbuf(grin) imgNudge.set_from_pixbuf(nudge) self.smilieButton.set_icon_widget(imgSmilie) self.nudgeButton.set_icon_widget(imgNudge) # set the global icons size settings = self.get_settings() settings.set_long_property('gtk-toolbar-icon-size', size, \ 'emesene:ConversationUI') def setFontBold(self, value): self.boldButton.set_active(value) def setFontUnderline(self, value): self.underlineButton.set_active(value) def setFontStrike(self, value): self.strikeButton.set_active(value) def setFontItalic(self, value): self.italicButton.set_active(value) def smilieSelected(self, smilie=None): '''this method is called when the user click a smilie in the smiliewindow''' if smilie: self.parentUI.input.appendInputText(smilie) self.parentUI.input.grabFocus() def showSmilieWindow(self, *args): '''this method is called when the user click the smilie button''' self.smilieWindow.show() def showInviteDialog(self, *args): '''this method is called when the user click the invite button''' conv = self.parentUI.parentConversation dictionary = conv.contactManager.getOnlineUsersDict() members = {} for i in conv.getMembers() + [conv.getUser()]: members[i] = self.controller.getContact(i) InviteWindow(self.controller, None, dictionary, members, self.controller.theme, conv.inviteUser).run() def clearOutputText(self, *args): self.parentUI.parentConversation.textBuffer.set_text('') def doNudge(self, *args): '''this method is called when the user click the nudge button''' self.parentUI.parentConversation.doNudge() def doBlock(self, *args): '''this method is called when the user clicks the block button''' users = self.parentUI.parentConversation.getMembers() if len(users) == 1: self.controller.blockUser(users[0]) def doUnblock(self, *args): '''this method is called when the user clicks the block button''' users = self.parentUI.parentConversation.getMembers() if len(users) == 1: self.controller.unblockUser(users[0]) class InviteWindow(gtk.Dialog): '''This class represent a list where the user can pick wich users he want to invite to a conversation''' def __init__(self, controller, father, onlineUsers, usersInConversation, theme, callback): '''Constructor callback is called when a user is selected with double click''' gtk.Dialog.__init__(self, _('Invite'), father, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, \ gtk.STOCK_ADD, gtk.RESPONSE_ACCEPT)) self.set_default_size(400, 300) self.callback = callback users = onlineUsers self.controller = controller for i in usersInConversation.keys(): if users.has_key(i): del users[i] self.userList = UserList.UserList(self.controller, theme, self.controller.config, False) self.userList.connect('item-selected', self.userListClicked) group = emesenelib.ContactData.Group(_('Users')) for contact in controller.msn.contactManager.contacts.values(): if contact.status != 'FLN' and contact.email not in usersInConversation: group.setUser(contact.email, contact) self.userList.fill({group.name : group}) self.scroll = gtk.ScrolledWindow() self.scroll.set_policy(gtk.POLICY_AUTOMATIC , gtk.POLICY_AUTOMATIC) self.scroll.add(self.userList) self.connect('response', self.invite) self.connect('delete-event', self.close) self.vbox.pack_start(self.scroll) self.vbox.show_all() def invite(self, dialog, response_id): '''get the user in the userlist and call the callback''' try: if response_id == gtk.RESPONSE_ACCEPT: self.userListClicked(self.userList, self.userList.getSelected()) return True else: self.close() return False except Exception, e: print e def userListClicked(self, userList, t): if t and t[0] == 'user': self.callback(t[1].email) self.close() def close(self, *args): '''close the window''' self.hide() class FtBarWidget(gtk.HBox): '''bar which represents active file transfers''' def __init__(self, controller, parentUI, parentConversation): gtk.HBox.__init__(self) self.controller = controller self.parentUI = parentUI self.parentConversation = parentConversation self.show_all() def add(self, transfer): widget = FtWidget(self.controller, self, transfer) transfer.ui = widget self.pack_start(widget, False, False) self.show_all() class FtWidget(gtk.HBox): '''this class represents the ui widget for one filetransfer''' def __init__(self, controller, transferBar, transfer): gtk.HBox.__init__(self) self.controller = controller self.transferBar = transferBar self.transfer = transfer self.eventBox = gtk.EventBox() self.progress = gtk.ProgressBar() self.progress.set_ellipsize(pango.ELLIPSIZE_END) self.eventBox.add(self.progress) self.pack_start(self.eventBox, False, False) self.buttons = [] self.show_all() self.tooltip = FileTransferTooltip(self.eventBox, self.transfer) self.eventBox.connect('event', self.onProgressBarEvent) self.updateProgress() self.stateChanged() def onProgressBarEvent(self, widget, event): if event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 and \ self.transfer.state == self.transfer.RECEIVED: self.transfer.open() def updateProgress(self): self.progress.set_fraction(self.transfer.getFraction()) self.progress.set_text(self.transfer.getFilename()) self.tooltip.update() def stateChanged(self): state = self.transfer.state # remove existing buttons for button in self.buttons: self.remove(button) self.buttons = [] if state == self.transfer.WAITING: button = gtk.Button(None, None) button.set_image(self.getButtonImage(gtk.STOCK_APPLY)) button.connect('clicked', self.onAcceptClicked) self.buttons.append(button) if state in (self.transfer.RECEIVED, self.transfer.FAILED): button = gtk.Button(None, None) button.set_image(self.getButtonImage(gtk.STOCK_CLOSE)) button.connect('clicked', self.onCloseClicked) self.buttons.append(button) if state == self.transfer.WAITING or state == self.transfer.TRANSFERING: cancelButton = gtk.Button(None, None) cancelButton.connect('clicked', self.onCancelClicked) cancelButton.set_image(self.getButtonImage(gtk.STOCK_CANCEL)) self.buttons.append(cancelButton) for button in self.buttons: self.pack_start(button, False, False) self.show_all() self.updateProgress() def getButtonImage(self, stockImage): img = gtk.Image() img.set_from_stock(stockImage, gtk.ICON_SIZE_MENU) return img def onCancelClicked(self, widget): self.transfer.cancel() def onAcceptClicked(self, widget): self.transfer.accept() def onCloseClicked(self, widget): self.transfer.remove() self.transferBar.remove(self) DELAY = 500 class FileTransferTooltip(gtk.Window): '''Class that implements the filetransfer tooltip''' def __init__(self, parentWidget, transfer): gtk.Window.__init__(self, gtk.WINDOW_POPUP) self.transfer = transfer self.set_name('gtk-tooltips') self.set_position(gtk.WIN_POS_MOUSE) self.set_resizable(False) self.set_border_width(4) self.set_app_paintable(True) self.image = gtk.Image() self.details = gtk.Label() self.details.set_alignment(0, 0) self.table = gtk.Table(3, 2, False) self.table.set_col_spacings(5) self.addLabel(_('Status:'), 0, 1, 0, 1) self.addLabel(_('Average speed:'), 0, 1, 1, 2) self.addLabel(_('Time elapsed:'), 0, 1, 2, 3) self.status = gtk.Label() self.speed = gtk.Label() self.elapsed = gtk.Label() self.addLabel('', 1, 2, 0, 1, self.status) self.addLabel('', 1, 2, 1, 2, self.speed) self.addLabel('', 1, 2, 2, 3, self.elapsed) vbox = gtk.VBox(False, 5) vbox.pack_start(self.details, False, False) vbox.pack_start(self.table, False, False) hbox = gtk.HBox(False, 5) hbox.pack_start(self.image, False, False) hbox.pack_start(vbox, True, True) self.add(hbox) self.connect('delete-event', self.reset) self.connect('expose-event', self.on_expose_event) parentWidget.connect('enter-notify-event', self.on_motion) parentWidget.connect('leave-notify-event', self.on_leave) self.tag = None self.path_array = None def addLabel(self, s, left, right, top, bottom, label = None): if label == None: label = gtk.Label(s) label.set_alignment(0, 0) self.table.attach(label, left, right, top, bottom) def hideTooltip(self): '''Hides the tooltip and removes any ongoing timeout for the tooltip to be shown''' self.hide() self.reset() def reset(self): if self.tag and not self.tag == -1: gobject.source_remove(self.tag) self.tag = None def on_motion(self, view, event): eventCoords = (event.x_root, event.y_root, int(event.y)) self.tag = gobject.timeout_add(DELAY, self.show_tooltip, \ view, eventCoords) def show_tooltip(self, view, origCoords): pixbuf = self.transfer.getPreviewImage() #amsn sends a big. black preview? :S if pixbuf != None and pixbuf.get_height() <= 96 and pixbuf.get_width() <= 96: self.image.set_from_pixbuf(pixbuf) # set the location of the tooltip x, y = self.computePosition(origCoords, view.window) self.move(x, y) self.update() self.show_all() return False def update(self): self.details.set_markup('' + self.transfer.getFilename() + '') bps = self.transfer.getAverageSpeed() seconds = self.transfer.getElapsedTime() received, total = self.transfer.getBytes() percentage = int(self.transfer.getFraction() * 100) self.status.set_text('%d%% (%d/%d KB)' % (percentage, int(received) / 1024, int(total) / 1024)) self.elapsed.set_text('%.2d:%.2d' % (int(seconds / 60), int(seconds % 60))) self.speed.set_text('%.2f KiB/s' % (float(bps) / 1024.0)) def on_leave(self, view, event): self.hide() self.reset() # display a border around the tooltip def on_expose_event(self, tooltip_window, event): width, height = tooltip_window.get_size() tooltip_window.style.paint_flat_box(tooltip_window.window, \ gtk.STATE_NORMAL, gtk.SHADOW_OUT, \ None, tooltip_window, 'tooltip', \ 0, 0, width, height) def computePosition(self, origCoords, viewWindow): x_root, y_root, origY = origCoords currentY = viewWindow.get_pointer()[1] width, height = self.get_size() s_width, s_height = gtk.gdk.screen_width(), gtk.gdk.screen_height() x = int(x_root) - width/2 if currentY >= origY: y = int(y_root) + 24 else: y = int(y_root) + 6 # check if over the screen if x + width > s_width: x = s_width - width elif x < 0: x = 0 if y + height > s_height: y = y - height - 24 elif y < 0: y = 0 return (x, y)