# Copyright (C) 2003-2014 Yann Leboulanger # Copyright (C) 2005-2006 Dimitur Kirov # Copyright (C) 2005-2007 Nikos Kouremenos # Copyright (C) 2006 Travis Shirk # Copyright (C) 2006-2007 Junglecow J # Copyright (C) 2006-2008 Jean-Marie Traissard # Copyright (C) 2007 James Newton # Julien Pivotto # Copyright (C) 2007-2008 Stephan Erb # Copyright (C) 2008 Jonathan Schleifer # # This file is part of Gajim. # # Gajim is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # # Gajim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Gajim. If not, see . import cairo import os import sys import math import logging from io import BytesIO import xml.etree.ElementTree as ET import xml.sax.saxutils from xml.sax import ContentHandler # type: ignore from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GdkPixbuf from gi.repository import GLib from gi.repository import Pango try: from PIL import Image except Exception: pass from gajim.common import i18n from gajim.common import app from gajim.common import configpaths from gajim.common.const import PEPEventType, ACTIVITIES, MOODS log = logging.getLogger('gajim.gtkgui_helpers') gtk_icon_theme = Gtk.IconTheme.get_default() gtk_icon_theme.append_search_path(configpaths.get('ICONS')) class Color: BLACK = Gdk.RGBA(red=0, green=0, blue=0, alpha=1) 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) GREY = Gdk.RGBA(red=195/255, green=195/255, blue=192/255, alpha=1) ORANGE = Gdk.RGBA(red=245/255, green=121/255, blue=0/255, alpha=1) def get_icon_pixmap(icon_name, size=16, color=None, quiet=False): try: iconinfo = gtk_icon_theme.lookup_icon(icon_name, size, 0) if not iconinfo: raise GLib.GError if color: pixbuf, _was_symbolic = iconinfo.load_symbolic(*color) return pixbuf return iconinfo.load_icon() except GLib.GError as error: if not quiet: log.error('Unable to load icon %s: %s', icon_name, str(error)) def get_icon_path(icon_name, size=16): try: icon_info = gtk_icon_theme.lookup_icon(icon_name, size, 0) if icon_info is None: log.error('Icon not found: %s', icon_name) return '' return icon_info.get_filename() except GLib.GError as error: log.error('Unable to find icon %s: %s', icon_name, str(error)) HAS_PYWIN32 = True if os.name == 'nt': try: import win32file import win32con import pywintypes except ImportError: HAS_PYWIN32 = False from gajim.common import helpers def get_total_screen_geometry(): screen = Gdk.Screen.get_default() window = Gdk.Screen.get_root_window(screen) w, h = window.get_width(), window.get_height() log.debug('Get screen geometry: %s %s', w, h) return w, h def add_image_to_button(button, icon_name): img = Gtk.Image() path_img = get_icon_path(icon_name) img.set_from_file(path_img) button.set_image(img) def get_image_button(icon_name, tooltip, toggle=False): if toggle: button = Gtk.ToggleButton() icon = get_icon_pixmap(icon_name) image = Gtk.Image() image.set_from_pixbuf(icon) button.set_image(image) else: button = Gtk.Button.new_from_icon_name( icon_name, Gtk.IconSize.MENU) button.set_tooltip_text(tooltip) return button def get_gtk_builder(file_name, widget=None): file_path = os.path.join(configpaths.get('GUI'), file_name) builder = Gtk.Builder() builder.set_translation_domain(i18n.DOMAIN) if sys.platform == "win32": # This is a workaround for non working translation on Windows tree = ET.parse(file_path) for node in tree.iter(): if 'translatable' in node.attrib and node.text is not None: node.text = _(node.text) xml_text = ET.tostring(tree.getroot(), encoding='unicode', method='xml') if widget is not None: builder.add_objects_from_string(xml_text, [widget]) else: # Workaround # https://gitlab.gnome.org/GNOME/pygobject/issues/255 Gtk.Builder.__mro__[1].add_from_string( builder, xml_text, len(xml_text.encode("utf-8"))) else: if widget is not None: builder.add_objects_from_file(file_path, [widget]) else: builder.add_from_file(file_path) return builder def get_completion_liststore(entry): """ Create a completion model for entry widget completion list consists of (Pixbuf, Text) rows """ completion = Gtk.EntryCompletion() liststore = Gtk.ListStore(str, str) render_pixbuf = Gtk.CellRendererPixbuf() completion.pack_start(render_pixbuf, False) completion.add_attribute(render_pixbuf, 'icon_name', 0) render_text = Gtk.CellRendererText() completion.pack_start(render_text, True) completion.add_attribute(render_text, 'text', 1) completion.set_property('text_column', 1) completion.set_model(liststore) entry.set_completion(completion) return liststore def move_window(window, x, y): """ Move the window, but also check if out of screen """ screen_w, screen_h = get_total_screen_geometry() 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) def resize_window(window, w, h): """ Resize window, but also checks if huge window or negative values """ screen_w, screen_h = get_total_screen_geometry() 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)) 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() return adj_v.get_value() == max_scroll_pos 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() if adj_v is None: # This can happen when the Widget is already destroyed when called # from GLib.idle_add return False 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 class ServersXMLHandler(ContentHandler): def __init__(self): ContentHandler.__init__(self) self.servers = [] def startElement(self, name, attributes): if name == 'item': if 'jid' in attributes.getNames(): self.servers.append(attributes.getValue('jid')) def endElement(self, name): pass def parse_server_xml(path_to_file): try: handler = ServersXMLHandler() xml.sax.parse(path_to_file, handler) return handler.servers # handle exception if unable to open file except IOError as message: print(_('Error reading file:') + str(message), file=sys.stderr) # handle exception parsing file except xml.sax.SAXParseException as message: print(_('Error parsing file:') + str(message), file=sys.stderr) def set_unset_urgency_hint(window, unread_messages_no): """ Sets/unset urgency hint in window argument depending if we have unread messages or not """ if app.config.get('use_urgency_hint'): window.props.urgency_hint = unread_messages_no > 0 def get_pixbuf_from_data(file_data): """ Get image data and returns GdkPixbuf.Pixbuf """ pixbufloader = GdkPixbuf.PixbufLoader() try: pixbufloader.write(file_data) pixbufloader.close() pixbuf = pixbufloader.get_pixbuf() except GLib.GError: pixbufloader.close() log.warning('loading avatar using pixbufloader failed, trying to ' 'convert avatar image using pillow') try: avatar = Image.open(BytesIO(file_data)).convert("RGBA") array = GLib.Bytes.new(avatar.tobytes()) width, height = avatar.size 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 def get_cursor(attr): display = Gdk.Display.get_default() cursor = getattr(Gdk.CursorType, attr) return Gdk.Cursor.new_for_display(display, cursor) def get_current_desktop(window): """ Return the current virtual desktop for given window 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') 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 def file_is_locked(path_to_file): """ 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( path_to_file, # path to file win32con.GENERIC_READ, # open for reading 0, # do not share with other proc secur_att, win32con.OPEN_EXISTING, # existing file only win32con.FILE_ATTRIBUTE_NORMAL, # normal file 0 # no attr. template ) except pywintypes.error: return True else: # in case all went ok, close file handle (go to hell WinAPI) hfile.Close() return False def get_fade_color(treeview, selected, focused): """ Get a gdk RGBA color that is between foreground and background in 0.3 0.7 respectively colors of the cell for the given treeview """ context = treeview.get_style_context() if selected: if focused: # is the window focused? state = Gtk.StateFlags.SELECTED else: # is it not? NOTE: many gtk themes change bg on this state = Gtk.StateFlags.ACTIVE else: state = Gtk.StateFlags.NORMAL bg = context.get_property('background-color', state) fg = context.get_color(state) p = 0.3 # background q = 0.7 # foreground # p + q should do 1.0 return Gdk.RGBA(bg.red*p + fg.red*q, bg.green*p + fg.green*q, bg.blue*p + fg.blue*q) def make_gtk_month_python_month(month): """ GTK starts counting months from 0, so January is 0 but Python's time start from 1, so align to Python NOTE: Month MUST be an integer. """ return month + 1 def make_python_month_gtk_month(month): return month - 1 def make_pixbuf_grayscale(pixbuf): pixbuf2 = pixbuf.copy() pixbuf.saturate_and_pixelate(pixbuf2, 0.0, False) return pixbuf2 def get_possible_button_event(event): """ Mouse or keyboard caused the event? """ if event.type == Gdk.EventType.KEY_PRESS: return 0 # no event.button so pass 0 # BUTTON_PRESS event, so pass event.button return event.button def destroy_widget(widget): widget.destroy() def scale_with_ratio(size, width, height): if height == width: return size, size if height > width: ratio = height / float(width) return int(size / ratio), size ratio = width / float(height) return size, int(size / ratio) 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) def on_avatar_save_as_menuitem_activate(widget, avatar, default_name=''): from gajim.gtk.dialogs import ErrorDialog from gajim.gtk.dialogs import ConfirmationDialog from gajim.gtk.dialogs import FTOverwriteConfirmationDialog from gajim.gtk.filechoosers import AvatarSaveDialog def on_continue(response, file_path): if response < 0: return app.config.set('last_save_dir', os.path.dirname(file_path)) if isinstance(avatar, str): # We got a SHA pixbuf = app.interface.get_avatar(avatar) else: # We got a pixbuf pixbuf = avatar extension = os.path.splitext(file_path)[1] if not extension: # Silently save as Jpeg image image_format = 'png' file_path += '.png' else: image_format = extension[1:] # remove leading dot # Save image try: pixbuf.savev(file_path, image_format, [], []) except Exception as error: log.error('Error saving avatar: %s', error) if os.path.exists(file_path): os.remove(file_path) new_file_path = '.'.join(file_path.split('.')[:-1]) + '.png' def on_ok(file_path, pixbuf): pixbuf.savev(file_path, 'png', [], []) ConfirmationDialog(_('Extension not supported'), _('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)) def on_ok(file_path): 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) ErrorDialog(_('Cannot overwrite existing file "%s"') % \ file_name, _('A file with this name already exists and you ' 'do not have permission to overwrite it.')) return dialog2 = FTOverwriteConfirmationDialog( _('This file already exists'), _('What do you want to do?'), propose_resume=False, on_response=(on_continue, file_path)) dialog2.set_destroy_with_parent(True) else: dirname = os.path.dirname(file_path) if not os.access(dirname, os.W_OK): ErrorDialog(_('Directory "%s" is not writable') % \ dirname, _('You do not have permission to create files in ' 'this directory.')) return on_continue(0, file_path) 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) def create_combobox(value_list, selected_value=None): """ Value_list is [(label1, value1)] """ liststore = Gtk.ListStore(str, str) combobox = Gtk.ComboBox.new_with_model(liststore) cell = Gtk.CellRendererText() combobox.pack_start(cell, True) 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 def create_list_multi(value_list, selected_values=None): """ Value_list is [(label1, value1)] """ liststore = Gtk.ListStore(str, str) treeview = Gtk.TreeView.new_with_model(liststore) treeview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) treeview.set_headers_visible(False) col = Gtk.TreeViewColumn() treeview.append_column(col) cell = Gtk.CellRendererText() col.pack_start(cell, True) 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 def load_iconset(path, pixbuf2=None, transport=False): """ 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) def load_mood_icon(icon_name): """ Load an icon from the mood iconset in 16x16 """ iconset = app.config.get('mood_iconset') path = os.path.join(helpers.get_mood_iconset_path(iconset), '') icon_list = _load_icon_list([icon_name], path) return icon_list[icon_name] def load_activity_icon(category, activity=None): """ Load an icon from the activity iconset in 16x16 """ iconset = app.config.get('activity_iconset') 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] def get_pep_as_pixbuf(pep_class): if pep_class == PEPEventType.MOOD: received_mood = pep_class.data['mood'] mood = received_mood if received_mood in MOODS else 'unknown' pixbuf = load_mood_icon(mood).get_pixbuf() return pixbuf if pep_class == PEPEventType.TUNE: icon = get_icon_pixmap('audio-x-generic', quiet=True) if not icon: path = os.path.join( configpaths.get('DATA'), 'emoticons', 'static', 'music.png') return GdkPixbuf.Pixbuf.new_from_file(path) return icon if pep_class == PEPEventType.ACTIVITY: pep_ = pep_class.data activity = pep_['activity'] has_known_activity = activity in ACTIVITIES has_known_subactivity = (has_known_activity and 'subactivity' in pep_ and pep_['subactivity'] in ACTIVITIES[activity]) if has_known_activity: if has_known_subactivity: subactivity = pep_['subactivity'] return load_activity_icon(activity, subactivity).get_pixbuf() return load_activity_icon(activity).get_pixbuf() return load_activity_icon('unknown').get_pixbuf() if pep_class == PEPEventType.LOCATION: icon = get_icon_pixmap('applications-internet', quiet=True) if not icon: icon = get_icon_pixmap('gajim-earth') return icon return None def get_iconset_name_for(name): if name == 'not in roster': name = 'notinroster' iconset = app.config.get('iconset') if not iconset: iconset = app.config.DEFAULT_ICONSET return '%s-%s' % (iconset, name) def load_icons_meta(): """ Load and return - AND + small icons to put on top left of an icon for meta contacts """ iconset = app.config.get('iconset') 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): pixo = GdkPixbuf.Pixbuf.new_from_file(path_opened) 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): pixc = GdkPixbuf.Pixbuf.new_from_file(path_closed) else: pixc = None return pixo, pixc def _load_icon_list(icons_list, path, pixbuf2=None): """ 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') image = Gtk.Image() 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_) if pixbuf2 and image.get_storage_type() == Gtk.ImageType.PIXBUF: # 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, GdkPixbuf.InterpType.NEAREST, 255) image.set_from_pixbuf(pixbuf1) break return imgs def make_jabber_state_images(): """ Initialize jabber_state_images dictionary """ iconset = app.config.get('iconset') if iconset: if helpers.get_iconset_path(iconset): path = os.path.join(helpers.get_iconset_path(iconset), '16x16') if not os.path.exists(path): iconset = app.config.DEFAULT_ICONSET app.config.set('iconset', iconset) else: iconset = app.config.DEFAULT_ICONSET app.config.set('iconset', iconset) else: iconset = app.config.DEFAULT_ICONSET app.config.set('iconset', iconset) path = os.path.join(helpers.get_iconset_path(iconset), '16x16') app.interface.jabber_state_images['16'] = load_iconset(path) pixo, pixc = load_icons_meta() app.interface.jabber_state_images['opened'] = load_iconset(path, pixo) app.interface.jabber_state_images['closed'] = load_iconset(path, pixc) path = os.path.join(helpers.get_iconset_path(iconset), '32x32') app.interface.jabber_state_images['32'] = load_iconset(path) path = os.path.join(helpers.get_iconset_path(iconset), '24x24') if os.path.exists(path): app.interface.jabber_state_images['24'] = load_iconset(path) else: # Resize 32x32 icons to 24x24 for each in app.interface.jabber_state_images['32']: img = Gtk.Image() pix = app.interface.jabber_state_images['32'][each] pix_type = pix.get_storage_type() if pix_type == Gtk.ImageType.ANIMATION: animation = pix.get_animation() pixbuf = animation.get_static_image() elif pix_type == Gtk.ImageType.EMPTY: pix = app.interface.jabber_state_images['16'][each] pix_16_type = pix.get_storage_type() if pix_16_type == Gtk.ImageType.ANIMATION: animation = pix.get_animation() pixbuf = animation.get_static_image() else: pixbuf = pix.get_pixbuf() else: pixbuf = pix.get_pixbuf() scaled_pix = pixbuf.scale_simple(24, 24, GdkPixbuf.InterpType.BILINEAR) img.set_from_pixbuf(scaled_pix) app.interface.jabber_state_images['24'][each] = img def reload_jabber_state_images(): make_jabber_state_images() app.interface.roster.update_jabber_state_images() def label_set_autowrap(widget): """ Make labels automatically re-wrap if their containers are resized. Accepts label or container widgets """ if isinstance(widget, Gtk.Container): children = widget.get_children() for i in list(range(len(children))): label_set_autowrap(children[i]) elif isinstance(widget, Gtk.Label): widget.set_line_wrap(True) widget.connect_after('size-allocate', __label_size_allocate) def __label_size_allocate(widget, allocation): """ Callback which re-allocates the size of a label """ layout = widget.get_layout() lw_old, lh_old = layout.get_size() # fixed width labels if lw_old/Pango.SCALE == allocation.width: return # set wrap width to the Pango.Layout of the labels ### widget.set_alignment(0.0, 0.0) layout.set_width(allocation.width * Pango.SCALE) lh = layout.get_size()[1] if lh_old != lh: widget.set_size_request(-1, lh / Pango.SCALE) def get_action(action): return app.app.lookup_action(action) def add_css_class(widget, class_name, prefix=None): if class_name and prefix: class_name = prefix + class_name style = widget.get_style_context() if prefix is not None: # Remove all css classes with prefix for css_cls in style.list_classes(): if css_cls.startswith(prefix): style.remove_class(css_cls) if class_name is not None: style.add_class(class_name) 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) def remove_css_class(widget, class_name): style = widget.get_style_context() style.remove_class(class_name) 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) 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 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()