gajim-avatarshapeplugin/plugin.py

248 lines
7.5 KiB
Python

# Copyright (C) 2024 Bohdan Horbeshko <bodqhrohro@gmail.com>
# This file is part of Gajim Avatar Shape Plugin.
#
# Gajim Avatar Shape Plugin 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.
#
# This software 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 can always obtain full license text at <http://www.gnu.org/licenses/>.
import logging
from functools import partial
from gajim.plugins import GajimPlugin
from gajim.common import app
import gajim.gtk.avatar as avatar
import cairo
from gi.repository import Gtk, Gdk
from math import pi, sin, cos
def _clone_surface_to_context(surface: cairo.ImageSurface) -> cairo.Context:
width = surface.get_width()
height = surface.get_height()
scale = surface.get_device_scale()
new_surface = cairo.ImageSurface(cairo.Format.ARGB32,
width,
height)
new_surface.set_device_scale(*scale)
context = cairo.Context(new_surface)
context.set_source_surface(surface, 0, 0)
return context, width / scale[0], height / scale[0]
def circle(surface: cairo.ImageSurface, mode: str) -> cairo.ImageSurface:
context, width, height = _clone_surface_to_context(surface)
radius = width / 2
context.arc(width / 2, height / 2, radius, 0, 2 * pi)
context.clip()
context.paint()
return context.get_target()
def rounded_corners(surface: cairo.ImageSurface, mode: str) -> cairo.ImageSurface:
context, width, height = _clone_surface_to_context(surface)
radius = 9
degrees = pi / 180
context.new_sub_path()
context.arc(width - radius, radius, radius, -90 * degrees, 0 * degrees)
context.arc(width - radius, height - radius, radius, 0 * degrees, 90 * degrees) # noqa: E501
context.arc(radius, height - radius, radius, 90 * degrees, 180 * degrees)
context.arc(radius, radius, radius, 180 * degrees, 270 * degrees)
context.close_path()
context.clip()
context.paint()
return context.get_target()
def triangle(surface: cairo.ImageSurface, mode: str) -> cairo.ImageSurface:
context, width, height = _clone_surface_to_context(surface)
r = width / 2
degrees = pi / 180
sin60 = sin(60 * degrees)
context.new_sub_path()
context.move_to(r, r / 4)
context.line_to(r + r * sin60, r * 7 / 4)
context.line_to(r - r * sin60, r * 7 / 4)
context.line_to(r, r / 4)
context.close_path()
context.clip()
context.paint()
return context.get_target()
def pentagon(surface: cairo.ImageSurface, mode: str) -> cairo.ImageSurface:
context, width, height = _clone_surface_to_context(surface)
r = width / 2
degrees = pi / 180
sin36 = sin(36 * degrees)
cos36 = cos(36 * degrees)
sin72 = sin(72 * degrees)
cos72 = cos(72 * degrees)
offset_y = (r - r * cos36) / 2
context.new_sub_path()
context.move_to(r, offset_y)
context.line_to(r + r * sin72, offset_y + r - r * cos72)
context.line_to(r + r * sin36, offset_y + r + r * cos36)
context.line_to(r - r * sin36, offset_y + r + r * cos36)
context.line_to(r - r * sin72, offset_y + r - r * cos72)
context.line_to(r, offset_y)
context.close_path()
context.clip()
context.paint()
return context.get_target()
def tapered_square(surface: cairo.ImageSurface, mode: str) -> cairo.ImageSurface:
context, width, height = _clone_surface_to_context(surface)
corner = width / 6
context.new_sub_path()
context.move_to(corner, 0)
context.line_to(width - corner, 0)
context.line_to(width, corner)
context.line_to(width, height - corner)
context.line_to(width - corner, height)
context.line_to(corner, height)
context.line_to(0, height - corner)
context.line_to(0, corner)
context.line_to(corner, 0)
context.close_path()
context.clip()
context.paint()
return context.get_target()
def square(surface: cairo.ImageSurface, mode: str) -> cairo.ImageSurface:
return surface
shapes = {
'Square': square,
'Rounded corners': rounded_corners,
'Circle': circle,
'Triangle': triangle,
'Pentagon': pentagon,
'Tapered square': tapered_square,
}
class AugmentedRadioButton(Gtk.RadioButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._value = kwargs['label']
# ...
class AvatarShapePlugin(GajimPlugin):
log = logging.getLogger('gajim.p.avatarshape')
_original_clip = None
# init plugin #
def init(self):
self.activatable = True
self.description = 'Allows to alter avatar shape'
self.config_dialog = partial(AvatarShapePluginConfigDialog, self)
self.config_default_values = {
'shape': 'Square',
}
def activate(self):
self._original_clip = avatar.clip
self.set_shape_func(shapes.get(self.config['shape'], square))
def deactivate(self):
if self._original_clip is not None:
self.set_shape_func(self._original_clip)
def set_shape_func(self, func):
avatar.clip = func
if hasattr(app.app, 'avatar_storage'):
app.app.avatar_storage._cache.clear()
avatar.generate_default_avatar.cache_clear()
avatar.make_workspace_avatar.cache_clear()
for client in app.get_clients():
for contact in client.get_module('Contacts')._contacts.values():
contact.notify('avatar-update')
if contact.is_groupchat:
for resource in contact._resources.values():
resource.notify('user-avatar-update')
if app.window is not None:
for workspace in app.window._workspace_side_bar._workspaces.values():
workspace.update_avatar()
class AvatarShapePluginConfigDialog(Gtk.ApplicationWindow):
def __init__(self, plugin: AvatarShapePlugin, transient: Gtk.Window) -> None:
Gtk.ApplicationWindow.__init__(self)
self.set_application(app.app)
self.set_show_menubar(False)
self.set_title('Avatar Shape Configuration')
self.set_transient_for(transient)
self.set_default_size(400, 400)
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
self.set_modal(False)
self.set_destroy_with_parent(True)
self._plugin = plugin
self.connect('key-press-event', self._on_key_press_event)
vbox = Gtk.VBox()
self.add(vbox)
group = None
self._radios = {}
for label, func in shapes.items():
radiobutton = AugmentedRadioButton(label=label, group=group)
if group is None:
group = radiobutton
if label == plugin.config['shape']:
radiobutton.set_active(True)
radiobutton.connect('toggled', self._on_radio_button_toggled)
vbox.pack_start(radiobutton, False, False, 0)
self._radios[radiobutton] = func
self.show_all()
def _on_key_press_event(self, widget, event):
if event.keyval == Gdk.KEY_Escape:
self.destroy()
def _on_radio_button_toggled(self, radiobutton):
if radiobutton.get_active():
self._plugin.config['shape'] = radiobutton._value
self._plugin.set_shape_func(self._radios[radiobutton])
def on_run(self):
pass