def __init__(self): w=gtk.Window() print (sys.getdefaultencoding()) msg1="""<span><p>This is a <a href="http://eq2.us">TEST</a>.</p>Enjoy.</span> """ print type(msg1) htmlview = HtmlTextView() def url_cb(view, url, type_): print ("url-clicked", url, type_) webbrowser.open(url) htmlview.connect("url-clicked", url_cb) w.connect("delete-event",self.window_close) w.set_property('width-request',300) w.set_property('height-request',200) htmlview.display_html(msg1) w.add(htmlview) w.show_all()
def __init__(self): w = gtk.Window() print(sys.getdefaultencoding()) msg1 = """<span><p>This is a <a href="http://eq2.us">TEST</a>.</p>Enjoy.</span> """ print type(msg1) htmlview = HtmlTextView() def url_cb(view, url, type_): print("url-clicked", url, type_) webbrowser.open(url) htmlview.connect("url-clicked", url_cb) w.connect("delete-event", self.window_close) w.set_property('width-request', 300) w.set_property('height-request', 200) htmlview.display_html(msg1) w.add(htmlview) w.show_all()
class crowbar: """Class for viewing and posting status updates. """ def get_username(self): """Get the user login name""" cachedir = self.get_cachedir() names = os.listdir(cachedir) names = [a[:-5] for a in names if a[-5:]=='.auth'] self.myname = dialog.select('Twitter username?','Login:'******'t42Eq9aoZWuOCQEvhrTe9A' self.consumer_secret = 'xVrGA1o0njIwtTtuw0n4GAhmfvPpndMcnl6fabbDPI' me = self.get_username() # load cached authorization file cachedir = self.get_cachedir() cachefile = cachedir+me+'.auth' if os.path.exists(cachefile): with open(cachefile) as f: data=f.read() access_token = twitter.simplejson.loads(data) return access_token request_token_url = 'https://api.twitter.com/oauth/request_token' access_token_url = 'https://api.twitter.com/oauth/access_token' authorize_url = 'https://api.twitter.com/oauth/authorize' consumer = oauth.Consumer(self.consumer_key, self.consumer_secret) client = oauth.Client(consumer) # Step 1: Get a request token. This is a temporary token that is used for # having the user authorize an access token and to sign the request to obtain # said access token. resp, content = client.request(request_token_url, "GET") if resp['status'] != '200': raise Exception("Invalid response %s." % resp['status']) request_token = dict(urlparse.parse_qsl(content)) # Step 2: Redirect to the provider. Since this is a CLI script we do not url="%s?oauth_token=%s" % (authorize_url, request_token['oauth_token']) webbrowser.open(url) # After the user has granted access to you, the consumer, the provider will # redirect you to whatever URL you have told them to redirect to. You can # usually define this in the oauth_callback argument as well. oauth_verifier = dialog.getText('What is the pin?',gtk.MESSAGE_QUESTION,'pin:', 'Fill out the form in the web page that pops up.\n' 'You will be given a PIN number.\n' 'Come back and enter that number here.').strip() # Step 3: Once the consumer has redirected the user back to the oauth_callback # URL you can request the access token the user has approved. You use the # request token to sign this request. After this is done you throw away the # request token and use the access token returned. You should store this # access token somewhere safe, like a database, for future use. token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret']) token.set_verifier(oauth_verifier) client = oauth.Client(consumer, token) resp, content = client.request(access_token_url, "POST") access_token = dict(urlparse.parse_qsl(content)) data=twitter.simplejson.dumps(access_token) if '<error' not in data: with open(cachefile,'w') as f: f.write(data) return access_token def update_followers_count(self): """Updates the count of followers""" self.me = api.GetUser(screen_name=self.myname) fo=str(self.me.followers_count) fr=str(self.me.friends_count) self.window2.set_title(fr+" friends "+fo+" followers") def get_cachedir(self): """Returns the application's Cache dir:""" cachedir = os.path.expanduser('~/.crowbar_cache/') if not os.path.exists(cachedir): os.mkdir(cachedir) return cachedir def getshouts(self,s): """get names of people mentioned in s[].text""" names = [] for a in s: try: atext = re.match("[^@]*(.*)",a.text).groups()[0] for b in atext.split("@")[1:]: al = re.match("(\w+)",b).groups()[0].lower() if al not in names: names.append(al) except: pass return names def get_screen_names(self,s): """get s[].user.screen_name from search results""" names = [] for a in s: al = a.user.screen_name.lower() if al not in names: names.append(al) return names def subem(self,sub): """sub everybody in sub""" global friends for a in sub: al = a.lower() if al not in friends: try: api.CreateFriendship(al) friends.append(al) except: pass def gtk_main_quit(self,widget,data = None): gtk.main_quit() return False # return False destroys window. def delete_event(self,widget,data = None): widget.hide() # hide the window return True # return True does not destroy window. def link_clicked(self,view,url,type_): """follow a url or reply link""" url = url.replace('%26','&') url = url.replace('&','&') print("url-clicked %s" % url) if 'http://' not in url: if '@' in url: self.entry1.set_text(url) elif '#' == url[0]: self.comboboxentry1.child.set_text(url) self.search_clicked(None,self.comboboxentry1.child.get_text) elif '☆' in url: api.CreateFavorite(api.GetStatus(int(str(url).replace('☆','')))) elif '☞' in url: api.PostRetweet(int(str(url).replace('☞',''))) elif '☠' in url: api.DestroyDirectMessage(int(str(url).replace('☠',''))) elif '✗' in url: api.DestroyStatus(int(str(url).replace('✗',''))) else: webbrowser.open(url) def post_update(self,widget,data = None): if data(): api.PostUpdate(data()) self.entry1.set_text('') return True def image_from_url(self,url,fname): """return a gtk.Image from url""" #~ cache img to speed up loading cachedir = self.get_cachedir() if url: ext = '.'+url[url.rindex('.')+1:].lower() if ext not in ['.jpg','.png']: ext = '.jpg' fname = str(fname)+ext cachefile = cachedir+fname if os.path.exists(cachefile): #~ cache hit pb = gtk.gdk.pixbuf_new_from_file(cachefile) pbs = pb.scale_simple(48,48,0) return gtk.image_new_from_pixbuf(pbs) fp = urllib.urlopen(url) pbl = gtk.gdk.PixbufLoader() data = fp.read() #~ read url into pixbuf if pbl.write(data): pb = pbl.get_pixbuf() try: pbs = pb.scale_simple(48,48,0) except: try: fp.close() pbl.close() except: pass print('could not scale image for %s '% fname) return gtk.image_new_from_file('blank.jpg') #~ pbs = pb #~ create image from pixbuf image = gtk.Image() image.set_from_pixbuf(pbs) #~ save cached copy if ext != '.png': pb.save(cachefile,"jpeg") else: pb.save(cachefile,"png") try: pbl.close() except: print('%s truncated or incomplete.' % url) else: #~ todo: make this a broken image print('broken image for %s'%fname) image = gtk.image_new_from_file('blank.jpg') fp.close() else: print('no url for image%'%fname) image = gtk.image_new_from_file('blank.jpg') return image def escape(self,s): s = s.replace('&','&') s = s.replace('<','<') s = s.replace('>','>') #~ s = s.replace('"','"') return s def unescape(self,s): p = htmllib.HTMLParser(None) p.save_bgn() p.feed(str(s)) s = p.save_end() #~ s = s.replace('<','<') #~ s = s.replace('>','>') s = s.replace(' rel="nofollow"','') #~ s = s.replace('"e;','"') s = s.replace('&','%26') s = s.replace('[1]','') return s def process_urls(self,s): s = self.escape(s) s = re.sub("(http://[^ ]+)", lambda m: "<a href='%s'>%s</a>" % (m.group(1).replace('&','&'), m.group(1).replace('&','&')),s) s = re.sub("(@)(\w+)", lambda m: "%s<a href='%s'>%s</a>" % (m.group(1),'http://twitter.com/'+m.group(2),m.group(2)),s) s = re.sub("(#)(\w+)", lambda m: "%s<a href='%s'>%s</a>" % (m.group(1),'#'+m.group(2),m.group(2)),s) return s def html_view(self,s,dm=False): """show search results and timelines in htmlview""" # clear, add viewport & scrolling table if self.sw1.get_child() is self.html: self.sw1.remove(self.html) self.sw1.set_property("hscrollbar-policy",gtk.POLICY_NEVER) self.viewport1 = gtk.Viewport() self.sw1.add(self.viewport1) else: self.viewport1.remove(self.table1) self.table1 = gtk.Table(23,2,False) self.table1.set_row_spacings(2) self.table1.set_col_spacings(2) self.viewport1.add(self.table1) self.pic = [] self.msg = [] t = [''] rows = 0 #display each user pic and instant message for x in s: if not dm: user = x.GetUser() else: user = api.GetUser(x.sender_screen_name) img = user.GetProfileImageUrl() usn = str(user.GetScreenName()) t[0] = x reply = usn shouts = self.getshouts(t) star = '☆' if not dm: if x.favorited: star = '★' if shouts: reply+=' @'+string.join(self.getshouts(t),' @') self.pic.append(self.image_from_url(img,usn)) text = self.process_urls(str(x.text)) #~ (re)construct html message self.msg.append(HtmlTextView()) self.msg[rows].connect("url-clicked",self.link_clicked) h = '<span>' if not dm: h+='<a title="Favorite" href="'+star+str(x.id)+'">'+star +'</a>' h+='<span style="font-weight: bold">'+'<a href="http://twitter.com/'+usn+'">' +usn+'</a></span>: '+ text + '<br /><span style="font-size:small">' if not dm: h+=x.relative_created_at+' via '+self.unescape(x.source)+' | ' if dm: h+='<a href="@'+reply+'">reply</a> | <a href="☠'+str(x.id)+'" title="Delete this.">Delete</a>' elif (usn==self.me.screen_name): h+='<a href="✗'+str(x.id)+'" title="Delete this tweet.">Delete</a>' else: h+='<a href="@'+reply+'">reply</a> | <a href="☞' +str(x.id)+'">retweet</a>' h+='</span></span>' try: self.msg[rows].display_html(str(h)) except: print('Error displaying:') print(h+'\n') self.table1.attach(self.pic[rows],0,1,rows,rows+1) self.table1.attach(self.msg[rows],1,2,rows,rows+1) rows+=1 #~ self.table1.attach(self.html,0,2,rows,rows+2) self.blank = gtk.Label() self.blank.set_property('height-request',100) self.table1.attach(self.blank,0,2,rows,rows+2) #~ self.table1.set_property("border-width", 5) self.sw1.show_all() def initialize_window(self): """Show the intro page""" self.html = HtmlTextView() self.html.connect("url-clicked",self.link_clicked) self.html.display_html('<div><span style="color: red; text-decoration:underline">Welcome to</span><br/>\n' ' <img src="http://comptune.com/images/penguin5.gif" alt="penguin" height="48" width="48" /><br/>\n' ' <span style="font-size: 500%; font-family: serif">crowbar</span>\n' '</div>\n') self.sw1.add(self.html) #~ add a search comboboxentry1 and 3 buttons self.liststore1 = gtk.ListStore(str) self.comboboxentry1 = gtk.ComboBoxEntry(self.liststore1, 0) popular = ['#teamfollowback','#tfb','#f4f'] trending=api.GetTrendsCurrent() trends = [x.name for x in trending] for row in popular: self.liststore1.append([row]) for row in trends: self.liststore1.append([row]) # insert a horizontal box with combo and buttons self.hbox2 = gtk.HBox(homogeneous = False) self.hbox2.pack_start(self.comboboxentry1) button = gtk.Button() button.set_label('search') button.set_property('width-request',55) button.connect('clicked',self.search_clicked,self.comboboxentry1.child.get_text) self.hbox2.pack_start(button,expand = False,fill = True) button = gtk.Button() button.set_label('friends') button.set_property('width-request',55) button.connect('clicked',self.timeline_clicked,self.comboboxentry1.child.get_text) self.hbox2.pack_start(button,expand = False,fill = True) button = gtk.Button() button.set_label('user') button.set_property('width-request',40) button.connect('clicked',self.user_timeline_clicked,self.comboboxentry1.child.get_text) self.hbox2.pack_start(button,expand = False,fill = True) button = gtk.Button() button.set_label('dm') button.set_property('width-request',35) button.connect('clicked',self.dm_clicked,self.comboboxentry1.child.get_text) self.hbox2.pack_start(button,expand = False,fill = True) button = gtk.Button() button.set_label('mention') button.set_property('width-request',70) button.connect('clicked',self.mentions_clicked,self.comboboxentry1.child.get_text) self.hbox2.pack_start(button,expand = False,fill = True) #~ testify.test(self) self.vbox1.pack_start(self.hbox2,expand = False) self.html.show_all() self.hbox2.show_all() def display_results(self,s,getnames,dm=False): """display results of searches and timelines""" lnames = '' if getnames: snames = self.get_screen_names(s) lnames = "@"+string.join(snames," @") shouts = self.getshouts(s) self.textview1.set_wrap_mode(gtk.WRAP_WORD) buf = self.textview1.get_buffer() buf.set_text(lnames+"@"+string.join(shouts," @")) self.html_view(s,dm) self.window2.show_all() def get_list(self,widget): it = widget.get_iter('0') data = [] while 1: try: data.append(widget.get_value(it,0)) it = widget.iter_next(it) except: return data def update_search(self,text): l = self.get_list(self.liststore1) if text and (text not in l): self.liststore1.append([text]) def search_clicked(self,widget,method = None): """get names and shouts from api search and display them""" text = method() s = api.GetSearch(text) self.display_results(s,True) self.last_action = self.search_clicked self.last_method = method self.update_search(text) self.update_followers_count() def timeline_clicked(self,widget,method = None): """get friends' timeline""" text = method() getnames = False if text: getnames = True if '#' in text: getmames = False text = None s = api.GetUserTimeline(text) else: s = api.GetHomeTimeline() self.display_results(s,getnames) self.last_action = self.timeline_clicked self.last_method = method self.update_search(text) self.update_followers_count() def user_timeline_clicked(self,widget,method = None): """get user timeline""" text = method() getnames = False s = api.GetUserTimeline(text) self.display_results(s,getnames) self.last_action = self.user_timeline_clicked self.last_method = method self.update_search(text) self.update_followers_count() def mentions_clicked(self,widget,method = None): """get mentions""" s = api.GetMentions() self.display_results(s,True) self.last_action = self.mentions_clicked self.last_method = method self.update_followers_count() def dm_clicked(self,widget,method = None): """get mentions""" s = api.GetDirectMessages() self.display_results(s,False,True) self.last_action = self.mentions_clicked self.last_method = method self.update_followers_count() def refresh_clicked(self,widget,method = None): self.liststore1.clear() popular = ['#teamfollowback','#tfb','#f4f'] trending=api.GetTrendsCurrent() trends = [x.name for x in trending] for row in popular: self.liststore1.append([row]) for row in trends: self.liststore1.append([row]) self.last_action(None,self.last_method) def get_friendIDs(self): """Get friend and follower IDs""" global friendIDs,followerIDs if not globals().has_key('friendIDs'): print('getting friends list...') try: friendIDs=api.GetFriendIDs()['ids'] except: print("User not properly authenticated. Will not be able to post updates.") return False if not globals().has_key('followerIDs'): print('getting list of followers...') followerIDs=api.GetFollowerIDs()['ids'] return True def follow_clicked(self,widget,data = None): """follow all poeple in textview1 by name""" #~ iterate through the text buffer buf = self.textview1.get_buffer() (iter_first, iter_last) = buf.get_bounds() text = buf.get_text(iter_first, iter_last) #~ remove spaces and build a list to follow text = text.replace(' ','') fol = [] #~ create fol list for x in text.split('@')[1:]: if x not in fol: fol.append(x) #~ ask if self.warn and (not dialog.ok("Follow %s people.\n" "Are you sure?" % len(fol))): return else: self.warn=False # load nofollow list if it exists me = self.me.screen_name nofol = [] cachedir = self.get_cachedir() nofollow = cachedir+me+'.nofollow' if os.path.exists(nofollow): with open(nofollow) as f: nofol = [x.strip() for x in f.readlines()] nofollen=len(nofol) #~ friend everybody unless nofol, add to nofol for a in fol: if a not in nofol: try: api.CreateFriendship(a) print('followed %s' % a) except TwitterError as err: print(err) if 'follow-limits' in str(err): dialog.alert("You have reached Twitter's follow limit!") break nofol.append(a) #~ Write nofol to disk. These could be invalid names, #~ existing friends, or blocked accounts. #~ We don't care. We just don't follow them. if nofollen < len(nofol): with open(nofollow,'a') as f: f.writelines([x+'\n' for x in nofol[nofollen:]]) def follow_followers(self,widget,data = None): """follow followers by id not by name""" global friendIDs,followerIDs if not self.get_friendIDs(): return #~ safety feature in case twitter f's up if len(friendIDs) < 100: print('not enough friends') return #~ load protected and pending list me = self.me.screen_name cachedir = self.get_cachedir() protfile = cachedir+me+'.prot' newprot = []; prot=[] if os.path.exists(protfile): with open(protfile) as f: prot = [int(x.strip()) for x in f.readlines()] #~ follow everyone in followerIDs for a in followerIDs: if (a not in friendIDs) and (a not in prot): try: ret = api.CreateFriendship(a) friendIDs.append(a) print('followed %s' % ret.screen_name) except TwitterError as err: print(err) if not 'follow-limits' in str(err): newprot.append(a) else: dialog.alert("You have reached Twitter's follow limit!") break #~ those we can not follow are probably protected with open(protfile,'a') as f: f.writelines([str(x)+'\n' for x in newprot]) def unfollow_non_followers(self,widget,data = None): """unfollow everybody who is not following me todo: add safelist""" if not self.get_friendIDs(): return me = self.me.screen_name # load safelist cachedir = self.get_cachedir() safefile = cachedir+me+'.safe' safe = []; unsub = [] if os.path.exists(safefile): with open(safefile) as f: safe = [x.strip() for x in f.readlines()] #~ safety feature in case twitter f*s up if len(followerIDs) < 100: print('Need at least 100 followers to use this feature.') return for a in friendIDs: if (a not in followerIDs) and (a not in safe): unsub.append(a) #~ unsub.remove(self.me.id) #~ save nofollow list so we do not re-follow #~ (unless they follow us, of course) unsub_names=[] if dialog.ok('Unfriend all '+str(len(unsub))+' non-followers?'): #UNSUB EVERYBODY IN unsub for a in unsub: try: u=api.DestroyFriendship(a) print('unfriended %s' % u.screen_name) unsub_names.append(u.screen_name) except TwitterError as err: print(err) nofollow = cachedir+me+'.nofollow' with open(nofollow,'a') as f: f.writelines([x+'\n' for x in unsub_names]) def __init__(self): """main initialization routine""" global api,friendIDs,friends friends=[] tok=self.authorize() self.warn=True api = twitter.Api(self.consumer_key,self.consumer_secret, tok['oauth_token'],tok['oauth_token_secret']) builder = gtk.Builder() builder.add_from_file("crowbar.glade") self.window1 = builder.get_object("window1") self.vbox1 = builder.get_object("vbox1") self.sw1 = builder.get_object("sw1") self.window2 = builder.get_object("window2") self.textview1 = builder.get_object("textview1") self.entry1 = builder.get_object("entry1") self.button1 = builder.get_object("button1") builder.connect_signals(self) self.window2.connect("delete-event",self.delete_event) self.button1.connect("clicked",self.post_update,self.entry1.get_text) self.update_followers_count() self.window1.set_title("Crowbar @"+self.me.screen_name) self.initialize_window() #~ some variables to remember what we did last self.last_action = None self.last_method = None
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, spacing=3) self.set_border_width(0) self.parentConversation = parentConversation self.controller = controller self.config = self.controller.config self.parser = controller.unifiedParser self.header = Header(self, controller) self.tabWidget = TabWidget(self, controller) self.tabWidget.show() self.input = InputWidget(self, controller, \ parentConversation.isCurrent) self.input.show() self.status = gtk.Statusbar() self.toolbarinput = gtk.HBox() self.toolbarinput.show() self.listOfUsers = UserList.UserList(self.controller, \ self.controller.theme, self.config, False) self.scrollList = gtk.ScrolledWindow() self.scrollList.set_shadow_type(gtk.SHADOW_IN) self.scrollList.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.scrollList.set_size_request(111, 0) self.scrollList.add(self.listOfUsers) self.scroll = gtk.ScrolledWindow() self.scroll.set_shadow_type(gtk.SHADOW_IN) self.scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.textview = HtmlTextView(controller, \ self.parentConversation.textBuffer, self.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) self.textview.connect('key-press-event', self.onTextviewKeyPress) self.scroll.add(self.textview) self.scroll.show_all() self.remoteAvatar = AvatarHBox(self, controller, self.parentConversation.switchboard.firstUser) self.controller.msn.connect('user-attr-changed', self.onUserAttrChanged) self.vboxside = gtk.VBox() self.vboxside.pack_start(self.remoteAvatar, False, False) self.vboxside.pack_start(self.scrollList, True, True) self.vboxside.show() self.hbox = gtk.HBox(spacing=2) self.hbox.set_border_width(2) self.hbox.pack_start(self.scroll, True, True) self.hbox.pack_start(self.vboxside, False, False) self.hbox.show() self.toolbarinput.pack_start(self.input, True, True) self.toolbarinput.connect('size-allocate', self.onToolbarinputResize) vpaned = gtk.VPaned() vpaned.pack1(self.hbox, True, True) vpaned.pack2(self.toolbarinput, False) vpaned.show() self.transfers = FileTransferUI.FtBarWidget(self.controller, self, parentConversation) self.transfers.set_no_show_all(True) self.pack_start(self.header, False, False) self.pack_start(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.show() self.update() def onToolbarinputResize(self, *args): alloc = self.toolbarinput.get_allocation() self.config.user['convInputHeight'] = alloc.height self.scrollToBottom() def onTextviewKeyPress(self, widget, event): 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()] 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 isGroupChat: self.showUserList() self.remoteAvatar.hide() elif self.config.user['showAvatars']: self.hideUserList() self.remoteAvatar.show_all() self.remoteAvatar.update() else: self.hideUserList() self.remoteAvatar.hide() if not self.config.user['showHeader']: self.header.hide() else: self.header.show() if not self.config.user['showTabCloseButton']: self.tabWidget.closeButton.hide() else: self.tabWidget.closeButton.show() if not self.config.user['showStatusBar']: self.status.hide() else: self.status.show() inputHeight = self.config.user['convInputHeight'] self.toolbarinput.set_size_request(0, inputHeight) def scrollToBottom(self): '''scroll to the end of the conversation''' self.textview.scrollToBottom() def showUserList(self): self.scrollList.show_all() self.listOfUsers.show() def hideUserList(self): self.scrollList.hide() self.listOfUsers.hide() 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: if self.config.user['showMailTyping']: string += str(mail) else: contact = self.controller.getContact(mail) if contact: parts = self.parser.getParser(contact.nick).get() for part in parts: string += str(part) else: string += str(mail) string += comma string = str(unicode(string)[:-len(comma)]) if len(mails) == 1: string += ' ' + _('is typing a message...') else: string += ' ' + _('are typing a message...') self.status.get_children()[0].get_children()[0].set_text(string) def setMessageWaiting(self, mail): if self.parentConversation.isCurrent: 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 mail in self.messageWaiting and self.messageWaiting[mail]: self.setMessageWaiting(mail) else: self.setDefault(mail) self.contactTyping[mail] = False return False def setDefault(self, mail): self.tabWidget.setDefault() self.header.setDefault() if mail: self.contactTyping[mail] = False self.messageWaiting[mail] = False self.rebuildStatusText()
class crowbar: """Class for viewing and posting status updates. """ def get_username(self): """Get the user login name""" cachedir = self.get_cachedir() names = os.listdir(cachedir) names = [a[:-5] for a in names if a[-5:] == '.auth'] self.myname = dialog.select('Twitter username?', 'Login:'******'t42Eq9aoZWuOCQEvhrTe9A' self.consumer_secret = 'xVrGA1o0njIwtTtuw0n4GAhmfvPpndMcnl6fabbDPI' me = self.get_username() # load cached authorization file cachedir = self.get_cachedir() cachefile = cachedir + me + '.auth' if os.path.exists(cachefile): with open(cachefile) as f: data = f.read() access_token = twitter.simplejson.loads(data) return access_token request_token_url = 'https://api.twitter.com/oauth/request_token' access_token_url = 'https://api.twitter.com/oauth/access_token' authorize_url = 'https://api.twitter.com/oauth/authorize' consumer = oauth.Consumer(self.consumer_key, self.consumer_secret) client = oauth.Client(consumer) # Step 1: Get a request token. This is a temporary token that is used for # having the user authorize an access token and to sign the request to obtain # said access token. resp, content = client.request(request_token_url, "GET") if resp['status'] != '200': raise Exception("Invalid response %s." % resp['status']) request_token = dict(urlparse.parse_qsl(content)) # Step 2: Redirect to the provider. Since this is a CLI script we do not url = "%s?oauth_token=%s" % (authorize_url, request_token['oauth_token']) webbrowser.open(url) # After the user has granted access to you, the consumer, the provider will # redirect you to whatever URL you have told them to redirect to. You can # usually define this in the oauth_callback argument as well. oauth_verifier = dialog.getText( 'What is the pin?', gtk.MESSAGE_QUESTION, 'pin:', 'Fill out the form in the web page that pops up.\n' 'You will be given a PIN number.\n' 'Come back and enter that number here.').strip() # Step 3: Once the consumer has redirected the user back to the oauth_callback # URL you can request the access token the user has approved. You use the # request token to sign this request. After this is done you throw away the # request token and use the access token returned. You should store this # access token somewhere safe, like a database, for future use. token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret']) token.set_verifier(oauth_verifier) client = oauth.Client(consumer, token) resp, content = client.request(access_token_url, "POST") access_token = dict(urlparse.parse_qsl(content)) data = twitter.simplejson.dumps(access_token) if '<error' not in data: with open(cachefile, 'w') as f: f.write(data) return access_token def update_followers_count(self): """Updates the count of followers""" self.me = api.GetUser(screen_name=self.myname) fo = str(self.me.followers_count) fr = str(self.me.friends_count) self.window2.set_title(fr + " friends " + fo + " followers") def get_cachedir(self): """Returns the application's Cache dir:""" cachedir = os.path.expanduser('~/.crowbar_cache/') if not os.path.exists(cachedir): os.mkdir(cachedir) return cachedir def getshouts(self, s): """get names of people mentioned in s[].text""" names = [] for a in s: try: atext = re.match("[^@]*(.*)", a.text).groups()[0] for b in atext.split("@")[1:]: al = re.match("(\w+)", b).groups()[0].lower() if al not in names: names.append(al) except: pass return names def get_screen_names(self, s): """get s[].user.screen_name from search results""" names = [] for a in s: al = a.user.screen_name.lower() if al not in names: names.append(al) return names def subem(self, sub): """sub everybody in sub""" global friends for a in sub: al = a.lower() if al not in friends: try: api.CreateFriendship(al) friends.append(al) except: pass def gtk_main_quit(self, widget, data=None): gtk.main_quit() return False # return False destroys window. def delete_event(self, widget, data=None): widget.hide() # hide the window return True # return True does not destroy window. def link_clicked(self, view, url, type_): """follow a url or reply link""" url = url.replace('%26', '&') url = url.replace('&', '&') print("url-clicked %s" % url) if 'http://' not in url: if '@' in url: self.entry1.set_text(url) elif '#' == url[0]: self.comboboxentry1.child.set_text(url) self.search_clicked(None, self.comboboxentry1.child.get_text) elif '☆' in url: api.CreateFavorite( api.GetStatus(int(str(url).replace('☆', '')))) elif '☞' in url: api.PostRetweet(int(str(url).replace('☞', ''))) elif '☠' in url: api.DestroyDirectMessage(int(str(url).replace('☠', ''))) elif '✗' in url: api.DestroyStatus(int(str(url).replace('✗', ''))) else: webbrowser.open(url) def post_update(self, widget, data=None): if data(): api.PostUpdate(data()) self.entry1.set_text('') return True def image_from_url(self, url, fname): """return a gtk.Image from url""" #~ cache img to speed up loading cachedir = self.get_cachedir() if url: ext = '.' + url[url.rindex('.') + 1:].lower() if ext not in ['.jpg', '.png']: ext = '.jpg' fname = str(fname) + ext cachefile = cachedir + fname if os.path.exists(cachefile): #~ cache hit pb = gtk.gdk.pixbuf_new_from_file(cachefile) pbs = pb.scale_simple(48, 48, 0) return gtk.image_new_from_pixbuf(pbs) fp = urllib.urlopen(url) pbl = gtk.gdk.PixbufLoader() data = fp.read() #~ read url into pixbuf if pbl.write(data): pb = pbl.get_pixbuf() try: pbs = pb.scale_simple(48, 48, 0) except: try: fp.close() pbl.close() except: pass print('could not scale image for %s ' % fname) return gtk.image_new_from_file('blank.jpg') #~ pbs = pb #~ create image from pixbuf image = gtk.Image() image.set_from_pixbuf(pbs) #~ save cached copy if ext != '.png': pb.save(cachefile, "jpeg") else: pb.save(cachefile, "png") try: pbl.close() except: print('%s truncated or incomplete.' % url) else: #~ todo: make this a broken image print('broken image for %s' % fname) image = gtk.image_new_from_file('blank.jpg') fp.close() else: print('no url for image%' % fname) image = gtk.image_new_from_file('blank.jpg') return image def escape(self, s): s = s.replace('&', '&') s = s.replace('<', '<') s = s.replace('>', '>') #~ s = s.replace('"','"') return s def unescape(self, s): p = htmllib.HTMLParser(None) p.save_bgn() p.feed(str(s)) s = p.save_end() #~ s = s.replace('<','<') #~ s = s.replace('>','>') s = s.replace(' rel="nofollow"', '') #~ s = s.replace('"e;','"') s = s.replace('&', '%26') s = s.replace('[1]', '') return s def process_urls(self, s): s = self.escape(s) s = re.sub( "(http://[^ ]+)", lambda m: "<a href='%s'>%s</a>" % (m.group(1).replace('&', '&'), m.group(1).replace( '&', '&')), s) s = re.sub( "(@)(\w+)", lambda m: "%s<a href='%s'>%s</a>" % (m.group(1), 'http://twitter.com/' + m.group(2), m.group(2)), s) s = re.sub( "(#)(\w+)", lambda m: "%s<a href='%s'>%s</a>" % (m.group(1), '#' + m.group(2), m.group(2)), s) return s def html_view(self, s, dm=False): """show search results and timelines in htmlview""" # clear, add viewport & scrolling table if self.sw1.get_child() is self.html: self.sw1.remove(self.html) self.sw1.set_property("hscrollbar-policy", gtk.POLICY_NEVER) self.viewport1 = gtk.Viewport() self.sw1.add(self.viewport1) else: self.viewport1.remove(self.table1) self.table1 = gtk.Table(23, 2, False) self.table1.set_row_spacings(2) self.table1.set_col_spacings(2) self.viewport1.add(self.table1) self.pic = [] self.msg = [] t = [''] rows = 0 #display each user pic and instant message for x in s: if not dm: user = x.GetUser() else: user = api.GetUser(x.sender_screen_name) img = user.GetProfileImageUrl() usn = str(user.GetScreenName()) t[0] = x reply = usn shouts = self.getshouts(t) star = '☆' if not dm: if x.favorited: star = '★' if shouts: reply += ' @' + string.join(self.getshouts(t), ' @') self.pic.append(self.image_from_url(img, usn)) text = self.process_urls(str(x.text)) #~ (re)construct html message self.msg.append(HtmlTextView()) self.msg[rows].connect("url-clicked", self.link_clicked) h = '<span>' if not dm: h += '<a title="Favorite" href="' + star + str( x.id) + '">' + star + '</a>' h += '<span style="font-weight: bold">' + '<a href="http://twitter.com/' + usn + '">' + usn + '</a></span>: ' + text + '<br /><span style="font-size:small">' if not dm: h += x.relative_created_at + ' via ' + self.unescape( x.source) + ' | ' if dm: h += '<a href="@' + reply + '">reply</a> | <a href="☠' + str( x.id) + '" title="Delete this.">Delete</a>' elif (usn == self.me.screen_name): h += '<a href="✗' + str( x.id) + '" title="Delete this tweet.">Delete</a>' else: h += '<a href="@' + reply + '">reply</a> | <a href="☞' + str( x.id) + '">retweet</a>' h += '</span></span>' try: self.msg[rows].display_html(str(h)) except: print('Error displaying:') print(h + '\n') self.table1.attach(self.pic[rows], 0, 1, rows, rows + 1) self.table1.attach(self.msg[rows], 1, 2, rows, rows + 1) rows += 1 #~ self.table1.attach(self.html,0,2,rows,rows+2) self.blank = gtk.Label() self.blank.set_property('height-request', 100) self.table1.attach(self.blank, 0, 2, rows, rows + 2) #~ self.table1.set_property("border-width", 5) self.sw1.show_all() def initialize_window(self): """Show the intro page""" self.html = HtmlTextView() self.html.connect("url-clicked", self.link_clicked) self.html.display_html( '<div><span style="color: red; text-decoration:underline">Welcome to</span><br/>\n' ' <img src="http://comptune.com/images/penguin5.gif" alt="penguin" height="48" width="48" /><br/>\n' ' <span style="font-size: 500%; font-family: serif">crowbar</span>\n' '</div>\n') self.sw1.add(self.html) #~ add a search comboboxentry1 and 3 buttons self.liststore1 = gtk.ListStore(str) self.comboboxentry1 = gtk.ComboBoxEntry(self.liststore1, 0) popular = ['#teamfollowback', '#tfb', '#f4f'] trending = api.GetTrendsCurrent() trends = [x.name for x in trending] for row in popular: self.liststore1.append([row]) for row in trends: self.liststore1.append([row]) # insert a horizontal box with combo and buttons self.hbox2 = gtk.HBox(homogeneous=False) self.hbox2.pack_start(self.comboboxentry1) button = gtk.Button() button.set_label('search') button.set_property('width-request', 55) button.connect('clicked', self.search_clicked, self.comboboxentry1.child.get_text) self.hbox2.pack_start(button, expand=False, fill=True) button = gtk.Button() button.set_label('friends') button.set_property('width-request', 55) button.connect('clicked', self.timeline_clicked, self.comboboxentry1.child.get_text) self.hbox2.pack_start(button, expand=False, fill=True) button = gtk.Button() button.set_label('user') button.set_property('width-request', 40) button.connect('clicked', self.user_timeline_clicked, self.comboboxentry1.child.get_text) self.hbox2.pack_start(button, expand=False, fill=True) button = gtk.Button() button.set_label('dm') button.set_property('width-request', 35) button.connect('clicked', self.dm_clicked, self.comboboxentry1.child.get_text) self.hbox2.pack_start(button, expand=False, fill=True) button = gtk.Button() button.set_label('mention') button.set_property('width-request', 70) button.connect('clicked', self.mentions_clicked, self.comboboxentry1.child.get_text) self.hbox2.pack_start(button, expand=False, fill=True) #~ testify.test(self) self.vbox1.pack_start(self.hbox2, expand=False) self.html.show_all() self.hbox2.show_all() def display_results(self, s, getnames, dm=False): """display results of searches and timelines""" lnames = '' if getnames: snames = self.get_screen_names(s) lnames = "@" + string.join(snames, " @") shouts = self.getshouts(s) self.textview1.set_wrap_mode(gtk.WRAP_WORD) buf = self.textview1.get_buffer() buf.set_text(lnames + "@" + string.join(shouts, " @")) self.html_view(s, dm) self.window2.show_all() def get_list(self, widget): it = widget.get_iter('0') data = [] while 1: try: data.append(widget.get_value(it, 0)) it = widget.iter_next(it) except: return data def update_search(self, text): l = self.get_list(self.liststore1) if text and (text not in l): self.liststore1.append([text]) def search_clicked(self, widget, method=None): """get names and shouts from api search and display them""" text = method() s = api.GetSearch(text) self.display_results(s, True) self.last_action = self.search_clicked self.last_method = method self.update_search(text) self.update_followers_count() def timeline_clicked(self, widget, method=None): """get friends' timeline""" text = method() getnames = False if text: getnames = True if '#' in text: getmames = False text = None s = api.GetUserTimeline(text) else: s = api.GetHomeTimeline() self.display_results(s, getnames) self.last_action = self.timeline_clicked self.last_method = method self.update_search(text) self.update_followers_count() def user_timeline_clicked(self, widget, method=None): """get user timeline""" text = method() getnames = False s = api.GetUserTimeline(text) self.display_results(s, getnames) self.last_action = self.user_timeline_clicked self.last_method = method self.update_search(text) self.update_followers_count() def mentions_clicked(self, widget, method=None): """get mentions""" s = api.GetMentions() self.display_results(s, True) self.last_action = self.mentions_clicked self.last_method = method self.update_followers_count() def dm_clicked(self, widget, method=None): """get mentions""" s = api.GetDirectMessages() self.display_results(s, False, True) self.last_action = self.mentions_clicked self.last_method = method self.update_followers_count() def refresh_clicked(self, widget, method=None): self.liststore1.clear() popular = ['#teamfollowback', '#tfb', '#f4f'] trending = api.GetTrendsCurrent() trends = [x.name for x in trending] for row in popular: self.liststore1.append([row]) for row in trends: self.liststore1.append([row]) self.last_action(None, self.last_method) def get_friendIDs(self): """Get friend and follower IDs""" global friendIDs, followerIDs if not globals().has_key('friendIDs'): print('getting friends list...') try: friendIDs = api.GetFriendIDs()['ids'] except: print( "User not properly authenticated. Will not be able to post updates." ) return False if not globals().has_key('followerIDs'): print('getting list of followers...') followerIDs = api.GetFollowerIDs()['ids'] return True def follow_clicked(self, widget, data=None): """follow all poeple in textview1 by name""" #~ iterate through the text buffer buf = self.textview1.get_buffer() (iter_first, iter_last) = buf.get_bounds() text = buf.get_text(iter_first, iter_last) #~ remove spaces and build a list to follow text = text.replace(' ', '') fol = [] #~ create fol list for x in text.split('@')[1:]: if x not in fol: fol.append(x) #~ ask if self.warn and (not dialog.ok("Follow %s people.\n" "Are you sure?" % len(fol))): return else: self.warn = False # load nofollow list if it exists me = self.me.screen_name nofol = [] cachedir = self.get_cachedir() nofollow = cachedir + me + '.nofollow' if os.path.exists(nofollow): with open(nofollow) as f: nofol = [x.strip() for x in f.readlines()] nofollen = len(nofol) #~ friend everybody unless nofol, add to nofol for a in fol: if a not in nofol: try: api.CreateFriendship(a) print('followed %s' % a) except TwitterError as err: print(err) if 'follow-limits' in str(err): dialog.alert( "You have reached Twitter's follow limit!") break nofol.append(a) #~ Write nofol to disk. These could be invalid names, #~ existing friends, or blocked accounts. #~ We don't care. We just don't follow them. if nofollen < len(nofol): with open(nofollow, 'a') as f: f.writelines([x + '\n' for x in nofol[nofollen:]]) def follow_followers(self, widget, data=None): """follow followers by id not by name""" global friendIDs, followerIDs if not self.get_friendIDs(): return #~ safety feature in case twitter f's up if len(friendIDs) < 100: print('not enough friends') return #~ load protected and pending list me = self.me.screen_name cachedir = self.get_cachedir() protfile = cachedir + me + '.prot' newprot = [] prot = [] if os.path.exists(protfile): with open(protfile) as f: prot = [int(x.strip()) for x in f.readlines()] #~ follow everyone in followerIDs for a in followerIDs: if (a not in friendIDs) and (a not in prot): try: ret = api.CreateFriendship(a) friendIDs.append(a) print('followed %s' % ret.screen_name) except TwitterError as err: print(err) if not 'follow-limits' in str(err): newprot.append(a) else: dialog.alert( "You have reached Twitter's follow limit!") break #~ those we can not follow are probably protected with open(protfile, 'a') as f: f.writelines([str(x) + '\n' for x in newprot]) def unfollow_non_followers(self, widget, data=None): """unfollow everybody who is not following me todo: add safelist""" if not self.get_friendIDs(): return me = self.me.screen_name # load safelist cachedir = self.get_cachedir() safefile = cachedir + me + '.safe' safe = [] unsub = [] if os.path.exists(safefile): with open(safefile) as f: safe = [x.strip() for x in f.readlines()] #~ safety feature in case twitter f*s up if len(followerIDs) < 100: print('Need at least 100 followers to use this feature.') return for a in friendIDs: if (a not in followerIDs) and (a not in safe): unsub.append(a) #~ unsub.remove(self.me.id) #~ save nofollow list so we do not re-follow #~ (unless they follow us, of course) unsub_names = [] if dialog.ok('Unfriend all ' + str(len(unsub)) + ' non-followers?'): #UNSUB EVERYBODY IN unsub for a in unsub: try: u = api.DestroyFriendship(a) print('unfriended %s' % u.screen_name) unsub_names.append(u.screen_name) except TwitterError as err: print(err) nofollow = cachedir + me + '.nofollow' with open(nofollow, 'a') as f: f.writelines([x + '\n' for x in unsub_names]) def __init__(self): """main initialization routine""" global api, friendIDs, friends friends = [] tok = self.authorize() self.warn = True api = twitter.Api(self.consumer_key, self.consumer_secret, tok['oauth_token'], tok['oauth_token_secret']) builder = gtk.Builder() builder.add_from_file("crowbar.glade") self.window1 = builder.get_object("window1") self.vbox1 = builder.get_object("vbox1") self.sw1 = builder.get_object("sw1") self.window2 = builder.get_object("window2") self.textview1 = builder.get_object("textview1") self.entry1 = builder.get_object("entry1") self.button1 = builder.get_object("button1") builder.connect_signals(self) self.window2.connect("delete-event", self.delete_event) self.button1.connect("clicked", self.post_update, self.entry1.get_text) self.update_followers_count() self.window1.set_title("Crowbar @" + self.me.screen_name) self.initialize_window() #~ some variables to remember what we did last self.last_action = None self.last_method = None
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 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)