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