248 lines
7.5 KiB
Python
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
|