gtkgui_helpers.py 32.4 KB
Newer Older
roidelapluie's avatar
roidelapluie committed
1
# -*- coding:utf-8 -*-
roidelapluie's avatar
roidelapluie committed
2
## src/gtkgui_helpers.py
3
##
Dicson's avatar
Dicson committed
4
## Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
roidelapluie's avatar
roidelapluie committed
5 6 7
## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
## Copyright (C) 2006 Travis Shirk <travis AT pobox.com>
roidelapluie's avatar
roidelapluie committed
8
## Copyright (C) 2006-2007 Junglecow J <junglecow AT gmail.com>
roidelapluie's avatar
roidelapluie committed
9 10 11 12 13
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2007 James Newton <redshodan AT gmail.com>
##                    Julien Pivotto <roidelapluie AT gmail.com>
## Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
14
##
15 16 17
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
18
## it under the terms of the GNU General Public License as published
19
## by the Free Software Foundation; version 3 only.
20
##
21
## Gajim is distributed in the hope that it will be useful,
22
## but WITHOUT ANY WARRANTY; without even the implied warranty of
roidelapluie's avatar
roidelapluie committed
23
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 25
## GNU General Public License for more details.
##
26
## You should have received a copy of the GNU General Public License
roidelapluie's avatar
roidelapluie committed
27
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
28
##
29

30
import xml.sax.saxutils
31 32 33
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
34
from gi.repository import GLib
35
from gi.repository import Pango
36
import cairo
37
import os
nkour's avatar
nkour committed
38
import sys
39
import math
40
import xml.etree.ElementTree as ET
41 42 43 44 45
try:
    from PIL import Image
except:
    pass
from io import BytesIO
46

47 48 49
import logging
log = logging.getLogger('gajim.gtkgui_helpers')

André's avatar
André committed
50
from gajim.common import i18n
51
from gajim.common import app
André's avatar
André committed
52
from gajim.common import configpaths
53
from gajim.common.const import PEPEventType, ACTIVITIES, MOODS
54
from gajim.filechoosers import AvatarSaveDialog
55

56
gtk_icon_theme = Gtk.IconTheme.get_default()
57
gtk_icon_theme.append_search_path(configpaths.get('ICONS'))
58

59 60
class Color:
    BLACK = Gdk.RGBA(red=0, green=0, blue=0, alpha=1)
Philipp Hörist's avatar
Philipp Hörist committed
61 62
    GREEN = Gdk.RGBA(red=115/255, green=210/255, blue=22/255, alpha=1)
    RED = Gdk.RGBA(red=204/255, green=0, blue=0, alpha=1)
63
    GREY = Gdk.RGBA(red=195/255, green=195/255, blue=192/255, alpha=1)
64
    ORANGE = Gdk.RGBA(red=245/255, green=121/255, blue=0/255, alpha=1)
65 66

def get_icon_pixmap(icon_name, size=16, color=None, quiet=False):
67
    try:
68
        iconinfo = gtk_icon_theme.lookup_icon(icon_name, size, 0)
69 70
        if not iconinfo:
            raise GLib.GError
71 72 73 74
        if color:
            pixbuf, was_symbolic = iconinfo.load_symbolic(*color)
            return pixbuf
        return iconinfo.load_icon()
75
    except GLib.GError as e:
76 77
        if not quiet:
            log.error('Unable to load icon %s: %s' % (icon_name, str(e)))
78 79

def get_icon_path(icon_name, size=16):
80 81 82 83 84 85 86
    try:
        icon_info = gtk_icon_theme.lookup_icon(icon_name, size, 0)
        if icon_info == None:
            log.error('Icon not found: %s' % icon_name)
            return ""
        else:
            return icon_info.get_filename()
87
    except GLib.GError as e:
88
        log.error("Unable to find icon %s: %s" % (icon_name, str(e)))
89

André's avatar
André committed
90

91 92
HAS_PYWIN32 = True
if os.name == 'nt':
93 94 95 96 97 98
    try:
        import win32file
        import win32con
        import pywintypes
    except ImportError:
        HAS_PYWIN32 = False
99

André's avatar
André committed
100
from gajim.common import helpers
101

102 103 104
def get_total_screen_geometry():
    screen = Gdk.Screen.get_default()
    window = Gdk.Screen.get_root_window(screen)
105 106 107
    w, h = window.get_width(), window.get_height()
    log.debug('Get screen geometry: %s %s', w, h)
    return w, h
108

109
def add_image_to_button(button, icon_name):
110
    img = Gtk.Image()
111 112
    path_img = get_icon_path(icon_name)
    img.set_from_file(path_img)
113
    button.set_image(img)
114

115 116 117
def get_image_button(icon_name, tooltip, toggle=False):
    if toggle:
        button = Gtk.ToggleButton()
Philipp Hörist's avatar
Philipp Hörist committed
118 119 120 121
        icon = get_icon_pixmap(icon_name)
        image = Gtk.Image()
        image.set_from_pixbuf(icon)
        button.set_image(image)
122
    else:
Philipp Hörist's avatar
Philipp Hörist committed
123 124
        button = Gtk.Button.new_from_icon_name(
            icon_name, Gtk.IconSize.MENU)
125
    button.set_tooltip_text(tooltip)
126 127
    return button

128
def get_gtk_builder(file_name, widget=None):
129
    file_path = os.path.join(configpaths.get('GUI'), file_name)
130 131

    builder = Gtk.Builder()
Philipp Hörist's avatar
Philipp Hörist committed
132
    builder.set_translation_domain(i18n.DOMAIN)
133

134
    if sys.platform == "win32":
135 136
        # This is a workaround for non working translation on Windows
        tree = ET.parse(file_path)
137 138 139 140 141 142
        for node in tree.iter():
            if 'translatable' in node.attrib:
                node.text = _(node.text)
        xml_text = ET.tostring(tree.getroot(),
                               encoding='unicode',
                               method='xml')
143

144 145
        if widget is not None:
            builder.add_objects_from_string(xml_text, [widget])
146 147
        else:
            builder.add_from_string(xml_text, -1)
148 149
    else:
        if widget is not None:
150 151 152 153
            builder.add_objects_from_file(file_path, [widget])
        else:
            builder.add_from_file(file_path)
    return builder
154

155
def get_completion_liststore(entry):
156 157 158 159
    """
    Create a completion model for entry widget completion list consists of
    (Pixbuf, Text) rows
    """
160
    completion = Gtk.EntryCompletion()
161
    liststore = Gtk.ListStore(str, str)
162

163
    render_pixbuf = Gtk.CellRendererPixbuf()
Yann Leboulanger's avatar
Yann Leboulanger committed
164
    completion.pack_start(render_pixbuf, False)
165
    completion.add_attribute(render_pixbuf, 'icon_name', 0)
166

167 168 169
    render_text = Gtk.CellRendererText()
    completion.pack_start(render_text, True)
    completion.add_attribute(render_text, 'text', 1)
170 171 172 173
    completion.set_property('text_column', 1)
    completion.set_model(liststore)
    entry.set_completion(completion)
    return liststore
174

175
def get_theme_font_for_option(theme, option):
176 177 178
    """
    Return string description of the font, stored in theme preferences
    """
179
    font_name = app.config.get_per('themes', theme, option)
180
    font_desc = Pango.FontDescription()
181
    font_prop_str =  app.config.get_per('themes', theme, option + 'attrs')
182 183
    if font_prop_str:
        if font_prop_str.find('B') != -1:
184
            font_desc.set_weight(Pango.Weight.BOLD)
185
        if font_prop_str.find('I') != -1:
186 187
            font_desc.set_style(Pango.Style.ITALIC)
    fd = Pango.FontDescription(font_name)
188 189
    fd.merge(font_desc, True)
    return fd.to_string()
190

191
def move_window(window, x, y):
192 193 194
    """
    Move the window, but also check if out of screen
    """
195
    screen_w, screen_h = get_total_screen_geometry()
196 197 198 199 200 201 202 203 204 205
    if x < 0:
        x = 0
    if y < 0:
        y = 0
    w, h = window.get_size()
    if x + w > screen_w:
        x = screen_w - w
    if y + h > screen_h:
        y = screen_h - h
    window.move(x, y)
206 207

def resize_window(window, w, h):
208 209 210
    """
    Resize window, but also checks if huge window or negative values
    """
211
    screen_w, screen_h = get_total_screen_geometry()
212 213 214 215 216 217 218
    if not w or not h:
        return
    if w > screen_w:
        w = screen_w
    if h > screen_h:
        h = screen_h
    window.resize(abs(w), abs(h))
nkour's avatar
nkour committed
219

220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
def at_the_end(widget):
    """Determines if a Scrollbar in a GtkScrolledWindow is at the end.

    Args:
        widget (GtkScrolledWindow)

    Returns:
        bool: The return value is True if at the end, False if not.
    """
    adj_v = widget.get_vadjustment()
    max_scroll_pos = adj_v.get_upper() - adj_v.get_page_size()
    at_the_end = (adj_v.get_value() == max_scroll_pos)
    return at_the_end

def scroll_to_end(widget):
    """Scrolls to the end of a GtkScrolledWindow.

    Args:
        widget (GtkScrolledWindow)

    Returns:
        bool: The return value is False so it can be used with GLib.idle_add.
    """
    adj_v = widget.get_vadjustment()
244 245 246 247
    if adj_v is None:
        # This can happen when the Widget is already destroyed when called
        # from GLib.idle_add
        return False
248 249 250 251 252 253 254 255
    max_scroll_pos = adj_v.get_upper() - adj_v.get_page_size()
    adj_v.set_value(max_scroll_pos)

    adj_h = widget.get_hadjustment()
    adj_h.set_value(0)
    return False


256
class ServersXMLHandler(xml.sax.ContentHandler):
257 258 259 260 261 262
    def __init__(self):
        xml.sax.ContentHandler.__init__(self)
        self.servers = []

    def startElement(self, name, attributes):
        if name == 'item':
263 264
            if 'jid' in attributes.getNames():
                self.servers.append(attributes.getValue('jid'))
265 266 267

    def endElement(self, name):
        pass
268 269

def parse_server_xml(path_to_file):
270 271 272 273 274
    try:
        handler = ServersXMLHandler()
        xml.sax.parse(path_to_file, handler)
        return handler.servers
    # handle exception if unable to open file
275
    except IOError as message:
276
        print(_('Error reading file:') + str(message), file=sys.stderr)
277
    # handle exception parsing file
278
    except xml.sax.SAXParseException as message:
279
        print(_('Error parsing file:') + str(message), file=sys.stderr)
280

nkour's avatar
nkour committed
281
def set_unset_urgency_hint(window, unread_messages_no):
282 283 284 285
    """
    Sets/unset urgency hint in window argument depending if we have unread
    messages or not
    """
286
    if app.config.get('use_urgency_hint'):
287 288 289 290
        if unread_messages_no > 0:
            window.props.urgency_hint = True
        else:
            window.props.urgency_hint = False
291

Philipp Hörist's avatar
Philipp Hörist committed
292
def get_pixbuf_from_data(file_data):
293
    """
Philipp Hörist's avatar
Philipp Hörist committed
294
    Get image data and returns GdkPixbuf.Pixbuf
295
    """
296
    pixbufloader = GdkPixbuf.PixbufLoader()
297
    try:
298 299 300
        pixbufloader.write(file_data)
        pixbufloader.close()
        pixbuf = pixbufloader.get_pixbuf()
Philipp Hörist's avatar
Philipp Hörist committed
301
    except GLib.GError:
302
        pixbufloader.close()
303

Philipp Hörist's avatar
Philipp Hörist committed
304 305
        log.warning('loading avatar using pixbufloader failed, trying to '
                    'convert avatar image using pillow')
306
        try:
307
            avatar = Image.open(BytesIO(file_data)).convert("RGBA")
Philipp Hörist's avatar
Philipp Hörist committed
308
            array = GLib.Bytes.new(avatar.tobytes())
309
            width, height = avatar.size
Philipp Hörist's avatar
Philipp Hörist committed
310 311 312 313 314 315 316 317 318
            pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
                array, GdkPixbuf.Colorspace.RGB,
                True, 8, width, height, width * 4)
        except Exception:
            log.warning('Could not use pillow to convert avatar image, '
                        'image cannot be displayed', exc_info=True)
            return

    return pixbuf
319

320 321 322 323 324
def get_cursor(attr):
    display = Gdk.Display.get_default()
    cursor = getattr(Gdk.CursorType, attr)
    return Gdk.Cursor.new_for_display(display, cursor)

325
def get_current_desktop(window):
326 327
    """
    Return the current virtual desktop for given window
328

329 330 331 332 333 334
    NOTE: Window is a GDK window.
    """
    prop = window.property_get('_NET_CURRENT_DESKTOP')
    if prop is None: # it means it's normal window (not root window)
        # so we look for it's current virtual desktop in another property
        prop = window.property_get('_NET_WM_DESKTOP')
335

336 337 338 339
    if prop is not None:
        # f.e. prop is ('CARDINAL', 32, [0]) we want 0 or 1.. from [0]
        current_virtual_desktop_no = prop[2][0]
        return current_virtual_desktop_no
nkour's avatar
nkour committed
340 341

def possibly_move_window_in_current_desktop(window):
342 343 344 345 346 347
    """
    Moves GTK window to current virtual desktop if it is not in the current
    virtual desktop

    NOTE: Window is a GDK window.
    """
348 349 350 351 352 353
    #TODO: property_get doesn't work:
    #prop_atom = Gdk.Atom.intern('_NET_CURRENT_DESKTOP', False)
    #type_atom = Gdk.Atom.intern("CARDINAL", False)
    #w = Gdk.Screen.get_default().get_root_window()
    #Gdk.property_get(w, prop_atom, type_atom, 0, 9999, False)
    return False
354 355 356
    if os.name == 'nt':
        return False

357
    root_window = Gdk.Screen.get_default().get_root_window()
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372
    # current user's vd
    current_virtual_desktop_no = get_current_desktop(root_window)

    # vd roster window is in
    window_virtual_desktop = get_current_desktop(window.window)

    # if one of those is None, something went wrong and we cannot know
    # VD info, just hide it (default action) and not show it afterwards
    if None not in (window_virtual_desktop, current_virtual_desktop_no):
        if current_virtual_desktop_no != window_virtual_desktop:
            # we are in another VD that the window was
            # so show it in current VD
            window.present()
            return True
    return False
373 374

def file_is_locked(path_to_file):
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
    """
    Return True if file is locked

    NOTE: Windows only.
    """
    if os.name != 'nt': # just in case
        return

    if not HAS_PYWIN32:
        return

    secur_att = pywintypes.SECURITY_ATTRIBUTES()
    secur_att.Initialize()

    try:
        # try make a handle for READING the file
        hfile = win32file.CreateFile(
Yann Leboulanger's avatar
Yann Leboulanger committed
392 393 394
                path_to_file,                   # path to file
                win32con.GENERIC_READ,          # open for reading
                0,                              # do not share with other proc
395
                secur_att,
Yann Leboulanger's avatar
Yann Leboulanger committed
396
                win32con.OPEN_EXISTING,         # existing file only
397
                win32con.FILE_ATTRIBUTE_NORMAL, # normal file
Yann Leboulanger's avatar
Yann Leboulanger committed
398
                0                               # no attr. template
399 400 401 402 403 404
        )
    except pywintypes.error:
        return True
    else: # in case all went ok, close file handle (go to hell WinAPI)
        hfile.Close()
        return False
405

406
def get_fade_color(treeview, selected, focused):
407
    """
408
    Get a gdk RGBA color that is between foreground and background in 0.3
409 410
    0.7 respectively colors of the cell for the given treeview
    """
411
    context = treeview.get_style_context()
412 413
    if selected:
        if focused: # is the window focused?
414
            state = Gtk.StateFlags.SELECTED
415
        else: # is it not? NOTE: many gtk themes change bg on this
416
            state = Gtk.StateFlags.ACTIVE
417
    else:
418
        state = Gtk.StateFlags.NORMAL
419 420

    bg = context.get_property('background-color', state)
421
    fg = context.get_color(state)
422 423 424

    p = 0.3 # background
    q = 0.7 # foreground # p + q should do 1.0
425 426
    return Gdk.RGBA(bg.red*p + fg.red*q, bg.green*p + fg.green*q,
        bg.blue*p + fg.blue*q)
427

428
def make_gtk_month_python_month(month):
429 430 431
    """
    GTK starts counting months from 0, so January is 0 but Python's time start
    from 1, so align to Python
432

433 434 435
    NOTE: Month MUST be an integer.
    """
    return month + 1
436 437

def make_python_month_gtk_month(month):
438
    return month - 1
nkour's avatar
nkour committed
439

440
def make_pixbuf_grayscale(pixbuf):
441 442 443
    pixbuf2 = pixbuf.copy()
    pixbuf.saturate_and_pixelate(pixbuf2, 0.0, False)
    return pixbuf2
444

nkour's avatar
nkour committed
445
def get_possible_button_event(event):
446 447 448
    """
    Mouse or keyboard caused the event?
    """
Yann Leboulanger's avatar
Yann Leboulanger committed
449
    if event.type == Gdk.EventType.KEY_PRESS:
450 451 452
        return 0 # no event.button so pass 0
    # BUTTON_PRESS event, so pass event.button
    return event.button
453 454

def destroy_widget(widget):
455
    widget.destroy()
456

Philipp Hörist's avatar
Philipp Hörist committed
457 458 459 460 461
def scale_with_ratio(size, width, height):
    if height == width:
        return size, size
    if height > width:
        ratio = height / float(width)
462
        return int(size / ratio), size
Philipp Hörist's avatar
Philipp Hörist committed
463 464 465 466
    else:
        ratio = width / float(height)
        return size, int(size / ratio)

467 468 469 470 471 472 473 474 475 476 477
def scale_pixbuf(pixbuf, size):
    width, height = scale_with_ratio(size,
                                     pixbuf.get_width(),
                                     pixbuf.get_height())
    return pixbuf.scale_simple(width, height,
                               GdkPixbuf.InterpType.BILINEAR)

def scale_pixbuf_from_data(data, size):
    pixbuf = get_pixbuf_from_data(data)
    return scale_pixbuf(pixbuf, size)

Philipp Hörist's avatar
Philipp Hörist committed
478
def on_avatar_save_as_menuitem_activate(widget, avatar, default_name=''):
479 480 481
    from gajim.gtk import ErrorDialog
    from gajim.gtk import ConfirmationDialog
    from gajim.gtk import FTOverwriteConfirmationDialog
482 483 484
    def on_continue(response, file_path):
        if response < 0:
            return
Philipp Hörist's avatar
Philipp Hörist committed
485

486
        app.config.set('last_save_dir', os.path.dirname(file_path))
Philipp Hörist's avatar
Philipp Hörist committed
487 488 489 490 491 492
        if isinstance(avatar, str):
            # We got a SHA
            pixbuf = app.interface.get_avatar(avatar)
        else:
            # We got a pixbuf
            pixbuf = avatar
Yann Leboulanger's avatar
Yann Leboulanger committed
493
        extension = os.path.splitext(file_path)[1]
494 495
        if not extension:
            # Silently save as Jpeg image
496 497
            image_format = 'png'
            file_path += '.png'
498 499 500 501 502
        else:
            image_format = extension[1:] # remove leading dot

        # Save image
        try:
503
            pixbuf.savev(file_path, image_format, [], [])
504
        except Exception as e:
Philipp Hörist's avatar
Philipp Hörist committed
505
            log.error('Error saving avatar: %s' % str(e))
506 507
            if os.path.exists(file_path):
                os.remove(file_path)
508
            new_file_path = '.'.join(file_path.split('.')[:-1]) + '.png'
509
            def on_ok(file_path, pixbuf):
510
                pixbuf.savev(file_path, 'png', [], [])
511
            ConfirmationDialog(_('Extension not supported'),
Yann Leboulanger's avatar
Yann Leboulanger committed
512 513 514 515
                _('Image cannot be saved in %(type)s format. Save as '
                '%(new_filename)s?') % {'type': image_format,
                'new_filename': new_file_path},
                on_response_ok = (on_ok, new_file_path, pixbuf))
516

517
    def on_ok(file_path):
518 519 520 521
        if os.path.exists(file_path):
            # check if we have write permissions
            if not os.access(file_path, os.W_OK):
                file_name = os.path.basename(file_path)
522
                ErrorDialog(_('Cannot overwrite existing file "%s"') % \
Yann Leboulanger's avatar
Yann Leboulanger committed
523 524
                    file_name, _('A file with this name already exists and you '
                    'do not have permission to overwrite it.'))
525
                return
526
            dialog2 = FTOverwriteConfirmationDialog(
Yann Leboulanger's avatar
Yann Leboulanger committed
527
                _('This file already exists'), _('What do you want to do?'),
528
                propose_resume=False, on_response=(on_continue, file_path))
529 530 531 532
            dialog2.set_destroy_with_parent(True)
        else:
            dirname = os.path.dirname(file_path)
            if not os.access(dirname, os.W_OK):
533
                ErrorDialog(_('Directory "%s" is not writable') % \
Yann Leboulanger's avatar
Yann Leboulanger committed
534 535
                    dirname, _('You do not have permission to create files in '
                    'this directory.'))
536 537 538 539
                return

        on_continue(0, file_path)

540 541 542 543 544
    transient = app.app.get_active_window()
    AvatarSaveDialog(on_ok,
                     path=app.config.get('last_save_dir'),
                     file_name='%s.png' % default_name,
                     transient_for=transient)
545

546
def create_combobox(value_list, selected_value = None):
547 548 549
    """
    Value_list is [(label1, value1)]
    """
550
    liststore = Gtk.ListStore(str, str)
Dicson's avatar
Dicson committed
551
    combobox = Gtk.ComboBox.new_with_model(liststore)
552
    cell = Gtk.CellRendererText()
Dicson's avatar
Dicson committed
553
    combobox.pack_start(cell, True)
554 555 556 557 558 559 560 561 562 563
    combobox.add_attribute(cell, 'text', 0)
    i = -1
    for value in value_list:
        liststore.append(value)
        if selected_value == value[1]:
            i = value_list.index(value)
    if i > -1:
        combobox.set_active(i)
    combobox.show_all()
    return combobox
564

565
def create_list_multi(value_list, selected_values=None):
566 567 568
    """
    Value_list is [(label1, value1)]
    """
569
    liststore = Gtk.ListStore(str, str)
Dicson's avatar
Dicson committed
570
    treeview = Gtk.TreeView.new_with_model(liststore)
571
    treeview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
572
    treeview.set_headers_visible(False)
573
    col = Gtk.TreeViewColumn()
574
    treeview.append_column(col)
575
    cell = Gtk.CellRendererText()
576
    col.pack_start(cell, True)
577 578 579 580 581 582 583
    col.set_attributes(cell, text=0)
    for value in value_list:
        iter = liststore.append(value)
        if value[1] in selected_values:
            treeview.get_selection().select_iter(iter)
    treeview.show_all()
    return treeview
584 585

def load_iconset(path, pixbuf2=None, transport=False):
586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
    """
    Load full iconset from the given path, and add pixbuf2 on top left of each
    static images
    """
    path += '/'
    if transport:
        list_ = ('online', 'chat', 'away', 'xa', 'dnd', 'offline',
                'not in roster')
    else:
        list_ = ('connecting', 'online', 'chat', 'away', 'xa', 'dnd',
                'invisible', 'offline', 'error', 'requested', 'event', 'opened',
                'closed', 'not in roster', 'muc_active', 'muc_inactive')
        if pixbuf2:
            list_ = ('connecting', 'online', 'chat', 'away', 'xa', 'dnd',
                    'offline', 'error', 'requested', 'event', 'not in roster')
    return _load_icon_list(list_, path, pixbuf2)
602

js's avatar
js committed
603
def load_mood_icon(icon_name):
604 605 606
    """
    Load an icon from the mood iconset in 16x16
    """
607
    iconset = app.config.get('mood_iconset')
608 609 610
    path = os.path.join(helpers.get_mood_iconset_path(iconset), '')
    icon_list = _load_icon_list([icon_name], path)
    return icon_list[icon_name]
611

js's avatar
js committed
612
def load_activity_icon(category, activity = None):
613 614 615
    """
    Load an icon from the activity iconset in 16x16
    """
616
    iconset = app.config.get('activity_iconset')
617 618 619 620 621 622
    path = os.path.join(helpers.get_activity_iconset_path(iconset),
            category, '')
    if activity is None:
        activity = 'category'
    icon_list = _load_icon_list([activity], path)
    return icon_list[activity]
js's avatar
js committed
623

624
def get_pep_as_pixbuf(pep_class):
625
    if pep_class == PEPEventType.MOOD:
626
        received_mood = pep_class._pep_specific_data['mood']
627
        mood = received_mood if received_mood in MOODS else 'unknown'
628 629
        pixbuf = load_mood_icon(mood).get_pixbuf()
        return pixbuf
630
    elif pep_class == PEPEventType.TUNE:
Dicson's avatar
Dicson committed
631
        icon = get_icon_pixmap('audio-x-generic', quiet=True)
632
        if not icon:
633
            path = os.path.join(configpaths.get('DATA'), 'emoticons', 'static',
634 635 636
                'music.png')
            return GdkPixbuf.Pixbuf.new_from_file(path)
        return icon
637
    elif pep_class == PEPEventType.ACTIVITY:
638 639 640
        pep_ = pep_class._pep_specific_data
        activity = pep_['activity']

641
        has_known_activity = activity in ACTIVITIES
642
        has_known_subactivity = (has_known_activity  and ('subactivity' in pep_)
643
                and (pep_['subactivity'] in ACTIVITIES[activity]))
644 645 646 647 648 649 650 651 652

        if has_known_activity:
            if has_known_subactivity:
                subactivity = pep_['subactivity']
                return load_activity_icon(activity, subactivity).get_pixbuf()
            else:
                return load_activity_icon(activity).get_pixbuf()
        else:
            return load_activity_icon('unknown').get_pixbuf()
653
    elif pep_class == PEPEventType.LOCATION:
Dicson's avatar
Dicson committed
654
        icon = get_icon_pixmap('applications-internet', quiet=True)
655
        if not icon:
Dicson's avatar
Dicson committed
656
            icon = get_icon_pixmap('gajim-earth')
657
        return icon
658 659
    return None

660
def get_iconset_name_for(name):
661 662
    if name == 'not in roster':
        name = 'notinroster'
663 664
    iconset = app.config.get('iconset')
    if not iconset:
665 666
        iconset = app.config.DEFAULT_ICONSET
    return '%s-%s' % (iconset, name)
667

668
def load_icons_meta():
669 670 671 672
    """
    Load and return  - AND + small icons to put on top left of an icon for meta
    contacts
    """
673
    iconset = app.config.get('iconset')
674 675 676 677 678 679
    path = os.path.join(helpers.get_iconset_path(iconset), '16x16')
    # try to find opened_meta.png file, else opened.png else nopixbuf merge
    path_opened = os.path.join(path, 'opened_meta.png')
    if not os.path.isfile(path_opened):
        path_opened = os.path.join(path, 'opened.png')
    if os.path.isfile(path_opened):
680
        pixo = GdkPixbuf.Pixbuf.new_from_file(path_opened)
681 682 683 684 685 686 687
    else:
        pixo = None
    # Same thing for closed
    path_closed = os.path.join(path, 'opened_meta.png')
    if not os.path.isfile(path_closed):
        path_closed = os.path.join(path, 'closed.png')
    if os.path.isfile(path_closed):
688
        pixc = GdkPixbuf.Pixbuf.new_from_file(path_closed)
689 690 691
    else:
        pixc = None
    return pixo, pixc
692 693

def _load_icon_list(icons_list, path, pixbuf2 = None):
694 695 696 697 698 699 700 701 702 703 704
    """
    Load icons in icons_list from the given path, and add pixbuf2 on top left of
    each static images
    """
    imgs = {}
    for icon in icons_list:
        # try to open a pixfile with the correct method
        icon_file = icon.replace(' ', '_')
        files = []
        files.append(path + icon_file + '.gif')
        files.append(path + icon_file + '.png')
705
        image = Gtk.Image()
706 707 708 709 710
        image.show()
        imgs[icon] = image
        for file_ in files: # loop seeking for either gif or png
            if os.path.exists(file_):
                image.set_from_file(file_)
711
                if pixbuf2 and image.get_storage_type() == Gtk.ImageType.PIXBUF:
712 713 714 715 716
                    # add pixbuf2 on top-left corner of image
                    pixbuf1 = image.get_pixbuf()
                    pixbuf2.composite(pixbuf1, 0, 0,
                            pixbuf2.get_property('width'),
                            pixbuf2.get_property('height'), 0, 0, 1.0, 1.0,
717
                            GdkPixbuf.InterpType.NEAREST, 255)
718 719 720
                    image.set_from_pixbuf(pixbuf1)
                break
    return imgs
721 722

def make_jabber_state_images():
723 724 725
    """
    Initialize jabber_state_images dictionary
    """
726
    iconset = app.config.get('iconset')
727 728 729 730
    if iconset:
        if helpers.get_iconset_path(iconset):
            path = os.path.join(helpers.get_iconset_path(iconset), '16x16')
            if not os.path.exists(path):
731 732
                iconset = app.config.DEFAULT_ICONSET
                app.config.set('iconset', iconset)
733
        else:
734 735
            iconset = app.config.DEFAULT_ICONSET
            app.config.set('iconset', iconset)
736
    else:
737 738
        iconset = app.config.DEFAULT_ICONSET
        app.config.set('iconset', iconset)
739

740
    path = os.path.join(helpers.get_iconset_path(iconset), '16x16')
741
    app.interface.jabber_state_images['16'] = load_iconset(path)
742 743

    pixo, pixc = load_icons_meta()
744 745
    app.interface.jabber_state_images['opened'] = load_iconset(path, pixo)
    app.interface.jabber_state_images['closed'] = load_iconset(path, pixc)
746

747
    path = os.path.join(helpers.get_iconset_path(iconset), '32x32')
748
    app.interface.jabber_state_images['32'] = load_iconset(path)
749

750 751
    path = os.path.join(helpers.get_iconset_path(iconset), '24x24')
    if (os.path.exists(path)):
752
        app.interface.jabber_state_images['24'] = load_iconset(path)
753 754
    else:
        # Resize 32x32 icons to 24x24
755
        for each in app.interface.jabber_state_images['32']:
756
            img = Gtk.Image()
757
            pix = app.interface.jabber_state_images['32'][each]
758
            pix_type = pix.get_storage_type()
759
            if pix_type == Gtk.ImageType.ANIMATION:
760 761
                animation = pix.get_animation()
                pixbuf = animation.get_static_image()
762
            elif pix_type == Gtk.ImageType.EMPTY:
763
                pix = app.interface.jabber_state_images['16'][each]
764
                pix_16_type = pix.get_storage_type()
765
                if pix_16_type == Gtk.ImageType.ANIMATION:
766 767
                    animation = pix.get_animation()
                    pixbuf = animation.get_static_image()
Yann Leboulanger's avatar
Yann Leboulanger committed
768 769
                else:
                    pixbuf = pix.get_pixbuf()
770 771
            else:
                pixbuf = pix.get_pixbuf()
772
            scaled_pix = pixbuf.scale_simple(24, 24, GdkPixbuf.InterpType.BILINEAR)
773
            img.set_from_pixbuf(scaled_pix)
774
            app.interface.jabber_state_images['24'][each] = img
775

776
def reload_jabber_state_images():
777
    make_jabber_state_images()
778
    app.interface.roster.update_jabber_state_images()
779

780
def label_set_autowrap(widget):
781 782 783 784
    """
    Make labels automatically re-wrap if their containers are resized.
    Accepts label or container widgets
    """
785
    if isinstance (widget, Gtk.Container):
786
        children = widget.get_children()
787
        for i in list(range (len (children))):
788
            label_set_autowrap(children[i])
789
    elif isinstance(widget, Gtk.Label):
790 791
        widget.set_line_wrap(True)
        widget.connect_after('size-allocate', __label_size_allocate)
792 793

def __label_size_allocate(widget, allocation):
794 795 796 797 798 799 800
    """
    Callback which re-allocates the size of a label
    """
    layout = widget.get_layout()

    lw_old, lh_old = layout.get_size()
    # fixed width labels
801
    if lw_old/Pango.SCALE == allocation.width:
802 803
        return

804
    # set wrap width to the Pango.Layout of the labels ###
805
    widget.set_alignment(0.0, 0.0)
806
    layout.set_width (allocation.width * Pango.SCALE)
Yann Leboulanger's avatar
Yann Leboulanger committed
807
    lh = layout.get_size()[1]
808 809

    if lh_old != lh:
810
        widget.set_size_request (-1, lh / Pango.SCALE)
811 812

def get_action(action):
813
    return app.app.lookup_action(action)
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833

def load_css():
    path = os.path.join(configpaths.get('DATA'), 'style', 'gajim.css')
    try:
        with open(path, "r") as f:
            css = f.read()
    except Exception as exc:
        print('Error loading css: %s', exc)
        return

    provider = Gtk.CssProvider()
    css = "\n".join((css, convert_config_to_css()))
    provider.load_from_data(bytes(css.encode()))
    Gtk.StyleContext.add_provider_for_screen(
        Gdk.Screen.get_default(),
        provider,
        Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

def convert_config_to_css():
    css = ''
834 835 836 837
    themed_widgets = {
        'ChatControl-BannerEventBox': ('bannerbgcolor', 'background'),
        'ChatControl-BannerNameLabel': ('bannertextcolor', 'color'),
        'ChatControl-BannerLabel': ('bannertextcolor', 'color'),
838 839 840
        'GroupChatControl-BannerEventBox': ('bannerbgcolor', 'background'),
        'GroupChatControl-BannerNameLabel': ('bannertextcolor', 'color'),
        'GroupChatControl-BannerLabel': ('bannertextcolor', 'color'),
841 842 843
        'Discovery-BannerEventBox': ('bannerbgcolor', 'background'),
        'Discovery-BannerLabel': ('bannertextcolor', 'color')}

844 845 846 847
    classes = {'state_composing_color': ('', 'color'),
               'state_inactive_color': ('', 'color'),
               'state_gone_color': ('', 'color'),
               'state_paused_color': ('', 'color'),
848 849 850 851
               'msgcorrectingcolor': ('text', 'background'),
               'state_muc_directed_msg_color': ('', 'color'),
               'state_muc_msg_color': ('', 'color')}

852

853
    theme = app.config.get('roster_theme')
854 855 856
    for key, values in themed_widgets.items():
        config, attr = values
        css += '#{} {{'.format(key)
857
        value = app.config.get_per('themes', theme, config)
858 859 860 861 862 863
        if value:
            css += '{attr}: {color};\n'.format(attr=attr, color=value)
        css += '}\n'

    for key, values in classes.items():
        node, attr = values
864
        value = app.config.get_per('themes', theme, key)
865 866 867 868
        if value:
            css += '.theme_{cls} {node} {{ {attr}: {color}; }}\n'.format(
                cls=key, node=node, attr=attr, color=value)

869 870
    css += add_css_font()

871
    return css
872 873 874 875 876 877

def add_css_class(widget, class_name):
    style = widget.get_style_context()
    for css_cls in style.list_classes():
        if css_cls.startswith('theme_'):
            style.remove_class(css_cls)
878 879
    if class_name:
        style.add_class('theme_' + class_name)
880

881 882 883 884 885 886 887
def add_css_to_widget(widget, css):
    provider = Gtk.CssProvider()
    provider.load_from_data(bytes(css.encode()))
    context = widget.get_style_context()
    context.add_provider(provider,
                         Gtk.STYLE_PROVIDER_PRIORITY_USER)

888 889 890
def remove_css_class(widget, class_name):
    style = widget.get_style_context()
    style.remove_class('theme_' + class_name)
891 892

def add_css_font():
893
    conversation_font = app.config.get('conversation_font')
894 895 896 897 898 899
    if not conversation_font:
        return ''
    font = Pango.FontDescription(conversation_font)
    unit = "pt" if Gtk.check_version(3, 22, 0) is None else "px"
    css = """
    .font_custom {{
900
      font-family: "{family}";
901 902 903 904 905 906
      font-size: {size}{unit};
      font-weight: {weight};
    }}""".format(
        family=font.get_family(),
        size=int(round(font.get_size() / Pango.SCALE)),
        unit=unit,
907
        weight=pango_to_css_weight(font.get_weight()))
908 909 910 911
    css = css.replace("font-size: 0{unit};".format(unit=unit), "")
    css = css.replace("font-weight: 0;", "")
    css = "\n".join(filter(lambda x: x.strip(), css.splitlines()))
    return css
912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931

def draw_affiliation(surface, affiliation):
    icon_size = 16
    size = 4 * 1
    if affiliation not in ('owner', 'admin', 'member'):
        return
    ctx = cairo.Context(surface)
    ctx.rectangle(icon_size-size, icon_size-size, size, size)
    if affiliation == 'owner':
        ctx.set_source_rgb(204/255, 0, 0)
    elif affiliation == 'admin':
        ctx.set_source_rgb(255/255, 140/255, 0)
    elif affiliation == 'member':
        ctx.set_source_rgb(0, 255/255, 0)
    ctx.fill()

def get_image_from_icon_name(icon_name, scale):
    icon = get_iconset_name_for(icon_name)
    surface = gtk_icon_theme.load_surface(icon, 16, scale, None, 0)
    return Gtk.Image.new_from_surface(surface)
932 933 934 935 936 937 938 939 940 941

def pango_to_css_weight(number):
    # Pango allows for weight values between 100 and 1000
    # CSS allows only full hundred numbers like 100, 200 ..
    number = int(number)
    if number < 100:
        return 100
    if number > 900:
        return 900
    return int(math.ceil(number / 100.0)) * 100
Philipp Hörist's avatar
Philipp Hörist committed
942 943 944 945 946 947 948 949

def get_monitor_scale_factor():
    display = Gdk.Display.get_default()
    monitor = display.get_primary_monitor()
    if monitor is None:
        log.warning('Could not determine scale factor')
        return 1
    return monitor.get_scale_factor()