gajim-plural/gajim/gtkgui_helpers.py

462 lines
16 KiB
Python
Raw Normal View History

# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# 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>
# Copyright (C) 2006-2007 Junglecow J <junglecow AT gmail.com>
# 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>
#
# 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 <http://www.gnu.org/licenses/>.
import os
import sys
import math
import logging
from io import BytesIO
import xml.etree.ElementTree as ET
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
2016-04-02 13:59:42 +02:00
from gi.repository import GLib
from gi.repository import Pango
try:
from PIL import Image
2018-09-16 23:45:01 +02:00
except Exception:
pass
2007-01-06 16:31:30 +01:00
2017-06-13 23:58:06 +02:00
from gajim.common import i18n
from gajim.common.i18n import _
from gajim.common import app
2017-06-13 23:58:06 +02:00
from gajim.common import configpaths
2018-07-05 18:52:43 +02:00
from gajim.common.const import PEPEventType, ACTIVITIES, MOODS
log = logging.getLogger('gajim.gtkgui_helpers')
2009-11-27 16:42:32 +01:00
class Color:
BLACK = Gdk.RGBA(red=0, green=0, blue=0, alpha=1)
2017-07-20 00:01:24 +02:00
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)
2018-01-12 00:21:11 +01:00
HAS_PYWIN32 = True
if os.name == 'nt':
try:
import win32file
import win32con
import pywintypes
except ImportError:
HAS_PYWIN32 = False
2017-06-13 23:58:06 +02:00
from gajim.common import helpers
2009-12-01 15:50:58 +01:00
def get_gtk_builder(file_name, widget=None):
file_path = os.path.join(configpaths.get('GUI'), file_name)
2018-08-06 17:14:07 +02:00
builder = Gtk.Builder()
builder.set_translation_domain(i18n.DOMAIN)
2018-06-23 17:02:23 +02:00
if sys.platform == "win32":
2018-08-06 17:14:07 +02:00
# This is a workaround for non working translation on Windows
tree = ET.parse(file_path)
2018-06-23 17:02:23 +02:00
for node in tree.iter():
2018-09-23 15:33:31 +02:00
if 'translatable' in node.attrib and node.text is not None:
2018-06-23 17:02:23 +02:00
node.text = _(node.text)
xml_text = ET.tostring(tree.getroot(),
encoding='unicode',
method='xml')
2018-08-06 17:14:07 +02:00
if widget is not None:
builder.add_objects_from_string(xml_text, [widget])
2018-08-06 17:14:07 +02:00
else:
2018-09-08 08:40:42 +02:00
# 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")))
2018-06-23 17:02:23 +02:00
else:
if widget is not None:
2018-08-06 17:14:07 +02:00
builder.add_objects_from_file(file_path, [widget])
else:
builder.add_from_file(file_path)
return builder
2018-06-23 17:02:23 +02:00
2005-09-25 20:37:50 +02:00
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 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(
2010-02-10 16:17:09 +01:00
path_to_file, # path to file
win32con.GENERIC_READ, # open for reading
0, # do not share with other proc
secur_att,
2010-02-10 16:17:09 +01:00
win32con.OPEN_EXISTING, # existing file only
win32con.FILE_ATTRIBUTE_NORMAL, # normal file
2010-02-10 16:17:09 +01:00
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
2006-03-29 18:46:12 +02:00
def get_possible_button_event(event):
"""
Mouse or keyboard caused the event?
"""
2012-12-27 21:58:52 +01:00
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()
2018-02-19 19:37:53 +01:00
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
2018-09-18 10:14:04 +02:00
ratio = width / float(height)
return size, int(size / ratio)
2018-02-19 19:37:53 +01:00
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
2018-08-19 17:44:17 +02:00
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
2010-02-10 16:17:09 +01:00
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:
2012-12-31 16:38:32 +01:00
pixbuf.savev(file_path, image_format, [], [])
2018-09-17 18:57:00 +02:00
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', [], [])
2018-07-16 23:22:33 +02:00
ConfirmationDialog(_('Extension not supported'),
2010-02-10 16:17:09 +01:00
_('Image cannot be saved in %(type)s format. Save as '
'%(new_filename)s?') % {'type': image_format,
'new_filename': new_file_path},
2018-09-18 12:06:01 +02:00
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)
2018-07-16 23:22:33 +02:00
ErrorDialog(_('Cannot overwrite existing file "%s"') % \
2010-02-10 16:17:09 +01:00
file_name, _('A file with this name already exists and you '
'do not have permission to overwrite it.'))
return
2018-07-16 23:22:33 +02:00
dialog2 = FTOverwriteConfirmationDialog(
2010-02-10 16:17:09 +01:00
_('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):
2018-07-16 23:22:33 +02:00
ErrorDialog(_('Directory "%s" is not writable') % \
2010-02-10 16:17:09 +01:00
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)
2006-11-18 21:52:28 +01:00
2018-09-18 12:06:01 +02:00
def create_combobox(value_list, selected_value=None):
"""
Value_list is [(label1, value1)]
"""
liststore = Gtk.ListStore(str, str)
2013-01-02 17:57:20 +01:00
combobox = Gtk.ComboBox.new_with_model(liststore)
cell = Gtk.CellRendererText()
2013-01-02 17:57:20 +01:00
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)
2013-01-02 17:57:20 +01:00
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:
2018-09-18 13:54:25 +02:00
iter_ = liststore.append(value)
if value[1] in selected_values:
2018-09-18 13:54:25 +02:00
treeview.get_selection().select_iter(iter_)
treeview.show_all()
return treeview
2008-07-22 23:08:52 +02:00
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]
2018-09-18 12:06:01 +02:00
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]
2008-07-28 22:57:56 +02:00
def get_pep_icon(pep_class):
2018-07-05 18:52:43 +02:00
if pep_class == PEPEventType.MOOD:
2018-09-12 00:01:54 +02:00
received_mood = pep_class.data['mood']
2018-07-05 18:52:43 +02:00
mood = received_mood if received_mood in MOODS else 'unknown'
2013-01-02 13:54:02 +01:00
pixbuf = load_mood_icon(mood).get_pixbuf()
return pixbuf
2018-09-18 10:14:04 +02:00
if pep_class == PEPEventType.TUNE:
return 'audio-x-generic'
2018-09-18 10:14:04 +02:00
if pep_class == PEPEventType.ACTIVITY:
2018-09-12 00:01:54 +02:00
pep_ = pep_class.data
2013-01-02 13:54:02 +01:00
activity = pep_['activity']
has_known_activity = activity in ACTIVITIES
2018-09-18 10:14:04 +02:00
has_known_subactivity = (has_known_activity and
'subactivity' in pep_ and
pep_['subactivity'] in ACTIVITIES[activity])
2013-01-02 13:54:02 +01:00
if has_known_activity:
if has_known_subactivity:
subactivity = pep_['subactivity']
return load_activity_icon(activity, subactivity).get_pixbuf()
2018-09-18 10:14:04 +02:00
return load_activity_icon(activity).get_pixbuf()
return load_activity_icon('unknown').get_pixbuf()
if pep_class == PEPEventType.LOCATION:
return 'applications-internet'
2018-09-18 10:14:04 +02:00
2013-01-02 13:54:02 +01:00
return None
2018-09-18 12:06:01 +02:00
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 label_set_autowrap(widget):
"""
Make labels automatically re-wrap if their containers are resized.
Accepts label or container widgets
"""
2018-09-18 12:06:01 +02:00
if isinstance(widget, Gtk.Container):
children = widget.get_children()
2018-09-18 12:06:01 +02:00
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)
2018-09-18 12:06:01 +02:00
layout.set_width(allocation.width * Pango.SCALE)
2010-02-10 16:17:09 +01:00
lh = layout.get_size()[1]
if lh_old != lh:
2018-09-18 12:06:01 +02:00
widget.set_size_request(-1, lh / Pango.SCALE)
2017-01-26 21:34:54 +01:00
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 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