# Copyright (C) 2024 Bohdan Horbeshko # 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 . 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