commit fe5874746b120dfb362013c3218403b214e397bb Author: Bohdan Horbeshko Date: Sat Jun 1 16:56:51 2024 +0300 Version 0.0.1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..29a5218 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +Allows to configure the shape of avatars instead of boring circle. + +#### Square: + +![Square](img/square.png) + +#### Rounded corners: + +![Rounded corners](img/rounded_corners.png) + +#### Circle: + +![Circle](img/circle.png) + +#### Triangle: + +![Triangle](img/triangle.png) + +#### Pentagon: + +![Pentagon](img/pentagon.png) + +#### Tapered square: + +![Tapered square](img/tapered_square.png) diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..b11cf30 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from .plugin import AvatarShapePlugin diff --git a/img/circle.png b/img/circle.png new file mode 100644 index 0000000..1ba5b4d Binary files /dev/null and b/img/circle.png differ diff --git a/img/pentagon.png b/img/pentagon.png new file mode 100644 index 0000000..a605db4 Binary files /dev/null and b/img/pentagon.png differ diff --git a/img/rounded_corners.png b/img/rounded_corners.png new file mode 100644 index 0000000..1ac5dfc Binary files /dev/null and b/img/rounded_corners.png differ diff --git a/img/square.png b/img/square.png new file mode 100644 index 0000000..0296801 Binary files /dev/null and b/img/square.png differ diff --git a/img/tapered_square.png b/img/tapered_square.png new file mode 100644 index 0000000..f6bbef1 Binary files /dev/null and b/img/tapered_square.png differ diff --git a/img/triangle.png b/img/triangle.png new file mode 100644 index 0000000..29ddfbc Binary files /dev/null and b/img/triangle.png differ diff --git a/plugin-manifest.json b/plugin-manifest.json new file mode 100644 index 0000000..c47380c --- /dev/null +++ b/plugin-manifest.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "Bohdan Horbeshko " + ], + "config_dialog": true, + "description": "Avatar shape plugin", + "homepage": "https://dev.narayana.im/narayana/gajim-avatarshapeplugin", + "name": "avatarshapeplugin", + "platforms": [ + "others", + "linux", + "darwin", + "win32" + ], + "requirements": [ + "gajim>=1.8,<1.9" + ], + "short_name": "avatarshapeplugin", + "version": "0.0.1" +} diff --git a/plugin.py b/plugin.py new file mode 100644 index 0000000..aa3b9b2 --- /dev/null +++ b/plugin.py @@ -0,0 +1,248 @@ +# 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 account in app.settings.get_accounts(): + client = app.get_client(account) + 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