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