Version 0.0.1
This commit is contained in:
commit
fe5874746b
25
README.md
Normal file
25
README.md
Normal file
|
@ -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)
|
1
__init__.py
Normal file
1
__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .plugin import AvatarShapePlugin
|
BIN
img/circle.png
Normal file
BIN
img/circle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
BIN
img/pentagon.png
Normal file
BIN
img/pentagon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
img/rounded_corners.png
Normal file
BIN
img/rounded_corners.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
BIN
img/square.png
Normal file
BIN
img/square.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
BIN
img/tapered_square.png
Normal file
BIN
img/tapered_square.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
BIN
img/triangle.png
Normal file
BIN
img/triangle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
20
plugin-manifest.json
Normal file
20
plugin-manifest.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Bohdan Horbeshko <bodqhrohro@gmail.com>"
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
248
plugin.py
Normal file
248
plugin.py
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
# 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 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
|
Loading…
Reference in a new issue