- A comment for every function. Improvements welcome

- fixes #94
This commit is contained in:
trigg 2020-10-20 16:49:04 +00:00
parent 86c5988fe7
commit f5b5eb1dfc
11 changed files with 619 additions and 183 deletions

View file

@ -14,6 +14,11 @@
The connector for discord.
Connects in as if it was Streamkit for OBS or Xsplit and
communicates to get voice & text info to display
Terminology:
GUILDS - Often called 'Servers' in dicord. It is the group of users and channels that make up
one server.
CHANNEL - Often called 'Rooms'. Both voice and text channels are types of channel
"""
import select
import time
@ -59,6 +64,9 @@ class DiscordConnector:
self.last_text_channel = None
def get_access_token_stage1(self):
"""
First stage of getting an access token. Request authorization from Discord client
"""
cmd = {
"cmd": "AUTHORIZE",
"args":
@ -72,6 +80,9 @@ class DiscordConnector:
self.websocket.send(json.dumps(cmd))
def get_access_token_stage2(self, code1):
"""
Second stage of getting an access token. Give auth code to streamkit
"""
url = "https://streamkit.discord.com/overlay/token"
myobj = {"code": code1}
response = requests.post(url, json=myobj)
@ -86,6 +97,9 @@ class DiscordConnector:
sys.exit(1)
def set_channel(self, channel, need_req=True):
"""
Set currently active voice channel
"""
if not channel:
self.current_voice = "0"
return
@ -99,6 +113,9 @@ class DiscordConnector:
self.req_channel_details(channel)
def set_text_channel(self, channel, need_req=True):
"""
Set currently active text channel
"""
if not channel:
self.current_text = "0"
return
@ -110,6 +127,9 @@ class DiscordConnector:
self.req_channel_details(channel)
def set_in_room(self, userid, present):
"""
Set user currently in given room
"""
if present:
if userid not in self.in_room:
self.in_room.append(userid)
@ -118,6 +138,9 @@ class DiscordConnector:
self.in_room.remove(userid)
def add_text(self, message):
"""
Add line of text to text list. Assumes the message is from the correct room
"""
utc_time = time.strptime(
message["timestamp"], "%Y-%m-%dT%H:%M:%S.%f%z")
epoch_time = calendar.timegm(utc_time)
@ -138,6 +161,9 @@ class DiscordConnector:
self.text_altered = True
def update_text(self, message_in):
"""
Update a line of text
"""
for idx in range(0, len(self.text)):
message = self.text[idx]
if message['id'] == message_in['id']:
@ -152,6 +178,9 @@ class DiscordConnector:
return
def delete_text(self, message_in):
"""
Delete a line of text
"""
for idx in range(0, len(self.text)):
message = self.text[idx]
if message['id'] == message_in['id']:
@ -160,6 +189,10 @@ class DiscordConnector:
return
def get_message_from_message(self, message):
"""
Messages are sent as JSON objects, with varying information.
Decides which bits are shown and which are discarded
"""
if "content_parsed" in message:
return message["content_parsed"]
elif "content" in message and len(message["content"]) > 0:
@ -174,11 +207,19 @@ class DiscordConnector:
return ""
def get_attachment_from_message(self, message):
"""
Messages with attachments come in different forms, decide what is and is
not an attachment
"""
if len(message["attachments"]) == 1:
return message["attachments"]
return None
def update_user(self, user):
"""
Update user information
Pass along our custom user information from version to version
"""
if user["id"] in self.userlist:
olduser = self.userlist[user["id"]]
if "mute" not in user and "mute" in olduser:
@ -200,6 +241,9 @@ class DiscordConnector:
self.userlist[user["id"]] = user
def on_message(self, message):
"""
Recieve websocket message super-function
"""
j = json.loads(message)
if j["cmd"] == "AUTHORIZE":
self.get_access_token_stage2(j["data"]["code"])
@ -322,7 +366,9 @@ class DiscordConnector:
logging.info(j)
def check_guilds(self):
# Check if all of the guilds contain a channel
"""
Check if all of the guilds contain a channel
"""
for guild in self.guilds.values():
if "channels" not in guild:
return
@ -330,6 +376,9 @@ class DiscordConnector:
self.on_connected()
def on_connected(self):
"""
Called when connection is finalised
"""
for guild in self.guilds.values():
channels = ""
for channel in guild["channels"]:
@ -342,13 +391,22 @@ class DiscordConnector:
self.sub_text_channel(self.last_text_channel)
def on_error(self, error):
"""
Called when an error has occured
"""
logging.error("ERROR : %s", error)
def on_close(self):
"""
Called when connection is closed
"""
logging.info("Connection closed")
self.websocket = None
def req_auth(self):
"""
Request authentication token
"""
cmd = {
"cmd": "AUTHENTICATE",
"args": {
@ -359,6 +417,9 @@ class DiscordConnector:
self.websocket.send(json.dumps(cmd))
def req_guilds(self):
"""
Request all guilds information for logged in user
"""
cmd = {
"cmd": "GET_GUILDS",
"args": {},
@ -367,6 +428,9 @@ class DiscordConnector:
self.websocket.send(json.dumps(cmd))
def req_channels(self, guild):
"""
Request all channels information for given guild
"""
cmd = {
"cmd": "GET_CHANNELS",
"args": {
@ -377,6 +441,9 @@ class DiscordConnector:
self.websocket.send(json.dumps(cmd))
def req_channel_details(self, channel):
"""
Request information about a specific channel
"""
cmd = {
"cmd": "GET_CHANNEL",
"args": {
@ -387,6 +454,17 @@ class DiscordConnector:
self.websocket.send(json.dumps(cmd))
def find_user(self):
"""
***Potential overload issue***
Asks the server for information about every single voice channel (type==2)
in the hope that one of them will say the user is present
because if asks about every single one without waiting for reply it is heavy even
if the user is relatively simple to find
It might be worth limiting the usage of this
"""
count = 0
for channel in self.channels:
if self.channels[channel]["type"] == 2:
@ -395,6 +473,9 @@ class DiscordConnector:
logging.warning("Getting %s rooms", count)
def sub_raw(self, event, args, nonce):
"""
Subscribe to event helper function
"""
cmd = {
"cmd": "SUBSCRIBE",
"args": args,
@ -404,18 +485,34 @@ class DiscordConnector:
self.websocket.send(json.dumps(cmd))
def sub_server(self):
"""
Subscribe to helpful events that report connectivity issues &
when the user has intentionally changed channel
Unfortunatly no event has been found to alert to being forcibly moved
or that reports the users current location
"""
self.sub_raw("VOICE_CHANNEL_SELECT", {}, "VOICE_CHANNEL_SELECT")
self.sub_raw("VOICE_CONNECTION_STATUS", {}, "VOICE_CONNECTION_STATUS")
def sub_channel(self, event, channel):
"""
Subscribe to event on channel
"""
self.sub_raw(event, {"channel_id": channel}, channel)
def sub_text_channel(self, channel):
"""
Subscribe to text-based events.
"""
self.sub_channel("MESSAGE_CREATE", channel)
self.sub_channel("MESSAGE_UPDATE", channel)
self.sub_channel("MESSAGE_DELETE", channel)
def sub_voice_channel(self, channel):
"""
Subscribe to voice-based events
"""
self.sub_channel("VOICE_STATE_CREATE", channel)
self.sub_channel("VOICE_STATE_UPDATE", channel)
self.sub_channel("VOICE_STATE_DELETE", channel)
@ -423,6 +520,15 @@ class DiscordConnector:
self.sub_channel("SPEAKING_STOP", channel)
def do_read(self):
"""
Poorly named logic center.
Checks for new data on socket, passes to on_message
Also passes out text data to text overlay and voice data to voice overlay
Called at 60Hz approximately but has near zero bearing on rendering
"""
# Ensure connection
if not self.websocket:
self.connect()
@ -466,12 +572,22 @@ class DiscordConnector:
return True
def start_listening_text(self, channel):
"""
Subscribe to text events on channel, or remember the channel for when we've connected
Helper function to avoid race conditions of reading config vs connecting to websocket
"""
if self.websocket:
self.sub_text_channel(channel)
else:
self.last_text_channel = channel
def connect(self):
"""
Attempt to connect to websocket
Should not throw simply for being unable to connect, only for more serious issues
"""
if self.websocket:
return
try:

View file

@ -53,6 +53,9 @@ class Discover:
Gtk.main()
def do_args(self, data):
"""
Read in arg list from command or RPC and act accordingly
"""
if "--help" in data:
pass
elif "--about" in data:
@ -63,12 +66,18 @@ class Discover:
sys.exit(0)
def rpc_changed(self, _a=None, _b=None, _c=None, _d=None):
"""
Called when the RPC file has been altered
"""
with open(self.rpc_file, "r") as tfile:
data = tfile.readlines()
if len(data) >= 1:
self.do_args(data[0])
def create_gui(self):
"""
Create Systray & associated menu, overlays & settings windows
"""
self.voice_overlay = VoiceOverlayWindow(self)
self.text_overlay = TextOverlayWindow(self)
self.menu = self.make_menu()
@ -77,7 +86,10 @@ class Discover:
self.text_overlay, self.voice_overlay)
def make_sys_tray_icon(self, menu):
# Create AppIndicator
"""
Attempt to create an AppIndicator icon, failing that attempt to make
a systemtray icon
"""
try:
gi.require_version('AppIndicator3', '0.1')
# pylint: disable=import-outside-toplevel
@ -95,7 +107,9 @@ class Discover:
self.tray.connect('popup-menu', self.show_menu)
def make_menu(self):
# Create System Menu
"""
Create System Menu
"""
menu = Gtk.Menu()
settings_opt = Gtk.MenuItem.new_with_label("Settings")
close_opt = Gtk.MenuItem.new_with_label("Close")
@ -109,18 +123,36 @@ class Discover:
return menu
def show_menu(self, obj, button, time):
"""
Show menu when System Tray icon is clicked
"""
self.menu.show_all()
self.menu.popup(
None, None, Gtk.StatusIcon.position_menu, obj, button, time)
def show_settings(self, _obj=None, _data=None):
"""
Show settings window
"""
self.settings.present_settings()
def close(self, _a=None, _b=None, _c=None):
"""
End of the program
"""
Gtk.main_quit()
def entrypoint():
"""
Entry Point.
Check for PID & RPC.
If an overlay is already running then pass the args along and close
Otherwise start up the overlay!
"""
config_dir = os.path.join(xdg_config_home, "discover_overlay")
os.makedirs(config_dir, exist_ok=True)
line = ""

View file

@ -38,6 +38,9 @@ class GeneralSettingsWindow(SettingsWindow):
self.create_gui()
def read_config(self):
"""
Read in the 'general' section of config and set overlays
"""
config = ConfigParser(interpolation=None)
config.read(self.config_file)
self.xshape = config.getboolean("general", "xshape", fallback=False)
@ -47,6 +50,9 @@ class GeneralSettingsWindow(SettingsWindow):
self.overlay2.set_force_xshape(self.xshape)
def save_config(self):
"""
Save the 'general' section of config
"""
config = ConfigParser(interpolation=None)
config.read(self.config_file)
if not config.has_section("general"):
@ -58,6 +64,9 @@ class GeneralSettingsWindow(SettingsWindow):
config.write(file)
def create_gui(self):
"""
Prepare the GUI
"""
box = Gtk.Grid()
# Auto start
@ -80,10 +89,16 @@ class GeneralSettingsWindow(SettingsWindow):
self.add(box)
def change_autostart(self, button):
"""
Autostart setting changed
"""
autostart = button.get_active()
self.autostart_helper.set_autostart(autostart)
def change_xshape(self, button):
"""
XShape setting changed
"""
self.overlay.set_force_xshape(button.get_active())
self.overlay2.set_force_xshape(button.get_active())
self.xshape = button.get_active()

View file

@ -33,6 +33,9 @@ class ImageGetter():
self.size = size
def get_url(self):
"""
Download and decode
"""
req = urllib.request.Request(self.url)
req.add_header(
'Referer', 'https://streamkit.discord.com/overlay/voice')
@ -150,11 +153,14 @@ def draw_img_to_rect(img, ctx,
width, height,
path=False, aspect=False,
anchor=0, hanchor=0):
"""Draw cairo surface onto context"""
# Path - only add the path do not fill : True/False
# Aspect - keep aspect ratio : True/False
# Anchor - with aspect : 0=left 1=middle 2=right
# HAnchor - with apect : 0=bottom 1=middle 2=top
"""Draw cairo surface onto context
Path - only add the path do not fill : True/False
Aspect - keep aspect ratio : True/False
Anchor - with aspect : 0=left 1=middle 2=right
HAnchor - with apect : 0=bottom 1=middle 2=top
"""
ctx.save()
offset_x = 0
offset_y = 0

View file

@ -34,6 +34,9 @@ class OverlayWindow(Gtk.Window):
"""
def detect_type(self):
"""
Helper function to determine if Wayland is being used and return the Window type needed
"""
window = Gtk.Window()
screen = window.get_screen()
screen_type = "%s" % (screen)
@ -87,6 +90,10 @@ class OverlayWindow(Gtk.Window):
self.context = None
def set_wayland_state(self):
"""
If wayland is in use then attempt to set up a GtkLayerShell
I have no idea how this should register a fail for Weston/KDE/Gnome
"""
if self.is_wayland:
GtkLayerShell.init_for_window(self)
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP)
@ -96,14 +103,22 @@ class OverlayWindow(Gtk.Window):
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.TOP, True)
def overlay_draw(self, _w, context, data=None):
pass
"""
Draw overlay
"""
def set_font(self, name, size):
"""
Set the font used by the overlay
"""
self.text_font = name
self.text_size = size
self.redraw()
def set_floating(self, floating, pos_x, pos_y, width, height):
"""
Set if the window is floating and what dimensions to use
"""
self.floating = floating
self.pos_x = pos_x
self.pos_y = pos_y
@ -112,6 +127,10 @@ class OverlayWindow(Gtk.Window):
self.force_location()
def set_untouchable(self):
"""
Create a custom input shape and tell it that all of the window is a cut-out
This allows us to have a window above everything but that never gets clicked on
"""
(width, height) = self.get_size()
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
surface_ctx = cairo.Context(surface)
@ -122,9 +141,16 @@ class OverlayWindow(Gtk.Window):
self.input_shape_combine_region(reg)
def unset_shape(self):
"""
Remove XShape (not input shape)
"""
self.get_window().shape_combine_region(None, 0, 0)
def force_location(self):
"""
On X11 enforce the location and sane defaults
On Wayland just store for later
"""
if not self.is_wayland:
self.set_decorated(False)
self.set_keep_above(True)
@ -155,6 +181,11 @@ class OverlayWindow(Gtk.Window):
self.redraw()
def redraw(self):
"""
Request a redraw.
If we're using XShape (optionally or forcibly) then render the image into the shape
so that we only cut out clear sections
"""
gdkwin = self.get_window()
if not self.floating:
(width, height) = self.get_size()
@ -175,6 +206,9 @@ class OverlayWindow(Gtk.Window):
self.queue_draw()
def set_monitor(self, idx=None, mon=None):
"""
Set the monitor this overlay should display on.
"""
self.monitor = idx
if self.is_wayland:
if mon:
@ -183,17 +217,42 @@ class OverlayWindow(Gtk.Window):
self.redraw()
def set_align_x(self, align_right):
"""
Set the alignment (True for right, False for left)
"""
self.align_right = align_right
self.force_location()
self.redraw()
def set_align_y(self, align_vert):
"""
Set the veritcal alignment
"""
self.align_vert = align_vert
self.force_location()
self.redraw()
def col(self, col, alpha=1.0):
"""
Convenience function to set the cairo context next colour
"""
self.context.set_source_rgba(col[0], col[1], col[2], col[3] * alpha)
def set_force_xshape(self, force):
"""
Set if XShape should be forced
"""
self.force_xshape = force
if self.is_wayland:
# Wayland and XShape are a bad idea unless you're a fan on artifacts
self.force_xshape = False
def set_enabled(self, enabled):
"""
Set if this overlay should be visible
"""
if enabled:
self.show_all()
else:
self.hide()

View file

@ -17,8 +17,10 @@ overlay types without copy-and-pasting too much code
import os
import logging
import gi
from .draggable_window import DraggableWindow
from .draggable_window_wayland import DraggableWindowWayland
gi.require_version("Gtk", "3.0")
# pylint: disable=wrong-import-position
# pylint: disable=wrong-import-position,wrong-import-order
from gi.repository import Gtk, Gdk
@ -40,18 +42,34 @@ class SettingsWindow(Gtk.VBox):
self.config_dir = None
self.config_file = None
self.overlay = None
self.floating = None
self.floating_x = None
self.floating_y = None
self.floating_w = None
self.floating_h = None
self.align_x_widget = None
self.align_y_widget = None
self.align_monitor_widget = None
self.align_placement_widget = None
self.monitor = None
self.align_x = None
self.align_y = None
self.enabled = None
def init_config(self):
"""
Locate the config and then read
"""
self.config_dir = os.path.join(xdg_config_home, "discover_overlay")
os.makedirs(self.config_dir, exist_ok=True)
self.config_file = os.path.join(self.config_dir, "config.ini")
self.read_config()
def close_window(self, _a=None, _b=None):
"""
Helper to ensure we don't lose changes to floating windows
Hide for later
"""
if self.placement_window:
(pos_x, pos_y) = self.placement_window.get_position()
(width, height) = self.placement_window.get_size()
@ -67,6 +85,9 @@ class SettingsWindow(Gtk.VBox):
return True
def get_monitor_index(self, name):
"""
Helper function to find the index number of the monitor
"""
display = Gdk.Display.get_default()
if "get_n_monitors" in dir(display):
for i in range(0, display.get_n_monitors()):
@ -77,6 +98,9 @@ class SettingsWindow(Gtk.VBox):
return 0
def get_monitor_obj(self, name):
"""
Helper function to find the monitor object of the monitor
"""
display = Gdk.Display.get_default()
if "get_n_monitors" in dir(display):
for i in range(0, display.get_n_monitors()):
@ -87,10 +111,118 @@ class SettingsWindow(Gtk.VBox):
return None
def present_settings(self):
"""
Show settings
"""
self.show_all()
def read_config(self):
pass
"""
Stub called when settings are needed to be read
"""
def save_config(self):
pass
"""
Stub called when settings are needed to be written
"""
def change_placement(self, button):
"""
Placement window button pressed.
"""
if self.placement_window:
(pos_x, pos_y, width, height) = self.placement_window.get_coords()
self.floating_x = pos_x
self.floating_y = pos_y
self.floating_w = width
self.floating_h = height
self.overlay.set_floating(True, pos_x, pos_y, width, height)
self.save_config()
if not self.overlay.is_wayland:
button.set_label("Place Window")
self.placement_window.close()
self.placement_window = None
else:
if self.overlay.is_wayland:
self.placement_window = DraggableWindowWayland(
pos_x=self.floating_x, pos_y=self.floating_y,
width=self.floating_w, height=self.floating_h,
message="Place & resize this window then press Green!", settings=self)
else:
self.placement_window = DraggableWindow(
pos_x=self.floating_x, pos_y=self.floating_y,
width=self.floating_w, height=self.floating_h,
message="Place & resize this window then press Save!", settings=self)
if not self.overlay.is_wayland:
button.set_label("Save this position")
def change_align_type_edge(self, button):
"""
Alignment setting changed
"""
if button.get_active():
self.overlay.set_floating(
False, self.floating_x, self.floating_y, self.floating_w, self.floating_h)
self.floating = False
self.save_config()
# Re-sort the screen
self.align_x_widget.show()
self.align_y_widget.show()
self.align_monitor_widget.show()
self.align_placement_widget.hide()
def change_align_type_floating(self, button):
"""
Alignment setting changed
"""
if button.get_active():
self.overlay.set_floating(
True, self.floating_x, self.floating_y, self.floating_w, self.floating_h)
self.floating = True
self.save_config()
self.align_x_widget.hide()
self.align_y_widget.hide()
self.align_monitor_widget.hide()
self.align_placement_widget.show()
def change_monitor(self, button):
"""
Alignment setting changed
"""
display = Gdk.Display.get_default()
if "get_monitor" in dir(display):
mon = display.get_monitor(button.get_active())
m_s = mon.get_model()
self.overlay.set_monitor(button.get_active(), mon)
self.monitor = m_s
self.save_config()
def change_align_x(self, button):
"""
Alignment setting changed
"""
self.overlay.set_align_x(button.get_active() == 1)
self.align_x = (button.get_active() == 1)
self.save_config()
def change_align_y(self, button):
"""
Alignment setting changed
"""
self.overlay.set_align_y(button.get_active())
self.align_y = button.get_active()
self.save_config()
def change_enabled(self, button):
"""
Overlay active state toggled
"""
self.overlay.set_enabled(button.get_active())
self.enabled = button.get_active()
self.save_config()

View file

@ -53,6 +53,9 @@ class MainSettingsWindow(Gtk.Window):
self.notebook = notebook
def close_window(self, widget=None, event=None):
"""
Hide the settings window for use at a later date
"""
self.text_settings.close_window(widget, event)
self.voice_settings.close_window(widget, event)
self.core_settings.close_window(widget, event)
@ -60,6 +63,9 @@ class MainSettingsWindow(Gtk.Window):
return True
def present_settings(self):
"""
Show the settings window
"""
self.voice_settings.present_settings()
self.text_settings.present_settings()
self.core_settings.present_settings()

View file

@ -52,35 +52,50 @@ class TextOverlayWindow(OverlayWindow):
self.set_title("Discover Text")
def set_text_time(self, timer):
"""
Set the duration that a message will be visible for.
"""
self.text_time = timer
def set_text_list(self, tlist, altered):
"""
Update the list of text messages to show
"""
self.content = tlist
if altered:
self.redraw()
def set_enabled(self, enabled):
if enabled:
self.show_all()
else:
self.hide()
def set_fg(self, fg_col):
"""
Set default text colour
"""
self.fg_col = fg_col
self.redraw()
def set_bg(self, bg_col):
"""
Set background colour
"""
self.bg_col = bg_col
self.redraw()
def set_show_attach(self, attachment):
"""
Set if attachments should be shown inline
"""
self.show_attach = attachment
self.redraw()
def set_popup_style(self, boolean):
"""
Set if message disappear after a certain duration
"""
self.popup_style = boolean
def set_font(self, name, size):
"""
Set font used to render text
"""
self.text_font = name
self.text_size = size
self.pango_rect = Pango.Rectangle()
@ -89,6 +104,9 @@ class TextOverlayWindow(OverlayWindow):
self.redraw()
def make_line(self, message):
"""
Decode a recursive JSON object into pango markup.
"""
ret = ""
if isinstance(message, list):
for inner_message in message:
@ -135,10 +153,16 @@ class TextOverlayWindow(OverlayWindow):
return ret
def recv_attach(self, identifier, pix):
"""
Called when an image has been downloaded by image_getter
"""
self.attachment[identifier] = pix
self.redraw()
def overlay_draw(self, _w, context, data=None):
"""
Draw the overlay
"""
self.context = context
context.set_antialias(cairo.ANTIALIAS_GOOD)
(width, height) = self.get_size()
@ -193,6 +217,9 @@ class TextOverlayWindow(OverlayWindow):
context.restore()
def draw_attach(self, pos_y, url):
"""
Draw an attachment
"""
if url in self.attachment and self.attachment[url]:
pix = self.attachment[url]
image_width = min(pix.get_width(), self.width)
@ -211,7 +238,9 @@ class TextOverlayWindow(OverlayWindow):
return pos_y
def draw_text(self, pos_y, text):
"""
Draw a text message, returning the Y position of the next message
"""
layout = self.create_pango_layout(text)
layout.set_markup(text, -1)
attr = layout.get_attributes()
@ -255,6 +284,9 @@ class TextOverlayWindow(OverlayWindow):
return pos_y - text_height
def render_custom(self, ctx, shape, path, _data):
"""
Draw an inline image as a custom emoticon
"""
key = self.image_list[shape.data]['url']
if key not in self.attachment:
get_surface(self.recv_attach,
@ -268,7 +300,9 @@ class TextOverlayWindow(OverlayWindow):
return True
def sanitize_string(self, string):
# I hate that Pango has nothing for this.
"""
Sanitize a text message so that it doesn't intefere with Pango's XML format
"""
string.replace("&", "&")
string.replace("<", "&lt;")
string .replace(">", "&gt;")

View file

@ -15,8 +15,6 @@ import json
import logging
from configparser import ConfigParser
import gi
from .draggable_window import DraggableWindow
from .draggable_window_wayland import DraggableWindowWayland
from .settings import SettingsWindow
gi.require_version("Gtk", "3.0")
@ -70,6 +68,11 @@ class TextSettingsWindow(SettingsWindow):
self.create_gui()
def update_channel_model(self):
"""
Update the Channel selector.
Populate with all channels from guild if a guild is chosen or all channels generall if not
"""
# potentially organize channels by their group/parent_id
# https://discord.com/developers/docs/resources/channel#channel-object-channel-structure
c_model = Gtk.ListStore(str, bool)
@ -107,11 +110,19 @@ class TextSettingsWindow(SettingsWindow):
idx += 1
def add_connector(self, conn):
"""
Add the discord_connector reference
If the user has previously selected a text channel then tell it to subscribe
"""
self.connector = conn
if self.channel:
self.connector.start_listening_text(self.channel)
def present_settings(self):
"""
Show contents of tab and update lists
"""
self.show_all()
if not self.floating:
self.align_x_widget.show()
@ -153,6 +164,9 @@ class TextSettingsWindow(SettingsWindow):
idxg += 1
def guild_list(self):
"""
Return a list of all guilds
"""
guilds = []
done = []
for guild in self.list_guilds.values():
@ -162,6 +176,9 @@ class TextSettingsWindow(SettingsWindow):
return guilds
def set_channels(self, in_list):
"""
Set the contents of list_channels
"""
self.list_channels = in_list
self.list_channels_keys = []
for key in in_list.keys():
@ -172,6 +189,9 @@ class TextSettingsWindow(SettingsWindow):
self.list_channels_keys.sort()
def set_guilds(self, in_list):
"""
Set the contents of list_guilds
"""
self.list_guilds = in_list
self.list_guilds_keys = []
for key in in_list.keys():
@ -179,6 +199,9 @@ class TextSettingsWindow(SettingsWindow):
self.list_guilds_keys.sort()
def read_config(self):
"""
Read in the 'text' section of the config
"""
config = ConfigParser(interpolation=None)
config.read(self.config_file)
self.enabled = config.getboolean("text", "enabled", fallback=False)
@ -221,6 +244,9 @@ class TextSettingsWindow(SettingsWindow):
self.overlay.set_show_attach(self.show_attach)
def save_config(self):
"""
Save the current settings to the 'text' section of the config
"""
config = ConfigParser(interpolation=None)
config.read(self.config_file)
if not config.has_section("text"):
@ -250,6 +276,9 @@ class TextSettingsWindow(SettingsWindow):
config.write(file)
def create_gui(self):
"""
Prepare the gui
"""
box = Gtk.Grid()
# Enabled
@ -404,6 +433,9 @@ class TextSettingsWindow(SettingsWindow):
self.add(box)
def change_font(self, button):
"""
Font settings changed
"""
font = button.get_font()
desc = Pango.FontDescription.from_string(font)
size = desc.get_size()
@ -415,6 +447,9 @@ class TextSettingsWindow(SettingsWindow):
self.save_config()
def change_channel(self, button):
"""
Channel setting changed
"""
if self.ignore_channel_change:
return
@ -424,6 +459,9 @@ class TextSettingsWindow(SettingsWindow):
self.save_config()
def change_guild(self, button):
"""
Guild setting changed
"""
if self.ignore_guild_change:
return
guild_id = self.guild_lookup[button.get_active()]
@ -431,87 +469,10 @@ class TextSettingsWindow(SettingsWindow):
self.save_config()
self.update_channel_model()
def change_placement(self, button):
if self.placement_window:
(pos_x, pos_y, width, height) = self.placement_window.get_coords()
self.floating_x = pos_x
self.floating_y = pos_y
self.floating_w = width
self.floating_h = height
self.overlay.set_floating(True, pos_x, pos_y, width, height)
self.save_config()
if not self.overlay.is_wayland:
button.set_label("Place Window")
self.placement_window.close()
self.placement_window = None
else:
if self.overlay.is_wayland:
self.placement_window = DraggableWindowWayland(
pos_x=self.floating_x, pos_y=self.floating_y,
width=self.floating_w, height=self.floating_h,
message="Place & resize this window then press Green!", settings=self)
else:
self.placement_window = DraggableWindow(
pos_x=self.floating_x, pos_y=self.floating_y,
width=self.floating_w, height=self.floating_h,
message="Place & resize this window then press Save!", settings=self)
if not self.overlay.is_wayland:
button.set_label("Save this position")
def change_align_type_edge(self, button):
if button.get_active():
self.overlay.set_floating(
False, self.floating_x, self.floating_y, self.floating_w, self.floating_h)
self.floating = False
self.save_config()
# Re-sort the screen
self.align_x_widget.show()
self.align_y_widget.show()
self.align_monitor_widget.show()
self.align_placement_widget.hide()
def change_align_type_floating(self, button):
if button.get_active():
self.overlay.set_floating(
True, self.floating_x, self.floating_y, self.floating_w, self.floating_h)
self.floating = True
self.save_config()
self.align_x_widget.hide()
self.align_y_widget.hide()
self.align_monitor_widget.hide()
self.align_placement_widget.show()
def change_monitor(self, button):
display = Gdk.Display.get_default()
if "get_monitor" in dir(display):
mon = display.get_monitor(button.get_active())
m_s = mon.get_model()
self.overlay.set_monitor(button.get_active(), mon)
self.monitor = m_s
self.save_config()
def change_align_x(self, button):
self.overlay.set_align_x(button.get_active() == 1)
self.align_x = (button.get_active() == 1)
self.save_config()
def change_align_y(self, button):
self.overlay.set_align_y(button.get_active())
self.align_y = button.get_active()
self.save_config()
def change_enabled(self, button):
self.overlay.set_enabled(button.get_active())
self.enabled = button.get_active()
self.save_config()
def change_popup_style(self, button):
"""
Popup style setting changed
"""
self.overlay.set_popup_style(button.get_active())
self.popup_style = button.get_active()
@ -526,15 +487,24 @@ class TextSettingsWindow(SettingsWindow):
self.text_time_label_widget.hide()
def change_text_time(self, button):
"""
Popup style setting changed
"""
self.overlay.set_text_time(button.get_value())
self.text_time = button.get_value()
self.save_config()
def get_channel(self):
"""
Return selected channel
"""
return self.channel
def change_bg(self, button):
"""
Background colour changed
"""
colour = button.get_rgba()
colour = [colour.red, colour.green, colour.blue, colour.alpha]
self.overlay.set_bg(colour)
@ -543,6 +513,9 @@ class TextSettingsWindow(SettingsWindow):
self.save_config()
def change_fg(self, button):
"""
Foreground colour changed
"""
colour = button.get_rgba()
colour = [colour.red, colour.green, colour.blue, colour.alpha]
self.overlay.set_fg(colour)
@ -551,6 +524,9 @@ class TextSettingsWindow(SettingsWindow):
self.save_config()
def change_show_attach(self, button):
"""
Attachment setting changed
"""
self.overlay.set_show_attach(button.get_active())
self.show_attach = button.get_active()

View file

@ -55,74 +55,134 @@ class VoiceOverlayWindow(OverlayWindow):
self.set_title("Discover Voice")
def set_bg(self, background_colour):
"""
Set the background colour
"""
self.norm_col = background_colour
self.redraw()
def set_fg(self, foreground_colour):
"""
Set the text colour
"""
self.text_col = foreground_colour
self.redraw()
def set_tk(self, talking_colour):
"""
Set the border colour for users who are talking
"""
self.talk_col = talking_colour
self.redraw()
def set_mt(self, mute_colour):
"""
Set the colour of mute and deafen logos
"""
self.mute_col = mute_colour
self.redraw()
def set_avatar_size(self, size):
"""
Set the size of the avatar icons
"""
self.avatar_size = size
self.redraw()
def set_icon_spacing(self, i):
"""
Set the spacing between avatar icons
"""
self.icon_spacing = i
self.redraw()
def set_text_padding(self, i):
"""
Set padding between text and border
"""
self.text_pad = i
self.redraw()
def set_vert_edge_padding(self, i):
"""
Set padding between top/bottom of screen and overlay contents
"""
self.vert_edge_padding = i
self.redraw()
def set_horz_edge_padding(self, i):
"""
Set padding between left/right of screen and overlay contents
"""
self.horz_edge_padding = i
self.redraw()
def set_square_avatar(self, i):
"""
Set if the overlay should crop avatars to a circle or show full square image
"""
self.round_avatar = not i
self.redraw()
def set_only_speaking(self, only_speaking):
"""
Set if overlay should only show people who are talking
"""
self.only_speaking = only_speaking
def set_highlight_self(self, highlight_self):
"""
Set if the overlay should highlight the user
"""
self.highlight_self = highlight_self
def set_order(self, i):
"""
Set the method used to order avatar icons & names
"""
self.order = i
def set_icon_only(self, i):
"""
Set if the overlay should draw only the icon
"""
self.icon_only = i
self.redraw()
def set_wind_col(self):
"""
Use window colour to draw
"""
self.col(self.wind_col)
def set_text_col(self):
"""
Use text colour to draw
"""
self.col(self.text_col)
def set_norm_col(self):
"""
Use background colour to draw
"""
self.col(self.norm_col)
def set_talk_col(self, alpha=1.0):
"""
Use talking colour to draw
"""
self.col(self.talk_col, alpha)
def set_mute_col(self, alpha=1.0):
"""
Use mute colour to draw
"""
self.col(self.mute_col, alpha)
def set_user_list(self, userlist, alt):
"""
Set the users in list to draw
"""
self.userlist = userlist
for user in userlist:
if "nick" in user:
@ -145,12 +205,18 @@ class VoiceOverlayWindow(OverlayWindow):
self.redraw()
def set_connection(self, connection):
"""
Set if discord has a clean connection to server
"""
is_connected = connection == "VOICE_CONNECTED"
if self.connected != is_connected:
self.connected = is_connected
self.redraw()
def overlay_draw(self, _w, context, _data=None):
"""
Draw the Overlay
"""
self.context = context
context.set_antialias(cairo.ANTIALIAS_GOOD)
# Get size of window
@ -228,17 +294,19 @@ class VoiceOverlayWindow(OverlayWindow):
self.context = None
def recv_avatar(self, identifier, pix):
"""
Called when image_getter has downloaded an image
"""
if identifier == 'def':
self.def_avatar = pix
else:
self.avatars[identifier] = pix
self.redraw()
def delete_avatar(self, identifier):
if id in self.avatars:
del self.avatars[identifier]
def draw_avatar(self, context, user, pos_y):
"""
Draw avatar at given Y position. Includes both text and image based on settings
"""
# Ensure pixbuf for avatar
if user["id"] not in self.avatars and user["avatar"]:
url = "https://cdn.discordapp.com/avatars/%s/%s.jpg" % (
@ -298,6 +366,9 @@ class VoiceOverlayWindow(OverlayWindow):
self.draw_mute(context, self.horz_edge_padding, pos_y)
def draw_text(self, context, string, pos_x, pos_y):
"""
Draw username & background at given position
"""
if self.text_font:
context.set_font_face(cairo.ToyFontFace(
self.text_font, cairo.FontSlant.NORMAL, cairo.FontWeight.NORMAL))
@ -338,6 +409,9 @@ class VoiceOverlayWindow(OverlayWindow):
context.show_text(string)
def draw_avatar_pix(self, context, pixbuf, pos_x, pos_y, border_colour):
"""
Draw avatar image at given position
"""
if not pixbuf:
pixbuf = self.def_avatar
if not pixbuf:
@ -375,6 +449,9 @@ class VoiceOverlayWindow(OverlayWindow):
context.stroke()
def draw_mute(self, context, pos_x, pos_y):
"""
Draw Mute logo
"""
context.save()
context.translate(pos_x, pos_y)
context.scale(self.avatar_size, self.avatar_size)
@ -429,6 +506,9 @@ class VoiceOverlayWindow(OverlayWindow):
context.restore()
def draw_deaf(self, context, pos_x, pos_y):
"""
Draw deaf logo
"""
context.save()
context.translate(pos_x, pos_y)
context.scale(self.avatar_size, self.avatar_size)

View file

@ -14,8 +14,6 @@
import json
from configparser import ConfigParser
import gi
from .draggable_window import DraggableWindow
from .draggable_window_wayland import DraggableWindowWayland
from .settings import SettingsWindow
gi.require_version("Gtk", "3.0")
# pylint: disable=wrong-import-position,wrong-import-order
@ -56,6 +54,9 @@ class VoiceSettingsWindow(SettingsWindow):
self.create_gui()
def present_settings(self):
"""
Show tab
"""
self.show_all()
if not self.floating:
self.align_x_widget.show()
@ -69,6 +70,9 @@ class VoiceSettingsWindow(SettingsWindow):
self.align_placement_widget.show()
def read_config(self):
"""
Read 'main' section of the config file
"""
config = ConfigParser(interpolation=None)
config.read(self.config_file)
self.align_x = config.getboolean("main", "rightalign", fallback=True)
@ -136,6 +140,9 @@ class VoiceSettingsWindow(SettingsWindow):
self.overlay.set_font(desc.get_family(), size)
def save_config(self):
"""
Write settings out to the 'main' section of the config file
"""
config = ConfigParser(interpolation=None)
config.read(self.config_file)
if not config.has_section("main"):
@ -172,6 +179,9 @@ class VoiceSettingsWindow(SettingsWindow):
config.write(file)
def create_gui(self):
"""
Prepare the gui
"""
box = Gtk.Grid()
# Font chooser
@ -372,60 +382,10 @@ class VoiceSettingsWindow(SettingsWindow):
self.add(box)
def change_placement(self, button):
if self.placement_window:
(pos_x, pos_y, width, height) = self.placement_window.get_coords()
self.floating_x = pos_x
self.floating_y = pos_y
self.floating_w = width
self.floating_h = height
self.overlay.set_floating(True, pos_x, pos_y, width, height)
self.save_config()
if not self.overlay.is_wayland:
button.set_label("Place Window")
self.placement_window.close()
self.placement_window = None
else:
if self.overlay.is_wayland:
self.placement_window = DraggableWindowWayland(
pos_x=self.floating_x, pos_y=self.floating_y,
width=self.floating_w, height=self.floating_h,
message="Place & resize this window then press Green!", settings=self)
else:
self.placement_window = DraggableWindow(
pos_x=self.floating_x, pos_y=self.floating_y,
width=self.floating_w, height=self.floating_h,
message="Place & resize this window then press Save!", settings=self)
if not self.overlay.is_wayland:
button.set_label("Save this position")
def change_align_type_edge(self, button):
if button.get_active():
self.overlay.set_floating(
False, self.floating_x, self.floating_y,
self.floating_w, self.floating_h)
self.floating = False
self.save_config()
# Re-sort the screen
self.align_x_widget.show()
self.align_y_widget.show()
self.align_monitor_widget.show()
self.align_placement_widget.hide()
def change_align_type_floating(self, button):
if button.get_active():
self.overlay.set_floating(
True, self.floating_x, self.floating_y,
self.floating_w, self.floating_h)
self.floating = True
self.save_config()
self.align_x_widget.hide()
self.align_y_widget.hide()
self.align_monitor_widget.hide()
self.align_placement_widget.show()
def change_font(self, button):
"""
Font settings changed
"""
font = button.get_font()
desc = Pango.FontDescription.from_string(font)
size = desc.get_size()
@ -437,6 +397,9 @@ class VoiceSettingsWindow(SettingsWindow):
self.save_config()
def change_bg(self, button):
"""
Background colour changed
"""
colour = button.get_rgba()
colour = [colour.red, colour.green, colour.blue, colour.alpha]
self.overlay.set_bg(colour)
@ -445,6 +408,9 @@ class VoiceSettingsWindow(SettingsWindow):
self.save_config()
def change_fg(self, button):
"""
Text colour changed
"""
colour = button.get_rgba()
colour = [colour.red, colour.green, colour.blue, colour.alpha]
self.overlay.set_fg(colour)
@ -453,6 +419,9 @@ class VoiceSettingsWindow(SettingsWindow):
self.save_config()
def change_tk(self, button):
"""
Talking colour changed
"""
colour = button.get_rgba()
colour = [colour.red, colour.green, colour.blue, colour.alpha]
self.overlay.set_tk(colour)
@ -461,6 +430,9 @@ class VoiceSettingsWindow(SettingsWindow):
self.save_config()
def change_mt(self, button):
"""
Mute colour changed
"""
colour = button.get_rgba()
colour = [colour.red, colour.green, colour.blue, colour.alpha]
self.overlay.set_mt(colour)
@ -469,82 +441,90 @@ class VoiceSettingsWindow(SettingsWindow):
self.save_config()
def change_avatar_size(self, button):
"""
Avatar size setting changed
"""
self.overlay.set_avatar_size(button.get_value())
self.avatar_size = button.get_value()
self.save_config()
def change_monitor(self, button):
display = Gdk.Display.get_default()
if "get_monitor" in dir(display):
mon = display.get_monitor(button.get_active())
m_s = mon.get_model()
self.overlay.set_monitor(button.get_active(), mon)
self.monitor = m_s
self.save_config()
def change_align_x(self, button):
self.overlay.set_align_x(button.get_active() == 1)
self.align_x = (button.get_active() == 1)
self.save_config()
def change_align_y(self, button):
self.overlay.set_align_y(button.get_active())
self.align_y = button.get_active()
self.save_config()
def change_icon_spacing(self, button):
"""
Inter-icon spacing changed
"""
self.overlay.set_icon_spacing(button.get_value())
self.icon_spacing = int(button.get_value())
self.save_config()
def change_text_padding(self, button):
"""
Text padding changed
"""
self.overlay.set_text_padding(button.get_value())
self.text_padding = button.get_value()
self.save_config()
def change_vert_edge_padding(self, button):
"""
Vertical padding changed
"""
self.overlay.set_vert_edge_padding(button.get_value())
self.vert_edge_padding = button.get_value()
self.save_config()
def change_horz_edge_padding(self, button):
"""
Horizontal padding changed
"""
self.overlay.set_horz_edge_padding(button.get_value())
self.horz_edge_padding = button.get_value()
self.save_config()
def change_square_avatar(self, button):
"""
Square avatar setting changed
"""
self.overlay.set_square_avatar(button.get_active())
self.square_avatar = button.get_active()
self.save_config()
def change_only_speaking(self, button):
"""
Show only speaking users setting changed
"""
self.overlay.set_only_speaking(button.get_active())
self.only_speaking = button.get_active()
self.save_config()
def change_highlight_self(self, button):
"""
Highlight self setting changed
"""
self.overlay.set_highlight_self(button.get_active())
self.highlight_self = button.get_active()
self.save_config()
def change_icon_only(self, button):
"""
Icon only setting changed
"""
self.overlay.set_icon_only(button.get_active())
self.icon_only = button.get_active()
self.save_config()
def change_order(self, button):
"""
Order user setting changed
"""
self.overlay.set_order(button.get_active())
self.order = button.get_active()