# This program 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, either version 3 of the License, or # (at your option) any later version. # # This program 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 should have received a copy of the GNU General Public License # along with this program. If not, see . """Overlay window for voice""" import random import gettext import logging import math import sys import locale from time import perf_counter import cairo import pkg_resources from .overlay import OverlayWindow from .image_getter import get_surface, draw_img_to_rect, draw_img_to_mask # pylint: disable=wrong-import-order import gi gi.require_version('PangoCairo', '1.0') # pylint: disable=wrong-import-position,wrong-import-order from gi.repository import Pango, PangoCairo, GLib # nopep8 log = logging.getLogger(__name__) t = gettext.translation( 'default', pkg_resources.resource_filename('discover_overlay', 'locales'), fallback=True) _ = t.gettext class VoiceOverlayWindow(OverlayWindow): """Overlay window for voice""" def __init__(self, discover, piggyback=None): OverlayWindow.__init__(self, discover, piggyback) self.avatars = {} self.avatar_masks = {} self.dummy_data = [] mostly_false = [False, False, False, False, False, False, False, True] for i in range(0, 100): speaking = mostly_false[random.randint(0, 7)] scream = '' if random.randint(0, 20) == 2: scream = random.randint(8, 15)*'a' name = f"Player {i} {scream}" self.dummy_data.append({ "id": i, "username": name, "avatar": None, "deaf": mostly_false[random.randint(0, 7)], "mute": mostly_false[random.randint(0, 7)], "speaking": speaking, 'lastspoken': random.randint(2000, 2100) if speaking else random.randint(10, 30), 'friendlyname': name, }) self.show_avatar = True self.avatar_size = 48 self.nick_length = 32 self.text_pad = 6 self.text_font = None self.title_font = None self.text_size = 13 self.text_baseline_adj = 0 self.icon_spacing = 8 self.vert_edge_padding = 0 self.horz_edge_padding = 0 self.only_speaking = None self.highlight_self = None self.order = None self.def_avatar = None self.def_avatar_mask = None self.channel_icon = None self.channel_mask = None self.channel_icon_url = None self.overflow = None self.use_dummy = False self.dummy_count = 10 self.show_title = True self.show_connection = True self.show_disconnected = True self.channel_title = "" self.border_width = 2 self.icon_transparency = 0.0 self.fancy_border = False self.only_speaking_grace_period = 0 self.fade_out_inactive = True self.fade_out_limit = 0.1 self.inactive_time = 10 # Seconds self.inactive_fade_time = 20 # Seconds self.fade_opacity = 1.0 self.fade_start = 0 self.inactive_timeout = None self.fadeout_timeout = None self.round_avatar = True self.icon_only = True self.talk_col = [0.0, 0.6, 0.0, 0.1] self.text_col = [1.0, 1.0, 1.0, 1.0] self.text_hili_col = [1.0, 1.0, 1.0, 1.0] self.norm_col = [0.0, 0.0, 0.0, 0.5] self.wind_col = [0.0, 0.0, 0.0, 0.0] self.mute_col = [0.7, 0.0, 0.0, 1.0] self.mute_bg_col = [0.0, 0.0, 0.0, 0.5] self.hili_col = [0.0, 0.0, 0.0, 0.9] self.border_col = [0.0, 0.0, 0.0, 0.0] self.avatar_bg_col = [0.0, 0.0, 1.0, 1.0] self.userlist = [] self.connection_status = "DISCONNECTED" self.horizontal = False self.guild_ids = tuple() self.force_location() get_surface(self.recv_avatar, "discover-overlay-default", 'def', self.avatar_size) self.set_title("Discover Voice") self.redraw() def reset_action_timer(self): """Reset time since last voice activity""" self.fade_opacity = 1.0 # Remove both fading-out effect and timer set last time this happened if self.inactive_timeout: GLib.source_remove(self.inactive_timeout) self.inactive_timeout = None if self.fadeout_timeout: GLib.source_remove(self.fadeout_timeout) self.fadeout_timeout = None # If we're using this feature, schedule a new inactivity timer if self.fade_out_inactive: self.inactive_timeout = GLib.timeout_add_seconds( self.inactive_time, self.overlay_inactive) def overlay_inactive(self): """Timed callback when inactivity limit is hit""" self.fade_start = perf_counter() # Fade out in 200 steps over X seconds. self.fadeout_timeout = GLib.timeout_add( self.inactive_fade_time/200 * 1000, self.overlay_fadeout) self.inactive_timeout = None return False def overlay_fadeout(self): """Repeated callback after inactivity started""" self.set_needs_redraw() # There's no guarantee over the granularity of the callback here, # so use our time-since to work out how faded out we should be # Might look choppy on systems under high cpu usage but that's just how it's going to be now = perf_counter() time_percent = (now - self.fade_start) / self.inactive_fade_time if time_percent >= 1.0: self.fade_opacity = self.fade_out_limit self.fadeout_timeout = None return False self.fade_opacity = self.fade_out_limit + \ ((1.0 - self.fade_out_limit) * (1.0 - time_percent)) return True def col(self, col, alpha=1.0): """Convenience function to set the cairo context next colour. Altered to account for fade-out function""" if alpha is None: self.context.set_source_rgba(col[0], col[1], col[2], col[3]) else: self.context.set_source_rgba( col[0], col[1], col[2], col[3] * alpha * self.fade_opacity) def set_icon_transparency(self, trans): """Config option: icon transparency""" if self.icon_transparency != trans: self.icon_transparency = trans self.set_needs_redraw() def set_blank(self): """Set data to blank and redraw""" self.userlist = [] self.channel_icon = None self.channel_icon_url = None self.channel_title = None self.connection_status = "DISCONNECTED" self.set_needs_redraw() def set_fade_out_inactive(self, enabled, fade_time, fade_duration, fade_to): """Config option: fade out options""" if (self.fade_out_inactive != enabled or self.inactive_time != fade_time or self.inactive_fade_time != fade_duration or self.fade_out_limit != fade_to): self.fade_out_inactive = enabled self.inactive_time = fade_time self.inactive_fade_time = fade_duration self.fade_out_limit = fade_to self.reset_action_timer() def set_title_font(self, font): """Config option: font used to render title""" if self.title_font != font: self.title_font = font self.set_needs_redraw() def set_show_connection(self, show_connection): """Config option: show connection status alongside users""" if self.show_connection != show_connection: self.show_connection = show_connection self.set_needs_redraw() def set_show_avatar(self, show_avatar): """Config option: show avatar icons""" if self.show_avatar != show_avatar: self.show_avatar = show_avatar self.set_needs_redraw() def set_show_title(self, show_title): """Config option: show channel title alongside users""" if self.show_title != show_title: self.show_title = show_title self.set_needs_redraw() def set_show_disconnected(self, show_disconnected): """Config option: show even when disconnected from voice chat""" if self.show_disconnected != show_disconnected: self.show_disconnected = show_disconnected self.set_needs_redraw() def set_show_dummy(self, show_dummy): """Config option: Show placeholder information""" if self.use_dummy != show_dummy: self.use_dummy = show_dummy self.set_needs_redraw() def set_dummy_count(self, dummy_count): """Config option: Change the count of placeholders""" if self.dummy_count != dummy_count: self.dummy_count = dummy_count self.set_needs_redraw() def set_overflow_style(self, overflow): """Config option: Change handling of too many users to render""" if self.overflow != overflow: self.overflow = overflow self.set_needs_redraw() def set_bg(self, background_colour): """Config option: Set background colour. Used to draw the transparent window. Should not be changed as then the entire screen is obscured""" if self.norm_col != background_colour: self.norm_col = background_colour self.set_needs_redraw() def set_fg(self, foreground_colour): """Config option: Set foreground colour. Used to render text""" if self.text_col != foreground_colour: self.text_col = foreground_colour self.set_needs_redraw() def set_tk(self, talking_colour): """Config option: Set talking border colour. Used to render border around users who are talking""" if self.talk_col != talking_colour: self.talk_col = talking_colour self.set_needs_redraw() def set_mt(self, mute_colour): """Config option: Set mute colour. Used to render mute and deaf images""" if self.mute_col != mute_colour: self.mute_col = mute_colour self.set_needs_redraw() def set_mute_bg(self, mute_bg_col): """Config option: Set mute background colour. Used to tint the user avatar before rendering the mute or deaf image above it""" if self.mute_bg_col != mute_bg_col: self.mute_bg_col = mute_bg_col self.set_needs_redraw() def set_avatar_bg_col(self, avatar_bg_col): """Config option: Set avatar background colour. Drawn before user avatar but only visible if default fallback avatar can't be found""" if self.avatar_bg_col != avatar_bg_col: self.avatar_bg_col = avatar_bg_col self.set_needs_redraw() def set_hi(self, highlight_colour): """Config option: Set talking background colour. Used to render the background behind users name.""" if self.hili_col != highlight_colour: self.hili_col = highlight_colour self.set_needs_redraw() def set_fg_hi(self, highlight_colour): """Config option: Set talking text colour. Used to render the usernames of users who are talking""" if self.text_hili_col != highlight_colour: self.text_hili_col = highlight_colour self.set_needs_redraw() def set_bo(self, border_colour): """Config option: Set border colour. Used to render border around users""" if self.border_col != border_colour: self.border_col = border_colour self.set_needs_redraw() def set_avatar_size(self, size): """Config option: Set avatar size in window-space pixels""" if self.avatar_size != size: self.avatar_size = size self.set_needs_redraw() def set_nick_length(self, size): """Config option: Limit username length""" if self.nick_length != size: self.nick_length = size self.set_needs_redraw() def set_icon_spacing(self, i): """Config option: Space between users in the list, in window-space pixels""" if self.icon_spacing != i: self.icon_spacing = i self.set_needs_redraw() def set_text_padding(self, i): """Config option: Space between user avatar and username, in window-space pixels""" if self.text_pad != i: self.text_pad = i self.set_needs_redraw() def set_text_baseline_adj(self, i): """Config option: Vertical offset used to render all text, in window-space pixels""" if self.text_baseline_adj != i: self.text_baseline_adj = i self.set_needs_redraw() def set_vert_edge_padding(self, i): """Config option: Vertical offset from edge of window, in window-space pixels""" if self.vert_edge_padding != i: self.vert_edge_padding = i self.set_needs_redraw() def set_horz_edge_padding(self, i): """Config option: Horizontal offset from edge of window, in window-space pixels""" if self.horz_edge_padding != i: self.horz_edge_padding = i self.set_needs_redraw() def set_square_avatar(self, i): """Config option: Mask avatar with a circle before rendering""" if self.round_avatar == i: self.round_avatar = not i self.set_needs_redraw() def set_fancy_border(self, border): """Config option: Use transparent edges of image as border, instead of mask (square/circle)""" if self.fancy_border != border: self.fancy_border = border self.set_needs_redraw() def set_only_speaking(self, only_speaking): """Config option: Filter user list to only those who are talking and those who have stopped talking recently""" if self.only_speaking != only_speaking: self.only_speaking = only_speaking self.set_needs_redraw() def set_only_speaking_grace_period(self, grace_period): """Config option: How long after stopping speaking the user remains shown""" self.only_speaking_grace_period = grace_period self.timer_after_draw = grace_period def set_highlight_self(self, highlight_self): """Config option: Local User should be kept at top of list""" if self.highlight_self != highlight_self: self.highlight_self = highlight_self self.set_needs_redraw() def set_order(self, i): """Config option: Set method used to order user list""" if self.order != i: self.order = i self.sort_list(self.userlist) self.set_needs_redraw() def set_icon_only(self, i): """Config option: Show only the avatar, without text or its background""" if self.icon_only != i: self.icon_only = i self.set_needs_redraw() def set_drawn_border_width(self, width): """Config option: Set width of border around username and avatar""" if self.border_width != width: self.border_width = width self.set_needs_redraw() def set_horizontal(self, horizontal=False): """Config option: Userlist should be drawn horizontally""" if self.horizontal != horizontal: self.horizontal = horizontal self.set_needs_redraw() def set_wind_col(self): """Use window colour to draw""" self.col(self.wind_col, None) def set_norm_col(self): """Use background colour to draw""" self.col(self.norm_col) def set_talk_col(self, alpha=1.0): """Use talking colour to draw""" self.col(self.talk_col, alpha) def set_mute_col(self): """Use mute colour to draw""" self.col(self.mute_col) def set_channel_title(self, channel_title): """Set title above voice list""" if self.channel_title != channel_title: self.channel_title = channel_title self.set_needs_redraw() def set_channel_icon(self, url): """Change the icon for channel""" if not url: self.channel_icon = None self.channel_icon_url = None else: get_surface(self.recv_avatar, url, "channel", self.avatar_size) self.channel_icon_url = url def set_user_list(self, userlist, alt): """Set the users in list to draw""" self.userlist = userlist for user in userlist: if "nick" in user: user["friendlyname"] = user["nick"] else: user["friendlyname"] = user["username"] self.sort_list(self.userlist) if alt: self.reset_action_timer() self.set_needs_redraw() def set_connection_status(self, connection): """Set if discord has a clean connection to server""" if self.connection_status != connection['state']: self.connection_status = connection['state'] self.set_needs_redraw() def sort_list(self, in_list): """Take a userlist and sort it according to config option""" if self.order == 1: # ID Sort in_list.sort(key=lambda x: x["id"]) elif self.order == 2: # Spoken sort in_list.sort(key=lambda x: x["lastspoken"], reverse=True) in_list.sort(key=lambda x: x["speaking"], reverse=True) else: # Name sort in_list.sort(key=lambda x: locale.strxfrm(x['friendlyname'])) return in_list def has_content(self): """Returns true if overlay has meaningful content to render""" if not self.enabled: return False if self.hidden: return False if self.use_dummy: return True return self.userlist def overlay_draw(self, w, context, data=None): """Draw the Overlay""" self.context = context context.set_antialias(cairo.ANTIALIAS_GOOD) # Get size of window (width, height) = self.get_size() # Make background transparent self.set_wind_col() # Don't layer drawing over each other, always replace context.set_operator(cairo.OPERATOR_SOURCE) context.paint() context.save() if self.piggyback: self.piggyback.overlay_draw(w, context, data) (floating_x, floating_y, floating_width, floating_height) = self.get_floating_coords() if self.is_wayland or self.piggyback_parent or self.discover.steamos: # Special case! # The window is full-screen regardless of what the user has selected. # We need to set a clip and a transform to imitate original behaviour # Used in wlroots & gamescope if self.floating: context.new_path() context.translate(floating_x, floating_y) context.rectangle(0, 0, floating_width, floating_height) context.clip() context.set_operator(cairo.OPERATOR_OVER) if (not self.show_disconnected and self.connection_status == "DISCONNECTED" and not self.use_dummy): return connection = self.discover.connection if not connection: return self_user = connection.user # Gather which users to draw users_to_draw = self.userlist[:] userlist = self.userlist if self.use_dummy: # Sorting every frame is an awful idea. Maybe put this off elsewhere? users_to_draw = self.sort_list(self.dummy_data[0:self.dummy_count]) userlist = self.dummy_data now = perf_counter() for user in userlist: # Bad object equality here, so we need to reassign if "id" in self_user and user["id"] == self_user["id"]: self_user = user # Update friendly name with nick if possible if "nick" in user: user["friendlyname"] = user["nick"] else: user["friendlyname"] = user["username"] # Remove users that haven't spoken within the grace period if self.only_speaking: speaking = "speaking" in user and user["speaking"] # Extend timer if mid-speaking if self.highlight_self and self_user == user: continue if speaking: user['lastspoken'] = perf_counter() else: grace = self.only_speaking_grace_period if ( grace > 0 and (last_spoke := user['lastspoken']) and (now - last_spoke) < grace ): # The user spoke within the grace period, so don't hide # them just yet pass elif user in users_to_draw: users_to_draw.remove(user) if self.highlight_self: if self_user in users_to_draw: users_to_draw.remove(self_user) users_to_draw.insert(0, self_user) avatar_size = self.avatar_size if self.show_avatar else 0 line_height = self.avatar_size avatars_per_row = sys.maxsize # Calculate height needed to show overlay do_title = False do_connection = False if self.show_connection: users_to_draw.insert(0, None) do_connection = True if self.show_title and self.channel_title: users_to_draw.insert(0, None) do_title = True if self.horizontal: needed_width = (len(users_to_draw) * line_height) + \ (len(users_to_draw) + 1) * self.icon_spacing if needed_width > width: if self.overflow == 1: # Wrap avatars_per_row = int( width / (avatar_size+self.icon_spacing)) elif self.overflow == 2: # Shrink available_size = width / len(users_to_draw) avatar_size = available_size - self.icon_spacing if avatar_size < 8: avatar_size = 8 current_y = 0 + self.vert_edge_padding offset_y = avatar_size + self.icon_spacing if self.align_right: # A lie. Align bottom current_y = (height - avatar_size) - self.vert_edge_padding offset_y = -(avatar_size + self.icon_spacing) rows_to_draw = [] while len(users_to_draw) > 0: row = [] for _i in range(0, min(avatars_per_row, len(users_to_draw))): row.append(users_to_draw.pop(0)) rows_to_draw.append(row) for row in rows_to_draw: needed_width = (len(row) * (line_height + self.icon_spacing)) current_x = 0 + self.horz_edge_padding if self.align_vert == 1: current_x = (width / 2) - (needed_width) / 2 elif self.align_vert == 2: current_x = width - needed_width - self.horz_edge_padding for user in row: if not user: if do_title: do_title = False text_width = self.draw_title( context, current_x, current_y, avatar_size, line_height) elif do_connection: text_width = self.draw_connection( context, current_x, current_y, avatar_size, line_height) do_connection = False else: self.draw_avatar(context, user, current_x, current_y, avatar_size, line_height) current_x += avatar_size + self.icon_spacing current_y += offset_y else: needed_height = ((len(users_to_draw)+0) * line_height) + \ (len(users_to_draw) + 1) * self.icon_spacing if needed_height > height: if self.overflow == 1: # Wrap avatars_per_row = int( height / (avatar_size + self.icon_spacing)) elif self.overflow == 2: # Shrink available_size = height / len(users_to_draw) avatar_size = available_size - self.icon_spacing if avatar_size < 8: avatar_size = 8 current_x = 0 + self.horz_edge_padding offset_x_mult = 1 offset_x = avatar_size + self.horz_edge_padding if self.align_right: offset_x_mult = -1 current_x = floating_width - avatar_size - self.horz_edge_padding # Choose where to start drawing current_y = 0 + self.vert_edge_padding if self.align_vert == 1: current_y = (height / 2) - (needed_height / 2) elif self.align_vert == 2: current_y = height - needed_height - self.vert_edge_padding cols_to_draw = [] while len(users_to_draw) > 0: col = [] for _i in range(0, min(avatars_per_row, len(users_to_draw))): col.append(users_to_draw.pop(0)) cols_to_draw.append(col) for col in cols_to_draw: needed_height = (len(col) * (line_height + self.icon_spacing)) current_y = 0 + self.vert_edge_padding if self.align_vert == 1: current_y = (height/2) - (needed_height / 2) elif self.align_vert == 2: current_y = height - needed_height - self.vert_edge_padding largest_text_width = 0 for user in col: if not user: if do_title: # Draw header text_width = self.draw_title( context, current_x, current_y, avatar_size, line_height) largest_text_width = max( text_width, largest_text_width) current_y += line_height + self.icon_spacing do_title = False elif do_connection: # Draw header text_width = self.draw_connection( context, current_x, current_y, avatar_size, line_height) largest_text_width = max( text_width, largest_text_width) current_y += line_height + self.icon_spacing do_connection = False else: text_width = self.draw_avatar( context, user, current_x, current_y, avatar_size, line_height) largest_text_width = max( text_width, largest_text_width) current_y += line_height + self.icon_spacing if largest_text_width > 0: largest_text_width += self.text_pad else: largest_text_width = self.icon_spacing current_x += offset_x_mult * (offset_x + largest_text_width) context.restore() self.context = None def recv_avatar(self, identifier, pix, mask): """Called when image_getter has downloaded an image""" if identifier == 'def': self.def_avatar = pix self.def_avatar_mask = mask elif identifier == 'channel': self.channel_icon = pix self.channel_mask = mask else: self.avatars[identifier] = pix self.avatar_masks[identifier] = mask self.set_needs_redraw() def delete_avatar(self, identifier): """Remove avatar image""" if identifier in self.avatars: del self.avatars[identifier] def draw_title(self, context, pos_x, pos_y, avatar_size, line_height): """Draw title at given Y position. Includes both text and image based on settings""" tw = 0 if not self.horizontal and not self.icon_only: title = self.channel_title if self.use_dummy: title = "Dummy Title" tw = self.draw_text( context, title, pos_x, pos_y, self.text_col, self.norm_col, avatar_size, line_height, self.title_font ) if self.channel_icon: self.draw_avatar_pix(context, self.channel_icon, self.channel_mask, pos_x, pos_y, None, avatar_size) else: self.blank_avatar(context, pos_x, pos_y, avatar_size) if self.channel_icon_url: get_surface(self.recv_avatar, self.channel_icon_url, "channel", self.avatar_size) return tw def unused_fn_needed_translations(self): """ These are here to force them to be picked up for translations They're fed right through from Discord client as string literals """ _("DISCONNECTED") _("NO_ROUTE") _("VOICE_DISCONNECTED") _("ICE_CHECKING") _("AWAITING_ENDPOINT") _("AUTHENTICATING") _("CONNECTING") _("CONNECTED") _("VOICE_CONNECTING") _("VOICE_CONNECTED") def draw_connection(self, context, pos_x, pos_y, avatar_size, line_height): """Draw title at given Y position. Includes both text and image based on settings""" tw = 0 if not self.horizontal and not self.icon_only: tw = self.draw_text( context, _(self.connection_status), pos_x, pos_y, self.text_col, self.norm_col, avatar_size, line_height, self.text_font ) self.blank_avatar(context, pos_x, pos_y, avatar_size) self.draw_connection_icon(context, pos_x, pos_y, avatar_size) return tw def draw_avatar(self, context, user, pos_x, pos_y, avatar_size, line_height): """Draw avatar at given Y position. Includes both text and image based on settings""" # Ensure pixbuf for avatar if user["id"] not in self.avatars and user["avatar"] and avatar_size > 0: url = f"https://cdn.discordapp.com/avatars/{user['id']}/{user['avatar']}.png" get_surface(self.recv_avatar, url, user["id"], self.avatar_size) # Set the key with no value to avoid spamming requests self.avatars[user["id"]] = None self.avatar_masks[user["id"]] = None colour = None mute = False deaf = False bg_col = None fg_col = None tw = 0 if "mute" in user and user["mute"]: mute = True if "deaf" in user and user["deaf"]: deaf = True if "speaking" in user and user["speaking"] and not deaf and not mute: colour = self.talk_col if "speaking" in user and user["speaking"] and not deaf and not mute: bg_col = self.hili_col fg_col = self.text_hili_col else: bg_col = self.norm_col fg_col = self.text_col pix = None mask = None if user["id"] in self.avatars: pix = self.avatars[user["id"]] mask = self.avatar_masks[user["id"]] if not self.horizontal: if not self.icon_only: tw = self.draw_text( context, user["friendlyname"], pos_x, pos_y, fg_col, bg_col, avatar_size, line_height, self.text_font ) self.draw_avatar_pix(context, pix, mask, pos_x, pos_y, colour, avatar_size) if deaf: self.draw_deaf(context, pos_x, pos_y, self.mute_bg_col, avatar_size) elif mute: self.draw_mute(context, pos_x, pos_y, self.mute_bg_col, avatar_size) return tw def draw_text(self, context, string, pos_x, pos_y, tx_col, bg_col, avatar_size, line_height, font): """Draw username & background at given position""" if self.nick_length < 32 and len(string) > self.nick_length: string = string[:(self.nick_length-1)] + "\u2026" context.save() layout = self.create_pango_layout(string) layout.set_auto_dir(True) layout.set_markup(string, -1) (_floating_x, _floating_y, floating_width, _floating_height) = self.get_floating_coords() layout.set_width(Pango.SCALE * floating_width) layout.set_spacing(Pango.SCALE * 3) if font: font = Pango.FontDescription(font) layout.set_font_description(font) (ink_rect, logical_rect) = layout.get_pixel_extents() text_height = logical_rect.height text_width = logical_rect.width self.col(tx_col) height_offset = (line_height / 2) - (text_height / 2) text_y_offset = height_offset + self.text_baseline_adj if self.align_right: context.move_to(0, 0) self.col(bg_col) context.rectangle( pos_x - text_width - (self.text_pad * 2), pos_y + height_offset - self.text_pad, text_width + (self.text_pad * 4), text_height + (self.text_pad * 2) ) context.fill() self.col(tx_col) context.move_to( pos_x - text_width - self.text_pad - ink_rect.x, pos_y + text_y_offset ) layout.set_alignment(Pango.Alignment.RIGHT) PangoCairo.show_layout(self.context, layout) else: context.move_to(0, 0) self.col(bg_col) context.rectangle( pos_x - (self.text_pad * 2) + avatar_size, pos_y + height_offset - self.text_pad, text_width + (self.text_pad * 4), text_height + (self.text_pad * 2) ) context.fill() self.col(tx_col) context.move_to( pos_x + self.text_pad + avatar_size- ink_rect.x, pos_y + text_y_offset ) layout.set_alignment(Pango.Alignment.LEFT) PangoCairo.show_layout(self.context, layout) context.restore() return text_width def blank_avatar(self, context, pos_x, pos_y, avatar_size): """Draw a cut-out of the previous shape with a forcible transparent hole""" context.save() if self.round_avatar: context.arc(pos_x + (avatar_size / 2), pos_y + (avatar_size / 2), avatar_size / 2, 0, 2 * math.pi) context.clip() self.col(self.avatar_bg_col) context.set_operator(cairo.OPERATOR_SOURCE) context.rectangle(pos_x, pos_y, avatar_size, avatar_size) context.fill() context.restore() def draw_avatar_pix(self, context, pixbuf, mask, pos_x, pos_y, border_colour, avatar_size): """Draw avatar image at given position""" if not self.show_avatar: return # Empty the space for this self.blank_avatar(context, pos_x, pos_y, avatar_size) # fallback default or fallback further to no image here if not pixbuf: pixbuf = self.def_avatar if not pixbuf: return if not mask: mask = self.def_avatar_mask if not mask: return # Draw the "border" by doing a scaled-up copy in a flat colour if border_colour: self.col(border_colour) if self.fancy_border: context.set_operator(cairo.OPERATOR_SOURCE) for off_x in range(-self.border_width, self.border_width+1): for off_y in range(-self.border_width, self.border_width+1): context.save() if self.round_avatar: context.new_path() context.arc(pos_x + off_x + (avatar_size / 2), pos_y + off_y + (avatar_size / 2), avatar_size / 2, 0, 2 * math.pi) context.clip() draw_img_to_mask(mask, context, pos_x + off_x, pos_y + off_y, avatar_size, avatar_size) context.restore() else: if self.round_avatar: context.new_path() context.arc(pos_x + (avatar_size / 2), pos_y + (avatar_size / 2), avatar_size / 2 + (self.border_width/2.0), 0, 2 * math.pi) context.set_line_width(self.border_width) context.stroke() else: context.new_path() context.rectangle(pos_x - (self.border_width/2), pos_y - (self.border_width/2), avatar_size + self.border_width, avatar_size + self.border_width) context.set_line_width(self.border_width) context.stroke() # Cut the image back out context.save() if self.round_avatar: context.new_path() context.arc(pos_x + (avatar_size / 2), pos_y + (avatar_size / 2), avatar_size / 2, 0, 2 * math.pi) context.clip() self.col([0.0, 0.0, 0.0, 0.0]) context.set_operator(cairo.OPERATOR_SOURCE) draw_img_to_mask(mask, context, pos_x, pos_y, avatar_size, avatar_size) context.restore() # Draw the image context.save() if self.round_avatar: context.new_path() context.arc(pos_x + (avatar_size / 2), pos_y + (avatar_size / 2), avatar_size / 2, 0, 2 * math.pi) context.clip() context.set_operator(cairo.OPERATOR_OVER) draw_img_to_rect(pixbuf, context, pos_x, pos_y, avatar_size, avatar_size, False, False, 0, 0, self.fade_opacity * self.icon_transparency) context.restore() def draw_mute(self, context, pos_x, pos_y, bg_col, avatar_size): """Draw Mute logo""" if avatar_size <= 0: return context.save() context.translate(pos_x, pos_y) context.scale(avatar_size, avatar_size) # Add a dark background context.set_operator(cairo.OPERATOR_ATOP) context.rectangle(0.0, 0.0, 1.0, 1.0) self.col(bg_col, None) context.fill() context.set_operator(cairo.OPERATOR_OVER) self.set_mute_col() context.save() # Clip Strike-through context.new_path() context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) context.set_line_width(0.1) context.move_to(0.0, 0.0) context.line_to(1.0, 0.0) context.line_to(1.0, 1.0) context.line_to(0.0, 1.0) context.line_to(0.0, 0.0) context.close_path() context.new_sub_path() context.arc(0.9, 0.1, 0.05, 1.25 * math.pi, 2.25 * math.pi) context.arc(0.1, 0.9, 0.05, .25 * math.pi, 1.25 * math.pi) context.close_path() context.clip() # Center context.set_line_width(0.07) context.arc(0.5, 0.3, 0.1, math.pi, 2 * math.pi) context.arc(0.5, 0.5, 0.1, 0, math.pi) context.close_path() context.fill() context.set_line_width(0.05) # Stand rounded context.arc(0.5, 0.5, 0.15, 0, 1.0 * math.pi) context.stroke() # Stand vertical context.move_to(0.5, 0.65) context.line_to(0.5, 0.75) context.stroke() # Stand horizontal context.move_to(0.35, 0.75) context.line_to(0.65, 0.75) context.stroke() context.restore() # Strike through context.arc(0.7, 0.3, 0.035, 1.25 * math.pi, 2.25 * math.pi) context.arc(0.3, 0.7, 0.035, .25 * math.pi, 1.25 * math.pi) context.close_path() context.fill() context.set_fill_rule(cairo.FILL_RULE_WINDING) context.restore() def draw_deaf(self, context, pos_x, pos_y, bg_col, avatar_size): """Draw deaf logo""" if avatar_size <= 0: return context.save() context.translate(pos_x, pos_y) context.scale(avatar_size, avatar_size) # Add a dark background context.set_operator(cairo.OPERATOR_ATOP) context.rectangle(0.0, 0.0, 1.0, 1.0) self.col(bg_col, None) context.fill() context.set_operator(cairo.OPERATOR_OVER) self.set_mute_col() context.save() # Clip Strike-through context.new_path() context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) context.set_line_width(0.1) context.move_to(0.0, 0.0) context.line_to(1.0, 0.0) context.line_to(1.0, 1.0) context.line_to(0.0, 1.0) context.line_to(0.0, 0.0) context.close_path() context.new_sub_path() context.arc(0.9, 0.1, 0.05, 1.25 * math.pi, 2.25 * math.pi) context.arc(0.1, 0.9, 0.05, .25 * math.pi, 1.25 * math.pi) context.close_path() context.clip() # Top band context.arc(0.5, 0.5, 0.2, 1.0 * math.pi, 0) context.stroke() # Left band context.arc(0.28, 0.65, 0.075, 1.5 * math.pi, 0.5 * math.pi) context.move_to(0.3, 0.5) context.line_to(0.3, 0.75) context.stroke() # Right band context.arc(0.72, 0.65, 0.075, 0.5 * math.pi, 1.5 * math.pi) context.move_to(0.7, 0.5) context.line_to(0.7, 0.75) context.stroke() context.restore() # Strike through context.arc(0.7, 0.3, 0.035, 1.25 * math.pi, 2.25 * math.pi) context.arc(0.3, 0.7, 0.035, .25 * math.pi, 1.25 * math.pi) context.close_path() context.fill() context.set_fill_rule(cairo.FILL_RULE_WINDING) context.restore() def draw_connection_icon(self, context, pos_x, pos_y, avatar_size): """Draw a series of bars to show connectivity state""" context.save() context.translate(pos_x, pos_y) context.scale(avatar_size, avatar_size) bars = 0 s = self.connection_status if s == "DISCONNECTED" or s == "NO_ROUTE" or s == "VOICE_DISCONNECTED": bars = 0 self.col([1.0, 0.0, 0.0, 1.0]) elif s == "ICE_CHECKING" or s == "AWAITING_ENDPOINT" or s == "AUTHENTICATING": bars = 1 self.col([1.0, 0.0, 0.0, 1.0]) elif s == "CONNECTING" or s == "CONNECTED" or s == "VOICE_CONNECTING": bars = 2 self.col([1.0, 1.0, 0.0, 1.0]) elif s == "VOICE_CONNECTED": bars = 3 self.col([0.0, 1.0, 0.0, 1.0]) context.set_line_width(0.1) if bars >= 1: context.move_to(0.3, 0.8) context.line_to(0.3, 0.6) context.stroke() if bars >= 2: context.move_to(0.5, 0.8) context.line_to(0.5, 0.4) context.stroke() if bars == 3: context.move_to(0.7, 0.8) context.line_to(0.7, 0.2) context.stroke() context.restore()