- First pass at pylint clean-up

This commit is contained in:
trigg 2024-07-08 23:32:59 +01:00
parent 062e8b102f
commit 17a9bc5f02
12 changed files with 587 additions and 603 deletions

4
.pylintrc Normal file
View file

@ -0,0 +1,4 @@
[MAIN]
max-line-length=150
max-module-lines=2000
generated-member=cairo.*

View file

@ -28,7 +28,7 @@ class Autostart:
def __init__(self, app_name): def __init__(self, app_name):
if not app_name.endswith(".desktop"): if not app_name.endswith(".desktop"):
app_name = "%s.desktop" % (app_name) app_name = f"{app_name}.desktop"
self.app_name = app_name self.app_name = app_name
self.auto_locations = [os.path.join( self.auto_locations = [os.path.join(
xdg_config_home, 'autostart/'), '/etc/xdg/autostart/'] xdg_config_home, 'autostart/'), '/etc/xdg/autostart/']
@ -77,7 +77,7 @@ class BazziteAutostart:
def __init__(self): def __init__(self):
self.auto = False self.auto = False
with open("/etc/default/discover-overlay") as f: with open("/etc/default/discover-overlay", encoding="utf-8") as f:
content = f.readlines() content = f.readlines()
for line in content: for line in content:
if line.startswith("AUTO_LAUNCH_DISCOVER_OVERLAY="): if line.startswith("AUTO_LAUNCH_DISCOVER_OVERLAY="):
@ -94,14 +94,15 @@ class BazziteAutostart:
self.auto = enable self.auto = enable
def change_file(self, value): def change_file(self, value):
"""Alter bazzite config via pkexec and sed"""
root = '' root = ''
if shutil.which('pkexec'): if shutil.which('pkexec'):
root = 'pkexec' root = 'pkexec'
else: else:
log.error("No ability to request root privs. Cancel") log.error("No ability to request root privs. Cancel")
return return
command = " sed -i 's/AUTO_LAUNCH_DISCOVER_OVERLAY=./AUTO_LAUNCH_DISCOVER_OVERLAY=%s/g' /etc/default/discover-overlay" % ( command = f" sed -i 's/AUTO_LAUNCH_DISCOVER_OVERLAY=./AUTO_LAUNCH_DISCOVER_OVERLAY={
value) value}/g' /etc/default/discover-overlay"
command_with_permissions = root + command command_with_permissions = root + command
os.system(command_with_permissions) os.system(command_with_permissions)

View file

@ -29,7 +29,6 @@ import calendar
import websocket import websocket
import requests import requests
import gi
from gi.repository import GLib from gi.repository import GLib
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -94,9 +93,14 @@ class DiscordConnector:
""" """
url = "https://streamkit.discord.com/overlay/token" url = "https://streamkit.discord.com/overlay/token"
myobj = {"code": code1} myobj = {"code": code1}
response = requests.post(url, json=myobj) response = requests.post(url, json=myobj, timeout=10)
try: try:
jsonresponse = json.loads(response.text) jsonresponse = json.loads(response.text)
except requests.exceptions.Timeout:
# TODO This probably needs a retry, not a quit
jsonresponse = {}
except requests.exceptions.TooManyRedirects:
jsonresponse = {}
except json.JSONDecodeError: except json.JSONDecodeError:
jsonresponse = {} jsonresponse = {}
if "access_token" in jsonresponse: if "access_token" in jsonresponse:
@ -192,8 +196,7 @@ class DiscordConnector:
""" """
Update a line of text Update a line of text
""" """
for idx in range(0, len(self.text)): for idx, message in enumerate(self.text):
message = self.text[idx]
if message['id'] == message_in['id']: if message['id'] == message_in['id']:
new_message = {'id': message['id'], new_message = {'id': message['id'],
'content': self.get_message_from_message(message_in), 'content': self.get_message_from_message(message_in),
@ -209,8 +212,7 @@ class DiscordConnector:
""" """
Delete a line of text Delete a line of text
""" """
for idx in range(0, len(self.text)): for idx, message in enumerate(self.text):
message = self.text[idx]
if message['id'] == message_in['id']: if message['id'] == message_in['id']:
del self.text[idx] del self.text[idx]
self.text_altered = True self.text_altered = True
@ -386,7 +388,8 @@ class DiscordConnector:
self.dump_channel_data() self.dump_channel_data()
return return
elif j["cmd"] == "GET_GUILD": elif j["cmd"] == "GET_GUILD":
# We currently only get here because of a "CHANNEL_CREATE" event. Stupidly long winded way around # We currently only get here because of a "CHANNEL_CREATE" event.
# Stupidly long winded way around
if j["data"]: if j["data"]:
guild = j["data"] guild = j["data"]
self.dump_channel_data() self.dump_channel_data()
@ -417,7 +420,8 @@ class DiscordConnector:
self.set_channel(j['data']['id'], j['data']['guild_id']) self.set_channel(j['data']['id'], j['data']['guild_id'])
self.discover.voice_overlay.set_channel_title( self.discover.voice_overlay.set_channel_title(
j["data"]["name"]) j["data"]["name"])
if self.current_guild in self.guilds and 'icon_url' in self.guilds[self.current_guild]: if (self.current_guild in self.guilds and
'icon_url' in self.guilds[self.current_guild]):
self.discover.voice_overlay.set_channel_icon( self.discover.voice_overlay.set_channel_icon(
self.guilds[self.current_guild]['icon_url']) self.guilds[self.current_guild]['icon_url'])
else: else:
@ -460,7 +464,8 @@ class DiscordConnector:
log.warning(j) log.warning(j)
def dump_channel_data(self): def dump_channel_data(self):
with open(self.discover.channel_file, 'w') as f: """ Write all channel data out to file"""
with open(self.discover.channel_file, 'w', encoding="utf-8") as f:
f.write(json.dumps( f.write(json.dumps(
{'channels': self.channels, 'guild': self.guilds})) {'channels': self.channels, 'guild': self.guilds}))
@ -536,7 +541,7 @@ class DiscordConnector:
if guild in self.guilds: if guild in self.guilds:
self.rate_limited_channels.append(guild) self.rate_limited_channels.append(guild)
else: else:
log.warning(f"Didn't find guild with id {guild}") log.warning("Didn't find guild with id %s", guild)
def req_channel_details(self, channel, nonce=None): def req_channel_details(self, channel, nonce=None):
"""message """message
@ -669,6 +674,7 @@ class DiscordConnector:
self.websocket.send(json.dumps(cmd)) self.websocket.send(json.dumps(cmd))
def set_mute(self, muted): def set_mute(self, muted):
""" Set client muted status """
cmd = { cmd = {
"cmd": "SET_VOICE_SETTINGS", "cmd": "SET_VOICE_SETTINGS",
"args": {"mute": muted}, "args": {"mute": muted},
@ -679,6 +685,7 @@ class DiscordConnector:
return False return False
def set_deaf(self, deaf): def set_deaf(self, deaf):
""" Set client deafened status """
cmd = { cmd = {
"cmd": "SET_VOICE_SETTINGS", "cmd": "SET_VOICE_SETTINGS",
"args": {"deaf": deaf}, "args": {"deaf": deaf},
@ -688,14 +695,14 @@ class DiscordConnector:
self.websocket.send(json.dumps(cmd)) self.websocket.send(json.dumps(cmd))
return False return False
def change_voice_room(self, id): def change_voice_room(self, room_id):
""" """
Switch to another voice room Switch to another voice room
""" """
cmd = { cmd = {
"cmd": "SELECT_VOICE_CHANNEL", "cmd": "SELECT_VOICE_CHANNEL",
"args": { "args": {
"channel_id": id, "channel_id": room_id,
"force": True "force": True
}, },
"nonce": "deadbeef" "nonce": "deadbeef"
@ -703,14 +710,14 @@ class DiscordConnector:
if self.websocket: if self.websocket:
self.websocket.send(json.dumps(cmd)) self.websocket.send(json.dumps(cmd))
def change_text_room(self, id): def change_text_room(self, room_id):
""" """
Switch to another text room Switch to another text room
""" """
cmd = { cmd = {
"cmd": "SELECT_TEXT_CHANNEL", "cmd": "SELECT_TEXT_CHANNEL",
"args": { "args": {
"channel_id": id "channel_id": room_id
}, },
"nonce": "deadbeef" "nonce": "deadbeef"
} }
@ -718,7 +725,8 @@ class DiscordConnector:
self.websocket.send(json.dumps(cmd)) self.websocket.send(json.dumps(cmd))
def update_overlays_from_data(self): def update_overlays_from_data(self):
if self.websocket == None: """Send new data out to overlay windows"""
if self.websocket is None:
self.discover.voice_overlay.set_blank() self.discover.voice_overlay.set_blank()
if self.discover.text_overlay: if self.discover.text_overlay:
self.discover.text_overlay.set_blank() self.discover.text_overlay.set_blank()
@ -773,12 +781,13 @@ class DiscordConnector:
This will be mixed in with 'None' in the list where a voice channel is This will be mixed in with 'None' in the list where a voice channel is
""" """
if (guild_id == 0): if guild_id == 0:
return return
self.rate_limited_channels.append(guild_id) self.rate_limited_channels.append(guild_id)
def schedule_reconnect(self): def schedule_reconnect(self):
if self.reconnect_cb == None: """Set a timer to attempt reconnection"""
if self.reconnect_cb is None:
log.info("Scheduled a reconnect") log.info("Scheduled a reconnect")
self.reconnect_cb = GLib.timeout_add_seconds(60, self.connect) self.reconnect_cb = GLib.timeout_add_seconds(60, self.connect)
else: else:
@ -792,25 +801,30 @@ class DiscordConnector:
""" """
log.info("Connecting...") log.info("Connecting...")
if self.websocket: if self.websocket:
log.warn("Already connected?") log.warning("Already connected?")
return return
if self.reconnect_cb: if self.reconnect_cb:
GLib.source_remove(self.reconnect_cb) GLib.source_remove(self.reconnect_cb)
self.reconnect_cb = None self.reconnect_cb = None
try: try:
self.websocket = websocket.create_connection( self.websocket = websocket.create_connection(
"ws://127.0.0.1:6463/?v=1&client_id=%s" % (self.oauth_token), f"ws://127.0.0.1:6463/?v=1&client_id={self.oauth_token}",
origin="http://localhost:3000", origin="http://localhost:3000",
timeout=0.1 timeout=0.1
) )
if self.socket_watch: if self.socket_watch:
GLib.source_remove(self.socket_watch) GLib.source_remove(self.socket_watch)
self.socket_watch = GLib.io_add_watch( self.socket_watch = GLib.io_add_watch(
self.websocket.sock, GLib.PRIORITY_DEFAULT_IDLE, GLib.IOCondition.HUP | GLib.IOCondition.IN | GLib.IOCondition.ERR, self.socket_glib) self.websocket.sock,
except ConnectionError as error: GLib.PRIORITY_DEFAULT_IDLE,
GLib.IOCondition.HUP | GLib.IOCondition.IN | GLib.IOCondition.ERR,
self.socket_glib
)
except ConnectionError as _error:
self.schedule_reconnect() self.schedule_reconnect()
def socket_glib(self, fd, condition): def socket_glib(self, _fd, condition):
"""Handle new data on socket"""
if condition == GLib.IO_IN and self.websocket: if condition == GLib.IO_IN and self.websocket:
recv, _w, _e = select.select((self.websocket.sock,), (), (), 0) recv, _w, _e = select.select((self.websocket.sock,), (), (), 0)
while recv: while recv:

View file

@ -13,16 +13,15 @@
"""Main application class""" """Main application class"""
import gettext import gettext
import os import os
import time
import sys import sys
import re import re
import traceback import traceback
import logging import logging
import pkg_resources
import json import json
import signal import signal
import gi
from configparser import ConfigParser from configparser import ConfigParser
import gi
import pkg_resources
from .settings_window import MainSettingsWindow from .settings_window import MainSettingsWindow
from .voice_overlay import VoiceOverlayWindow from .voice_overlay import VoiceOverlayWindow
@ -33,7 +32,7 @@ from .audio_assist import DiscoverAudioAssist
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
# pylint: disable=wrong-import-position,wrong-import-order # pylint: disable=wrong-import-position,wrong-import-order
from gi.repository import Gtk, GLib, Gio, Gdk # nopep8 from gi.repository import Gtk, GLib, Gio # nopep8
try: try:
from xdg.BaseDirectory import xdg_config_home from xdg.BaseDirectory import xdg_config_home
@ -99,7 +98,7 @@ class Discover:
Read in arg list from command or RPC and act accordingly Read in arg list from command or RPC and act accordingly
""" """
if "--help" in data or "-h" in data: if "--help" in data or "-h" in data:
print("%s: discover-overlay [OPTIONS]... " % (_("Usage"))) print(f"{_("Usage")}: discover-overlay [OPTIONS]... ")
print(_("Show an X11 or wlroots overlay with information")) print(_("Show an X11 or wlroots overlay with information"))
print(_("from Discord client")) print(_("from Discord client"))
print("") print("")
@ -158,14 +157,16 @@ class Discover:
self.connection.request_text_rooms_for_guild(match.group(1)) self.connection.request_text_rooms_for_guild(match.group(1))
def config_set(self, context, key, value): def config_set(self, context, key, value):
"""Set a config value and save to disk"""
config = self.config() config = self.config()
if not context in config.sections(): if not context in config.sections():
config.add_section(context) config.add_section(context)
config.set(context, key, value) config.set(context, key, value)
with open(self.config_file, 'w') as file: with open(self.config_file, 'w', encoding="utf-8") as file:
config.write(file) config.write(file)
def config(self): def config(self):
"""Read config from disk"""
config = ConfigParser(interpolation=None) config = ConfigParser(interpolation=None)
config.read(self.config_file) config.read(self.config_file)
return config return config
@ -174,7 +175,7 @@ class Discover:
""" """
Called when the RPC file has been altered Called when the RPC file has been altered
""" """
with open(self.rpc_file, "r") as tfile: with open(self.rpc_file, "r", encoding="utf-8") as tfile:
data = tfile.readlines() data = tfile.readlines()
if len(data) >= 1: if len(data) >= 1:
self.do_args(data[0].strip().split(" "), False) self.do_args(data[0].strip().split(" "), False)
@ -249,9 +250,7 @@ class Discover:
self.voice_overlay.set_horizontal(config.getboolean( self.voice_overlay.set_horizontal(config.getboolean(
"main", "horizontal", fallback=False)) "main", "horizontal", fallback=False))
self.voice_overlay.set_guild_ids(self.parse_guild_ids( self.voice_overlay.set_overflow_style(
config.get("main", "guild_ids", fallback="")))
self.voice_overlay.set_overflow(
config.getint("main", "overflow", fallback=0)) config.getint("main", "overflow", fallback=0))
self.voice_overlay.set_show_connection(config.getboolean( self.voice_overlay.set_show_connection(config.getboolean(
"main", "show_connection", fallback=False)) "main", "show_connection", fallback=False))
@ -259,7 +258,7 @@ class Discover:
"main", "show_title", fallback=False)) "main", "show_title", fallback=False))
self.voice_overlay.set_show_disconnected(config.getboolean( self.voice_overlay.set_show_disconnected(config.getboolean(
"main", "show_disconnected", fallback=False)) "main", "show_disconnected", fallback=False))
self.voice_overlay.set_border_width( self.voice_overlay.set_drawn_border_width(
config.getint("main", "border_width", fallback=2)) config.getint("main", "border_width", fallback=2))
self.voice_overlay.set_icon_transparency(config.getfloat( self.voice_overlay.set_icon_transparency(config.getfloat(
"main", "icon_transparency", fallback=1.0)) "main", "icon_transparency", fallback=1.0))
@ -432,6 +431,7 @@ class Discover:
self.config_file, self.rpc_file, self.channel_file, []) self.config_file, self.rpc_file, self.channel_file, [])
def toggle_show(self, _obj=None): def toggle_show(self, _obj=None):
"""Toggle all overlays off or on"""
if self.voice_overlay: if self.voice_overlay:
hide = not self.voice_overlay.hidden hide = not self.voice_overlay.hidden
self.voice_overlay.set_hidden(hide) self.voice_overlay.set_hidden(hide)
@ -457,6 +457,8 @@ class Discover:
self.notification_overlay.set_force_xshape(force) self.notification_overlay.set_force_xshape(force)
def set_show_task(self, visible): def set_show_task(self, visible):
"""Set if the overlay should allow itself to appear on taskbar.
Not working at last check"""
if self.voice_overlay: if self.voice_overlay:
self.voice_overlay.set_task(visible) self.voice_overlay.set_task(visible)
if self.text_overlay: if self.text_overlay:
@ -465,11 +467,13 @@ class Discover:
self.notification_overlay.set_task(visible) self.notification_overlay.set_task(visible)
def set_mute_async(self, mute): def set_mute_async(self, mute):
if mute != None: """Set mute status from another thread"""
if mute is not None:
GLib.idle_add(self.connection.set_mute, mute) GLib.idle_add(self.connection.set_mute, mute)
def set_deaf_async(self, deaf): def set_deaf_async(self, deaf):
if deaf != None: """Set deaf status from another thread"""
if deaf is not None:
GLib.idle_add(self.connection.set_deaf, deaf) GLib.idle_add(self.connection.set_deaf, deaf)
@ -499,13 +503,12 @@ def entrypoint():
# Prepare logger # Prepare logger
logging.getLogger().setLevel(logging.INFO) logging.getLogger().setLevel(logging.INFO)
FORMAT = "%(levelname)s - %(name)s - %(message)s" log_format = "%(levelname)s - %(name)s - %(message)s"
if "--debug" in sys.argv or "-v" in sys.argv: if "--debug" in sys.argv or "-v" in sys.argv:
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
logging.basicConfig(filename=debug_file, format=FORMAT) logging.basicConfig(filename=debug_file, format=log_format)
else: else:
logging.basicConfig(format=FORMAT) logging.basicConfig(format=log_format)
log = logging.getLogger(__name__)
log.info("Starting Discover Overlay: %s", log.info("Starting Discover Overlay: %s",
pkg_resources.get_distribution('discover_overlay').version) pkg_resources.get_distribution('discover_overlay').version)
@ -520,26 +523,26 @@ def entrypoint():
# Send command to overlay # Send command to overlay
line = "" line = ""
for arg in sys.argv[1:]: for arg in sys.argv[1:]:
line = "%s %s" % (line, arg) line = f"{line} {arg}"
with open(rpc_file, "w") as tfile: with open(rpc_file, "w", encoding="utf-8") as tfile:
tfile.write(line) tfile.write(line)
log.warning("Sent RPC command") log.warning("Sent RPC command")
else: else:
if "-c" in sys.argv or "--configure" in sys.argv: if "-c" in sys.argv or "--configure" in sys.argv:
# Show config window # Show config window
settings = MainSettingsWindow( _settings = MainSettingsWindow(
config_file, rpc_file, channel_file, sys.argv[1:]) config_file, rpc_file, channel_file, sys.argv[1:])
Gtk.main() Gtk.main()
else: else:
# Tell any other running overlay to close # Tell any other running overlay to close
with open(rpc_file, "w") as tfile: with open(rpc_file, "w", encoding="utf-8") as tfile:
tfile.write("--close") tfile.write("--close")
# Show the overlay # Show the overlay
Discover(rpc_file, config_file, channel_file, Discover(rpc_file, config_file, channel_file,
debug_file, sys.argv[1:]) debug_file, sys.argv[1:])
return return
except Exception as ex: except Exception as ex: # pylint: disable=broad-except
log.error(ex) log.error(ex)
log.error(traceback.format_exc()) log.error(traceback.format_exc())
sys.exit(1) sys.exit(1)

View file

@ -11,9 +11,9 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
"""An X11 window which can be moved and resized""" """An X11 window which can be moved and resized"""
import logging
import gi import gi
import cairo import cairo
import logging
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
from gi.repository import Gtk, Gdk # nopep8 from gi.repository import Gtk, Gdk # nopep8
@ -24,10 +24,12 @@ log = logging.getLogger(__name__)
class DraggableWindow(Gtk.Window): class DraggableWindow(Gtk.Window):
"""An X11 window which can be moved and resized""" """An X11 window which can be moved and resized"""
def __init__(self, pos_x=0.0, pos_y=0.0, width=0.1, height=0.1, message="Message", settings=None, monitor=None): def __init__(self, pos_x=0.0, pos_y=0.0, width=0.1, height=0.1,
message="Message", settings=None, monitor=None):
Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP) Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP)
self.monitor = monitor self.monitor = monitor
(screen_x, screen_y, screen_width, screen_height) = self.get_display_coords() (_screen_x, _screen_y, screen_width,
screen_height) = self.get_display_coords()
self.pos_x = pos_x * screen_width self.pos_x = pos_x * screen_width
self.pos_y = pos_y * screen_height self.pos_y = pos_y * screen_height
self.width = max(40, width * screen_width) self.width = max(40, width * screen_width)
@ -84,8 +86,8 @@ class DraggableWindow(Gtk.Window):
if event.state & Gdk.ModifierType.BUTTON1_MASK: if event.state & Gdk.ModifierType.BUTTON1_MASK:
if self.drag_type == 1: if self.drag_type == 1:
# Center is move # Center is move
(screen_x, screen_y, screen_width, (screen_x, screen_y, _screen_width,
screen_height) = self.get_display_coords() _screen_height) = self.get_display_coords()
self.pos_x = (event.x_root - screen_x) - self.drag_x self.pos_x = (event.x_root - screen_x) - self.drag_x
self.pos_y = (event.y_root - screen_y) - self.drag_y self.pos_y = (event.y_root - screen_y) - self.drag_y
self.force_location() self.force_location()
@ -151,6 +153,7 @@ class DraggableWindow(Gtk.Window):
context.fill() context.fill()
def get_display_coords(self): def get_display_coords(self):
"""Get coordinates for this display"""
display = Gdk.Display.get_default() display = Gdk.Display.get_default()
if "get_monitor" in dir(display): if "get_monitor" in dir(display):
monitor = display.get_monitor(self.monitor) monitor = display.get_monitor(self.monitor)
@ -171,4 +174,5 @@ class DraggableWindow(Gtk.Window):
height = float(height) height = float(height)
pos_x = pos_x / scale pos_x = pos_x / scale
pos_y = pos_y / scale pos_y = pos_y / scale
return (pos_x / screen_width, pos_y / screen_height, width / screen_width, height / screen_height) return (pos_x / screen_width, pos_y / screen_height,
width / screen_width, height / screen_height)

View file

@ -11,9 +11,9 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
"""A Wayland full-screen window which can be moved and resized""" """A Wayland full-screen window which can be moved and resized"""
import logging
import cairo import cairo
import gi import gi
import logging
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
from gi.repository import Gtk, Gdk # nopep8 from gi.repository import Gtk, Gdk # nopep8
@ -29,12 +29,14 @@ log = logging.getLogger(__name__)
class DraggableWindowWayland(Gtk.Window): class DraggableWindowWayland(Gtk.Window):
"""A Wayland full-screen window which can be moved and resized""" """A Wayland full-screen window which can be moved and resized"""
def __init__(self, pos_x=0.0, pos_y=0.0, width=0.1, height=0.1, message="Message", settings=None, steamos=False, monitor=None): def __init__(self, pos_x=0.0, pos_y=0.0, width=0.1, height=0.1,
message="Message", settings=None, steamos=False, monitor=None):
Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL) Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL)
if steamos: if steamos:
monitor = 0 monitor = 0
self.monitor = monitor self.monitor = monitor
(screen_x, screen_y, screen_width, screen_height) = self.get_display_coords() (_screen_x, _screen_y, screen_width,
screen_height) = self.get_display_coords()
self.pos_x = pos_x * screen_width self.pos_x = pos_x * screen_width
self.pos_y = pos_y * screen_height self.pos_y = pos_y * screen_height
self.width = max(40, width * screen_width) self.width = max(40, width * screen_width)
@ -48,8 +50,8 @@ class DraggableWindowWayland(Gtk.Window):
self.connect('button-press-event', self.button_press) self.connect('button-press-event', self.button_press)
self.connect('button-release-event', self.button_release) self.connect('button-release-event', self.button_release)
log.info("Starting: %d,%d %d x %d" % log.info("Starting: %d,%d %d x %d",
(self.pos_x, self.pos_y, self.width, self.height)) self.pos_x, self.pos_y, self.width, self.height)
self.set_app_paintable(True) self.set_app_paintable(True)
@ -76,21 +78,24 @@ class DraggableWindowWayland(Gtk.Window):
self.force_location() self.force_location()
def set_steamos_window_size(self): def set_steamos_window_size(self):
"""Prepare window for a gamescope steamos session"""
# Huge bunch of assumptions. # Huge bunch of assumptions.
# Gamescope only has one monitor # Gamescope only has one monitor
# Gamescope has no scale factor # Gamescope has no scale factor
# Probably never possible to reach here, as Gamescope/SteamOS
# is X11 for overlays
display = Gdk.Display.get_default() display = Gdk.Display.get_default()
if "get_monitor" in dir(display): if "get_monitor" in dir(display):
monitor = display.get_monitor(0) monitor = display.get_monitor(0)
if monitor: if monitor:
geometry = monitor.get_geometry() geometry = monitor.get_geometry()
scale_factor = monitor.get_scale_factor() log.info("%d %d", geometry.width, geometry.height)
log.info("%d %d" % (geometry.width, geometry.height))
self.set_size_request(geometry.width, geometry.height) self.set_size_request(geometry.width, geometry.height)
def force_location(self): def force_location(self):
"""Move the window to previously given co-ords. In wayland just clip to current screen""" """Move the window to previously given co-ords. In wayland just clip to current screen"""
(screen_x, screen_y, screen_width, screen_height) = self.get_display_coords() (_screen_x, _screen_y, screen_width,
screen_height) = self.get_display_coords()
self.width = min(self.width, screen_width) self.width = min(self.width, screen_width)
self.height = min(self.height, screen_height) self.height = min(self.height, screen_height)
self.pos_x = max(0, self.pos_x) self.pos_x = max(0, self.pos_x)
@ -189,6 +194,7 @@ class DraggableWindowWayland(Gtk.Window):
context.restore() context.restore()
def get_display_coords(self): def get_display_coords(self):
"""Get coordinates from display"""
display = Gdk.Display.get_default() display = Gdk.Display.get_default()
if "get_monitor" in dir(display): if "get_monitor" in dir(display):
monitor = display.get_monitor(self.monitor) monitor = display.get_monitor(self.monitor)
@ -199,5 +205,7 @@ class DraggableWindowWayland(Gtk.Window):
def get_coords(self): def get_coords(self):
"""Return the position and size of the window""" """Return the position and size of the window"""
(screen_x, screen_y, screen_width, screen_height) = self.get_display_coords() (_screen_x, _screen_y, screen_width,
return (float(self.pos_x) / screen_width, float(self.pos_y) / screen_height, float(self.width) / screen_width, float(self.height) / screen_height) screen_height) = self.get_display_coords()
return (float(self.pos_x) / screen_width, float(self.pos_y) / screen_height,
float(self.width) / screen_width, float(self.height) / screen_height)

View file

@ -11,21 +11,19 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Functions & Classes to assist image loading.""" """Functions & Classes to assist image loading."""
import urllib
import threading import threading
import logging import logging
import os
import copy
import gi import gi
import requests import requests
import cairo import cairo
import PIL import PIL
import PIL.Image as Image import PIL.Image as Image
import os
import io
import copy
gi.require_version('GdkPixbuf', '2.0') gi.require_version('GdkPixbuf', '2.0')
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
from gi.repository import Gio, GdkPixbuf, Gtk # nopep8 from gi.repository import Gtk # nopep8
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -43,7 +41,7 @@ class SurfaceGetter():
"""Downloads and decodes""" """Downloads and decodes"""
try: try:
resp = requests.get( resp = requests.get(
self.url, stream=True, headers={ self.url, stream=True, timeout=10, headers={
'Referer': 'https://streamkit.discord.com/overlay/voice', 'Referer': 'https://streamkit.discord.com/overlay/voice',
'User-Agent': 'Mozilla/5.0' 'User-Agent': 'Mozilla/5.0'
} }
@ -69,6 +67,7 @@ class SurfaceGetter():
log.error("Unknown image type: %s", self.url) log.error("Unknown image type: %s", self.url)
def get_file(self): def get_file(self):
"""Attempt to load the file"""
errors = [] errors = []
# Grab icon from icon theme # Grab icon from icon theme
icon_theme = Gtk.IconTheme.get_default() icon_theme = Gtk.IconTheme.get_default()
@ -99,13 +98,13 @@ class SurfaceGetter():
try: try:
image = Image.open(mixpath) image = Image.open(mixpath)
except ValueError: except ValueError:
errors.append("Value Error - Unable to read %s" % (mixpath)) errors.append(f"Value Error - Unable to read {mixpath}")
except TypeError: except TypeError:
errors.append("Type Error - Unable to read %s" % (mixpath)) errors.append(f"Type Error - Unable to read {mixpath}")
except PIL.UnidentifiedImageError: except PIL.UnidentifiedImageError:
errors.append("Unknown image type: %s" % (mixpath)) errors.append(f"Unknown image type: {mixpath}")
except FileNotFoundError: except FileNotFoundError:
errors.append("File not found: %s" % (mixpath)) errors.append(f"File not found: {mixpath}")
if image: if image:
(surface, mask) = from_pil(image) (surface, mask) = from_pil(image)
if surface: if surface:
@ -115,7 +114,7 @@ class SurfaceGetter():
log.error(error) log.error(error)
def from_pil(image, alpha=1.0, format='BGRa'): def from_pil(image, alpha=1.0, image_format='BGRa'):
""" """
:param im: Pillow Image :param im: Pillow Image
:param alpha: 0..1 alpha to add to non-alpha images :param alpha: 0..1 alpha to add to non-alpha images
@ -125,10 +124,10 @@ def from_pil(image, alpha=1.0, format='BGRa'):
mask = bytearray() mask = bytearray()
if 'A' not in image.getbands(): if 'A' not in image.getbands():
image.putalpha(int(alpha * 255.0)) image.putalpha(int(alpha * 255.0))
arr = bytearray(image.tobytes('raw', format)) arr = bytearray(image.tobytes('raw', image_format))
mask = arr mask = arr
else: else:
arr = bytearray(image.tobytes('raw', format)) arr = bytearray(image.tobytes('raw', image_format))
mask = copy.deepcopy((arr)) mask = copy.deepcopy((arr))
idx = 0 idx = 0
while idx < len(arr): while idx < len(arr):
@ -148,9 +147,12 @@ def from_pil(image, alpha=1.0, format='BGRa'):
def to_pil(surface): def to_pil(surface):
"""Return a PIL Image from the Cairo surface"""
if surface.get_format() == cairo.Format.ARGB32: if surface.get_format() == cairo.Format.ARGB32:
return Image.frombuffer('RGBA', (surface.get_width(), surface.get_height()), surface.get_data(), 'raw', "BGRA", surface.get_stride()) return Image.frombuffer('RGBA', (surface.get_width(), surface.get_height()),
return Image.frombuffer("RGB", (surface.get_width(), surface.get_height()), surface.get_data(), 'raw', "BGRX", surface.get_stride()) surface.get_data(), 'raw', "BGRA", surface.get_stride())
return Image.frombuffer("RGB", (surface.get_width(), surface.get_height()),
surface.get_data(), 'raw', "BGRX", surface.get_stride())
def get_surface(func, identifier, ava, size): def get_surface(func, identifier, ava, size):

View file

@ -13,16 +13,15 @@
"""Notification window for text""" """Notification window for text"""
import logging import logging
import time import time
import re
import cairo
import math import math
import cairo
import gi import gi
from .image_getter import get_surface, draw_img_to_rect, get_aspected_size from .image_getter import get_surface, draw_img_to_rect
from .overlay import OverlayWindow from .overlay import OverlayWindow
gi.require_version("Gtk", "3.0")
gi.require_version('PangoCairo', '1.0') gi.require_version('PangoCairo', '1.0')
# pylint: disable=wrong-import-position,wrong-import-order # pylint: disable=wrong-import-position,wrong-import-order
from gi.repository import Gtk, Pango, PangoCairo # nopep8 from gi.repository import Pango, PangoCairo # nopep8
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -34,12 +33,54 @@ class NotificationOverlayWindow(OverlayWindow):
OverlayWindow.__init__(self, discover, piggyback) OverlayWindow.__init__(self, discover, piggyback)
self.text_spacing = 4 self.text_spacing = 4
self.content = [] self.content = []
self.test_content = [{"icon": "https://cdn.discordapp.com/icons/951077080769114172/991abffc0d2a5c040444be4d1a4085f4.webp?size=96", "title": "Title1"}, self.test_content = [
{"title": "Title2", "body": "Body", "icon": None}, {
{"icon": "https://cdn.discordapp.com/icons/951077080769114172/991abffc0d2a5c040444be4d1a4085f4.webp?size=96", "title": "Title 3", "icon": (
"body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}, "https://cdn.discordapp.com/"
{"icon": None, "title": "Title 3", "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}, "icons/951077080769114172/991abffc0d2a5c040444be4d1a4085f4.webp?size=96"
{"icon": "https://cdn.discordapp.com/avatars/147077941317206016/6a6935192076489fa6dc1eb5dafbf6e7.webp?size=128", "title": "PM", "body": "Birdy test"}] ),
"title": "Title1"
},
{
"title": "Title2",
"body": "Body",
"icon": None
},
{
"icon": ("https://cdn.discordapp.com/"
"icons/951077080769114172/991abffc0d2a5c040444be4d1a4085f4.webp?size=96"
),
"title": "Title 3",
"body": ("Lorem ipsum dolor sit amet, consectetur adipiscing elit,"
" sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris "
"nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in "
"reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla "
"pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa "
"qui officia deserunt mollit anim id est laborum."
)
},
{
"icon": None,
"title": "Title 3",
"body": ("Lorem ipsum dolor sit amet, consectetur adipiscing elit, "
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "
"Ut enim ad minim veniam, quis nostrud exercitation ullamco "
"laboris nisi ut aliquip ex ea commodo consequat. Duis aute "
"irure dolor in reprehenderit in voluptate velit esse cillum "
"dolore eu fugiat nulla pariatur. Excepteur sint occaecat "
"cupidatat non proident, sunt in culpa qui officia deserunt "
"mollit anim id est laborum."
)
},
{
"icon": ("https://cdn.discordapp.com/"
"avatars/147077941317206016/6a6935192076489fa6dc1eb5dafbf6e7.webp?size=128"
),
"title": "PM",
"body": "Birdy test"
}
]
self.text_font = None self.text_font = None
self.text_size = 13 self.text_size = 13
self.text_time = None self.text_time = None
@ -67,11 +108,12 @@ class NotificationOverlayWindow(OverlayWindow):
self.redraw() self.redraw()
def set_blank(self): def set_blank(self):
"""Set to no data and redraw"""
self.content = [] self.content = []
self.set_needs_redraw() self.set_needs_redraw()
def tick(self): def tick(self):
# This doesn't really belong in overlay or settings """Remove old messages from dataset"""
now = time.time() now = time.time()
newlist = [] newlist = []
oldsize = len(self.content) oldsize = len(self.content)
@ -85,6 +127,7 @@ class NotificationOverlayWindow(OverlayWindow):
self.set_needs_redraw() self.set_needs_redraw()
def add_notification_message(self, data): def add_notification_message(self, data):
"""Add new message to dataset"""
noti = None noti = None
data = data['data'] data = data['data']
message_id = data['message']['id'] message_id = data['message']['id']
@ -108,59 +151,50 @@ class NotificationOverlayWindow(OverlayWindow):
self.get_all_images() self.get_all_images()
def set_padding(self, padding): def set_padding(self, padding):
""" """Config option: Padding between notifications, in window-space pixels"""
Set the padding between notifications
"""
if self.padding != padding: if self.padding != padding:
self.padding = padding self.padding = padding
self.set_needs_redraw() self.set_needs_redraw()
def set_border_radius(self, radius): def set_border_radius(self, radius):
""" """Config option: Radius of the border, in window-space pixels"""
Set the radius of the border
"""
if self.border_radius != radius: if self.border_radius != radius:
self.border_radius = radius self.border_radius = radius
self.set_needs_redraw() self.set_needs_redraw()
def set_icon_size(self, size): def set_icon_size(self, size):
""" """Config option: Size of icons, in window-space pixels"""
Set Icon size
"""
if self.icon_size != size: if self.icon_size != size:
self.icon_size = size self.icon_size = size
self.image_list = {} self.image_list = {}
self.get_all_images() self.get_all_images()
def set_icon_pad(self, pad): def set_icon_pad(self, pad):
""" """Config option: Padding between icon and message, in window-space pixels"""
Set padding between icon and message
"""
if self.icon_pad != pad: if self.icon_pad != pad:
self.icon_pad = pad self.icon_pad = pad
self.set_needs_redraw() self.set_needs_redraw()
def set_icon_left(self, left): def set_icon_left(self, left):
"""Config option: Icon on left or right of text"""
if self.icon_left != left: if self.icon_left != left:
self.icon_left = left self.icon_left = left
self.set_needs_redraw() self.set_needs_redraw()
def set_text_time(self, timer): def set_text_time(self, timer):
""" """Config option: Duration that a message will be visible for, in seconds"""
Set the duration that a message will be visible for.
"""
self.text_time = timer self.text_time = timer
self.timer_after_draw = timer self.timer_after_draw = timer
def set_limit_width(self, limit): def set_limit_width(self, limit):
""" """Config option: Word wrap limit, in window-space pixels
Set the word wrap limit in pixels
""" """
if self.limit_width != limit: if self.limit_width != limit:
self.limit_width = limit self.limit_width = limit
self.set_needs_redraw() self.set_needs_redraw()
def get_all_images(self): def get_all_images(self):
"""Return a list of all downloaded images"""
the_list = self.content the_list = self.content
if self.testing: if self.testing:
the_list = self.test_content the_list = self.test_content
@ -171,47 +205,38 @@ class NotificationOverlayWindow(OverlayWindow):
get_surface(self.recv_icon, icon, icon, get_surface(self.recv_icon, icon, icon,
self.icon_size) self.icon_size)
def recv_icon(self, identifier, pix, mask): def recv_icon(self, identifier, pix, _mask):
""" """Callback from image_getter for icons"""
Called when image_getter has downloaded an image
"""
self.image_list[identifier] = pix self.image_list[identifier] = pix
self.set_needs_redraw() self.set_needs_redraw()
def set_fg(self, fg_col): def set_fg(self, fg_col):
""" """Config option: Set default text colour"""
Set default text colour
"""
if self.fg_col != fg_col: if self.fg_col != fg_col:
self.fg_col = fg_col self.fg_col = fg_col
self.set_needs_redraw() self.set_needs_redraw()
def set_bg(self, bg_col): def set_bg(self, bg_col):
""" """Config option: Set background colour"""
Set background colour
"""
if self.bg_col != bg_col: if self.bg_col != bg_col:
self.bg_col = bg_col self.bg_col = bg_col
self.set_needs_redraw() self.set_needs_redraw()
def set_show_icon(self, icon): def set_show_icon(self, icon):
""" """Config option: Set if icons should be shown inline"""
Set if icons should be shown inline
"""
if self.show_icon != icon: if self.show_icon != icon:
self.show_icon = icon self.show_icon = icon
self.set_needs_redraw() self.set_needs_redraw()
self.get_all_images() self.get_all_images()
def set_reverse_order(self, rev): def set_reverse_order(self, rev):
"""Config option: Reverse order of messages"""
if self.reverse_order != rev: if self.reverse_order != rev:
self.reverse_order = rev self.reverse_order = rev
self.set_needs_redraw() self.set_needs_redraw()
def set_font(self, font): def set_font(self, font):
""" """Config option: Font used to render text"""
Set font used to render text
"""
if self.text_font != font: if self.text_font != font:
self.text_font = font self.text_font = font
@ -222,13 +247,13 @@ class NotificationOverlayWindow(OverlayWindow):
self.set_needs_redraw() self.set_needs_redraw()
def recv_attach(self, identifier, pix): def recv_attach(self, identifier, pix):
""" """Callback from image_getter for attachments"""
Called when an image has been downloaded by image_getter
"""
self.icons[identifier] = pix self.icons[identifier] = pix
self.set_needs_redraw() self.set_needs_redraw()
def calc_all_height(self): def calc_all_height(self):
"""Return the height in window-space pixels required
to draw this overlay with current dataset"""
h = 0 h = 0
my_list = self.content my_list = self.content
if self.testing: if self.testing:
@ -240,6 +265,7 @@ class NotificationOverlayWindow(OverlayWindow):
return h return h
def calc_height(self, line): def calc_height(self, line):
"""Return height in window-space pixels required to draw individual notification"""
icon_width = 0 icon_width = 0
icon_pad = 0 icon_pad = 0
icon = line['icon'] icon = line['icon']
@ -257,9 +283,8 @@ class NotificationOverlayWindow(OverlayWindow):
layout = self.create_pango_layout(message) layout = self.create_pango_layout(message)
layout.set_auto_dir(True) layout.set_auto_dir(True)
layout.set_markup(message, -1) layout.set_markup(message, -1)
attr = layout.get_attributes() (_floating_x, _floating_y, floating_width,
(floating_x, floating_y, floating_width, _floating_height) = self.get_floating_coords()
floating_height) = self.get_floating_coords()
width = self.limit_width if floating_width > self.limit_width else floating_width width = self.limit_width if floating_width > self.limit_width else floating_width
layout.set_width((Pango.SCALE * (width - layout.set_width((Pango.SCALE * (width -
(self.border_radius * 4 + icon_width + icon_pad)))) (self.border_radius * 4 + icon_width + icon_pad))))
@ -267,12 +292,13 @@ class NotificationOverlayWindow(OverlayWindow):
if self.text_font: if self.text_font:
font = Pango.FontDescription(self.text_font) font = Pango.FontDescription(self.text_font)
layout.set_font_description(font) layout.set_font_description(font)
text_width, text_height = layout.get_pixel_size() _text_width, text_height = layout.get_pixel_size()
if text_height < icon_width: if text_height < icon_width:
text_height = icon_width text_height = icon_width
return text_height + (self.border_radius*4) + self.padding return text_height + (self.border_radius*4) + self.padding
def has_content(self): def has_content(self):
"""Return true if this overlay has meaningful content to show"""
if not self.enabled: if not self.enabled:
return False return False
if self.hidden: if self.hidden:
@ -282,15 +308,13 @@ class NotificationOverlayWindow(OverlayWindow):
return self.content return self.content
def overlay_draw(self, w, context, data=None): def overlay_draw(self, w, context, data=None):
""" """Draw the overlay"""
Draw the overlay
"""
if self.piggyback: if self.piggyback:
self.piggyback.overlay_draw(w, context, data) self.piggyback.overlay_draw(w, context, data)
if not self.enabled: if not self.enabled:
return return
self.context = context self.context = context
(width, height) = self.get_size() (_width, height) = self.get_size()
if not self.piggyback_parent: if not self.piggyback_parent:
context.set_antialias(cairo.ANTIALIAS_GOOD) context.set_antialias(cairo.ANTIALIAS_GOOD)
@ -319,7 +343,6 @@ class NotificationOverlayWindow(OverlayWindow):
current_y = 0 current_y = 0
if self.align_vert == 1: # Center. Oh god why if self.align_vert == 1: # Center. Oh god why
current_y = (height/2.0) - (self.calc_all_height() / 2.0) current_y = (height/2.0) - (self.calc_all_height() / 2.0)
tnow = time.time()
if self.testing: if self.testing:
the_list = self.test_content the_list = self.test_content
else: else:
@ -354,10 +377,7 @@ class NotificationOverlayWindow(OverlayWindow):
self.context = None self.context = None
def draw_text(self, pos_y, text, icon): def draw_text(self, pos_y, text, icon):
""" """Draw a text message, returning the Y position of the next message"""
Draw a text message, returning the Y position of the next message
"""
icon_width = self.icon_size icon_width = self.icon_size
icon_pad = self.icon_pad icon_pad = self.icon_pad
if not self.show_icon: if not self.show_icon:
@ -371,8 +391,8 @@ class NotificationOverlayWindow(OverlayWindow):
layout.set_markup(text, -1) layout.set_markup(text, -1)
attr = layout.get_attributes() attr = layout.get_attributes()
(floating_x, floating_y, floating_width, (_floating_x, _floating_y, floating_width,
floating_height) = self.get_floating_coords() _floating_height) = self.get_floating_coords()
width = self.limit_width if floating_width > self.limit_width else floating_width width = self.limit_width if floating_width > self.limit_width else floating_width
layout.set_width((Pango.SCALE * (width - layout.set_width((Pango.SCALE * (width -
(self.border_radius * 4 + icon_width + icon_pad)))) (self.border_radius * 4 + icon_width + icon_pad))))
@ -475,7 +495,6 @@ class NotificationOverlayWindow(OverlayWindow):
self.get_pango_context(), self.render_custom, None) self.get_pango_context(), self.render_custom, None)
text = layout.get_text() text = layout.get_text()
count = 0
layout.set_attributes(attr) layout.set_attributes(attr)
@ -486,7 +505,6 @@ class NotificationOverlayWindow(OverlayWindow):
self.get_pango_context(), self.render_custom, None) self.get_pango_context(), self.render_custom, None)
text = layout.get_text() text = layout.get_text()
count = 0
layout.set_attributes(attr) layout.set_attributes(attr)
@ -501,11 +519,9 @@ class NotificationOverlayWindow(OverlayWindow):
return next_y return next_y
def render_custom(self, ctx, shape, path, _data): def render_custom(self, ctx, shape, path, _data):
""" """Draw an inline image as a custom emoticon"""
Draw an inline image as a custom emoticon
"""
if shape.data >= len(self.image_list): if shape.data >= len(self.image_list):
log.warning(f"{shape.data} >= {len(self.image_list)}") log.warning("%s >= %s", shape.data, len(self.image_list))
return return
# key is the url to the image # key is the url to the image
key = self.image_list[shape.data] key = self.image_list[shape.data]
@ -521,9 +537,7 @@ class NotificationOverlayWindow(OverlayWindow):
return True return True
def sanitize_string(self, string): def sanitize_string(self, string):
""" """Sanitize a text message so that it doesn't intefere with Pango's XML format"""
Sanitize a text message so that it doesn't intefere with Pango's XML format
"""
string = string.replace("&", "&amp;") string = string.replace("&", "&amp;")
string = string.replace("<", "&lt;") string = string.replace("<", "&lt;")
string = string .replace(">", "&gt;") string = string .replace(">", "&gt;")
@ -532,6 +546,7 @@ class NotificationOverlayWindow(OverlayWindow):
return string return string
def set_testing(self, testing): def set_testing(self, testing):
"""Toggle placeholder images for testing"""
self.testing = testing self.testing = testing
self.set_needs_redraw() self.set_needs_redraw()
self.get_all_images() self.get_all_images()

View file

@ -19,7 +19,6 @@ import sys
import logging import logging
import gi import gi
import cairo import cairo
import Xlib
from Xlib.display import Display from Xlib.display import Display
from Xlib import X, Xatom from Xlib import X, Xatom
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
@ -46,7 +45,7 @@ class OverlayWindow(Gtk.Window):
""" """
window = Gtk.Window() window = Gtk.Window()
screen = window.get_screen() screen = window.get_screen()
screen_type = "%s" % (screen) screen_type = f"{screen}"
self.is_wayland = False self.is_wayland = False
if "Wayland" in screen_type: if "Wayland" in screen_type:
self.is_wayland = True self.is_wayland = True
@ -124,10 +123,12 @@ class OverlayWindow(Gtk.Window):
# this process hanging if it happens # this process hanging if it happens
self.connect('destroy', self.window_exited) self.connect('destroy', self.window_exited)
def window_exited(self, window=None): def window_exited(self, _window=None):
"""Window closed. Exit app"""
sys.exit(1) sys.exit(1)
def set_gamescope_xatom(self, enabled): def set_gamescope_xatom(self, enabled):
"""Set Gamescope XAtom to identify self as an overlay candidate"""
if self.piggyback_parent: if self.piggyback_parent:
return return
@ -136,7 +137,7 @@ class OverlayWindow(Gtk.Window):
self.is_xatom_set = enabled self.is_xatom_set = enabled
display = Display() display = Display()
atom = display.intern_atom("GAMESCOPE_EXTERNAL_OVERLAY") atom = display.intern_atom("GAMESCOPE_EXTERNAL_OVERLAY")
opaq = display.intern_atom("_NET_WM_WINDOW_OPACITY") # Since unused: _NET_WM_WINDOW_OPACITY
if self.get_toplevel().get_window(): if self.get_toplevel().get_window():
topw = display.create_resource_object( topw = display.create_resource_object(
@ -148,7 +149,7 @@ class OverlayWindow(Gtk.Window):
log.info("Setting GAMESCOPE_EXTERNAL_OVERLAY to %s", enabled) log.info("Setting GAMESCOPE_EXTERNAL_OVERLAY to %s", enabled)
display.sync() display.sync()
else: else:
log.warn("Unable to set GAMESCOPE_EXTERNAL_OVERLAY") log.warning("Unable to set GAMESCOPE_EXTERNAL_OVERLAY")
def set_wayland_state(self): def set_wayland_state(self):
""" """
@ -169,13 +170,16 @@ class OverlayWindow(Gtk.Window):
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.TOP, True) GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.TOP, True)
def set_piggyback(self, other_overlay): def set_piggyback(self, other_overlay):
"""Sets as piggybacking off the given (other) overlay"""
other_overlay.piggyback = self other_overlay.piggyback = self
self.piggyback_parent = other_overlay self.piggyback_parent = other_overlay
def has_content(self): def has_content(self):
"""Return true if overlay has meaningful content"""
return False return False
def overlay_draw_pre(self, _w, context, data=None): def overlay_draw_pre(self, _w, context, data=None):
"""Prepare for drawing the overlay. Calls overlay_draw after preparations"""
content = self.has_content() content = self.has_content()
if self.piggyback and self.piggyback.has_content(): if self.piggyback and self.piggyback.has_content():
content = True content = True
@ -224,16 +228,18 @@ class OverlayWindow(Gtk.Window):
""" """
if width > 1.0 and height > 1.0: if width > 1.0 and height > 1.0:
# Old data. # Old data.
(screen_x, screen_y, screen_width, (_screen_x, _screen_y, screen_width,
screen_height) = self.get_display_coords() screen_height) = self.get_display_coords()
pos_x = float(pos_x) / screen_width pos_x = float(pos_x) / screen_width
pos_y = float(pos_y) / screen_height pos_y = float(pos_y) / screen_height
width = float(width) / screen_width width = float(width) / screen_width
height = float(height) / screen_height height = float(height) / screen_height
if self.floating != floating or self.pos_x != pos_x or self.pos_y != pos_y or self.width != width or self.height != height: if (self.floating != floating or self.pos_x != pos_x or
self.pos_y != pos_y or self.width != width or self.height != height):
# Special case for Cinnamon desktop : see https://github.com/trigg/Discover/issues/322 # Special case for Cinnamon desktop : see https://github.com/trigg/Discover/issues/322
if 'XDG_SESSION_DESKTOP' in os.environ and os.environ['XDG_SESSION_DESKTOP'] == 'cinnamon': if ('XDG_SESSION_DESKTOP' in os.environ and
os.environ['XDG_SESSION_DESKTOP'] == 'cinnamon'):
floating = True floating = True
self.floating = floating self.floating = floating
@ -258,6 +264,7 @@ class OverlayWindow(Gtk.Window):
self.input_shape_combine_region(reg) self.input_shape_combine_region(reg)
def set_hide_on_mouseover(self, hide): def set_hide_on_mouseover(self, hide):
"""Set if the overlay should hide when mouse moves over it"""
if self.hide_on_mouseover != hide: if self.hide_on_mouseover != hide:
self.hide_on_mouseover = hide self.hide_on_mouseover = hide
if self.hide_on_mouseover: if self.hide_on_mouseover:
@ -266,6 +273,7 @@ class OverlayWindow(Gtk.Window):
self.set_untouchable() self.set_untouchable()
def set_mouseover_timer(self, time): def set_mouseover_timer(self, time):
"""Set the time until the overlay reappears after mouse over"""
self.timeout_mouse_over = time self.timeout_mouse_over = time
def unset_shape(self): def unset_shape(self):
@ -299,6 +307,7 @@ class OverlayWindow(Gtk.Window):
self.set_needs_redraw() self.set_needs_redraw()
def get_display_coords(self): def get_display_coords(self):
"""Get screen space co-ordinates of the monitor"""
if self.piggyback_parent: if self.piggyback_parent:
return self.piggyback_parent.get_display_coords() return self.piggyback_parent.get_display_coords()
monitor = self.get_monitor_from_plug() monitor = self.get_monitor_from_plug()
@ -311,29 +320,35 @@ class OverlayWindow(Gtk.Window):
return (0, 0, 1920, 1080) return (0, 0, 1920, 1080)
def get_floating_coords(self): def get_floating_coords(self):
"""Get screen space co-ordinates of the window"""
(screen_x, screen_y, screen_width, screen_height) = self.get_display_coords() (screen_x, screen_y, screen_width, screen_height) = self.get_display_coords()
if self.floating: if self.floating:
if self.pos_x == None or self.pos_y == None or self.width == None or self.height == None: if (self.pos_x is None or self.pos_y is None or
self.width is None or self.height is None):
log.error("No usable floating position") log.error("No usable floating position")
if not self.is_wayland: if not self.is_wayland:
return (screen_x + self.pos_x * screen_width, screen_y + self.pos_y * screen_height, self.width * screen_width, self.height * screen_height) return (screen_x + self.pos_x * screen_width, screen_y + self.pos_y * screen_height,
return (self.pos_x * screen_width, self.pos_y * screen_height, self.width * screen_width, self.height * screen_height) self.width * screen_width, self.height * screen_height)
return (self.pos_x * screen_width, self.pos_y * screen_height,
self.width * screen_width, self.height * screen_height)
else: else:
return (screen_x, screen_y, screen_width, screen_height) return (screen_x, screen_y, screen_width, screen_height)
def set_needs_redraw(self, be_pushy=False): def set_needs_redraw(self, be_pushy=False):
"""Schedule this overlay for a redraw. If part of a
piggyback chain, pass it up to be redrawn by topmost parent"""
if (not self.hidden and self.enabled) or be_pushy: if (not self.hidden and self.enabled) or be_pushy:
if self.piggyback_parent: if self.piggyback_parent:
self.piggyback_parent.set_needs_redraw(be_pushy=True) self.piggyback_parent.set_needs_redraw(be_pushy=True)
if self.redraw_id == None: if self.redraw_id is None:
self.redraw_id = GLib.idle_add(self.redraw) self.redraw_id = GLib.idle_add(self.redraw)
else: else:
log.debug("Already awaiting paint") log.debug("Already awaiting paint")
# If this overlay has data that expires after draw, plan for that here # If this overlay has data that expires after draw, plan for that here
if self.timer_after_draw != None: if self.timer_after_draw is not None:
GLib.timeout_add_seconds(self.timer_after_draw, self.redraw) GLib.timeout_add_seconds(self.timer_after_draw, self.redraw)
def redraw(self): def redraw(self):
@ -364,6 +379,7 @@ class OverlayWindow(Gtk.Window):
return False return False
def set_hidden(self, hidden): def set_hidden(self, hidden):
"""Set if the overlay should be hidden"""
self.hidden = hidden self.hidden = hidden
self.set_enabled(self.enabled) self.set_enabled(self.enabled)
@ -371,7 +387,7 @@ class OverlayWindow(Gtk.Window):
""" """
Set the monitor this overlay should display on. Set the monitor this overlay should display on.
""" """
plug_name = "%s" % (idx) plug_name = f"{idx}"
if self.monitor != plug_name: if self.monitor != plug_name:
self.monitor = plug_name self.monitor = plug_name
if self.is_wayland: if self.is_wayland:
@ -387,6 +403,8 @@ class OverlayWindow(Gtk.Window):
self.set_needs_redraw() self.set_needs_redraw()
def get_monitor_from_plug(self): def get_monitor_from_plug(self):
"""Return a GDK Monitor filtered by plug name
(HDMI-1, eDP-1, VGA etc)"""
if not self.monitor or self.monitor == "Any": if not self.monitor or self.monitor == "Any":
return None return None
display = Gdk.Display.get_default() display = Gdk.Display.get_default()
@ -454,26 +472,31 @@ class OverlayWindow(Gtk.Window):
self.hide() self.hide()
def set_task(self, visible): def set_task(self, visible):
"""Set visible on taskbar. Not working at last check"""
self.set_skip_pager_hint(not visible) self.set_skip_pager_hint(not visible)
self.set_skip_taskbar_hint(not visible) self.set_skip_taskbar_hint(not visible)
def check_composite(self, _a=None, _b=None): def check_composite(self, _a=None, _b=None):
# Called when an X11 session switched compositing on or off """Callback for compositing started/stopped in X11"""
self.redraw() self.redraw()
def screen_changed(self, screen=None): def screen_changed(self, _screen=None):
"""Callback to set monitor to display on"""
self.set_monitor(self.monitor) self.set_monitor(self.monitor)
def mouseover(self, a=None, b=None): def mouseover(self, _a=None, _b=None):
"""Callback when mouseover occurs, hides overlay"""
self.draw_blank = True self.draw_blank = True
self.set_needs_redraw() self.set_needs_redraw()
return True return True
def mouseout(self, a=None, b=None): def mouseout(self, _a=None, _b=None):
"""Callback when mouseout occurs, sets a timer to show overlay"""
GLib.timeout_add_seconds(self.timeout_mouse_over, self.mouseout_timed) GLib.timeout_add_seconds(self.timeout_mouse_over, self.mouseout_timed)
return True return True
def mouseout_timed(self, a=None, b=None): def mouseout_timed(self, _a=None, _b=None):
"""Callback a short while after mouseout occured, shows overlay"""
self.draw_blank = False self.draw_blank = False
self.set_needs_redraw() self.set_needs_redraw()
return False return False

View file

@ -11,18 +11,19 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Settings window holding all settings tab""" """Settings window holding all settings tab"""
# pylint: disable=missing-function-docstring
import gettext import gettext
import gi
import logging import logging
import pkg_resources
import sys import sys
import os import os
import json import json
from configparser import ConfigParser
import gi
import pkg_resources
from .autostart import Autostart, BazziteAutostart from .autostart import Autostart, BazziteAutostart
from .draggable_window import DraggableWindow from .draggable_window import DraggableWindow
from .draggable_window_wayland import DraggableWindowWayland from .draggable_window_wayland import DraggableWindowWayland
from configparser import ConfigParser
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
# pylint: disable=wrong-import-position,wrong-import-order # pylint: disable=wrong-import-position,wrong-import-order
from gi.repository import Gtk, Gdk, Gio # nopep8 from gi.repository import Gtk, Gdk, Gio # nopep8
@ -75,6 +76,14 @@ class MainSettingsWindow():
self.current_guild = "0" self.current_guild = "0"
self.current_channel = "0" self.current_channel = "0"
self.hidden_overlay = False self.hidden_overlay = False
self.voice_floating_x = 0
self.voice_floating_y = 0
self.voice_floating_w = 0
self.voice_floating_h = 0
self.text_floating_x = 0
self.text_floating_y = 0
self.text_floating_w = 0
self.text_floating_h = 0
self.menu = self.make_menu() self.menu = self.make_menu()
self.make_sys_tray_icon(self.menu) self.make_sys_tray_icon(self.menu)
@ -111,16 +120,30 @@ class MainSettingsWindow():
if name.endswith("_all"): if name.endswith("_all"):
widget.set_label(_(widget.get_label())) widget.set_label(_(widget.get_label()))
self.widget['overview_main_text'].set_markup("<span size=\"larger\">%s (%s)</span>\n\n%s\n\n%s (<a href=\"https://discord.gg/jRKWMuDy5V\">https://discord.gg/jRKWMuDy5V</a>) %s (<a href=\"https://github.com/trigg/Discover\">https://github.com/trigg/Discover</a>)\n\n\n\n\n\n" % ( self.widget['overview_main_text'].set_markup(
"%s%s (%s)%s%s\n\n%s %s %s %s%s\n\n\n\n\n\n" % (
"<span size=\"larger\">",
_("Welcome to Discover Overlay"), _("Welcome to Discover Overlay"),
pkg_resources.get_distribution('discover_overlay').version, pkg_resources.get_distribution('discover_overlay').version,
_("Discover-Overlay is a GTK3 overlay written in Python3. It can be configured to show who is currently talking on discord or it can be set to display text and images from a preconfigured channel. It is fully customisable and can be configured to display anywhere on the screen. We fully support X11 and wlroots based environments. We felt the need to make this project due to the shortcomings in support on Linux by the official discord client."), "</span>\n\n",
_(("Discover-Overlay is a GTK3 overlay written in Python3."
" It can be configured to show who is currently talking"
" on discord or it can be set to display text and images"
" from a preconfigured channel. It is fully customisable"
" and can be configured to display anywhere on the screen."
" We fully support X11 and wlroots based environments. We "
"felt the need to make this project due to the shortcomings"
" in support on Linux by the official discord client.")),
_("Please visit our discord"), _("Please visit our discord"),
_(" for support. Or open an issue on our GitHub ") "(<a href=\"https://discord.gg/jRKWMuDy5V\">https://discord.gg/jRKWMuDy5V</a>)",
)) _(" for support. Or open an issue on our GitHub "),
"(<a href=\"https://github.com/trigg/Discover\">",
"https://github.com/trigg/Discover</a>)"
)
)
screen = window.get_screen() screen = window.get_screen()
screen_type = "%s" % (screen) screen_type = f"{screen}"
self.is_wayland = False self.is_wayland = False
if "Wayland" in screen_type: if "Wayland" in screen_type:
self.is_wayland = True self.is_wayland = True
@ -185,6 +208,7 @@ class MainSettingsWindow():
self.widget['window'].set_default_icon_name(self.icon_name) self.widget['window'].set_default_icon_name(self.icon_name)
def set_steamos_window_size(self): def set_steamos_window_size(self):
"""Set window based on steamos usage"""
# Huge bunch of assumptions. # Huge bunch of assumptions.
# Gamescope only has one monitor # Gamescope only has one monitor
# Gamescope has no scale factor # Gamescope has no scale factor
@ -193,23 +217,21 @@ class MainSettingsWindow():
monitor = display.get_monitor(0) monitor = display.get_monitor(0)
if monitor: if monitor:
geometry = monitor.get_geometry() geometry = monitor.get_geometry()
scale_factor = monitor.get_scale_factor() log.info("%d %d", geometry.width, geometry.height)
log.info("%d %d" % (geometry.width, geometry.height))
self.window.set_size_request(geometry.width, geometry.height) self.window.set_size_request(geometry.width, geometry.height)
def keypress_in_settings(self, window, event): def keypress_in_settings(self, window, event):
"""Callback to steal keypresses to assist SteamOS gamepad control"""
if self.spinning_focus: if self.spinning_focus:
match event.keyval: match event.keyval:
case Gdk.KEY_Right: case Gdk.KEY_Right:
step = self.spinning_focus.get_increments().step step = self.spinning_focus.get_increments().step
value = self.spinning_focus.get_value() value = self.spinning_focus.get_value()
self.spinning_focus.set_value(value + step) self.spinning_focus.set_value(value + step)
pass
case Gdk.KEY_Left: case Gdk.KEY_Left:
step = self.spinning_focus.get_increments().step step = self.spinning_focus.get_increments().step
value = self.spinning_focus.get_value() value = self.spinning_focus.get_value()
self.spinning_focus.set_value(value - step) self.spinning_focus.set_value(value - step)
pass
case Gdk.KEY_Up: case Gdk.KEY_Up:
step = self.spinning_focus.get_increments().step step = self.spinning_focus.get_increments().step
value = self.spinning_focus.get_value() value = self.spinning_focus.get_value()
@ -230,11 +252,9 @@ class MainSettingsWindow():
case Gdk.KEY_Right: case Gdk.KEY_Right:
value = self.scale_focus.get_value() value = self.scale_focus.get_value()
self.scale_focus.set_value(value + 0.1) self.scale_focus.set_value(value + 0.1)
pass
case Gdk.KEY_Left: case Gdk.KEY_Left:
value = self.scale_focus.get_value() value = self.scale_focus.get_value()
self.scale_focus.set_value(value - 0.1) self.scale_focus.set_value(value - 0.1)
pass
case Gdk.KEY_Up: case Gdk.KEY_Up:
value = self.scale_focus.get_value() value = self.scale_focus.get_value()
self.scale_focus.set_value(value + 0.1) self.scale_focus.set_value(value + 0.1)
@ -267,7 +287,7 @@ class MainSettingsWindow():
widget = self.window.get_focus() widget = self.window.get_focus()
if widget: if widget:
# I really want there to be a better way... # I really want there to be a better way...
widget_type = "%s" % (widget) widget_type = f"{widget}"
if 'Gtk.SpinButton' in widget_type: if 'Gtk.SpinButton' in widget_type:
self.spinning_focus = widget self.spinning_focus = widget
@ -285,16 +305,19 @@ class MainSettingsWindow():
return True return True
def request_channels_from_guild(self, guild_id): def request_channels_from_guild(self, guild_id):
with open(self.rpc_file, 'w') as f: """Send RPC to overlay to request updated channel list"""
f.write('--rpc --guild-request=%s' % (guild_id)) with open(self.rpc_file, 'w', encoding="utf-8") as f:
f.write(f"--rpc --guild-request={guild_id}")
def populate_guild_menu(self, _a=None, _b=None, _c=None, _d=None): def populate_guild_menu(self, _a=None, _b=None, _c=None, _d=None):
"""Read guild data and repopulate widget.
Disable signal handling meanwhile to avoid recursive logic"""
g = self.widget['text_server'] g = self.widget['text_server']
c = self.widget['text_channel'] c = self.widget['text_channel']
g.handler_block(self.server_handler) g.handler_block(self.server_handler)
c.handler_block(self.channel_handler) c.handler_block(self.channel_handler)
try: try:
with open(self.channel_file, "r") as tfile: with open(self.channel_file, "r", encoding="utf-8") as tfile:
data = tfile.readlines() data = tfile.readlines()
if len(data) >= 1: if len(data) >= 1:
data = json.loads(data[0]) data = json.loads(data[0])
@ -322,21 +345,22 @@ class MainSettingsWindow():
c.handler_unblock(self.channel_handler) c.handler_unblock(self.channel_handler)
def populate_monitor_menus(self, _a=None, _b=None): def populate_monitor_menus(self, _a=None, _b=None):
v = self.widget['voice_monitor'] """Get Monitor list from GTK and repopulate widget"""
t = self.widget['text_monitor'] voice = self.widget['voice_monitor']
m = self.widget['notification_monitor'] text = self.widget['text_monitor']
notify = self.widget['notification_monitor']
v_value = v.get_active() v_value = voice.get_active()
t_value = t.get_active() t_value = text.get_active()
m_value = m.get_active() m_value = notify.get_active()
v.remove_all() voice.remove_all()
t.remove_all() text.remove_all()
m.remove_all() notify.remove_all()
v.append_text("Any") voice.append_text("Any")
t.append_text("Any") text.append_text("Any")
m.append_text("Any") notify.append_text("Any")
display = Gdk.Display.get_default() display = Gdk.Display.get_default()
screen = self.window.get_screen() screen = self.window.get_screen()
@ -348,41 +372,38 @@ class MainSettingsWindow():
manufacturer = this_mon.get_manufacturer() manufacturer = this_mon.get_manufacturer()
model = this_mon.get_model() model = this_mon.get_model()
connector = screen.get_monitor_plug_name(i) connector = screen.get_monitor_plug_name(i)
monitor_label = "%s %s\n%s" % ( monitor_label = f"{manufacturer} {model}\n{connector}"
manufacturer, model, connector) voice.append_text(monitor_label)
v.append_text(monitor_label) text.append_text(monitor_label)
t.append_text(monitor_label) notify.append_text(monitor_label)
m.append_text(monitor_label)
v.set_active(v_value) voice.set_active(v_value)
t.set_active(t_value) text.set_active(t_value)
m.set_active(m_value) notify.set_active(m_value)
def close_window(self, widget=None, event=None): def close_window(self, _widget=None, _event=None):
""" """Hide the settings window for use at a later date"""
Hide the settings window for use at a later date
"""
self.window.hide() self.window.hide()
if self.ind == None and self.tray == None: if self.ind is None and self.tray is None:
sys.exit(0) sys.exit(0)
if self.ind != None: if self.ind is not None:
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
from gi.repository import AppIndicator3 from gi.repository import AppIndicator3
if self.ind.get_status() == AppIndicator3.IndicatorStatus.PASSIVE: if self.ind.get_status() == AppIndicator3.IndicatorStatus.PASSIVE:
sys.exit(0) sys.exit(0)
return True return True
def close_app(self, widget=None, event=None): def close_app(self, _widget=None, _event=None):
"""Close the app"""
sys.exit(0) sys.exit(0)
def present_settings(self, _a=None): def present_settings(self, _a=None):
""" """Show the settings window"""
Show the settings window
"""
self.widget['notebook'].set_current_page(0) self.widget['notebook'].set_current_page(0)
self.window.show() self.window.show()
def set_alignment_labels(self, horz): def set_alignment_labels(self, horz):
"""Relabel alignment pulldowns"""
m1 = self.widget['voice_align_1'].get_model() m1 = self.widget['voice_align_1'].get_model()
m2 = self.widget['voice_align_2'].get_model() m2 = self.widget['voice_align_2'].get_model()
i = m1.get_iter_first() i = m1.get_iter_first()
@ -409,6 +430,7 @@ class MainSettingsWindow():
m2.set_value(i2, 0, _("Bottom")) m2.set_value(i2, 0, _("Bottom"))
def read_config(self): def read_config(self):
"""Read config from disk"""
self.loading_config = True self.loading_config = True
# Read config and put into gui # Read config and put into gui
@ -522,8 +544,8 @@ class MainSettingsWindow():
self.widget['voice_square_avatar'].set_active(config.getboolean( self.widget['voice_square_avatar'].set_active(config.getboolean(
"main", "square_avatar", fallback=True)) "main", "square_avatar", fallback=True))
self.widget['voice_fancy_avatar_shapes'].set_active(config.getboolean("main", self.widget['voice_fancy_avatar_shapes'].set_active(
"fancy_border", fallback=True)) config.getboolean("main", "fancy_border", fallback=True))
self.widget['voice_order_avatars_by'].set_active( self.widget['voice_order_avatars_by'].set_active(
config.getint("main", "order", fallback=0)) config.getint("main", "order", fallback=0))
@ -719,6 +741,7 @@ class MainSettingsWindow():
self.loading_config = False self.loading_config = False
def make_colour(self, col): def make_colour(self, col):
"""Create a Gdk Color from a col tuple"""
col = json.loads(col) col = json.loads(col)
return Gdk.RGBA(col[0], col[1], col[2], col[3]) return Gdk.RGBA(col[0], col[1], col[2], col[3])
@ -732,6 +755,7 @@ class MainSettingsWindow():
return guild_ids return guild_ids
def get_monitor_index_from_plug(self, monitor): def get_monitor_index_from_plug(self, monitor):
"""Get monitor index from plug name"""
if not monitor or monitor == "Any": if not monitor or monitor == "Any":
return 0 return 0
display = Gdk.Display.get_default() display = Gdk.Display.get_default()
@ -746,9 +770,7 @@ class MainSettingsWindow():
return 0 return 0
def get_monitor_obj(self, idx): def get_monitor_obj(self, idx):
""" """Helper function to find the monitor object of the monitor"""
Helper function to find the monitor object of the monitor
"""
display = Gdk.Display.get_default() display = Gdk.Display.get_default()
return display.get_monitor(idx) return display.get_monitor(idx)
@ -780,17 +802,13 @@ class MainSettingsWindow():
self.tray.set_visible(False) self.tray.set_visible(False)
def show_menu(self, obj, button, time): def show_menu(self, obj, button, time):
""" """Show menu when System Tray icon is clicked"""
Show menu when System Tray icon is clicked
"""
self.menu.show_all() self.menu.show_all()
self.menu.popup( self.menu.popup(
None, None, Gtk.StatusIcon.position_menu, obj, button, time) None, None, Gtk.StatusIcon.position_menu, obj, button, time)
def set_sys_tray_icon_visible(self, visible): def set_sys_tray_icon_visible(self, visible):
""" """Sets whether the tray icon is visible"""
Sets whether the tray icon is visible
"""
if self.ind is not None: if self.ind is not None:
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
from gi.repository import AppIndicator3 from gi.repository import AppIndicator3
@ -800,9 +818,7 @@ class MainSettingsWindow():
self.tray.set_visible(visible) self.tray.set_visible(visible)
def make_menu(self): def make_menu(self):
""" """Create System Menu"""
Create System Menu
"""
menu = Gtk.Menu() menu = Gtk.Menu()
settings_opt = Gtk.MenuItem.new_with_label(_("Settings")) settings_opt = Gtk.MenuItem.new_with_label(_("Settings"))
self.toggle_opt = Gtk.MenuItem.new_with_label(_("Hide overlay")) self.toggle_opt = Gtk.MenuItem.new_with_label(_("Hide overlay"))
@ -822,11 +838,13 @@ class MainSettingsWindow():
return menu return menu
def toggle_overlay(self, _a=None, _b=None): def toggle_overlay(self, _a=None, _b=None):
"""Toggle overlay visibility"""
self.hidden_overlay = not self.hidden_overlay self.hidden_overlay = not self.hidden_overlay
self.config_set("general", "hideoverlay", "%s" % (self.hidden_overlay)) self.config_set("general", "hideoverlay", f"{self.hidden_overlay}")
self.update_toggle_overlay() self.update_toggle_overlay()
def update_toggle_overlay(self, _a=None, _b=None): def update_toggle_overlay(self, _a=None, _b=None):
"""Update gui to reflect state of overlay visibility"""
self.widget['core_hide_overlay'].handler_block( self.widget['core_hide_overlay'].handler_block(
self.hidden_overlay_handler) self.hidden_overlay_handler)
@ -840,14 +858,17 @@ class MainSettingsWindow():
self.toggle_opt.set_label(_("Hide overlay")) self.toggle_opt.set_label(_("Hide overlay"))
def close_overlay(self, _a=None, _b=None): def close_overlay(self, _a=None, _b=None):
with open(self.rpc_file, 'w') as f: """Send RPC to tell the overlay to close"""
with open(self.rpc_file, 'w', encoding="utf-8") as f:
f.write('--rpc --close') f.write('--rpc --close')
def overview_close(self, button): def overview_close(self, _button):
"""Gui callback to close overlay. Remove and use close_overlay?"""
log.info("Quit pressed") log.info("Quit pressed")
self.close_overlay() self.close_overlay()
def voice_place_window(self, button): def voice_place_window(self, button):
"""Toggle the voice placement"""
if self.voice_placement_window: if self.voice_placement_window:
(pos_x, pos_y, width, height) = self.voice_placement_window.get_coords() (pos_x, pos_y, width, height) = self.voice_placement_window.get_coords()
self.voice_floating_x = pos_x self.voice_floating_x = pos_x
@ -857,18 +878,14 @@ class MainSettingsWindow():
config = ConfigParser(interpolation=None) config = ConfigParser(interpolation=None)
config.read(self.config_file) config.read(self.config_file)
if not "main" in config.sections(): if "main" not in config.sections():
config.add_section("main") config.add_section("main")
config.set("main", "floating_x", "%f" % config.set("main", "floating_x", f"{self.voice_floating_x:f}")
(self.voice_floating_x)) config.set("main", "floating_y", f"{self.voice_floating_y:f}")
config.set("main", "floating_y", "%f" % config.set("main", "floating_w", f"{self.voice_floating_w:f}")
(self.voice_floating_y)) config.set("main", "floating_h", f"{self.voice_floating_h:f}")
config.set("main", "floating_w", "%f" %
(self.voice_floating_w))
config.set("main", "floating_h", "%f" %
(self.voice_floating_h))
with open(self.config_file, 'w') as file: with open(self.config_file, 'w', encoding="utf-8") as file:
config.write(file) config.write(file)
if button: if button:
button.set_label(_("Place Window")) button.set_label(_("Place Window"))
@ -890,11 +907,13 @@ class MainSettingsWindow():
self.voice_placement_window = DraggableWindow( self.voice_placement_window = DraggableWindow(
pos_x=self.voice_floating_x, pos_y=self.voice_floating_y, pos_x=self.voice_floating_x, pos_y=self.voice_floating_y,
width=self.voice_floating_w, height=self.voice_floating_h, width=self.voice_floating_w, height=self.voice_floating_h,
message=_("Place & resize this window then press Save!"), settings=self, monitor=self.widget['voice_monitor'].get_active()-1) message=_("Place & resize this window then press Save!"),
settings=self, monitor=self.widget['voice_monitor'].get_active()-1)
if button: if button:
button.set_label(_("Save this position")) button.set_label(_("Save this position"))
def text_place_window(self, button): def text_place_window(self, button):
"""Toggle the text placement"""
if self.text_placement_window: if self.text_placement_window:
(pos_x, pos_y, width, height) = self.text_placement_window.get_coords() (pos_x, pos_y, width, height) = self.text_placement_window.get_coords()
self.text_floating_x = pos_x self.text_floating_x = pos_x
@ -904,18 +923,14 @@ class MainSettingsWindow():
config = ConfigParser(interpolation=None) config = ConfigParser(interpolation=None)
config.read(self.config_file) config.read(self.config_file)
if not "text" in config.sections(): if "text" not in config.sections():
config.add_section("text") config.add_section("text")
config.set("text", "floating_x", "%f" % config.set("text", "floating_x", f"{self.text_floating_x:f}")
(self.text_floating_x)) config.set("text", "floating_y", f"{self.text_floating_y:f}")
config.set("text", "floating_y", "%f" % config.set("text", "floating_w", f"{self.text_floating_w:f}")
(self.text_floating_y)) config.set("text", "floating_h", f"{self.text_floating_h:f}")
config.set("text", "floating_w", "%f" %
(self.text_floating_w))
config.set("text", "floating_h", "%f" %
(self.text_floating_h))
with open(self.config_file, 'w') as file: with open(self.config_file, 'w', encoding="utf-8") as file:
config.write(file) config.write(file)
if button: if button:
button.set_label(_("Place Window")) button.set_label(_("Place Window"))
@ -937,21 +952,25 @@ class MainSettingsWindow():
self.text_placement_window = DraggableWindow( self.text_placement_window = DraggableWindow(
pos_x=self.text_floating_x, pos_y=self.text_floating_y, pos_x=self.text_floating_x, pos_y=self.text_floating_y,
width=self.text_floating_w, height=self.text_floating_h, width=self.text_floating_w, height=self.text_floating_h,
message=_("Place & resize this window then press Save!"), settings=self, monitor=self.widget['text_monitor'].get_active()-1) message=_("Place & resize this window then press Save!"),
settings=self, monitor=self.widget['text_monitor'].get_active()-1)
if button: if button:
button.set_label(_("Save this position")) button.set_label(_("Save this position"))
def change_placement(self, placement_window): def change_placement(self, placement_window):
"""Finish window placement"""
if placement_window == self.text_placement_window: if placement_window == self.text_placement_window:
self.text_place_window(None) self.text_place_window(None)
elif placement_window == self.voice_placement_window: elif placement_window == self.voice_placement_window:
self.voice_place_window(None) self.voice_place_window(None)
def text_server_refresh(self, button): def text_server_refresh(self, _button):
with open(self.rpc_file, 'w') as f: """Send RPC to overlay to request a list of text channels"""
with open(self.rpc_file, 'w', encoding="utf-8") as f:
f.write('--rpc --refresh-guilds') f.write('--rpc --refresh-guilds')
def config_set(self, context, key, value): def config_set(self, context, key, value):
"""Write one key to config and save to disk"""
if self.loading_config: if self.loading_config:
return return
config = ConfigParser(interpolation=None) config = ConfigParser(interpolation=None)
@ -959,10 +978,11 @@ class MainSettingsWindow():
if not context in config.sections(): if not context in config.sections():
config.add_section(context) config.add_section(context)
config.set(context, key, value) config.set(context, key, value)
with open(self.config_file, 'w') as file: with open(self.config_file, 'w', encoding="utf-8") as file:
config.write(file) config.write(file)
def config_remove_section(self, context): def config_remove_section(self, context):
"""Remove a section from config and save to disk"""
if self.loading_config: if self.loading_config:
return return
config = ConfigParser(interpolation=None) config = ConfigParser(interpolation=None)
@ -970,12 +990,12 @@ class MainSettingsWindow():
if context in config.sections(): if context in config.sections():
config.remove_section(context) config.remove_section(context)
else: else:
log.error("Unable to remove section %s" % (context)) log.error("Unable to remove section %s", context)
with open(self.config_file, 'w') as file: with open(self.config_file, 'w', encoding="utf-8") as file:
config.write(file) config.write(file)
def voice_anchor_float_changed(self, button): def voice_anchor_float_changed(self, button):
self.config_set("main", "floating", "%s" % (button.get_active() == 0)) self.config_set("main", "floating", f"{(button.get_active() == 0)}")
self.update_floating_anchor() self.update_floating_anchor()
def update_floating_anchor(self): def update_floating_anchor(self):
@ -1003,10 +1023,10 @@ class MainSettingsWindow():
self.config_set("main", "monitor", plug) self.config_set("main", "monitor", plug)
def voice_align_1_changed(self, button): def voice_align_1_changed(self, button):
self.config_set("main", "rightalign", "%s" % (button.get_active())) self.config_set("main", "rightalign", f"{button.get_active()}")
def voice_align_2_changed(self, button): def voice_align_2_changed(self, button):
self.config_set("main", "topalign", "%s" % (button.get_active())) self.config_set("main", "topalign", f"{button.get_active()}")
def voice_font_changed(self, button): def voice_font_changed(self, button):
self.config_set("main", "font", button.get_font()) self.config_set("main", "font", button.get_font())
@ -1015,41 +1035,39 @@ class MainSettingsWindow():
self.config_set("main", "title_font", button.get_font()) self.config_set("main", "title_font", button.get_font())
def voice_icon_spacing_changed(self, button): def voice_icon_spacing_changed(self, button):
self.config_set("main", "icon_spacing", "%s" % self.config_set("main", "icon_spacing", f"{int(button.get_value())}")
(int(button.get_value())))
def voice_text_padding_changed(self, button): def voice_text_padding_changed(self, button):
self.config_set("main", "text_padding", "%s" % self.config_set("main", "text_padding", f"{int(button.get_value())}")
(int(button.get_value())))
def voice_text_vertical_offset_changed(self, button): def voice_text_vertical_offset_changed(self, button):
self.config_set("main", "text_baseline_adj", "%s" % self.config_set("main", "text_baseline_adj",
(int(button.get_value()))) f"{int(button.get_value())}")
def voice_vertical_padding_changed(self, button): def voice_vertical_padding_changed(self, button):
self.config_set("main", "vert_edge_padding", "%s" % self.config_set("main", "vert_edge_padding",
(int(button.get_value()))) f"{int(button.get_value())}")
def voice_horizontal_padding_changed(self, button): def voice_horizontal_padding_changed(self, button):
self.config_set("main", "horz_edge_padding", "%s" % self.config_set("main", "horz_edge_padding",
(int(button.get_value()))) f"{int(button.get_value())}")
def voice_display_horizontally_changed(self, button): def voice_display_horizontally_changed(self, button):
self.config_set("main", "horizontal", "%s" % (button.get_active())) self.config_set("main", "horizontal", f"{button.get_active()}")
self.set_alignment_labels(button.get_active()) self.set_alignment_labels(button.get_active())
def voice_highlight_self_changed(self, button): def voice_highlight_self_changed(self, button):
self.config_set("main", "highlight_self", "%s" % (button.get_active())) self.config_set("main", "highlight_self", f"{button.get_active()}")
def voice_display_speakers_only(self, button): def voice_display_speakers_only(self, button):
self.config_set("main", "only_speaking", "%s" % (button.get_active())) self.config_set("main", "only_speaking", f"{button.get_active()}")
def voice_display_speakers_grace_period(self, button): def voice_display_speakers_grace_period(self, button):
self.config_set("main", "only_speaking_grace", "%s" % self.config_set("main", "only_speaking_grace",
(int(button.get_value()))) f"{int(button.get_value())}")
def voice_toggle_test_content(self, button): def voice_toggle_test_content(self, button):
self.config_set("main", "show_dummy", "%s" % (button.get_active())) self.config_set("main", "show_dummy", f"{button.get_active()}")
def voice_talking_foreground_changed(self, button): def voice_talking_foreground_changed(self, button):
colour = button.get_rgba() colour = button.get_rgba()
@ -1097,54 +1115,48 @@ class MainSettingsWindow():
self.config_set("main", "avatar_bg_col", json.dumps(colour)) self.config_set("main", "avatar_bg_col", json.dumps(colour))
def voice_avatar_opacity_changed(self, button): def voice_avatar_opacity_changed(self, button):
self.config_set("main", "icon_transparency", "%.2f" % self.config_set("main", "icon_transparency",
(button.get_value())) f"{button.get_value():.2f}")
def voice_avatar_size_changed(self, button): def voice_avatar_size_changed(self, button):
self.config_set("main", "avatar_size", "%s" % self.config_set("main", "avatar_size", f"{int(button.get_value())}")
(int(button.get_value())))
def voice_nick_length_changed(self, button): def voice_nick_length_changed(self, button):
self.config_set("main", "nick_length", "%s" % self.config_set("main", "nick_length", f"{int(button.get_value())}")
(int(button.get_value())))
def voice_display_icon_only_changed(self, button): def voice_display_icon_only_changed(self, button):
self.config_set("main", "icon_only", "%s" % (not button.get_active())) self.config_set("main", "icon_only", f"{(not button.get_active())}")
self.voice_show_name_hide_others(button.get_active()) self.voice_show_name_hide_others(button.get_active())
def voice_square_avatar_changed(self, button): def voice_square_avatar_changed(self, button):
self.config_set("main", "square_avatar", "%s" % (button.get_active())) self.config_set("main", "square_avatar", f"{button.get_active()}")
def voice_fancy_avatar_shapes_changed(self, button): def voice_fancy_avatar_shapes_changed(self, button):
self.config_set("main", "fancy_border", "%s" % (button.get_active())) self.config_set("main", "fancy_border", f"{button.get_active()}")
def voice_order_avatars_by_changed(self, button): def voice_order_avatars_by_changed(self, button):
self.config_set("main", "order", "%s" % (button.get_active())) self.config_set("main", "order", f"{button.get_active()}")
def voice_border_width_changed(self, button): def voice_border_width_changed(self, button):
self.config_set("main", "border_width", "%s" % self.config_set("main", "border_width", f"{int(button.get_value())}")
(int(button.get_value())))
def voice_overflow_style_changed(self, button): def voice_overflow_style_changed(self, button):
self.config_set("main", "overflow", "%s" % (int(button.get_active()))) self.config_set("main", "overflow", f"{int(button.get_active())}")
def voice_show_title_changed(self, button): def voice_show_title_changed(self, button):
self.config_set("main", "show_title", "%s" % (button.get_active())) self.config_set("main", "show_title", f"{button.get_active()}")
def voice_show_connection_status_changed(self, button): def voice_show_connection_status_changed(self, button):
self.config_set("main", "show_connection", "%s" % self.config_set("main", "show_connection", f"{button.get_active()}")
(button.get_active()))
def voice_show_disconnected_changed(self, button): def voice_show_disconnected_changed(self, button):
self.config_set("main", "show_disconnected", "%s" % self.config_set("main", "show_disconnected", f"{button.get_active()}")
(button.get_active()))
def voice_dummy_count_changed(self, button): def voice_dummy_count_changed(self, button):
self.config_set("main", "dummy_count", "%s" % self.config_set("main", "dummy_count", f"{int(button.get_value())}")
(int(button.get_value())))
def voice_show_avatar_changed(self, button): def voice_show_avatar_changed(self, button):
self.config_set("main", "show_avatar", "%s" % (button.get_active())) self.config_set("main", "show_avatar", f"{button.get_active()}")
self.voice_show_avatar_hide_others(button.get_active()) self.voice_show_avatar_hide_others(button.get_active())
def voice_show_name_hide_others(self, val): def voice_show_name_hide_others(self, val):
@ -1176,13 +1188,13 @@ class MainSettingsWindow():
self.widget['voice_avatar_opacity'].set_sensitive(False) self.widget['voice_avatar_opacity'].set_sensitive(False)
def text_enable_changed(self, button): def text_enable_changed(self, button):
self.config_set("text", "enabled", "%s" % (button.get_active())) self.config_set("text", "enabled", f"{button.get_active()}")
def text_popup_style_changed(self, button): def text_popup_style_changed(self, button):
self.config_set("text", "popup_style", "%s" % (button.get_active())) self.config_set("text", "popup_style", f"{button.get_active()}")
def text_popup_time_changed(self, button): def text_popup_time_changed(self, button):
self.config_set("text", "text_time", "%s" % (int(button.get_value()))) self.config_set("text", "text_time", f"{int(button.get_value())}")
def text_server_changed(self, button): def text_server_changed(self, button):
if button.get_active() < 0: if button.get_active() < 0:
@ -1225,25 +1237,24 @@ class MainSettingsWindow():
self.config_set("text", "monitor", plug) self.config_set("text", "monitor", plug)
def text_show_attachments_changed(self, button): def text_show_attachments_changed(self, button):
self.config_set("text", "show_attach", "%s" % (button.get_active())) self.config_set("text", "show_attach", f"{button.get_active()}")
def text_line_limit_changed(self, button): def text_line_limit_changed(self, button):
self.config_set("text", "line_limit", "%s" % (int(button.get_value()))) self.config_set("text", "line_limit", f"{int(button.get_value())}")
def notification_enable_changed(self, button): def notification_enable_changed(self, button):
self.config_set("notification", "enabled", "%s" % self.config_set("notification", "enabled", f"{button.get_active()}")
(button.get_active()))
def notification_reverse_order_changed(self, button): def notification_reverse_order_changed(self, button):
self.config_set("notification", "rev", "%s" % (button.get_active())) self.config_set("notification", "rev", f"{button.get_active()}")
def notification_popup_timer_changed(self, button): def notification_popup_timer_changed(self, button):
self.config_set("notification", "text_time", "%s" % self.config_set("notification", "text_time",
(int(button.get_value()))) f"{int(button.get_value())}")
def notification_limit_popup_width_changed(self, button): def notification_limit_popup_width_changed(self, button):
self.config_set("notification", "limit_width", "%s" % self.config_set("notification", "limit_width",
(int(button.get_value()))) f"{int(button.get_value())}")
def notification_font_changed(self, button): def notification_font_changed(self, button):
self.config_set("notification", "font", button.get_font()) self.config_set("notification", "font", button.get_font())
@ -1267,40 +1278,36 @@ class MainSettingsWindow():
self.config_set("notification", "monitor", plug) self.config_set("notification", "monitor", plug)
def notification_align_1_changed(self, button): def notification_align_1_changed(self, button):
self.config_set("notification", "rightalign", "%s" % self.config_set("notification", "rightalign", f"{button.get_active()}")
(button.get_active()))
def notification_align_2_changed(self, button): def notification_align_2_changed(self, button):
self.config_set("notification", "topalign", "%s" % self.config_set("notification", "topalign", f"{button.get_active()}")
(button.get_active()))
def notification_show_icon(self, button): def notification_show_icon(self, button):
self.config_set("notification", "show_icon", "%s" % self.config_set("notification", "show_icon", f"{button.get_active()}")
(button.get_active()))
def notification_icon_position_changed(self, button): def notification_icon_position_changed(self, button):
self.config_set("notification", "icon_left", "%s" % self.config_set("notification", "icon_left", f"{
(int(button.get_active() != 1))) int(button.get_active() != 1)}")
def notification_icon_padding_changed(self, button): def notification_icon_padding_changed(self, button):
self.config_set("notification", "icon_padding", "%s" % self.config_set("notification", "icon_padding",
(int(button.get_value()))) f"{int(button.get_value())}")
def notification_icon_size_changed(self, button): def notification_icon_size_changed(self, button):
self.config_set("notification", "icon_size", "%s" % self.config_set("notification", "icon_size",
(int(button.get_value()))) f"{int(button.get_value())}")
def notification_padding_between_changed(self, button): def notification_padding_between_changed(self, button):
self.config_set("notification", "padding", "%s" % self.config_set("notification", "padding",
(int(button.get_value()))) f"{int(button.get_value())}")
def notification_border_radius_changed(self, button): def notification_border_radius_changed(self, button):
self.config_set("notification", "border_radius", "%s" % self.config_set("notification", "border_radius",
(int(button.get_value()))) f"{int(button.get_value())}")
def notification_show_test_content_changed(self, button): def notification_show_test_content_changed(self, button):
self.config_set("notification", "show_dummy", "%s" % self.config_set("notification", "show_dummy", f"{button.get_active()}")
(button.get_active()))
def core_run_on_startup_changed(self, button): def core_run_on_startup_changed(self, button):
self.autostart_helper.set_autostart(button.get_active()) self.autostart_helper.set_autostart(button.get_active())
@ -1309,65 +1316,60 @@ class MainSettingsWindow():
self.autostart_helper_conf.set_autostart(button.get_active()) self.autostart_helper_conf.set_autostart(button.get_active())
def core_force_xshape_changed(self, button): def core_force_xshape_changed(self, button):
self.config_set("general", "xshape", "%s" % (button.get_active())) self.config_set("general", "xshape", f"{button.get_active()}")
def core_show_tray_icon_changed(self, button): def core_show_tray_icon_changed(self, button):
self.set_sys_tray_icon_visible(button.get_active()) self.set_sys_tray_icon_visible(button.get_active())
self.config_set("general", "showsystray", "%s" % (button.get_active())) self.config_set("general", "showsystray", f"{button.get_active()}")
self.widget['core_settings_min'].set_sensitive(button.get_active()) self.widget['core_settings_min'].set_sensitive(button.get_active())
def core_hide_overlay_changed(self, button): def core_hide_overlay_changed(self, _button):
self.toggle_overlay() self.toggle_overlay()
def core_settings_min_changed(self, button): def core_settings_min_changed(self, button):
self.config_set("general", "start_min", "%s" % (button.get_active())) self.config_set("general", "start_min", f"{button.get_active()}")
def core_reset_all(self, button): def core_reset_all(self, _button):
self.config_remove_section("general") self.config_remove_section("general")
self.read_config() self.read_config()
def voice_reset_all(self, button): def voice_reset_all(self, _button):
self.config_remove_section("main") self.config_remove_section("main")
self.read_config() self.read_config()
def text_reset_all(self, button): def text_reset_all(self, _button):
self.config_remove_section("text") self.config_remove_section("text")
self.read_config() self.read_config()
def notification_reset_all(self, button): def notification_reset_all(self, _button):
self.config_remove_section("notification") self.config_remove_section("notification")
self.read_config() self.read_config()
def voice_hide_mouseover_changed(self, button): def voice_hide_mouseover_changed(self, button):
self.config_set("main", "autohide", "%s" % (button.get_active())) self.config_set("main", "autohide", f"{button.get_active()}")
def text_hide_mouseover_changed(self, button): def text_hide_mouseover_changed(self, button):
self.config_set("text", "autohide", "%s" % (button.get_active())) self.config_set("text", "autohide", f"{button.get_active()}")
def voice_mouseover_timeout_changed(self, button): def voice_mouseover_timeout_changed(self, button):
self.config_set("main", "autohide_timer", "%s" % self.config_set("main", "autohide_timer", f"{int(button.get_value())}")
(int(button.get_value())))
def text_mouseover_timeout_changed(self, button): def text_mouseover_timeout_changed(self, button):
self.config_set("text", "autohide_timer", "%s" % self.config_set("text", "autohide_timer", f"{int(button.get_value())}")
(int(button.get_value())))
def inactive_fade_changed(self, button): def inactive_fade_changed(self, button):
self.config_set("main", "fade_out_inactive", "%s" % self.config_set("main", "fade_out_inactive", f"{button.get_active()}")
(button.get_active()))
def inactive_fade_opacity_changed(self, button): def inactive_fade_opacity_changed(self, button):
self.config_set("main", "fade_out_limit", "%.2f" % self.config_set("main", "fade_out_limit",
(button.get_value())) f"{button.get_value():.2f}")
def inactive_time_changed(self, button): def inactive_time_changed(self, button):
self.config_set("main", "inactive_time", "%s" % self.config_set("main", "inactive_time", f"{int(button.get_value())}")
(int(button.get_value())))
def inactive_fade_time_changed(self, button): def inactive_fade_time_changed(self, button):
self.config_set("main", "inactive_fade_time", "%s" % self.config_set("main", "inactive_fade_time",
(int(button.get_value()))) f"{int(button.get_value())}")
def core_audio_assist_changed(self, button): def core_audio_assist_changed(self, button):
self.config_set("general", "audio_assist", "%s" % self.config_set("general", "audio_assist", f"{button.get_active()}")
(button.get_active()))

View file

@ -21,7 +21,7 @@ from .overlay import OverlayWindow
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
gi.require_version('PangoCairo', '1.0') gi.require_version('PangoCairo', '1.0')
# pylint: disable=wrong-import-position,wrong-import-order # pylint: disable=wrong-import-position,wrong-import-order
from gi.repository import Pango, PangoCairo, GLib # nopep8 from gi.repository import Pango, PangoCairo # nopep8
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -56,10 +56,12 @@ class TextOverlayWindow(OverlayWindow):
self.redraw() self.redraw()
def set_blank(self): def set_blank(self):
""" Set contents blank and redraw """
self.content = [] self.content = []
self.set_needs_redraw() self.set_needs_redraw()
def tick(self): def tick(self):
""" Check for old images """
if len(self.attachment) > self.line_limit: if len(self.attachment) > self.line_limit:
# We've probably got old images! # We've probably got old images!
oldlist = self.attachment oldlist = self.attachment
@ -72,57 +74,43 @@ class TextOverlayWindow(OverlayWindow):
self.attachment[url] = oldlist[url] self.attachment[url] = oldlist[url]
def set_text_time(self, timer): def set_text_time(self, timer):
""" """Config option: Time before messages disappear from overlay"""
Set the duration that a message will be visible for.
"""
if self.text_time != timer or self.timer_after_draw != timer: if self.text_time != timer or self.timer_after_draw != timer:
self.text_time = timer self.text_time = timer
self.timer_after_draw = timer self.timer_after_draw = timer
self.set_needs_redraw() self.set_needs_redraw()
def set_text_list(self, tlist, altered): def set_text_list(self, tlist, altered):
""" """Change contents of overlay"""
Update the list of text messages to show
"""
self.content = tlist[-self.line_limit:] self.content = tlist[-self.line_limit:]
if altered: if altered:
self.set_needs_redraw() self.set_needs_redraw()
def set_fg(self, fg_col): def set_fg(self, fg_col):
""" """Config option: Sets the text colour"""
Set default text colour
"""
if self.fg_col != fg_col: if self.fg_col != fg_col:
self.fg_col = fg_col self.fg_col = fg_col
self.set_needs_redraw() self.set_needs_redraw()
def set_bg(self, bg_col): def set_bg(self, bg_col):
""" """Config option: Set the background colour"""
Set background colour
"""
if self.bg_col != bg_col: if self.bg_col != bg_col:
self.bg_col = bg_col self.bg_col = bg_col
self.set_needs_redraw() self.set_needs_redraw()
def set_show_attach(self, attachment): def set_show_attach(self, attachment):
""" """Config option: Show image attachments"""
Set if attachments should be shown inline
"""
if self.attachment != attachment: if self.attachment != attachment:
self.show_attach = attachment self.show_attach = attachment
self.set_needs_redraw() self.set_needs_redraw()
def set_popup_style(self, boolean): def set_popup_style(self, boolean):
""" """Config option: Messages should disappear after being shown for some time"""
Set if message disappear after a certain duration
"""
if self.popup_style != boolean: if self.popup_style != boolean:
self.popup_style = boolean self.popup_style = boolean
def set_font(self, font): def set_font(self, font):
""" """Config option: Set font used for rendering"""
Set font used to render text
"""
if self.text_font != font: if self.text_font != font:
self.text_font = font self.text_font = font
@ -133,28 +121,24 @@ class TextOverlayWindow(OverlayWindow):
self.set_needs_redraw() self.set_needs_redraw()
def set_line_limit(self, limit): def set_line_limit(self, limit):
""" """Config option: Limit number of lines rendered"""
Change maximum number of lines in overlay
"""
if self.line_limit != limit: if self.line_limit != limit:
self.line_limit = limit self.line_limit = limit
def make_line(self, message): def make_line(self, message):
""" """Decode a recursive JSON object into pango markup."""
Decode a recursive JSON object into pango markup.
"""
ret = "" ret = ""
if isinstance(message, list): if isinstance(message, list):
for inner_message in message: for inner_message in message:
ret = "%s%s" % (ret, self.make_line(inner_message)) ret = f"{ret}{self.make_line(inner_message)}"
elif isinstance(message, str): elif isinstance(message, str):
ret = self.sanitize_string(message) ret = self.sanitize_string(message)
elif message['type'] == 'strong': elif message['type'] == 'strong':
ret = "<b>%s</b>" % (self.make_line(message['content'])) ret = f"<b>{self.make_line(message['content'])}</b>"
elif message['type'] == 'text': elif message['type'] == 'text':
ret = self.sanitize_string(message['content']) ret = self.sanitize_string(message['content'])
elif message['type'] == 'link': elif message['type'] == 'link':
ret = "<u>%s</u>" % (self.make_line(message['content'])) ret = f"<u>{self.make_line(message['content'])}</u>"
elif message['type'] == 'emoji': elif message['type'] == 'emoji':
if 'surrogate' in message: if 'surrogate' in message:
# ['src'] is SVG URL # ['src'] is SVG URL
@ -163,20 +147,21 @@ class TextOverlayWindow(OverlayWindow):
else: else:
### Add Image ### ### Add Image ###
self.image_list.append( self.image_list.append(
f"https://cdn.discordapp.com/emojis/{message['emojiId']}.png?v=1" f"https://cdn.discordapp.com/emojis/{
message['emojiId']}.png?v=1"
) )
ret = "`" ret = "`"
elif (message['type'] == 'inlineCode' or elif (message['type'] == 'inlineCode' or
message['type'] == 'codeBlock' or message['type'] == 'codeBlock' or
message['type'] == 'blockQuote'): message['type'] == 'blockQuote'):
ret = "<span font_family=\"monospace\" background=\"#0004\">%s</span>" % ( ret = f"<span font_family=\"monospace\" background=\"#0004\">{
self.make_line(message['content'])) self.make_line(message['content'])}</span>"
elif message['type'] == 'u': elif message['type'] == 'u':
ret = "<u>%s</u>" % (self.make_line(message['content'])) ret = f"<u>{self.make_line(message['content'])}</u>"
elif message['type'] == 'em': elif message['type'] == 'em':
ret = "<i>%s</i>" % (self.make_line(message['content'])) ret = f"<i>{self.make_line(message['content'])}</i>"
elif message['type'] == 's': elif message['type'] == 's':
ret = "<s>%s</s>" % (self.make_line(message['content'])) ret = f"<s>{self.make_line(message['content'])}</s>"
elif message['type'] == 'channel': elif message['type'] == 'channel':
ret = self.make_line(message['content']) ret = self.make_line(message['content'])
elif message['type'] == 'mention': elif message['type'] == 'mention':
@ -189,14 +174,13 @@ class TextOverlayWindow(OverlayWindow):
self.warned_filetypes.append(message['type']) self.warned_filetypes.append(message['type'])
return ret return ret
def recv_attach(self, identifier, pix, mask): def recv_attach(self, identifier, pix, _mask):
""" """Callback from image_getter"""
Called when an image has been downloaded by image_getter
"""
self.attachment[identifier] = pix self.attachment[identifier] = pix
self.set_needs_redraw() self.set_needs_redraw()
def has_content(self): def has_content(self):
"""Returns true if overlay has meaningful content to render"""
if self.piggyback and self.piggyback.has_content(): if self.piggyback and self.piggyback.has_content():
return True return True
if not self.enabled: if not self.enabled:
@ -206,15 +190,12 @@ class TextOverlayWindow(OverlayWindow):
return self.content return self.content
def overlay_draw(self, w, context, data=None): def overlay_draw(self, w, context, data=None):
""" """Draw the overlay"""
Draw the overlay
"""
if self.piggyback: if self.piggyback:
self.piggyback.overlay_draw(w, context, data) self.piggyback.overlay_draw(w, context, data)
if not self.enabled: if not self.enabled:
return return
self.context = context self.context = context
(width, height) = self.get_size()
if not self.piggyback_parent: if not self.piggyback_parent:
context.set_antialias(cairo.ANTIALIAS_GOOD) context.set_antialias(cairo.ANTIALIAS_GOOD)
context.set_source_rgba(0.0, 0.0, 0.0, 0.0) context.set_source_rgba(0.0, 0.0, 0.0, 0.0)
@ -234,7 +215,6 @@ class TextOverlayWindow(OverlayWindow):
context.translate(floating_x, floating_y) context.translate(floating_x, floating_y)
context.rectangle(0, 0, floating_width, floating_height) context.rectangle(0, 0, floating_width, floating_height)
context.clip() context.clip()
pass
(floating_x, floating_y, floating_width, (floating_x, floating_y, floating_width,
floating_height) = self.get_floating_coords() floating_height) = self.get_floating_coords()
current_y = floating_height current_y = floating_height
@ -249,7 +229,7 @@ class TextOverlayWindow(OverlayWindow):
if 'nick_col' in line and line['nick_col']: if 'nick_col' in line and line['nick_col']:
col = line['nick_col'] col = line['nick_col']
for in_line in line['content']: for in_line in line['content']:
out_line = "%s%s" % (out_line, self.make_line(in_line)) out_line = f"{out_line}{self.make_line(in_line)}"
if line['attach'] and self.show_attach: if line['attach'] and self.show_attach:
attachment = line['attach'][0] attachment = line['attach'][0]
url = attachment['url'] url = attachment['url']
@ -267,10 +247,8 @@ class TextOverlayWindow(OverlayWindow):
else: else:
log.warning("Unknown file extension '%s'", extension) log.warning("Unknown file extension '%s'", extension)
# cy = self.draw_text(cy, "%s" % (line['attach'])) # cy = self.draw_text(cy, "%s" % (line['attach']))
message = "<span foreground='%s'>%s</span>: %s" % (self.sanitize_string(col), message = f"<span foreground='{self.sanitize_string(col)}'>{self.sanitize_string(
self.sanitize_string( line["nick"])}</span>: {out_line}"
line["nick"]),
out_line)
current_y = self.draw_text(current_y, message) current_y = self.draw_text(current_y, message)
if current_y <= 0: if current_y <= 0:
# We've done enough # We've done enough
@ -279,10 +257,8 @@ class TextOverlayWindow(OverlayWindow):
self.context = None self.context = None
def draw_attach(self, pos_y, url): def draw_attach(self, pos_y, url):
""" """Draw an attachment"""
Draw an attachment (_floating_x, _floating_y, floating_width,
"""
(floating_x, floating_y, floating_width,
floating_height) = self.get_floating_coords() floating_height) = self.get_floating_coords()
if url in self.attachment and self.attachment[url]: if url in self.attachment and self.attachment[url]:
pix = self.attachment[url] pix = self.attachment[url]
@ -302,16 +278,14 @@ class TextOverlayWindow(OverlayWindow):
return pos_y return pos_y
def draw_text(self, pos_y, text): def draw_text(self, pos_y, text):
""" """Draw a text message, returning the Y position of the next message"""
Draw a text message, returning the Y position of the next message
"""
layout = self.create_pango_layout(text) layout = self.create_pango_layout(text)
layout.set_auto_dir(True) layout.set_auto_dir(True)
layout.set_markup(text, -1) layout.set_markup(text, -1)
attr = layout.get_attributes() attr = layout.get_attributes()
(floating_x, floating_y, floating_width, (_floating_x, _floating_y, floating_width,
floating_height) = self.get_floating_coords() _floating_height) = self.get_floating_coords()
layout.set_width(Pango.SCALE * floating_width) layout.set_width(Pango.SCALE * floating_width)
layout.set_spacing(Pango.SCALE * 3) layout.set_spacing(Pango.SCALE * 3)
if self.text_font: if self.text_font:
@ -351,11 +325,9 @@ class TextOverlayWindow(OverlayWindow):
return pos_y - text_height return pos_y - text_height
def render_custom(self, ctx, shape, path, _data): def render_custom(self, ctx, shape, path, _data):
""" """Draw an inline image as a custom emoticon"""
Draw an inline image as a custom emoticon
"""
if shape.data >= len(self.image_list): if shape.data >= len(self.image_list):
log.warning(f"{shape.data} >= {len(self.image_list)}") log.warning("%s >= %s", shape.data, len(self.image_list))
return return
# key is the url to the image # key is the url to the image
key = self.image_list[shape.data] key = self.image_list[shape.data]
@ -371,9 +343,7 @@ class TextOverlayWindow(OverlayWindow):
return True return True
def sanitize_string(self, string): def sanitize_string(self, string):
""" """Sanitize a text message so that it doesn't intefere with Pango's XML format"""
Sanitize a text message so that it doesn't intefere with Pango's XML format
"""
string = string.replace("&", "&amp;") string = string.replace("&", "&amp;")
string = string.replace("<", "&lt;") string = string.replace("<", "&lt;")
string = string .replace(">", "&gt;") string = string .replace(">", "&gt;")

View file

@ -15,16 +15,15 @@ import random
import gettext import gettext
import logging import logging
import math import math
import cairo
import sys import sys
import locale import locale
import pkg_resources
from time import perf_counter from time import perf_counter
import cairo
import pkg_resources
from .overlay import OverlayWindow from .overlay import OverlayWindow
from .image_getter import get_surface, draw_img_to_rect, draw_img_to_mask from .image_getter import get_surface, draw_img_to_rect, draw_img_to_mask
# pylint: disable=wrong-import-order # pylint: disable=wrong-import-order
import gi import gi
gi.require_version("Gtk", "3.0")
gi.require_version('PangoCairo', '1.0') gi.require_version('PangoCairo', '1.0')
# pylint: disable=wrong-import-position,wrong-import-order # pylint: disable=wrong-import-position,wrong-import-order
from gi.repository import Pango, PangoCairo, GLib # nopep8 from gi.repository import Pango, PangoCairo, GLib # nopep8
@ -55,12 +54,11 @@ class VoiceOverlayWindow(OverlayWindow):
scream = '' scream = ''
if random.randint(0, 20) == 2: if random.randint(0, 20) == 2:
scream = random.randint(8, 15)*'a' scream = random.randint(8, 15)*'a'
name = "Player %d %s" % (i, scream) name = f"Player {i} {scream}"
self.dummy_data.append({ self.dummy_data.append({
"id": i, "id": i,
"username": name, "username": name,
"avatar": None, "avatar": None,
"mute": False,
"deaf": mostly_false[random.randint(0, 7)], "deaf": mostly_false[random.randint(0, 7)],
"mute": mostly_false[random.randint(0, 7)], "mute": mostly_false[random.randint(0, 7)],
"speaking": speaking, "speaking": speaking,
@ -82,6 +80,7 @@ class VoiceOverlayWindow(OverlayWindow):
self.highlight_self = None self.highlight_self = None
self.order = None self.order = None
self.def_avatar = None self.def_avatar = None
self.def_avatar_mask = None
self.channel_icon = None self.channel_icon = None
self.channel_mask = None self.channel_mask = None
self.channel_icon_url = None self.channel_icon_url = None
@ -95,6 +94,7 @@ class VoiceOverlayWindow(OverlayWindow):
self.border_width = 2 self.border_width = 2
self.icon_transparency = 0.0 self.icon_transparency = 0.0
self.fancy_border = False self.fancy_border = False
self.only_speaking_grace_period = 0
self.fade_out_inactive = True self.fade_out_inactive = True
self.fade_out_limit = 0.1 self.fade_out_limit = 0.1
@ -130,7 +130,7 @@ class VoiceOverlayWindow(OverlayWindow):
self.redraw() self.redraw()
def reset_action_timer(self): def reset_action_timer(self):
# Reset time since last voice activity """Reset time since last voice activity"""
self.fade_opacity = 1.0 self.fade_opacity = 1.0
# Remove both fading-out effect and timer set last time this happened # Remove both fading-out effect and timer set last time this happened
@ -141,13 +141,13 @@ class VoiceOverlayWindow(OverlayWindow):
GLib.source_remove(self.fadeout_timeout) GLib.source_remove(self.fadeout_timeout)
self.fadeout_timeout = None self.fadeout_timeout = None
# If we're using this feature, schedule a new iactivity timer # If we're using this feature, schedule a new inactivity timer
if self.fade_out_inactive: if self.fade_out_inactive:
self.inactive_timeout = GLib.timeout_add_seconds( self.inactive_timeout = GLib.timeout_add_seconds(
self.inactive_time, self.overlay_inactive) self.inactive_time, self.overlay_inactive)
def overlay_inactive(self): def overlay_inactive(self):
# Inactivity has hit the first threshold, start fading out """Timed callback when inactivity limit is hit"""
self.fade_start = perf_counter() self.fade_start = perf_counter()
# Fade out in 200 steps over X seconds. # Fade out in 200 steps over X seconds.
self.fadeout_timeout = GLib.timeout_add( self.fadeout_timeout = GLib.timeout_add(
@ -156,8 +156,10 @@ class VoiceOverlayWindow(OverlayWindow):
return False return False
def overlay_fadeout(self): def overlay_fadeout(self):
"""Repeated callback after inactivity started"""
self.set_needs_redraw() 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 # 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 # Might look choppy on systems under high cpu usage but that's just how it's going to be
now = perf_counter() now = perf_counter()
time_percent = (now - self.fade_start) / self.inactive_fade_time time_percent = (now - self.fade_start) / self.inactive_fade_time
@ -171,21 +173,22 @@ class VoiceOverlayWindow(OverlayWindow):
return True return True
def col(self, col, alpha=1.0): def col(self, col, alpha=1.0):
""" """Convenience function to set the cairo context next colour.
Convenience function to set the cairo context next colour. Altered to account for fade-out function Altered to account for fade-out function"""
""" if alpha is None:
if alpha == None:
self.context.set_source_rgba(col[0], col[1], col[2], col[3]) self.context.set_source_rgba(col[0], col[1], col[2], col[3])
else: else:
self.context.set_source_rgba( self.context.set_source_rgba(
col[0], col[1], col[2], col[3] * alpha * self.fade_opacity) col[0], col[1], col[2], col[3] * alpha * self.fade_opacity)
def set_icon_transparency(self, trans): def set_icon_transparency(self, trans):
"""Config option: icon transparency"""
if self.icon_transparency != trans: if self.icon_transparency != trans:
self.icon_transparency = trans self.icon_transparency = trans
self.set_needs_redraw() self.set_needs_redraw()
def set_blank(self): def set_blank(self):
"""Set data to blank and redraw"""
self.userlist = [] self.userlist = []
self.channel_icon = None self.channel_icon = None
self.channel_icon_url = None self.channel_icon_url = None
@ -194,7 +197,9 @@ class VoiceOverlayWindow(OverlayWindow):
self.set_needs_redraw() self.set_needs_redraw()
def set_fade_out_inactive(self, enabled, fade_time, fade_duration, fade_to): def set_fade_out_inactive(self, enabled, fade_time, fade_duration, fade_to):
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: """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.fade_out_inactive = enabled
self.inactive_time = fade_time self.inactive_time = fade_time
self.inactive_fade_time = fade_duration self.inactive_fade_time = fade_duration
@ -202,288 +207,235 @@ class VoiceOverlayWindow(OverlayWindow):
self.reset_action_timer() self.reset_action_timer()
def set_title_font(self, font): def set_title_font(self, font):
"""Config option: font used to render title"""
if self.title_font != font: if self.title_font != font:
self.title_font = font self.title_font = font
self.set_needs_redraw() self.set_needs_redraw()
def set_show_connection(self, show_connection): def set_show_connection(self, show_connection):
"""Config option: show connection status alongside users"""
if self.show_connection != show_connection: if self.show_connection != show_connection:
self.show_connection = show_connection self.show_connection = show_connection
self.set_needs_redraw() self.set_needs_redraw()
def set_show_avatar(self, show_avatar): def set_show_avatar(self, show_avatar):
"""Config option: show avatar icons"""
if self.show_avatar != show_avatar: if self.show_avatar != show_avatar:
self.show_avatar = show_avatar self.show_avatar = show_avatar
self.set_needs_redraw() self.set_needs_redraw()
def set_show_title(self, show_title): def set_show_title(self, show_title):
"""Config option: show channel title alongside users"""
if self.show_title != show_title: if self.show_title != show_title:
self.show_title = show_title self.show_title = show_title
self.set_needs_redraw() self.set_needs_redraw()
def set_show_disconnected(self, show_disconnected): def set_show_disconnected(self, show_disconnected):
"""Config option: show even when disconnected from voice chat"""
if self.show_disconnected != show_disconnected: if self.show_disconnected != show_disconnected:
self.show_disconnected = show_disconnected self.show_disconnected = show_disconnected
self.set_needs_redraw() self.set_needs_redraw()
def set_show_dummy(self, show_dummy): def set_show_dummy(self, show_dummy):
""" """Config option: Show placeholder information"""
Toggle use of dummy userdata to help choose settings
"""
if self.use_dummy != show_dummy: if self.use_dummy != show_dummy:
self.use_dummy = show_dummy self.use_dummy = show_dummy
self.set_needs_redraw() self.set_needs_redraw()
def set_dummy_count(self, dummy_count): def set_dummy_count(self, dummy_count):
"""Config option: Change the count of placeholders"""
if self.dummy_count != dummy_count: if self.dummy_count != dummy_count:
self.dummy_count = dummy_count self.dummy_count = dummy_count
self.set_needs_redraw() self.set_needs_redraw()
def set_overflow(self, overflow): def set_overflow_style(self, overflow):
""" """Config option: Change handling of too many users to render"""
How should excessive numbers of users be dealt with?
"""
if self.overflow != overflow: if self.overflow != overflow:
self.overflow = overflow self.overflow = overflow
self.set_needs_redraw() self.set_needs_redraw()
def set_bg(self, background_colour): def set_bg(self, background_colour):
""" """Config option: Set background colour. Used to draw the transparent window.
Set the background colour Should not be changed as then the entire screen is obscured"""
"""
if self.norm_col != background_colour: if self.norm_col != background_colour:
self.norm_col = background_colour self.norm_col = background_colour
self.set_needs_redraw() self.set_needs_redraw()
def set_fg(self, foreground_colour): def set_fg(self, foreground_colour):
""" """Config option: Set foreground colour. Used to render text"""
Set the text colour
"""
if self.text_col != foreground_colour: if self.text_col != foreground_colour:
self.text_col = foreground_colour self.text_col = foreground_colour
self.set_needs_redraw() self.set_needs_redraw()
def set_tk(self, talking_colour): def set_tk(self, talking_colour):
""" """Config option: Set talking border colour.
Set the border colour for users who are talking Used to render border around users who are talking"""
"""
if self.talk_col != talking_colour: if self.talk_col != talking_colour:
self.talk_col = talking_colour self.talk_col = talking_colour
self.set_needs_redraw() self.set_needs_redraw()
def set_mt(self, mute_colour): def set_mt(self, mute_colour):
""" """Config option: Set mute colour. Used to render mute and deaf images"""
Set the colour of mute and deafen logos
"""
if self.mute_col != mute_colour: if self.mute_col != mute_colour:
self.mute_col = mute_colour self.mute_col = mute_colour
self.set_needs_redraw() self.set_needs_redraw()
def set_mute_bg(self, mute_bg_col): def set_mute_bg(self, mute_bg_col):
""" """Config option: Set mute background colour.
Set the background colour for mute/deafen icon Used to tint the user avatar before rendering the mute or deaf image above it"""
"""
if self.mute_bg_col != mute_bg_col: if self.mute_bg_col != mute_bg_col:
self.mute_bg_col = mute_bg_col self.mute_bg_col = mute_bg_col
self.set_needs_redraw() self.set_needs_redraw()
def set_avatar_bg_col(self, avatar_bg_col): def set_avatar_bg_col(self, avatar_bg_col):
""" """Config option: Set avatar background colour.
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: if self.avatar_bg_col != avatar_bg_col:
self.avatar_bg_col = avatar_bg_col self.avatar_bg_col = avatar_bg_col
self.set_needs_redraw() self.set_needs_redraw()
def set_hi(self, highlight_colour): def set_hi(self, highlight_colour):
""" """Config option: Set talking background colour.
Set the colour of background for speaking users Used to render the background behind users name."""
"""
if self.hili_col != highlight_colour: if self.hili_col != highlight_colour:
self.hili_col = highlight_colour self.hili_col = highlight_colour
self.set_needs_redraw() self.set_needs_redraw()
def set_fg_hi(self, highlight_colour): def set_fg_hi(self, highlight_colour):
""" """Config option: Set talking text colour.
Set the colour of background for speaking users Used to render the usernames of users who are talking"""
"""
if self.text_hili_col != highlight_colour: if self.text_hili_col != highlight_colour:
self.text_hili_col = highlight_colour self.text_hili_col = highlight_colour
self.set_needs_redraw() self.set_needs_redraw()
def set_bo(self, border_colour): def set_bo(self, border_colour):
""" """Config option: Set border colour. Used to render border around users"""
Set the colour for idle border
"""
if self.border_col != border_colour: if self.border_col != border_colour:
self.border_col = border_colour self.border_col = border_colour
self.set_needs_redraw() self.set_needs_redraw()
def set_avatar_size(self, size): def set_avatar_size(self, size):
""" """Config option: Set avatar size in window-space pixels"""
Set the size of the avatar icons
"""
if self.avatar_size != size: if self.avatar_size != size:
self.avatar_size = size self.avatar_size = size
self.set_needs_redraw() self.set_needs_redraw()
def set_nick_length(self, size): def set_nick_length(self, size):
""" """Config option: Limit username length"""
Set the length of nickname
"""
if self.nick_length != size: if self.nick_length != size:
self.nick_length = size self.nick_length = size
self.set_needs_redraw() self.set_needs_redraw()
def set_icon_spacing(self, i): def set_icon_spacing(self, i):
""" """Config option: Space between users in the list, in window-space pixels"""
Set the spacing between avatar icons
"""
if self.icon_spacing != i: if self.icon_spacing != i:
self.icon_spacing = i self.icon_spacing = i
self.set_needs_redraw() self.set_needs_redraw()
def set_text_padding(self, i): def set_text_padding(self, i):
""" """Config option: Space between user avatar and username, in window-space pixels"""
Set padding between text and border
"""
if self.text_pad != i: if self.text_pad != i:
self.text_pad = i self.text_pad = i
self.set_needs_redraw() self.set_needs_redraw()
def set_text_baseline_adj(self, i): def set_text_baseline_adj(self, i):
""" """Config option: Vertical offset used to render all text, in window-space pixels"""
Set padding between text and border
"""
if self.text_baseline_adj != i: if self.text_baseline_adj != i:
self.text_baseline_adj = i self.text_baseline_adj = i
self.set_needs_redraw() self.set_needs_redraw()
def set_vert_edge_padding(self, i): def set_vert_edge_padding(self, i):
""" """Config option: Vertical offset from edge of window, in window-space pixels"""
Set padding between top/bottom of screen and overlay contents
"""
if self.vert_edge_padding != i: if self.vert_edge_padding != i:
self.vert_edge_padding = i self.vert_edge_padding = i
self.set_needs_redraw() self.set_needs_redraw()
def set_horz_edge_padding(self, i): def set_horz_edge_padding(self, i):
""" """Config option: Horizontal offset from edge of window, in window-space pixels"""
Set padding between left/right of screen and overlay contents
"""
if self.horz_edge_padding != i: if self.horz_edge_padding != i:
self.horz_edge_padding = i self.horz_edge_padding = i
self.set_needs_redraw() self.set_needs_redraw()
def set_square_avatar(self, i): def set_square_avatar(self, i):
""" """Config option: Mask avatar with a circle before rendering"""
Set if the overlay should crop avatars to a circle or show full square image
"""
if self.round_avatar == i: if self.round_avatar == i:
self.round_avatar = not i self.round_avatar = not i
self.set_needs_redraw() self.set_needs_redraw()
def set_fancy_border(self, border): def set_fancy_border(self, border):
""" """Config option: Use transparent edges of image as border,
Sets if border should wrap around non-square avatar images instead of mask (square/circle)"""
"""
if self.fancy_border != border: if self.fancy_border != border:
self.fancy_border = border self.fancy_border = border
self.set_needs_redraw() self.set_needs_redraw()
def set_only_speaking(self, only_speaking): def set_only_speaking(self, only_speaking):
""" """Config option: Filter user list to only those who
Set if overlay should only show people who are talking are talking and those who have stopped talking recently"""
"""
if self.only_speaking != only_speaking: if self.only_speaking != only_speaking:
self.only_speaking = only_speaking self.only_speaking = only_speaking
self.set_needs_redraw() self.set_needs_redraw()
def set_only_speaking_grace_period(self, grace_period): def set_only_speaking_grace_period(self, grace_period):
""" """Config option: How long after stopping speaking the user remains shown"""
Set grace period before hiding people who are not talking
"""
self.only_speaking_grace_period = grace_period self.only_speaking_grace_period = grace_period
self.timer_after_draw = grace_period self.timer_after_draw = grace_period
def set_highlight_self(self, highlight_self): def set_highlight_self(self, highlight_self):
""" """Config option: Local User should be kept at top of list"""
Set if the overlay should highlight the user
"""
if self.highlight_self != highlight_self: if self.highlight_self != highlight_self:
self.highlight_self = highlight_self self.highlight_self = highlight_self
self.set_needs_redraw() self.set_needs_redraw()
def set_order(self, i): def set_order(self, i):
""" """Config option: Set method used to order user list"""
Set the method used to order avatar icons & names
"""
if self.order != i: if self.order != i:
self.order = i self.order = i
self.sort_list(self.userlist) self.sort_list(self.userlist)
self.set_needs_redraw() self.set_needs_redraw()
def set_icon_only(self, i): def set_icon_only(self, i):
""" """Config option: Show only the avatar, without text or its background"""
Set if the overlay should draw only the icon
"""
if self.icon_only != i: if self.icon_only != i:
self.icon_only = i self.icon_only = i
self.set_needs_redraw() self.set_needs_redraw()
def set_border_width(self, width): def set_drawn_border_width(self, width):
"""Config option: Set width of border around username and avatar"""
if self.border_width != width: if self.border_width != width:
self.border_width = width self.border_width = width
self.set_needs_redraw() self.set_needs_redraw()
def set_horizontal(self, horizontal=False): def set_horizontal(self, horizontal=False):
"""Config option: Userlist should be drawn horizontally"""
if self.horizontal != horizontal: if self.horizontal != horizontal:
self.horizontal = horizontal self.horizontal = horizontal
self.set_needs_redraw() self.set_needs_redraw()
def set_guild_ids(self, guild_ids=tuple()):
if self.discover.connection:
for _id in guild_ids:
if _id not in self.guild_ids:
self.discover.connection.req_channels(_id)
self.guild_ids = guild_ids
def set_wind_col(self): def set_wind_col(self):
""" """Use window colour to draw"""
Use window colour to draw
"""
self.col(self.wind_col, None) self.col(self.wind_col, None)
def set_norm_col(self): def set_norm_col(self):
""" """Use background colour to draw"""
Use background colour to draw
"""
self.col(self.norm_col) self.col(self.norm_col)
def set_talk_col(self, alpha=1.0): def set_talk_col(self, alpha=1.0):
""" """Use talking colour to draw"""
Use talking colour to draw
"""
self.col(self.talk_col, alpha) self.col(self.talk_col, alpha)
def set_mute_col(self): def set_mute_col(self):
""" """Use mute colour to draw"""
Use mute colour to draw
"""
self.col(self.mute_col) self.col(self.mute_col)
def set_channel_title(self, channel_title): def set_channel_title(self, channel_title):
""" """Set title above voice list"""
Set title above voice list
"""
if self.channel_title != channel_title: if self.channel_title != channel_title:
self.channel_title = channel_title self.channel_title = channel_title
self.set_needs_redraw() self.set_needs_redraw()
def set_channel_icon(self, url): def set_channel_icon(self, url):
""" """Change the icon for channel"""
Change the icon for channel
"""
if not url: if not url:
self.channel_icon = None self.channel_icon = None
self.channel_icon_url = None self.channel_icon_url = None
@ -493,9 +445,7 @@ class VoiceOverlayWindow(OverlayWindow):
self.channel_icon_url = url self.channel_icon_url = url
def set_user_list(self, userlist, alt): def set_user_list(self, userlist, alt):
""" """Set the users in list to draw"""
Set the users in list to draw
"""
self.userlist = userlist self.userlist = userlist
for user in userlist: for user in userlist:
if "nick" in user: if "nick" in user:
@ -508,14 +458,13 @@ class VoiceOverlayWindow(OverlayWindow):
self.set_needs_redraw() self.set_needs_redraw()
def set_connection_status(self, connection): def set_connection_status(self, connection):
""" """Set if discord has a clean connection to server"""
Set if discord has a clean connection to server
"""
if self.connection_status != connection['state']: if self.connection_status != connection['state']:
self.connection_status = connection['state'] self.connection_status = connection['state']
self.set_needs_redraw() self.set_needs_redraw()
def sort_list(self, in_list): def sort_list(self, in_list):
"""Take a userlist and sort it according to config option"""
if self.order == 1: # ID Sort if self.order == 1: # ID Sort
in_list.sort(key=lambda x: x["id"]) in_list.sort(key=lambda x: x["id"])
elif self.order == 2: # Spoken sort elif self.order == 2: # Spoken sort
@ -526,6 +475,7 @@ class VoiceOverlayWindow(OverlayWindow):
return in_list return in_list
def has_content(self): def has_content(self):
"""Returns true if overlay has meaningful content to render"""
if not self.enabled: if not self.enabled:
return False return False
if self.hidden: if self.hidden:
@ -535,9 +485,7 @@ class VoiceOverlayWindow(OverlayWindow):
return self.userlist return self.userlist
def overlay_draw(self, w, context, data=None): def overlay_draw(self, w, context, data=None):
""" """Draw the Overlay"""
Draw the Overlay
"""
self.context = context self.context = context
context.set_antialias(cairo.ANTIALIAS_GOOD) context.set_antialias(cairo.ANTIALIAS_GOOD)
# Get size of window # Get size of window
@ -566,7 +514,8 @@ class VoiceOverlayWindow(OverlayWindow):
context.clip() context.clip()
context.set_operator(cairo.OPERATOR_OVER) context.set_operator(cairo.OPERATOR_OVER)
if not self.show_disconnected and self.connection_status == "DISCONNECTED" and not self.use_dummy: if (not self.show_disconnected and self.connection_status == "DISCONNECTED"
and not self.use_dummy):
return return
connection = self.discover.connection connection = self.discover.connection
@ -624,14 +573,14 @@ class VoiceOverlayWindow(OverlayWindow):
avatars_per_row = sys.maxsize avatars_per_row = sys.maxsize
# Calculate height needed to show overlay # Calculate height needed to show overlay
doTitle = False do_title = False
doConnection = False do_connection = False
if self.show_connection: if self.show_connection:
users_to_draw.insert(0, None) users_to_draw.insert(0, None)
doConnection = True do_connection = True
if self.show_title and self.channel_title: if self.show_title and self.channel_title:
users_to_draw.insert(0, None) users_to_draw.insert(0, None)
doTitle = True do_title = True
if self.horizontal: if self.horizontal:
needed_width = (len(users_to_draw) * line_height) + \ needed_width = (len(users_to_draw) * line_height) + \
@ -655,7 +604,7 @@ class VoiceOverlayWindow(OverlayWindow):
rows_to_draw = [] rows_to_draw = []
while len(users_to_draw) > 0: while len(users_to_draw) > 0:
row = [] row = []
for i in range(0, min(avatars_per_row, len(users_to_draw))): for _i in range(0, min(avatars_per_row, len(users_to_draw))):
row.append(users_to_draw.pop(0)) row.append(users_to_draw.pop(0))
rows_to_draw.append(row) rows_to_draw.append(row)
for row in rows_to_draw: for row in rows_to_draw:
@ -668,14 +617,14 @@ class VoiceOverlayWindow(OverlayWindow):
for user in row: for user in row:
if not user: if not user:
if doTitle: if do_title:
doTitle = False do_title = False
text_width = self.draw_title( text_width = self.draw_title(
context, current_x, current_y, avatar_size, line_height) context, current_x, current_y, avatar_size, line_height)
elif doConnection: elif do_connection:
text_width = self.draw_connection( text_width = self.draw_connection(
context, current_x, current_y, avatar_size, line_height) context, current_x, current_y, avatar_size, line_height)
doConnection = False do_connection = False
else: else:
self.draw_avatar(context, user, current_x, self.draw_avatar(context, user, current_x,
current_y, avatar_size, line_height) current_y, avatar_size, line_height)
@ -712,7 +661,7 @@ class VoiceOverlayWindow(OverlayWindow):
cols_to_draw = [] cols_to_draw = []
while len(users_to_draw) > 0: while len(users_to_draw) > 0:
col = [] col = []
for i in range(0, min(avatars_per_row, len(users_to_draw))): for _i in range(0, min(avatars_per_row, len(users_to_draw))):
col.append(users_to_draw.pop(0)) col.append(users_to_draw.pop(0))
cols_to_draw.append(col) cols_to_draw.append(col)
for col in cols_to_draw: for col in cols_to_draw:
@ -725,22 +674,22 @@ class VoiceOverlayWindow(OverlayWindow):
largest_text_width = 0 largest_text_width = 0
for user in col: for user in col:
if not user: if not user:
if doTitle: if do_title:
# Draw header # Draw header
text_width = self.draw_title( text_width = self.draw_title(
context, current_x, current_y, avatar_size, line_height) context, current_x, current_y, avatar_size, line_height)
largest_text_width = max( largest_text_width = max(
text_width, largest_text_width) text_width, largest_text_width)
current_y += line_height + self.icon_spacing current_y += line_height + self.icon_spacing
doTitle = False do_title = False
elif doConnection: elif do_connection:
# Draw header # Draw header
text_width = self.draw_connection( text_width = self.draw_connection(
context, current_x, current_y, avatar_size, line_height) context, current_x, current_y, avatar_size, line_height)
largest_text_width = max( largest_text_width = max(
text_width, largest_text_width) text_width, largest_text_width)
current_y += line_height + self.icon_spacing current_y += line_height + self.icon_spacing
doConnection = False do_connection = False
else: else:
text_width = self.draw_avatar( text_width = self.draw_avatar(
@ -758,9 +707,7 @@ class VoiceOverlayWindow(OverlayWindow):
self.context = None self.context = None
def recv_avatar(self, identifier, pix, mask): def recv_avatar(self, identifier, pix, mask):
""" """Called when image_getter has downloaded an image"""
Called when image_getter has downloaded an image
"""
if identifier == 'def': if identifier == 'def':
self.def_avatar = pix self.def_avatar = pix
self.def_avatar_mask = mask self.def_avatar_mask = mask
@ -773,16 +720,12 @@ class VoiceOverlayWindow(OverlayWindow):
self.set_needs_redraw() self.set_needs_redraw()
def delete_avatar(self, identifier): def delete_avatar(self, identifier):
""" """Remove avatar image"""
Remove avatar image
"""
if identifier in self.avatars: if identifier in self.avatars:
del self.avatars[identifier] del self.avatars[identifier]
def draw_title(self, context, pos_x, pos_y, avatar_size, line_height): 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"""
Draw title at given Y position. Includes both text and image based on settings
"""
tw = 0 tw = 0
if not self.horizontal and not self.icon_only: if not self.horizontal and not self.icon_only:
title = self.channel_title title = self.channel_title
@ -826,9 +769,7 @@ class VoiceOverlayWindow(OverlayWindow):
_("VOICE_CONNECTED") _("VOICE_CONNECTED")
def draw_connection(self, context, pos_x, pos_y, avatar_size, line_height): 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"""
Draw title at given Y position. Includes both text and image based on settings
"""
tw = 0 tw = 0
if not self.horizontal and not self.icon_only: if not self.horizontal and not self.icon_only:
tw = self.draw_text( tw = self.draw_text(
@ -846,13 +787,11 @@ class VoiceOverlayWindow(OverlayWindow):
return tw return tw
def draw_avatar(self, context, user, pos_x, pos_y, avatar_size, line_height): 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"""
Draw avatar at given Y position. Includes both text and image based on settings
"""
# Ensure pixbuf for avatar # Ensure pixbuf for avatar
if user["id"] not in self.avatars and user["avatar"] and avatar_size > 0: if user["id"] not in self.avatars and user["avatar"] and avatar_size > 0:
url = "https://cdn.discordapp.com/avatars/%s/%s.png" % ( url = f"https://cdn.discordapp.com/avatars/{
user['id'], user['avatar']) user['id']}/{user['avatar']}.png"
get_surface(self.recv_avatar, url, user["id"], get_surface(self.recv_avatar, url, user["id"],
self.avatar_size) self.avatar_size)
@ -907,19 +846,18 @@ class VoiceOverlayWindow(OverlayWindow):
self.mute_bg_col, avatar_size) self.mute_bg_col, avatar_size)
return tw return tw
def draw_text(self, context, string, pos_x, pos_y, tx_col, bg_col, avatar_size, line_height, font): 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 """Draw username & background at given position"""
"""
if self.nick_length < 32 and len(string) > self.nick_length: if self.nick_length < 32 and len(string) > self.nick_length:
string = string[:(self.nick_length-1)] + u"\u2026" string = string[:(self.nick_length-1)] + "\u2026"
context.save() context.save()
layout = self.create_pango_layout(string) layout = self.create_pango_layout(string)
layout.set_auto_dir(True) layout.set_auto_dir(True)
layout.set_markup(string, -1) layout.set_markup(string, -1)
(floating_x, floating_y, floating_width, (_floating_x, _floating_y, floating_width,
floating_height) = self.get_floating_coords() _floating_height) = self.get_floating_coords()
layout.set_width(Pango.SCALE * floating_width) layout.set_width(Pango.SCALE * floating_width)
layout.set_spacing(Pango.SCALE * 3) layout.set_spacing(Pango.SCALE * 3)
if font: if font:
@ -971,6 +909,7 @@ class VoiceOverlayWindow(OverlayWindow):
return text_width return text_width
def blank_avatar(self, context, pos_x, pos_y, avatar_size): 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() context.save()
if self.round_avatar: if self.round_avatar:
context.arc(pos_x + (avatar_size / 2), pos_y + context.arc(pos_x + (avatar_size / 2), pos_y +
@ -983,9 +922,7 @@ class VoiceOverlayWindow(OverlayWindow):
context.restore() context.restore()
def draw_avatar_pix(self, context, pixbuf, mask, pos_x, pos_y, border_colour, avatar_size): def draw_avatar_pix(self, context, pixbuf, mask, pos_x, pos_y, border_colour, avatar_size):
""" """Draw avatar image at given position"""
Draw avatar image at given position
"""
if not self.show_avatar: if not self.show_avatar:
return return
# Empty the space for this # Empty the space for this
@ -1021,13 +958,16 @@ class VoiceOverlayWindow(OverlayWindow):
if self.round_avatar: if self.round_avatar:
context.new_path() context.new_path()
context.arc(pos_x + (avatar_size / 2), pos_y + context.arc(pos_x + (avatar_size / 2), pos_y +
(avatar_size / 2), avatar_size / 2 + (self.border_width/2.0), 0, 2 * math.pi) (avatar_size / 2), avatar_size / 2 +
(self.border_width/2.0), 0, 2 * math.pi)
context.set_line_width(self.border_width) context.set_line_width(self.border_width)
context.stroke() context.stroke()
else: else:
context.new_path() context.new_path()
context.rectangle(pos_x - (self.border_width/2), pos_y - (self.border_width/2), context.rectangle(pos_x - (self.border_width/2),
avatar_size + self.border_width, avatar_size + self.border_width) pos_y - (self.border_width/2),
avatar_size + self.border_width,
avatar_size + self.border_width)
context.set_line_width(self.border_width) context.set_line_width(self.border_width)
context.stroke() context.stroke()
@ -1053,13 +993,12 @@ class VoiceOverlayWindow(OverlayWindow):
context.clip() context.clip()
context.set_operator(cairo.OPERATOR_OVER) context.set_operator(cairo.OPERATOR_OVER)
draw_img_to_rect(pixbuf, context, pos_x, pos_y, draw_img_to_rect(pixbuf, context, pos_x, pos_y,
avatar_size, avatar_size, False, False, 0, 0, self.fade_opacity * self.icon_transparency) avatar_size, avatar_size, False, False, 0, 0,
self.fade_opacity * self.icon_transparency)
context.restore() context.restore()
def draw_mute(self, context, pos_x, pos_y, bg_col, avatar_size): def draw_mute(self, context, pos_x, pos_y, bg_col, avatar_size):
""" """Draw Mute logo"""
Draw Mute logo
"""
if avatar_size <= 0: if avatar_size <= 0:
return return
context.save() context.save()
@ -1126,9 +1065,7 @@ class VoiceOverlayWindow(OverlayWindow):
context.restore() context.restore()
def draw_deaf(self, context, pos_x, pos_y, bg_col, avatar_size): def draw_deaf(self, context, pos_x, pos_y, bg_col, avatar_size):
""" """Draw deaf logo"""
Draw deaf logo
"""
if avatar_size <= 0: if avatar_size <= 0:
return return
context.save() context.save()
@ -1188,6 +1125,7 @@ class VoiceOverlayWindow(OverlayWindow):
context.restore() context.restore()
def draw_connection_icon(self, context, pos_x, pos_y, avatar_size): def draw_connection_icon(self, context, pos_x, pos_y, avatar_size):
"""Draw a series of bars to show connectivity state"""
context.save() context.save()
context.translate(pos_x, pos_y) context.translate(pos_x, pos_y)
context.scale(avatar_size, avatar_size) context.scale(avatar_size, avatar_size)