root/branches/gajim_0.10/src/gtkgui_helpers.py

Revision 6407, 19.5 kB (checked in by dkirov, 2 years ago)

r6265, r6266, r6267, r6269, r6350, r6366

  • Property svn:eol-style set to LF
Line 
1##      gtkgui_helpers.py
2##
3## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org>
4## Copyright (C) 2004-2005 Vincent Hanquez <tab@snarc.org>
5## Copyright (C) 2005-2006 Nikos Kouremenos <nkour@jabber.org>
6## Copyright (C) 2005 Dimitur Kirov <dkirov@gmail.com>
7## Copyright (C) 2005 Travis Shirk <travis@pobox.com>
8## Copyright (C) 2005 Norman Rasmussen <norman@rasmussen.co.za>
9##
10## This program is free software; you can redistribute it and/or modify
11## it under the terms of the GNU General Public License as published
12## by the Free Software Foundation; version 2 only.
13##
14## This program is distributed in the hope that it will be useful,
15## but WITHOUT ANY WARRANTY; without even the implied warranty of
16## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17## GNU General Public License for more details.
18##
19
20import xml.sax.saxutils
21import gtk
22import gobject
23import pango
24import os
25import sys
26
27import vcard
28
29
30HAS_PYWIN32 = True
31if os.name == 'nt':
32        try:
33                import win32file
34                import win32con
35                import pywintypes
36        except ImportError:
37                HAS_PYWIN32 = False
38
39from common import i18n
40i18n.init()
41_ = i18n._
42from common import gajim
43from common import helpers
44
45screen_w = gtk.gdk.screen_width()
46screen_h = gtk.gdk.screen_height()
47
48GLADE_DIR = os.path.join('..', 'data', 'glade')
49def get_glade(file_name, root = None):
50        file_path = os.path.join(GLADE_DIR, file_name)
51        return gtk.glade.XML(file_path, root=root, domain=i18n.APP)
52
53def popup_emoticons_under_button(menu, button, parent_win):
54        ''' pops emoticons menu under button, which is in parent_win'''
55        window_x1, window_y1 = parent_win.get_origin()
56        def position_menu_under_button(menu):
57                # inline function, which will not keep refs, when used as CB
58                button_x, button_y = button.allocation.x, button.allocation.y
59               
60                # now convert them to X11-relative
61                window_x, window_y = window_x1, window_y1
62                x = window_x + button_x
63                y = window_y + button_y
64
65                menu_width, menu_height = menu.size_request()
66
67                ## should we pop down or up?
68                if (y + button.allocation.height + menu_height
69                        < gtk.gdk.screen_height()):
70                        # now move the menu below the button
71                        y += button.allocation.height
72                else:
73                        # now move the menu above the button
74                        y -= menu_height
75
76                # push_in is True so all the menuitems are always inside screen
77                push_in = True
78                return (x, y, push_in)
79
80        menu.popup(None, None, position_menu_under_button, 1, 0)
81       
82def get_theme_font_for_option(theme, option):
83        '''return string description of the font, stored in
84        theme preferences'''
85        font_name = gajim.config.get_per('themes', theme, option)
86        font_desc = pango.FontDescription()
87        font_prop_str =  gajim.config.get_per('themes', theme, option + 'attrs')
88        if font_prop_str:
89                if font_prop_str.find('B') != -1:
90                        font_desc.set_weight(pango.WEIGHT_BOLD)
91                if font_prop_str.find('I') != -1:
92                        font_desc.set_style(pango.STYLE_ITALIC)
93        fd = pango.FontDescription(font_name)
94        fd.merge(font_desc, True)
95        return fd.to_string()
96       
97def get_default_font():
98        '''Get the desktop setting for application font
99        first check for GNOME, then XFCE and last KDE
100        it returns None on failure or else a string 'Font Size' '''
101       
102        try:
103                import gconf
104                # in try because daemon may not be there
105                client = gconf.client_get_default()
106
107                return helpers.ensure_unicode_string(
108                        client.get_string('/desktop/gnome/interface/font_name'))
109        except:
110                pass
111
112        # try to get xfce default font
113        # Xfce 4.2 adopts freedesktop.org's Base Directory Specification
114        # see http://www.xfce.org/~benny/xfce/file-locations.html
115        # and http://freedesktop.org/Standards/basedir-spec
116        xdg_config_home = os.environ.get('XDG_CONFIG_HOME', '')
117        if xdg_config_home == '':
118                xdg_config_home = os.path.expanduser('~/.config') # default     
119        xfce_config_file = os.path.join(xdg_config_home, 'xfce4/mcs_settings/gtk.xml')
120       
121        kde_config_file = os.path.expanduser('~/.kde/share/config/kdeglobals')
122       
123        if os.path.exists(xfce_config_file):
124                try:
125                        for line in file(xfce_config_file):
126                                if line.find('name="Gtk/FontName"') != -1:
127                                        start = line.find('value="') + 7
128                                        return helpers.ensure_unicode_string(
129                                                line[start:line.find('"', start)])
130                except:
131                        #we talk about file
132                        print >> sys.stderr, _('Error: cannot open %s for reading') % xfce_config_file
133       
134        elif os.path.exists(kde_config_file):
135                try:
136                        for line in file(kde_config_file):
137                                if line.find('font=') == 0: # font=Verdana,9,other_numbers
138                                        start = 5 # 5 is len('font=')
139                                        line = line[start:]
140                                        values = line.split(',')
141                                        font_name = values[0]
142                                        font_size = values[1]
143                                        font_string = '%s %s' % (font_name, font_size) # Verdana 9
144                                        return helpers.ensure_unicode_string(font_string)
145                except:
146                        #we talk about file
147                        print >> sys.stderr, _('Error: cannot open %s for reading') % kde_config_file
148       
149        return None
150       
151def reduce_chars_newlines(text, max_chars = 0, max_lines = 0):
152        '''Cut the chars after 'max_chars' on each line
153        and show only the first 'max_lines'.
154        If any of the params is not present (None or 0) the action
155        on it is not performed'''
156
157        def _cut_if_long(str):
158                if len(str) > max_chars:
159                        str = str[:max_chars - 3] + '...'
160                return str
161       
162        if max_lines == 0:
163                lines = text.split('\n')
164        else:
165                lines = text.split('\n', max_lines)[:max_lines]
166        if max_chars > 0:
167                if lines:
168                        lines = map(lambda e: _cut_if_long(e), lines)
169        if lines:
170                reduced_text = reduce(lambda e, e1: e + '\n' + e1, lines)
171        else:
172                reduced_text = ''
173        return reduced_text
174
175def escape_for_pango_markup(string):
176        # escapes < > & ' "
177        # for pango markup not to break
178        if string is None:
179                return
180        if gtk.pygtk_version >= (2, 8, 0) and gtk.gtk_version >= (2, 8, 0):
181                escaped_str = gobject.markup_escape_text(string)
182        else:
183                escaped_str = xml.sax.saxutils.escape(string, {"'": '&apos;',
184                        '"': '&quot;'})
185       
186        return escaped_str
187
188def autodetect_browser_mailer():
189        # recognize the environment for appropriate browser/mailer
190        if os.path.isdir('/proc'):
191                # under Linux: checking if 'gnome-session' or
192                # 'startkde' programs were run before gajim, by
193                # checking /proc (if it exists)
194                #
195                # if something is unclear, read `man proc`;
196                # if /proc exists, directories that have only numbers
197                # in their names contain data about processes.
198                # /proc/[xxx]/exe is a symlink to executable started
199                # as process number [xxx].
200                # filter out everything that we are not interested in:
201                files = os.listdir('/proc')
202
203                # files that doesn't have only digits in names...
204                files = filter(str.isdigit, files)
205
206                # files that aren't directories...
207                files = filter(lambda f:os.path.isdir('/proc/' + f), files)
208
209                # processes owned by somebody not running gajim...
210                # (we check if we have access to that file)
211                files = filter(lambda f:os.access('/proc/' + f +'/exe', os.F_OK), files)
212
213                # be sure that /proc/[number]/exe is really a symlink
214                # to avoid TBs in incorrectly configured systems
215                files = filter(lambda f:os.path.islink('/proc/' + f + '/exe'), files)
216
217                # list of processes
218                processes = [os.path.basename(os.readlink('/proc/' + f +'/exe')) for f in files]
219                if 'gnome-session' in processes:
220                        gajim.config.set('openwith', 'gnome-open')
221                elif 'startkde' in processes:
222                        gajim.config.set('openwith', 'kfmclient exec')
223                else:
224                        gajim.config.set('openwith', 'custom')
225
226def move_window(window, x, y):
227        '''moves the window but also checks if out of screen'''
228        if x < 0:
229                x = 0
230        if y < 0:
231                y = 0
232        window.move(x, y)
233
234def resize_window(window, w, h):
235        '''resizes window but also checks if huge window or negative values'''
236        if not w or not h:
237                return
238        if w > screen_w:
239                w = screen_w
240        if h > screen_h:
241                h = screen_h
242        window.resize(abs(w), abs(h))
243
244class TagInfoHandler(xml.sax.ContentHandler):
245        def __init__(self, tagname1, tagname2):
246                xml.sax.ContentHandler.__init__(self)
247                self.tagname1 = tagname1
248                self.tagname2 = tagname2
249                self.servers = []
250
251        def startElement(self, name, attributes):
252                if name == self.tagname1:
253                        for attribute in attributes.getNames():
254                                if attribute == 'jid':
255                                        jid = attributes.getValue(attribute)
256                                        # we will get the port next time so we just set it 0 here
257                                        self.servers.append([jid, 0])
258                elif name == self.tagname2:
259                        for attribute in attributes.getNames():
260                                if attribute == 'port':
261                                        port = attributes.getValue(attribute)
262                                        # we received the jid last time, so we now assign the port
263                                        # number to the last jid in the list
264                                        self.servers[-1][1] = port
265
266        def endElement(self, name):
267                pass
268
269def parse_server_xml(path_to_file):
270        try:
271                handler = TagInfoHandler('item', 'active')
272                xml.sax.parse(path_to_file, handler)
273                return handler.servers
274        # handle exception if unable to open file
275        except IOError, message:
276                print >> sys.stderr, _('Error reading file:'), message
277        # handle exception parsing file
278        except xml.sax.SAXParseException, message:
279                print >> sys.stderr, _('Error parsing file:'), message
280
281def set_unset_urgency_hint(window, unread_messages_no):
282        '''sets/unsets urgency hint in window argument
283        depending if we have unread messages or not'''
284        if gtk.gtk_version >= (2, 8, 0) and gtk.pygtk_version >= (2, 8, 0) and \
285                gajim.config.get('use_urgency_hint'):
286                if unread_messages_no > 0:
287                        window.props.urgency_hint = True
288                else:
289                        window.props.urgency_hint = False
290
291def get_abspath_for_script(scriptname, want_type = False):
292        '''checks if we are svn or normal user and returns abspath to asked script
293        if want_type is True we return 'svn' or 'install' '''
294        if os.path.isdir('.svn'): # we are svn user
295                type = 'svn'
296                cwd = os.getcwd() # it's always ending with src
297
298                if scriptname == 'gajim-remote':
299                        path_to_script = cwd + '/gajim-remote.py'
300               
301                elif scriptname == 'gajim':
302                        script = '#!/bin/sh\n' # the script we may create
303                        script += 'cd %s' % cwd
304                        path_to_script = cwd + '/../scripts/gajim_sm_script'
305                               
306                        try:
307                                if os.path.exists(path_to_script):
308                                        os.remove(path_to_script)
309
310                                f = open(path_to_script, 'w')
311                                script += '\nexec python -OOt gajim.py $0 $@\n'
312                                f.write(script)
313                                f.close()
314                                os.chmod(path_to_script, 0700)
315                        except OSError: # do not traceback (could be a permission problem)
316                                #we talk about a file here
317                                s = _('Could not write to %s. Session Management support will not work') % path_to_script
318                                print >> sys.stderr, s
319
320        else: # normal user (not svn user)
321                type = 'install'
322                # always make it like '/usr/local/bin/gajim'
323                path_to_script = helpers.is_in_path(scriptname, True)
324               
325       
326        if want_type:
327                return path_to_script, type
328        else:
329                return path_to_script
330
331def get_pixbuf_from_data(file_data, want_type = False):
332        '''Gets image data and returns gtk.gdk.Pixbuf
333        if want_type is True it also returns 'jpeg', 'png' etc'''
334        pixbufloader = gtk.gdk.PixbufLoader()
335        try:
336                pixbufloader.write(file_data)
337                pixbufloader.close()
338                pixbuf = pixbufloader.get_pixbuf()
339        except gobject.GError: # 'unknown image format'
340                pixbuf = None
341
342        if want_type:
343                typ = pixbufloader.get_format()['name']
344                return pixbuf, typ
345        else:
346                return pixbuf
347
348def get_invisible_cursor():
349        pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
350        color = gtk.gdk.Color()
351        cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
352        return cursor
353
354def get_current_desktop(window):
355        '''returns the current virtual desktop for given window
356        NOTE: window is GDK window'''
357        prop = window.property_get('_NET_CURRENT_DESKTOP')
358        if prop is None: # it means it's normal window (not root window)
359                # so we look for it's current virtual desktop in another property
360                prop = window.property_get('_NET_WM_DESKTOP')
361
362        if prop is not None:
363                # f.e. prop is ('CARDINAL', 32, [0]) we want 0 or 1.. from [0]
364                current_virtual_desktop_no = prop[2][0]
365                return current_virtual_desktop_no
366
367def possibly_move_window_in_current_desktop(window):
368        '''moves GTK window to current virtual desktop if it is not in the
369        current virtual desktop
370        window is GTK window'''
371        if os.name == 'nt':
372                return
373
374        root_window = gtk.gdk.screen_get_default().get_root_window()
375        # current user's vd
376        current_virtual_desktop_no = get_current_desktop(root_window)
377       
378        # vd roster window is in
379        window_virtual_desktop = get_current_desktop(window.window)
380
381        # if one of those is None, something went wrong and we cannot know
382        # VD info, just hide it (default action) and not show it afterwards
383        if None not in (window_virtual_desktop, current_virtual_desktop_no):
384                if current_virtual_desktop_no != window_virtual_desktop:
385                        # we are in another VD that the window was
386                        # so show it in current VD
387                        window.show()
388
389def file_is_locked(path_to_file):
390        '''returns True if file is locked (WINDOWS ONLY)'''
391        if os.name != 'nt': # just in case
392                return
393       
394        if not HAS_PYWIN32:
395                return
396       
397        secur_att = pywintypes.SECURITY_ATTRIBUTES()
398        secur_att.Initialize()
399       
400        try:
401                # try make a handle for READING the file
402                hfile = win32file.CreateFile(
403                        path_to_file,                                   # path to file
404                        win32con.GENERIC_READ,                  # open for reading
405                        0,                                                              # do not share with other proc
406                        secur_att,
407                        win32con.OPEN_EXISTING,                 # existing file only
408                        win32con.FILE_ATTRIBUTE_NORMAL, # normal file
409                        0                                                               # no attr. template
410                )
411        except pywintypes.error, e:
412                return True
413        else: # in case all went ok, close file handle (go to hell WinAPI)
414                hfile.Close()
415                return False
416
417def _get_fade_color(treeview, selected, focused):
418        '''get a gdk color that is between foreground and background in 0.3
419        0.7 respectively colors of the cell for the given treeview'''
420        style = treeview.style
421        if selected:
422                if focused: # is the window focused?
423                        state = gtk.STATE_SELECTED
424                else: # is it not? NOTE: many gtk themes change bg on this
425                        state = gtk.STATE_ACTIVE
426        else:
427                state = gtk.STATE_NORMAL
428        bg = style.base[state]
429        fg = style.text[state]
430
431        p = 0.3 # background
432        q = 0.7 # foreground # p + q should do 1.0
433        return gtk.gdk.Color(int(bg.red*p + fg.red*q),
434                              int(bg.green*p + fg.green*q),
435                              int(bg.blue*p + fg.blue*q))
436
437def get_scaled_pixbuf(pixbuf, kind):
438        '''returns scaled pixbuf, keeping ratio etc or None
439        kind is either "chat" or "roster" or "notification" or "tooltip"'''
440       
441        # resize to a width / height for the avatar not to have distortion
442        # (keep aspect ratio)
443        width = gajim.config.get(kind + '_avatar_width')
444        height = gajim.config.get(kind + '_avatar_height')
445        if width < 1 or height < 1:
446                return None
447
448        # Pixbuf size
449        pix_width = pixbuf.get_width()
450        pix_height = pixbuf.get_height()
451        # don't make avatars bigger than they are
452        if pix_width < width and pix_height < height:
453                return pixbuf # we don't want to make avatar bigger
454
455        ratio = float(pix_width) / float(pix_height)
456        if ratio > 1:
457                w = width
458                h = int(w / ratio)
459        else:
460                h = height
461                w = int(h * ratio)
462        scaled_buf = pixbuf.scale_simple(w, h, gtk.gdk.INTERP_HYPER)
463        return scaled_buf
464
465def get_avatar_pixbuf_from_cache(fjid, is_fake_jid = False):
466        '''checks if jid has cached avatar and if that avatar is valid image
467        (can be shown)
468        returns None if there is no image in vcard
469        returns 'ask' if cached vcard should not be used (user changed his vcard,
470        so we have new sha) or if we don't have the vcard'''
471
472        jid, nick = gajim.get_room_and_nick_from_fjid(fjid)
473        if gajim.config.get('hide_avatar_of_transport') and\
474                gajim.jid_is_transport(jid):
475                # don't show avatar for the transport itself
476                return None
477
478        puny_jid = helpers.sanitize_filename(jid)
479        if is_fake_jid:
480                puny_nick = helpers.sanitize_filename(nick)
481                path = os.path.join(gajim.VCARD_PATH, puny_jid, puny_nick)
482        else:
483                path = os.path.join(gajim.VCARD_PATH, puny_jid)
484        if not os.path.isfile(path):
485                return 'ask'
486
487        vcard_dict = gajim.connections.values()[0].get_cached_vcard(fjid,
488                is_fake_jid)
489        if not vcard_dict: # This can happen if cached vcard is too old
490                return 'ask'
491        if not vcard_dict.has_key('PHOTO'):
492                return None
493        pixbuf = vcard.get_avatar_pixbuf_encoded_mime(vcard_dict['PHOTO'])[0]
494        return pixbuf
495
496def make_gtk_month_python_month(month):
497        '''gtk start counting months from 0, so January is 0
498        but python's time start from 1, so align to python
499        month MUST be integer'''
500        return month + 1
501
502def make_python_month_gtk_month(month):
503        return month - 1
504
505def make_color_string(color):
506        '''create #aabbcc color string from gtk color'''
507        col = '#'
508        for i in (color.red, color.green, color.blue):
509                h = hex(i)[2:4]
510                if len(h) == 1:
511                        h = '0' + h
512                col += h
513        return col
514
515def make_pixbuf_grayscale(pixbuf):
516        pixbuf2 = pixbuf.copy()
517        pixbuf.saturate_and_pixelate(pixbuf2, 0.0, False)
518        return pixbuf2
519
520def get_path_to_generic_or_avatar(generic, jid = None, suffix = None):
521        '''Chooses between avatar image and default image.
522        Returns full path to the avatar image if it exists,
523        otherwise returns full path to the image.'''
524        if jid:
525                puny_jid = helpers.sanitize_filename(jid)
526                path_to_file = os.path.join(gajim.AVATAR_PATH, puny_jid) + suffix
527                if os.path.exists(path_to_file):
528                        return path_to_file
529        return os.path.abspath(generic)
530
531def decode_filechooser_file_paths(file_paths):
532        '''decode as UTF-8 under Windows and
533        ask sys.getfilesystemencoding() in POSIX
534        file_paths MUST be LIST'''
535        file_paths_list = list()
536       
537        if os.name == 'nt': # decode as UTF-8 under Windows
538                for file_path in file_paths:
539                        file_path = file_path.decode('utf8')
540                        file_paths_list.append(file_path)
541        else:
542                for file_path in file_paths:
543                        try:
544                                file_path = file_path.decode(sys.getfilesystemencoding())
545                        except:
546                                try:
547                                        file_path = file_path.decode('utf-8')
548                                except:
549                                        pass
550                        file_paths_list.append(file_path)
551       
552        return file_paths_list
553
554def possibly_set_gajim_as_xmpp_handler():
555        '''registers (by default only the first time) xmmp: to Gajim.'''
556        try:
557                import gconf
558                # in try because daemon may not be there
559                client = gconf.client_get_default()
560        except:
561                return
562       
563       
564        path_to_dot_kde = os.path.expanduser('~/.kde')
565        if os.path.exists(path_to_dot_kde):
566                path_to_kde_file = os.path.join(path_to_dot_kde, 
567                        'share/services/xmpp.protocol')
568                if not os.path.exists(path_to_kde_file):
569                        path_to_kde_file = None
570        else:
571                path_to_kde_file = None
572       
573        if gajim.config.get('set_xmpp://_handler_everytime'):
574                        # it's false by default
575                        we_set = True
576        elif client.get_string('/desktop/gnome/url-handlers/xmpp/command') is None:
577                # only the first time (GNOME/GCONF)
578                we_set = True
579        elif path_to_kde_file is not None and not os.path.exists(path_to_kde_file):
580                # only the first time (KDE)
581                we_set = True
582        else:
583                we_set = False
584               
585        if not we_set:
586                return
587       
588        path_to_gajim_script, typ = get_abspath_for_script('gajim-remote', True)
589        if path_to_gajim_script:
590                if typ == 'svn':
591                        command = path_to_gajim_script + ' open_chat %s'
592                else: # 'installed'
593                        command = 'gajim-remote open_chat %s'
594               
595                # setting for GNOME/Gconf
596                client.set_bool('/desktop/gnome/url-handlers/xmpp/enabled', True)
597                client.set_string('/desktop/gnome/url-handlers/xmpp/command', command)
598                client.set_bool('/desktop/gnome/url-handlers/xmpp/needs_terminal', False)
599               
600                # setting for KDE
601                if path_to_kde_file is not None: # user has run kde at least once
602                        f = open(path_to_kde_file, 'w')
603                        f.write('''\
604[Protocol]
605exec=%s "%%u"
606protocol=xmpp
607input=none
608output=none
609helper=true
610listing=false
611reading=false
612writing=false
613makedir=false
614deleting=false
615icon=gajim
616Description=xmpp
617''' % command)
618                        f.close()
619
620def escape_underscore(s):
621        '''Escape underlines to prevent them from being interpreted
622        as keyboard accelerators'''
623        return s.replace('_', '__')
624
625def get_state_image_from_file_path_show(file_path, show):
626        state_file = show.replace(' ', '_')
627        files = []
628        files.append(os.path.join(file_path, state_file + '.png'))
629        files.append(os.path.join(file_path, state_file + '.gif'))
630        image = gtk.Image()
631        image.set_from_pixbuf(None)
632        for file_ in files:
633                if os.path.exists(file_):
634                        image.set_from_file(file_)
635                        break
636
637        return image
638
639def get_possible_button_event(event):
640        '''mouse or keyboard caused the event?'''
641        if event.type == gtk.gdk.KEY_PRESS:
642                event_button = 0 # no event.button so pass 0
643        else: # BUTTON_PRESS event, so pass event.button
644                event_button = event.button
645
646        return event_button
647
648def destroy_widget(widget):
649        widget.destroy()
Note: See TracBrowser for help on using the browser.