diff --git a/discover_overlay/discord_connector.py b/discover_overlay/discord_connector.py new file mode 100644 index 0000000..a3bb329 --- /dev/null +++ b/discover_overlay/discord_connector.py @@ -0,0 +1,397 @@ +import websocket +import select +import time +import json +import requests + + +class DiscordConnector: + def __init__(self, text_settings, voice_settings, text_overlay, voice_overlay): + self.text_settings = text_settings + self.text_overlay = text_overlay + self.voice_settings = voice_settings + self.voice_overlay = voice_overlay + self.ws = None + self.access_token = "none" + # TODO Magic number + self.oauth_token = "207646673902501888" + self.access_delay = 0 + self.warn_connection = True + self.error_connection = True + + self.guilds = {} + self.channels = {} + self.user = {} + self.userlist = {} + self.in_room = [] + self.current_voice = "0" + self.current_text = "0" + self.list_altered = False + self.text_altered = False + self.last_connection = "" + self.text = [] + self.authed = False + + def get_access_token_stage1(self): + global oauth_token + self.ws.send("{\"cmd\":\"AUTHORIZE\",\"args\":{\"client_id\":\"%s\",\"scopes\":[\"rpc\",\"messages.read\"],\"prompt\":\"none\"},\"nonce\":\"deadbeef\"}" % ( + self.oauth_token)) + + def get_access_token_stage2(self, code1): + url = "https://streamkit.discord.com/overlay/token" + myobj = {"code": code1} + x = requests.post(url, json=myobj) + try: + j = json.loads(x.text) + except: + j = {} + if "access_token" in j: + self.access_token = j["access_token"] + self.req_auth() + else: + sys.exit(1) + + def set_channel(self, channel, need_req=True): + if not channel: + self.current_voice = "0" + return + if channel != self.current_voice: + cn = self.channels[channel]['name'] + print("Joined room: %s" % (cn)) + self.current_voice = channel + if need_req: + self.req_channel_details(channel) + + def set_text_channel(self, channel, need_req=True): + if not channel: + self.current_text = "0" + return + if channel != self.current_text: + self.current_text = channel + print("Changing text room: %s" % (channel)) + if need_req: + self.req_channel_details(channel) + + def set_in_room(self, userid, present): + if present: + if userid not in self.in_room: + self.in_room.append(userid) + else: + if userid in self.in_room: + self.in_room.remove(userid) + + def add_text(self, message): + un = message["author"]["username"] + if "nick" in message and message['nick'] and len(message["nick"]) > 1: + un = message["nick"] + ac = "#ffffff" + if "author_color" in message: + ac = message["author_color"] + + self.text.append({'id': message["id"], + 'content': self.get_message_from_message(message), + 'nick': un, + 'nick_col': ac, + }) + self.text_altered = True + + def update_text(self, message_in): + for idx in range(0, len(self.text)): + message = self.text[idx] + if message['id'] == message_in['id']: + new_message = {'id': message['id'], + 'content': self.get_message_from_message(message_in), + 'nick': message['nick'], + 'nick_col': message['nick_col']} + self.text[idx] = new_message + self.text_altered = True + return + + def delete_text(self, message_in): + global text, text_altered + for idx in range(0, len(self.text)): + message = self.text[idx] + if message['id'] == message_in['id']: + del self.text[idx] + self.text_altered = True + return + + def get_message_from_message(self, message): + if "content" in message and len(message["content"]) > 0: + return message["content"] + elif len(message["embeds"]) == 1: + if "rawDescription" in message["embeds"][0]: + return message["embeds"][0]["rawDescription"] + if "author" in message["embeds"][0]: + return message["embeds"][0]["author"]["name"] + elif len(message["attachments"]) == 1: + # Need to care + return "-- attachment --" + return "" + + def update_user(self, user): + if user["id"] in self.userlist: + if not "mute" in user and "mute" in self.userlist[user["id"]]: + user["mute"] = self.userlist[user["id"]]["mute"] + if not "deaf" in user and "deaf" in self.userlist[user["id"]]: + user["deaf"] = self.userlist[user["id"]]["deaf"] + if not "speaking" in user and "speaking" in self.userlist[user["id"]]: + user["speaking"] = self.userlist[user["id"]]["speaking"] + self.userlist[user["id"]] = user + + def on_message(self, message): + j = json.loads(message) + if j["cmd"] == "AUTHORIZE": + self.get_access_token_stage2(j["data"]["code"]) + return + elif j["cmd"] == "DISPATCH": + if j["evt"] == "READY": + self.req_auth() + elif j["evt"] == "VOICE_STATE_UPDATE": + self.list_altered = True + thisuser = j["data"]["user"] + un = j["data"]["user"]["username"] + mute = j["data"]["voice_state"]["mute"] or j["data"]["voice_state"]["self_mute"] + deaf = j["data"]["voice_state"]["deaf"] or j["data"]["voice_state"]["self_deaf"] + thisuser["mute"] = mute + thisuser["deaf"] = deaf + self.update_user(thisuser) + elif j["evt"] == "VOICE_STATE_CREATE": + self.list_altered = True + self.update_user(j["data"]["user"]) + # If someone joins any voice room grab it fresh from server + self.req_channel_details(self.current_voice) + un = j["data"]["user"]["username"] + if j["data"]["user"]["id"] == self.user["id"]: + self.find_user() + elif j["evt"] == "VOICE_STATE_DELETE": + self.list_altered = True + self.set_in_room(j["data"]["user"]["id"], False) + if j["data"]["user"]["id"] == self.user["id"]: + self.in_room = [] + self.sub_all_voice() + else: + un = j["data"]["user"]["username"] + elif j["evt"] == "SPEAKING_START": + self.list_altered = True + # It's only possible to get alerts for the room you're in + self.set_channel(j["data"]["channel_id"]) + self.userlist[j["data"]["user_id"]]["speaking"] = True + self.set_in_room(j["data"]["user_id"], True) + elif j["evt"] == "SPEAKING_STOP": + self.list_altered = True + # It's only possible to get alerts for the room you're in + self.set_channel(j["data"]["channel_id"]) + if j["data"]["user_id"] in self.userlist: + self.userlist[j["data"]["user_id"]]["speaking"] = False + self.set_in_room(j["data"]["user_id"], True) + elif j["evt"] == "VOICE_CHANNEL_SELECT": + self.set_channel(j["data"]["channel_id"]) + elif j["evt"] == "VOICE_CONNECTION_STATUS": + # VOICE_CONNECTED > CONNECTING > AWAITING_ENDPOINT > DISCONNECTED + self.last_connection = j["data"]["state"] + elif j["evt"] == "MESSAGE_CREATE": + if self.current_text == j["data"]["channel_id"]: + self.add_text(j["data"]["message"]) + elif j["evt"] == "MESSAGE_UPDATE": + if self.current_text == j["data"]["channel_id"]: + self.update_text(j["data"]["message"]) + elif j["evt"] == "MESSAGE_DELETE": + if self.current_text == j["data"]["channel_id"]: + self.delete_text(j["data"]["message"]) + else: + print(j) + return + elif j["cmd"] == "AUTHENTICATE": + if j["evt"] == "ERROR": + self.get_access_token_stage1() + return + else: + self.req_guilds() + self.user = j["data"]["user"] + print("ID is %s" % (self.user["id"])) + print("Logged in as %s" % (self.user["username"])) + self.authed = True + return + elif j["cmd"] == "GET_GUILDS": + for guild in j["data"]["guilds"]: + self.req_channels(guild["id"]) + self.guilds[guild["id"]] = guild + return + elif j["cmd"] == "GET_CHANNELS": + self.guilds[j['nonce']]["channels"] = j["data"]["channels"] + for channel in j["data"]["channels"]: + channel['guild_id'] = j['nonce'] + channel['guild_name'] = self.guilds[j['nonce']]["name"] + self.channels[channel["id"]] = channel + if channel["type"] == 2: + self.req_channel_details(channel["id"]) + self.check_guilds() + self.sub_all_voice_guild(j["nonce"]) + self.sub_all_text_guild(j["nonce"]) + return + elif j["cmd"] == "SUBSCRIBE": + return + elif j["cmd"] == "GET_CHANNEL": + if j["evt"] == "ERROR": + print("Could not get room") + return + for voice in j["data"]["voice_states"]: + if voice["user"]["id"] == self.user["id"]: + self.set_channel(j["data"]["id"], False) + if j["data"]["id"] == self.current_voice: + self.list_altered = True + self.in_room = [] + for voice in j["data"]["voice_states"]: + self.update_user(voice["user"]) + self.set_in_room(voice["user"]["id"], True) + if self.current_text == j["data"]["id"]: + self.text = [] + for message in j["data"]["messages"]: + self.add_text(message) + return + print(j) + + def check_guilds(self): + # Check if all of the guilds contain a channel + for guild in self.guilds.values(): + if "channels" not in guild: + return + # All guilds are filled! + self.on_connected() + + def on_connected(self): + for guild in self.guilds.values(): + channels = "" + for channel in guild["channels"]: + channels = channels + " " + channel["name"] + print("%s: %s" % (guild["name"], channels)) + self.sub_server() + self.find_user() + + def on_error(self, error): + print("ERROR : %s" % (error)) + + def on_close(self): + print("Connection closed") + self.ws = None + + def req_auth(self): + self.ws.send("{\"cmd\":\"AUTHENTICATE\",\"args\":{\"access_token\":\"%s\"},\"nonce\":\"deadbeef\"}" % ( + self.access_token)) + + def req_guilds(self): + self.ws.send("{\"cmd\":\"GET_GUILDS\",\"args\":{},\"nonce\":\"3333\"}") + + def req_channels(self, guild): + self.ws.send("{\"cmd\":\"GET_CHANNELS\",\"args\":{\"guild_id\":\"%s\"},\"nonce\":\"%s\"}" % ( + guild, guild)) + + def req_channel_details(self, channel): + self.ws.send("{\"cmd\":\"GET_CHANNEL\",\"args\":{\"channel_id\":\"%s\"},\"nonce\":\"%s\"}" % ( + channel, channel)) + + def find_user(self): + for channel in self.channels: + if self.channels[channel]["type"] == 2: + self.req_channel_details(channel) + + def sub_raw(self, cmd, channel, nonce): + self.ws.send("{\"cmd\":\"SUBSCRIBE\",\"args\":{%s},\"evt\":\"%s\",\"nonce\":\"%s\"}" % ( + channel, cmd, nonce)) + + def sub_server(self): + # Experimental + self.sub_raw("VOICE_CHANNEL_SELECT", "", "VOICE_CHANNEL_SELECT") + self.sub_raw("VOICE_CONNECTION_STATUS", "", "VOICE_CONNECTION_STATUS") + #sub_raw(ws,"ACTIVITY_JOIN", "","ACTIVITY_JOIN") + #sub_raw(ws,"ACTIVITY_JOIN_REQUEST", "","ACTIVITY_JOIN_REQUEST") + #sub_raw(ws,"ACTIVITY_SPECTATE", "", "ACTIVITY_SPECTATE") + # sub_raw(ws,"ACTIVITY_INVITE","","ACTIVITY_INVITE") + #sub_raw(ws,"GAME_JOIN", "", "GAME_JOIN") + #sub_raw(ws,"GAME_SPECTATE", "", "GAME_SPECTATE") + #sub_raw(ws,"VOICE_SETTINGS_UPDATE", "", "VOICE_SETTINGS_UPDATE") + #sub_raw(ws,"GUILD_STATUS", "\"guild_id\":\"147073008450666496\"", "GUILD_STATUS") + + def sub_channel(self, cmd, channel): + self.sub_raw(cmd, "\"channel_id\":\"%s\"" % (channel), channel) + + def sub_text_channel(self, channel): + self.sub_channel("MESSAGE_CREATE", channel) + self.sub_channel("MESSAGE_UPDATE", channel) + self.sub_channel("MESSAGE_DELETE", channel) + + def sub_voice_channel(self, channel): + self.sub_channel("VOICE_STATE_CREATE", channel) + self.sub_channel("VOICE_STATE_UPDATE", channel) + self.sub_channel("VOICE_STATE_DELETE", channel) + self.sub_channel("SPEAKING_START", channel) + self.sub_channel("SPEAKING_STOP", channel) + + def sub_all_voice_guild(self, gid): + for channel in self.guilds[gid]["channels"]: + if channel["type"] == 2: + self.sub_voice_channel(channel["id"]) + + def sub_all_text_guild(self, gid): + for channel in self.guilds[gid]["channels"]: + if channel["type"] == 0: + self.sub_text_channel(channel["id"]) + + def sub_all_voice(self): + for guild in self.guilds: + self.sub_all_voice_guild(guild) + + def sub_all_text(self): + for guild in self.guilds: + self.sub_all_text_guild(guild) + + def do_read(self): + # Ensure connection + if not self.ws: + self.connect() + if self.warn_connection: + print("Unable to connect to Discord client") + self.warn_connection = False + return True + # Recreate a list of users in current room + newlist = [] + for userid in self.in_room: + newlist.append(self.userlist[userid]) + self.voice_overlay.set_user_list(newlist, self.list_altered) + self.voice_overlay.set_connection(self.last_connection) + list_altered = False + # Update text list + if self.text_altered: + self.text_overlay.set_text_list(self.text, self.text_altered) + self.text_altered = False + # Update text channels + self.text_settings.set_channels(self.channels) + # Check for changed channel + if self.authed: + self.set_text_channel(self.text_settings.get_channel()) + + # Poll socket for new information + r, w, e = select.select((self.ws.sock,), (), (), 0) + while r: + try: + # Recieve & send to on_message + msg = self.ws.recv() + self.on_message(msg) + r, w, e = select.select((self.ws.sock,), (), (), 0) + except websocket._exceptions.WebSocketConnectionClosedException: + self.on_close() + return True + return True + + def connect(self): + if self.ws: + return + try: + self.ws = websocket.create_connection("ws://127.0.0.1:6463/?v=1&client_id=%s" % (self.oauth_token), + origin="https://streamkit.discord.com") + except Exception as e: + if self.error_connection: + print(e) + self.error_connection = False + pass diff --git a/discover_overlay/discover_overlay.py b/discover_overlay/discover_overlay.py index cbcfb8c..9334fcd 100755 --- a/discover_overlay/discover_overlay.py +++ b/discover_overlay/discover_overlay.py @@ -11,6 +11,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . import gi +gi.require_version("Gtk", "3.0") +gi.require_version('PangoCairo', '1.0') +gi.require_version('GdkPixbuf', '2.0') +from gi.repository.GdkPixbuf import Pixbuf +from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo import os import sys import json @@ -22,1845 +27,87 @@ import select import urllib import requests import websocket +from .voice_settings import VoiceSettingsWindow +from .text_settings import TextSettingsWindow +from .voice_overlay import VoiceOverlayWindow +from .text_overlay import TextOverlayWindow +from .discord_connector import DiscordConnector -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo -from gi.repository.GdkPixbuf import Pixbuf -from configparser import ConfigParser -try: - from xdg.BaseDirectory import xdg_config_home -except ModuleNotFoundError: - from xdg import XDG_CONFIG_HOME as xdg_config_home - - -access_token = "none" -# TODO Magic number -oauth_token = "207646673902501888" -access_delay = 0 - -guilds = {} -channels = {} -user = {} -userlist = {} -in_room= [] -current_voice = "0" -current_text = "0" -list_altered = False -text_altered = False -last_connection="" -text=[] -authed=False - - -def get_access_token_stage1(ws): - global oauth_token - ws.send("{\"cmd\":\"AUTHORIZE\",\"args\":{\"client_id\":\"%s\",\"scopes\":[\"rpc\",\"messages.read\"],\"prompt\":\"none\"},\"nonce\":\"deadbeef\"}" % (oauth_token)) - -def get_access_token_stage2(ws, code1): - global access_token, access_delay - time.sleep(access_delay) - access_delay+=1 - if access_delay > 5: - access_delay = 5 - url = "https://streamkit.discord.com/overlay/token" - myobj = {"code" : code1} - x = requests.post(url, json=myobj) - try: - j = json.loads(x.text) - except: - j = {} - if "access_token" in j: - access_token = j["access_token"] - req_auth(ws) - else: - sys.exit(1) - -def set_channel(ws,channel,need_req=True): - global current_voice, channels - if not channel: - current_voice="0" - return - if channel != current_voice: - cn = channels[channel]['name'] - print("Joined room: %s" % (cn)) - current_voice = channel - if need_req: - req_channel_details(ws, channel) - -def set_text_channel(ws,channel,need_req=True): - global current_text, channels - if not channel: - current_text="0" - return - if channel != current_text: - current_text = channel - print("Changing text room: %s" % (channel)) - if need_req: - req_channel_details(ws, channel) - -def set_in_room(userid, present): - global in_room - if present: - if userid not in in_room: - in_room.append(userid) - else: - if userid in in_room: - in_room.remove(userid) - -def add_text(message): - global text, text_altered - un = message["author"]["username"] - if "nick" in message and message['nick'] and len(message["nick"])>1: - un = message["nick"] - ac = "#ffffff" - if "author_color" in message: - ac = message["author_color"] - - text.append({'id':message["id"], - 'content' : get_message_from_message(message), - 'nick' : un, - 'nick_col' : ac, - }) - text_altered = True - -def update_text(message_in): - global text, text_altered - for idx in range(0, len(text)): - message = text[idx] - if message['id'] == message_in['id']: - new_message = {'id': message['id'], - 'content': get_message_from_message(message_in), - 'nick': message['nick'], - 'nick_col': message['nick_col']} - text[idx] = new_message - text_altered = True - return - -def delete_text(message_in): - global text, text_altered - for idx in range(0, len(text)): - message = text[idx] - if message['id'] == message_in['id']: - del text[idx] - #text[idx] = {"id":message["id"], "content":"-- redacted --", "nick":"-- redacted --","nick_col":"#ffffff"} - text_altered = True - return - -def get_message_from_message(message): - if len(message["content"])>0: - return message["content"] - elif len(message["embeds"]) == 1: - if "rawDescription" in message["embeds"][0]: - return message["embeds"][0]["rawDescription"] - if "author" in message["embeds"][0]: - return message["embeds"][0]["author"]["name"] - elif len(message["attachments"]) ==1: - # Need to care - return "-- attachment --" - return "" - - -def update_user(user): - global userlist - if user["id"] in userlist: - if not "mute" in user and "mute" in userlist[user["id"]]: - user["mute"] = userlist[user["id"]]["mute"] - if not "deaf" in user and "deaf" in userlist[user["id"]]: - user["deaf"] = userlist[user["id"]]["deaf"] - if not "speaking" in user and "speaking" in userlist[user["id"]]: - user["speaking"] = userlist[user["id"]]["speaking"] - userlist[user["id"]]=user - -def on_message(ws, message): - global guilds, user, access_delay, channels, userlist, current_voice, current_text,list_altered,in_room, last_connection, text,authed - j = json.loads(message) - if j["cmd"] == "AUTHORIZE": - get_access_token_stage2(ws,j["data"]["code"]) - return - elif j["cmd"] == "DISPATCH": - if j["evt"] == "READY": - req_auth(ws) - elif j["evt"] == "VOICE_STATE_UPDATE": - list_altered=True - thisuser = j["data"]["user"] - un=j["data"]["user"]["username"] - mute = j["data"]["voice_state"]["mute"] or j["data"]["voice_state"]["self_mute"] - deaf = j["data"]["voice_state"]["deaf"] or j["data"]["voice_state"]["self_deaf"] - thisuser["mute"]=mute - thisuser["deaf"]=deaf - update_user(thisuser) - elif j["evt"] == "VOICE_STATE_CREATE": - list_altered=True - update_user(j["data"]["user"]) - # If someone joins any voice room grab it fresh from server - req_channel_details(ws,current_voice) - un=j["data"]["user"]["username"] - if j["data"]["user"]["id"] == user["id"]: - find_user(ws) - elif j["evt"] == "VOICE_STATE_DELETE": - list_altered=True - set_in_room(j["data"]["user"]["id"], False) - if j["data"]["user"]["id"] == user["id"]: - in_room=[] - sub_all_voice(ws) - else: - un = j["data"]["user"]["username"] - elif j["evt"] == "SPEAKING_START": - list_altered=True - # It's only possible to get alerts for the room you're in - set_channel(ws,j["data"]["channel_id"]) - userlist[j["data"]["user_id"]]["speaking"] = True - set_in_room(j["data"]["user_id"],True) - elif j["evt"] == "SPEAKING_STOP": - list_altered=True - # It's only possible to get alerts for the room you're in - set_channel(ws,j["data"]["channel_id"]) - if j["data"]["user_id"] in userlist: - userlist[j["data"]["user_id"]]["speaking"] = False - set_in_room(j["data"]["user_id"],True) - elif j["evt"] == "VOICE_CHANNEL_SELECT": - set_channel(ws, j["data"]["channel_id"]) - elif j["evt"] == "VOICE_CONNECTION_STATUS": - # VOICE_CONNECTED > CONNECTING > AWAITING_ENDPOINT > DISCONNECTED - last_connection = j["data"]["state"] - elif j["evt"] == "MESSAGE_CREATE": - if current_text == j["data"]["channel_id"]: - add_text(j["data"]["message"]) - elif j["evt"] == "MESSAGE_UPDATE": - if current_text == j["data"]["channel_id"]: - update_text(j["data"]["message"]) - elif j["evt"] == "MESSAGE_DELETE": - if current_text == j["data"]["channel_id"]: - delete_text(j["data"]["message"]) - else: - print(j) - return - elif j["cmd"] == "AUTHENTICATE": - if j["evt"] == "ERROR": - get_access_token_stage1(ws) - return - else: - req_guilds(ws) - user=j["data"]["user"] - print("ID is %s" %(user["id"])) - print("Logged in as %s" % (user["username"])) - authed=True - return - elif j["cmd"] == "GET_GUILDS": - for guild in j["data"]["guilds"]: - req_channels(ws, guild["id"]) - guilds[guild["id"]]=guild - return - elif j["cmd"] == "GET_CHANNELS": - guilds[j['nonce']]["channels"] = j["data"]["channels"] - for channel in j["data"]["channels"]: - channel['guild_id'] = j['nonce'] - channel['guild_name'] = guilds[j['nonce']]["name"] - channels[channel["id"]] = channel - if channel["type"] == 2: - req_channel_details(ws, channel["id"]) - check_guilds() - sub_all_voice_guild(ws,j["nonce"]) - sub_all_text_guild(ws,j["nonce"]) - return - elif j["cmd"] == "SUBSCRIBE": - return - elif j["cmd"] == "GET_CHANNEL": - if j["evt"] == "ERROR": - print("Could not get room") - return - for voice in j["data"]["voice_states"]: - if voice["user"]["id"] == user["id"]: - set_channel(ws, j["data"]["id"], False) - if j["data"]["id"] == current_voice: - list_altered=True - in_room=[] - for voice in j["data"]["voice_states"]: - update_user(voice["user"]) - set_in_room(voice["user"]["id"], True) - if current_text == j["data"]["id"]: - text=[] - for message in j["data"]["messages"]: - add_text(message) - return - print(j) - -def check_guilds(): - global guilds - # Check if all of the guilds contain a channel - for guild in guilds.values(): - if "channels" not in guild: - return - # All guilds are filled! - on_connected() - -def on_connected(): - global guilds, ws - for guild in guilds.values(): - channels = "" - for channel in guild["channels"]: - channels = channels+" "+channel["name"] - print("%s: %s" % (guild["name"], channels)) - sub_server(ws) - find_user(ws) - -def on_error(ws, error): - print("ERROR : %s" % (error)) - -def on_close(): - global ws - print("Connection closed") - ws = None - -def req_auth(ws): - ws.send("{\"cmd\":\"AUTHENTICATE\",\"args\":{\"access_token\":\"%s\"},\"nonce\":\"deadbeef\"}" % (access_token)) - -def req_guilds(ws): - ws.send("{\"cmd\":\"GET_GUILDS\",\"args\":{},\"nonce\":\"3333\"}") - -def req_channels(ws, guild): - ws.send("{\"cmd\":\"GET_CHANNELS\",\"args\":{\"guild_id\":\"%s\"},\"nonce\":\"%s\"}" % (guild, guild)) - -def req_channel_details(ws, channel): - ws.send("{\"cmd\":\"GET_CHANNEL\",\"args\":{\"channel_id\":\"%s\"},\"nonce\":\"%s\"}" % (channel, channel)) - -def find_user(ws): - global channels - for channel in channels: - if channels[channel]["type"]==2: - req_channel_details(ws, channel) - -def sub_raw(ws, cmd, channel, nonce): - ws.send("{\"cmd\":\"SUBSCRIBE\",\"args\":{%s},\"evt\":\"%s\",\"nonce\":\"%s\"}" % (channel, cmd, nonce)) - -def sub_server(ws): - # Experimental - sub_raw(ws,"VOICE_CHANNEL_SELECT", "", "VOICE_CHANNEL_SELECT") - sub_raw(ws,"VOICE_CONNECTION_STATUS", "", "VOICE_CONNECTION_STATUS") - #sub_raw(ws,"ACTIVITY_JOIN", "","ACTIVITY_JOIN") - #sub_raw(ws,"ACTIVITY_JOIN_REQUEST", "","ACTIVITY_JOIN_REQUEST") - #sub_raw(ws,"ACTIVITY_SPECTATE", "", "ACTIVITY_SPECTATE") - #sub_raw(ws,"ACTIVITY_INVITE","","ACTIVITY_INVITE") - #sub_raw(ws,"GAME_JOIN", "", "GAME_JOIN") - #sub_raw(ws,"GAME_SPECTATE", "", "GAME_SPECTATE") - #sub_raw(ws,"VOICE_SETTINGS_UPDATE", "", "VOICE_SETTINGS_UPDATE") - #sub_raw(ws,"GUILD_STATUS", "\"guild_id\":\"147073008450666496\"", "GUILD_STATUS") - -def sub_channel(ws, cmd, channel): - sub_raw(ws,cmd,"\"channel_id\":\"%s\""%(channel),channel) - -def sub_text_channel(ws, channel): - sub_channel(ws,"MESSAGE_CREATE", channel) - sub_channel(ws,"MESSAGE_UPDATE", channel) - sub_channel(ws,"MESSAGE_DELETE", channel) - -def sub_voice_channel(ws, channel): - sub_channel(ws,"VOICE_STATE_CREATE", channel) - sub_channel(ws,"VOICE_STATE_UPDATE", channel) - sub_channel(ws,"VOICE_STATE_DELETE", channel) - sub_channel(ws,"SPEAKING_START", channel) - sub_channel(ws,"SPEAKING_STOP", channel) - -def sub_all_voice_guild(ws, gid): - global guilds - for channel in guilds[gid]["channels"]: - if channel["type"]==2: - sub_voice_channel(ws, channel["id"]) - -def sub_all_text_guild(ws, gid): - global guilds - for channel in guilds[gid]["channels"]: - if channel["type"]==0: - sub_text_channel(ws, channel["id"]) - -def sub_all_voice(ws): - for guild in guilds: - sub_all_voice_guild(ws, guild) - -def sub_all_text(ws): - for guild in guilds: - sub_all_text_guild(ws, guild) - -def do_read(): - global ws, twin, win, userlist, list_altered, warn_connection, text_altered, tsettings, channels,authed - # Ensure connection - if not ws: - connect() - if warn_connection: - print("Unable to connect to Discord client") - warn_connection=False - return True - # Recreate a list of users in current room - newlist = [] - for userid in in_room: - newlist.append(userlist[userid]) - win.set_user_list(newlist, list_altered) - list_altered=False - win.set_connection(last_connection) - # Update text list - if text_altered: - twin.set_text_list(text, text_altered) - text_altered = False - # Update text channels - tsettings.set_channels(channels) - # Check for changed channel - if authed: - set_text_channel(ws,tsettings.get_channel()) - - - # Poll socket for new information - r,w,e=select.select((ws.sock,),(),(),0) - while r: - try: - # Recieve & send to on_message - a = ws.recv() - on_message(ws, a) - r,w,e=select.select((ws.sock,),(),(),0) - except websocket._exceptions.WebSocketConnectionClosedException: - on_close() - return True - return True - -class DraggableWindow(Gtk.Window): - def __init__(self, x=0, y=0, w=300, h=300, message="Message"): - Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP) - if w < 100: - w = 100 - if h < 100: - h = 100 - self.x = x - self.y = y - self.w = w - self.h = h - self.message = message - self.set_size_request(50,50) - - self.connect('draw', self.draw) - self.connect('motion-notify-event', self.drag) - self.connect('button-press-event', self.button_press) - self.connect('button-release-event', self.button_release) - - self.compositing = False - # Set RGBA - screen = self.get_screen() - visual = screen.get_rgba_visual() - if visual: - # Set the visual even if we can't use it right now - self.set_visual(visual) - if screen.is_composited(): - self.compositing=True - - self.set_app_paintable(True) - self.monitor = 0 - - self.drag_type = None - self.drag_x = 0 - self.drag_y = 0 - self.force_location() - self.show_all() - - def force_location(self): - self.set_decorated(False) - self.set_keep_above(True) - display = Gdk.Display.get_default() - monitor = display.get_monitor(self.monitor) - geometry = monitor.get_geometry() - scale_factor = monitor.get_scale_factor() - w = scale_factor * geometry.width - h = scale_factor * geometry.height - x = geometry.x - y = geometry.y - #self.resize(400, h) - self.move(self.x,self.y) - self.resize(self.w,self.h) - - def drag(self,w,event): - if event.state & Gdk.ModifierType.BUTTON1_MASK: - if self.drag_type == 1: - # Center is move - self.x = event.x_root - self.drag_x - self.y = event.y_root - self.drag_y - self.force_location() - elif self.drag_type == 2: - # Right edge - self.w += event.x - self.drag_x - self.drag_x = event.x - self.force_location() - elif self.drag_type == 3: - # Bottom edge - self.h += event.y - self.drag_y - self.drag_y = event.y - self.force_location() - else: - # Bottom Right - self.w += event.x - self.drag_x - self.h += event.y - self.drag_y - self.drag_x = event.x - self.drag_y = event.y - self.force_location() - - def button_press(self, w, event): - (w, h) = self.get_size() - if not self.drag_type: - self.drag_type = 1 - # Where in the window did we press? - if event.y> h-32: - self.drag_type+=2 - if event.x> w-32: - self.drag_type+=1 - self.drag_x = event.x - self.drag_y = event.y - - def button_release(self, w, event): - self.drag_type = None - - def draw(self,widget,context): - context.set_source_rgba(1.0,1.0,0.0,0.7) - # Don't layer drawing over each other, always replace - context.set_operator(cairo.OPERATOR_SOURCE) - context.paint() - context.set_operator(cairo.OPERATOR_OVER) - # Get size of window - (sw,sh) = self.get_size() - - # Draw text - context.set_source_rgba(0.0,0.0,0.0,1.0) - xb, yb, w, h, dx, dy = context.text_extents(self.message) - context.move_to(sw/2-w/2,sh/2-h/2) - context.show_text(self.message) - - # Draw resizing edges - context.set_source_rgba(0.0,0.0,1.0,0.5) - context.rectangle(sw-32,0,32,sh) - context.fill() - - context.rectangle(0,sh-32,sw,32) - context.fill() - -class SettingsWindow(Gtk.Window): - def init_config(self): - self.configDir = os.path.join(xdg_config_home, "discover") - os.makedirs(self.configDir, exist_ok=True) - self.configFile = os.path.join(self.configDir, "discover.ini") - self.read_config() - - def close_window(self,a=None,b=None): - if self.placement_window: - (x,y) = self.placement_window.get_position() - (w,h) = self.placement_window.get_size() - self.floating_x = x - self.floating_y = y - self.floating_w = w - self.floating_h = h - self.overlay.set_floating(True, x, y, w, h) - self.save_config() - self.placement_window.close() - self.placement_window=None - self.hide() - return True - - def get_monitor_index(self,name): - display = Gdk.Display.get_default() - for i in range(0, display.get_n_monitors()): - if display.get_monitor(i).get_model() == name: - return i - print("Could not find monitor : %s" % (name)) - return 0 - - def present(self): - self.show_all() - -class TextSettingsWindow(SettingsWindow): - def __init__(self, overlay): - Gtk.Window.__init__(self) - self.overlay = overlay - self.set_size_request(400,200) - self.connect("destroy", self.close_window) - self.connect("delete-event", self.close_window) - self.placement_window = None - self.init_config() - self.list_channels_keys = [] - self.ignore_channel_change = False - self.create_gui() - - def present(self): - self.show_all() - if not self.floating: - self.align_x_widget.show() - self.align_y_widget.show() - self.align_monitor_widget.show() - self.align_placement_widget.hide() - else: - self.align_x_widget.hide() - self.align_y_widget.hide() - self.align_monitor_widget.hide() - self.align_placement_widget.show() - model = monitor_store = Gtk.ListStore(str, bool) - #for c in self.list_channels_keys: - # print(self.list_channels[c]) - # model.append([self.list_channels[c]["name"]]) - self.channel_lookup=[] - for guild in self.guild_list(): - guild_id, guild_name = guild - self.channel_lookup.append('0') - model.append([guild_name, False]) - for c in self.list_channels_keys: - chan = self.list_channels[c] - if chan['guild_id'] == guild_id: - model.append([chan["name"], True]) - self.channel_lookup.append(c) - - self.channel_widget.set_model(model) - self.channel_model = model - - idx = 0 - for c in self.channel_lookup: - if c == self.channel: - self.ignore_channel_change = True - self.channel_widget.set_active(idx) - self.ignore_channel_change = False - break - idx+=1 - - def guild_list(self): - guilds = [] - done = [] - for channel in self.list_channels.values(): - if not channel["guild_id"] in done: - done.append(channel["guild_id"]) - guilds.append([channel["guild_id"],channel["guild_name"]]) - return guilds - - def set_channels(self, in_list): - self.list_channels = in_list - self.list_channels_keys = [] - for key in in_list.keys(): - if in_list[key]["type"]==0: - self.list_channels_keys.append(key) - self.list_channels_keys.sort() - - def read_config(self): - config = ConfigParser(interpolation=None) - config.read(self.configFile) - self.enabled = config.getboolean("text", "enabled", fallback = False) - self.align_x = config.getboolean("text", "rightalign", fallback=True) - self.align_y = config.getint("text", "topalign", fallback=2) - self.monitor = config.get("text", "monitor", fallback="None") - self.floating = config.getboolean("text", "floating", fallback=True) - self.floating_x = config.getint("text","floating_x", fallback=0) - self.floating_y = config.getint("text","floating_y", fallback=0) - self.floating_w = config.getint("text","floating_w", fallback=400) - self.floating_h = config.getint("text","floating_h", fallback=400) - self.channel = config.get("text","channel", fallback="0") - self.font = config.get("text","font",fallback=None) - self.bg_col = json.loads(config.get("text","bg_col",fallback="[0.0,0.0,0.0,0.5]")) - self.fg_col = json.loads(config.get("text","fg_col",fallback="[1.0,1.0,1.0,1.0]")) - - print("Loading saved channel %s" % (self.channel)) - - - # Pass all of our config over to the overlay - self.overlay.set_enabled(self.enabled) - self.overlay.set_align_x(self.align_x) - self.overlay.set_align_y(self.align_y) - self.overlay.set_monitor(self.get_monitor_index(self.monitor)) - self.overlay.set_floating(self.floating, self.floating_x, self.floating_y, self.floating_w, self.floating_h) - self.overlay.set_bg(self.bg_col) - self.overlay.set_fg(self.fg_col) - - def save_config(self): - config = ConfigParser(interpolation=None) - config.read(self.configFile) - if not config.has_section("text"): - config.add_section("text") - - config.set("text","rightalign", "%d" % (int(self.align_x))) - config.set("text","topalign", "%d" % (self.align_y)) - config.set("text","monitor",self.monitor) - config.set("text","enabled","%d"%(int(self.enabled))) - config.set("text","floating","%s"%(int(self.floating))) - config.set("text","floating_x","%s"%(self.floating_x)) - config.set("text","floating_y","%s"%(self.floating_y)) - config.set("text","floating_w","%s"%(self.floating_w)) - config.set("text","floating_h","%s"%(self.floating_h)) - config.set("text","channel",self.channel) - config.set("text","bg_col",json.dumps(self.bg_col)) - config.set("text","fg_col",json.dumps(self.fg_col)) - - if self.font: - config.set("text","font",self.font) - - with open(self.configFile, 'w') as file: - config.write(file) +class Discover: + def __init__(self): + pass def create_gui(self): - box = Gtk.Grid() - - # Enabled - enabled_label = Gtk.Label.new("Enable") - enabled = Gtk.CheckButton.new() - enabled.set_active(self.enabled) - enabled.connect("toggled", self.change_enabled) - - # Font chooser - font_label = Gtk.Label.new("Font") - font = Gtk.FontButton() - if self.font: - font.set_font(self.font) - font.connect("font-set", self.change_font) - - # Colours - bg_col_label = Gtk.Label.new("Background colour") - bg_col = Gtk.ColorButton.new_with_rgba(Gdk.RGBA(self.bg_col[0],self.bg_col[1],self.bg_col[2],self.bg_col[3])) - fg_col_label = Gtk.Label.new("Text colour") - fg_col = Gtk.ColorButton.new_with_rgba(Gdk.RGBA(self.fg_col[0],self.fg_col[1],self.fg_col[2],self.fg_col[3])) - bg_col.set_use_alpha(True) - fg_col.set_use_alpha(True) - bg_col.connect("color-set", self.change_bg) - fg_col.connect("color-set", self.change_fg) - - # Monitor & Alignment - align_label = Gtk.Label.new("Overlay Location") - - align_type_box = Gtk.HBox() - align_type_edge = Gtk.RadioButton.new_with_label(None, "Anchor to edge") - align_type_floating = Gtk.RadioButton.new_with_label_from_widget(align_type_edge,"Floating") - if self.floating: - align_type_floating.set_active(True) - align_type_box.add(align_type_edge) - align_type_box.add(align_type_floating) - - monitor_store = Gtk.ListStore(str) - display = Gdk.Display.get_default() - for i in range(0, display.get_n_monitors()): - monitor_store.append([display.get_monitor(i).get_model()]) - monitor = Gtk.ComboBox.new_with_model(monitor_store) - monitor.set_active(self.get_monitor_index(self.monitor)) - monitor.connect("changed", self.change_monitor) - rt = Gtk.CellRendererText() - monitor.pack_start(rt,True) - monitor.add_attribute(rt, "text", 0) - - align_x_store = Gtk.ListStore(str) - align_x_store.append(["Left"]) - align_x_store.append(["Right"]) - align_x = Gtk.ComboBox.new_with_model(align_x_store) - align_x.set_active(True if self.align_x else False) - align_x.connect("changed", self.change_align_x) - rt = Gtk.CellRendererText() - align_x.pack_start(rt, True) - align_x.add_attribute(rt,"text",0) - - align_y_store = Gtk.ListStore(str) - align_y_store.append(["Top"]) - align_y_store.append(["Middle"]) - align_y_store.append(["Bottom"]) - align_y = Gtk.ComboBox.new_with_model(align_y_store) - align_y.set_active(self.align_y) - align_y.connect("changed", self.change_align_y) - rt = Gtk.CellRendererText() - align_y.pack_start(rt, True) - align_y.add_attribute(rt,"text",0) - - align_placement_button = Gtk.Button.new_with_label("Place Window") - - align_type_edge.connect("toggled", self.change_align_type_edge) - align_type_floating.connect("toggled", self.change_align_type_floating) - align_placement_button.connect("pressed", self.change_placement) - - channel_label = Gtk.Label.new("Channel") - channel = Gtk.ComboBox.new() - - channel.connect("changed", self.change_channel) - rt = Gtk.CellRendererText() - #channel.set_row_separator_func(lambda model, path: model[path][1]) - channel.pack_start(rt, True) - channel.add_attribute(rt,"text",0) - channel.add_attribute(rt,'sensitive',1) - - self.align_x_widget= align_x - self.align_y_widget= align_y - self.align_monitor_widget= monitor - self.align_placement_widget = align_placement_button - self.channel_widget = channel - - box.attach(enabled_label,0,0,1,1) - box.attach(enabled,1,0,1,1) - box.attach(channel_label,0,1,1,1) - box.attach(channel,1,1,1,1) - - box.attach(font_label,0,2,1,1) - box.attach(font,1,2,1,1) - box.attach(fg_col_label,0,3,1,1) - box.attach(fg_col,1,3,1,1) - box.attach(bg_col_label,0,4,1,1) - box.attach(bg_col,1,4,1,1) - box.attach(align_label,0,5,1,5) - box.attach(align_type_box,1,5,1,1) - box.attach(monitor,1,6,1,1) - box.attach(align_x,1,7,1,1) - box.attach(align_y,1,8,1,1) - box.attach(align_placement_button,1,9,1,1) - - self.add(box) - - def change_font(self, button): - font = button.get_font() - desc = Pango.FontDescription.from_string(font) - s = desc.get_size() - if not desc.get_size_is_absolute(): - s = s / Pango.SCALE - self.overlay.set_font(desc.get_family(), s) - - self.font = desc.to_string() - self.save_config() - - def change_channel(self, button): - if self.ignore_channel_change: - return - c = self.channel_lookup[button.get_active()] - self.channel = c - self.save_config() - - def change_placement(self, button): - if self.placement_window: - (x,y) = self.placement_window.get_position() - (w,h) = self.placement_window.get_size() - self.floating_x = x - self.floating_y = y - self.floating_w = w - self.floating_h = h - self.overlay.set_floating(True, x, y, w, h) - self.save_config() - button.set_label("Place Window") - - self.placement_window.close() - self.placement_window=None - else: - self.placement_window = DraggableWindow(x = self.floating_x,y=self.floating_y,w=self.floating_w, h=self.floating_h, message="Place & resize this window then press Save!") - 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() - mon = display.get_monitor(button.get_active()) - m_s = mon.get_model() - self.overlay.set_monitor(button.get_active()) - - 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 get_channel(self): - return self.channel - - - def change_bg(self, button): - c= button.get_rgba() - c = [c.red, c.green, c.blue, c.alpha] - self.overlay.set_bg(c) - - self.bg_col = c - self.save_config() - - def change_fg(self, button): - c = button.get_rgba() - c = [c.red, c.green, c.blue, c.alpha] - self.overlay.set_fg(c) - - self.fg_col = c - self.save_config() - -class VoiceSettingsWindow(SettingsWindow): - def __init__(self, overlay): - Gtk.Window.__init__(self) - self.overlay = overlay - self.set_size_request(400,200) - self.connect("destroy", self.close_window) - self.connect("delete-event", self.close_window) - self.placement_window = None - self.init_config() - - self.create_gui() - - def present(self): - self.show_all() - if not self.floating: - self.align_x_widget.show() - self.align_y_widget.show() - self.align_monitor_widget.show() - self.align_placement_widget.hide() - else: - self.align_x_widget.hide() - self.align_y_widget.hide() - self.align_monitor_widget.hide() - self.align_placement_widget.show() - - def read_config(self): - config = ConfigParser(interpolation=None) - config.read(self.configFile) - self.align_x = config.getboolean("main", "rightalign", fallback=True) - self.align_y = config.getint("main", "topalign", fallback=1) - self.bg_col = json.loads(config.get("main","bg_col",fallback="[0.0,0.0,0.0,0.5]")) - self.fg_col = json.loads(config.get("main","fg_col",fallback="[1.0,1.0,1.0,1.0]")) - self.tk_col = json.loads(config.get("main","tk_col",fallback="[0.0,0.7,0.0,1.0]")) - self.mt_col = json.loads(config.get("main","mt_col",fallback="[0.6,0.0,0.0,1.0]")) - self.avatar_size = config.getint("main","avatar_size", fallback=48) - self.icon_spacing = config.getint("main","icon_spacing", fallback=8) - self.text_padding = config.getint("main","text_padding", fallback=6) - self.font = config.get("main","font",fallback=None) - self.square_avatar = config.getboolean("main","square_avatar", fallback=False) - self.monitor = config.get("main", "monitor", fallback="None") - self.edge_padding = config.getint("main","edge_padding", fallback=0) - self.floating = config.getboolean("main", "floating", fallback=False) - self.floating_x = config.getint("main","floating_x", fallback=0) - self.floating_y = config.getint("main","floating_y", fallback=0) - self.floating_w = config.getint("main","floating_w", fallback=400) - self.floating_h = config.getint("main","floating_h", fallback=400) - - # Pass all of our config over to the overlay - self.overlay.set_align_x(self.align_x) - self.overlay.set_align_y(self.align_y) - self.overlay.set_bg(self.bg_col) - self.overlay.set_fg(self.fg_col) - self.overlay.set_tk(self.tk_col) - self.overlay.set_mt(self.mt_col) - self.overlay.set_avatar_size(self.avatar_size) - self.overlay.set_icon_spacing(self.icon_spacing) - self.overlay.set_text_padding(self.text_padding) - self.overlay.set_square_avatar(self.square_avatar) - self.overlay.set_monitor(self.get_monitor_index(self.monitor)) - self.overlay.set_edge_padding(self.edge_padding) - - self.overlay.set_floating(self.floating, self.floating_x, self.floating_y, self.floating_w, self.floating_h) - - - if self.font: - desc = Pango.FontDescription.from_string(self.font) - s = desc.get_size() - if not desc.get_size_is_absolute(): - s = s / Pango.SCALE - self.overlay.set_font(desc.get_family(), s) - - - def save_config(self): - config = ConfigParser(interpolation=None) - config.read(self.configFile) - if not config.has_section("main"): - config.add_section("main") - - config.set("main","rightalign", "%d" % (int(self.align_x))) - config.set("main","topalign", "%d" % (self.align_y)) - config.set("main","bg_col",json.dumps(self.bg_col)) - config.set("main","fg_col",json.dumps(self.fg_col)) - config.set("main","tk_col",json.dumps(self.tk_col)) - config.set("main","mt_col",json.dumps(self.mt_col)) - config.set("main","avatar_size", "%d" % (self.avatar_size)) - config.set("main","icon_spacing", "%d" % (self.icon_spacing)) - config.set("main","text_padding", "%d" % (self.text_padding)) - if self.font: - config.set("main","font",self.font) - config.set("main","square_avatar","%d"%(int(self.square_avatar))) - config.set("main","monitor",self.monitor) - config.set("main","edge_padding","%d"%(self.edge_padding)) - config.set("main","floating","%s"%(int(self.floating))) - config.set("main","floating_x","%s"%(self.floating_x)) - config.set("main","floating_y","%s"%(self.floating_y)) - config.set("main","floating_w","%s"%(self.floating_w)) - config.set("main","floating_h","%s"%(self.floating_h)) - - with open(self.configFile, 'w') as file: - config.write(file) - - def create_gui(self): - box = Gtk.Grid() - - # Font chooser - font_label = Gtk.Label.new("Font") - font = Gtk.FontButton() - if self.font: - font.set_font(self.font) - font.connect("font-set", self.change_font) - - # Colours - bg_col_label = Gtk.Label.new("Background colour") - bg_col = Gtk.ColorButton.new_with_rgba(Gdk.RGBA(self.bg_col[0],self.bg_col[1],self.bg_col[2],self.bg_col[3])) - fg_col_label = Gtk.Label.new("Text colour") - fg_col = Gtk.ColorButton.new_with_rgba(Gdk.RGBA(self.fg_col[0],self.fg_col[1],self.fg_col[2],self.fg_col[3])) - tk_col_label = Gtk.Label.new("Talk colour") - tk_col = Gtk.ColorButton.new_with_rgba(Gdk.RGBA(self.tk_col[0],self.tk_col[1],self.tk_col[2],self.tk_col[3])) - mt_col_label = Gtk.Label.new("Mute colour") - mt_col = Gtk.ColorButton.new_with_rgba(Gdk.RGBA(self.mt_col[0],self.mt_col[1],self.mt_col[2],self.mt_col[3])) - bg_col.set_use_alpha(True) - fg_col.set_use_alpha(True) - tk_col.set_use_alpha(True) - mt_col.set_use_alpha(True) - bg_col.connect("color-set", self.change_bg) - fg_col.connect("color-set", self.change_fg) - tk_col.connect("color-set", self.change_tk) - mt_col.connect("color-set", self.change_mt) - - # Avatar size - avatar_size_label = Gtk.Label.new("Avatar size") - avatar_adjustment = Gtk.Adjustment.new(self.avatar_size,8,128,1,1,8) - avatar_size = Gtk.SpinButton.new(avatar_adjustment,0,0) - avatar_size.connect("value-changed", self.change_avatar_size) - - # Monitor & Alignment - align_label = Gtk.Label.new("Overlay Location") - - align_type_box = Gtk.HBox() - align_type_edge = Gtk.RadioButton.new_with_label(None, "Anchor to edge") - align_type_floating = Gtk.RadioButton.new_with_label_from_widget(align_type_edge, "Floating") - if self.floating: - align_type_floating.set_active(True) - align_type_box.add(align_type_edge) - align_type_box.add(align_type_floating) - - monitor_store = Gtk.ListStore(str) - display = Gdk.Display.get_default() - for i in range(0, display.get_n_monitors()): - monitor_store.append([display.get_monitor(i).get_model()]) - monitor = Gtk.ComboBox.new_with_model(monitor_store) - monitor.set_active(self.get_monitor_index(self.monitor)) - monitor.connect("changed", self.change_monitor) - rt = Gtk.CellRendererText() - monitor.pack_start(rt,True) - monitor.add_attribute(rt, "text", 0) - - align_x_store = Gtk.ListStore(str) - align_x_store.append(["Left"]) - align_x_store.append(["Right"]) - align_x = Gtk.ComboBox.new_with_model(align_x_store) - align_x.set_active(True if self.align_x else False) - align_x.connect("changed", self.change_align_x) - rt = Gtk.CellRendererText() - align_x.pack_start(rt, True) - align_x.add_attribute(rt,"text",0) - - align_y_store = Gtk.ListStore(str) - align_y_store.append(["Top"]) - align_y_store.append(["Middle"]) - align_y_store.append(["Bottom"]) - align_y = Gtk.ComboBox.new_with_model(align_y_store) - align_y.set_active(self.align_y) - align_y.connect("changed", self.change_align_y) - rt = Gtk.CellRendererText() - align_y.pack_start(rt, True) - align_y.add_attribute(rt,"text",0) - - align_placement_button = Gtk.Button.new_with_label("Place Window") - - align_type_edge.connect("toggled", self.change_align_type_edge) - align_type_floating.connect("toggled", self.change_align_type_floating) - align_placement_button.connect("pressed", self.change_placement) - - self.align_x_widget = align_x - self.align_y_widget = align_y - self.align_monitor_widget= monitor - self.align_placement_widget = align_placement_button - - # Icon spacing - icon_spacing_label = Gtk.Label.new("Icon Spacing") - icon_spacing_adjustment = Gtk.Adjustment.new(self.icon_spacing,0,64,1,1,0) - icon_spacing = Gtk.SpinButton.new(icon_spacing_adjustment,0,0) - icon_spacing.connect("value-changed", self.change_icon_spacing) - - # Text padding - text_padding_label = Gtk.Label.new("Text Padding") - text_padding_adjustment = Gtk.Adjustment.new(self.text_padding,0,64,1,1,0) - text_padding = Gtk.SpinButton.new(text_padding_adjustment,0,0) - text_padding.connect("value-changed", self.change_text_padding) - - # Edge padding - edge_padding_label = Gtk.Label.new("Edge Padding") - edge_padding_adjustment = Gtk.Adjustment.new(self.edge_padding,0,1000,1,1,0) - edge_padding = Gtk.SpinButton.new(edge_padding_adjustment,0,0) - edge_padding.connect("value-changed", self.change_edge_padding) - - # Avatar shape - square_avatar_label = Gtk.Label.new("Square Avatar") - square_avatar = Gtk.CheckButton.new() - square_avatar.set_active(self.square_avatar) - square_avatar.connect("toggled", self.change_square_avatar) - - box.attach(font_label,0,0,1,1) - box.attach(font,1,0,1,1) - box.attach(bg_col_label,0,1,1,1) - box.attach(bg_col,1,1,1,1) - box.attach(fg_col_label,0,2,1,1) - box.attach(fg_col,1,2,1,1) - box.attach(tk_col_label,0,3,1,1) - box.attach(tk_col,1,3,1,1) - box.attach(mt_col_label,0,4,1,1) - box.attach(mt_col,1,4,1,1) - box.attach(avatar_size_label,0,5,1,1) - box.attach(avatar_size,1,5,1,1) - box.attach(align_label,0,6,1,5) - box.attach(align_type_box,1,6,1,1) - box.attach(monitor,1,7,1,1) - box.attach(align_x,1,8,1,1) - box.attach(align_y,1,9,1,1) - box.attach(align_placement_button,1,10,1,1) - box.attach(icon_spacing_label,0,11,1,1) - box.attach(icon_spacing,1,11,1,1) - box.attach(text_padding_label,0,12,1,1) - box.attach(text_padding,1,12,1,1) - box.attach(edge_padding_label,0,13,1,1) - box.attach(edge_padding,1,13,1,1) - box.attach(square_avatar_label,0,14,1,1) - box.attach(square_avatar,1,14,1,1) - - self.add(box) - - pass - - def change_placement(self, button): - if self.placement_window: - (x,y) = self.placement_window.get_position() - (w,h) = self.placement_window.get_size() - self.floating_x = x - self.floating_y = y - self.floating_w = w - self.floating_h = h - self.overlay.set_floating(True, x, y, w, h) - self.save_config - button.set_label("Place Window") - - self.placement_window.close() - self.placement_window=None - else: - self.placement_window = DraggableWindow(x = self.floating_x,y=self.floating_y,w=self.floating_w, h=self.floating_h, message="Place & resize this window then press Save!") - 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 = button.get_font() - desc = Pango.FontDescription.from_string(font) - s = desc.get_size() - if not desc.get_size_is_absolute(): - s = s / Pango.SCALE - self.overlay.set_font(desc.get_family(), s) - - self.font = desc.to_string() - self.save_config() - - def change_bg(self, button): - c= button.get_rgba() - c = [c.red, c.green, c.blue, c.alpha] - self.overlay.set_bg(c) - - self.bg_col = c - self.save_config() - - def change_fg(self, button): - c = button.get_rgba() - c = [c.red, c.green, c.blue, c.alpha] - self.overlay.set_fg(c) - - self.fg_col = c - self.save_config() - - def change_tk(self, button): - c = button.get_rgba() - c = [c.red, c.green, c.blue, c.alpha] - self.overlay.set_tk(c) - - self.tk_col = c - self.save_config() - - def change_mt(self, button): - c = button.get_rgba() - c = [c.red, c.green, c.blue, c.alpha] - self.overlay.set_mt(c) - - self.mt_col = c - self.save_config() - - def change_avatar_size(self, button): - 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() - mon = display.get_monitor(button.get_active()) - m_s = mon.get_model() - self.overlay.set_monitor(button.get_active()) - - 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): - self.overlay.set_icon_spacing(button.get_value()) - - self.icon_spacing = int(button.get_value()) - self.save_config() - - def change_text_padding(self, button): - self.overlay.set_text_padding(button.get_value()) - - self.text_padding = button.get_value() - self.save_config() - - def change_edge_padding(self,button): - self.overlay.set_edge_padding(button.get_value()) - - self.edge_padding = button.get_value() - self.save_config() - - def change_square_avatar(self, button): - self.overlay.set_square_avatar(button.get_active()) - - self.square_avatar = button.get_active() - self.save_config() - -class OverlayWindow(Gtk.Window): - def __init__(self): - Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP) - self.set_size_request(50, 50) - - self.connect('draw', self.draw) - - self.compositing = False - # Set RGBA - screen = self.get_screen() - visual = screen.get_rgba_visual() - if not self.get_display().supports_input_shapes(): - print("Input shapes not available. Quitting") - sys.exit(1) - if visual: - # Set the visual even if we can't use it right now - self.set_visual(visual) - if screen.is_composited(): - self.compositing=True - - self.set_app_paintable(True) - self.set_untouchable() - self.set_skip_pager_hint(True) - self.set_skip_taskbar_hint(True) - - self.show_all() - self.monitor = 0 - self.align_right=True - self.align_vert=1 - self.floating=False - - def draw(self,widget,context): - pass - - def do_draw(self, context): - pass - - def set_font(self, name, size): - self.text_font=name - self.text_size=size - self.redraw() - - def set_floating(self, floating, x, y, w, h): - self.floating = floating - self.x = x - self.y = y - self.w = w - self.h = h - self.force_location() - - def set_untouchable(self): - (w, h) = self.get_size() - surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) - surface_ctx = cairo.Context(surface) - surface_ctx.set_source_rgba(0.0,0.0,0.0,0.0) - surface_ctx.set_operator(cairo.OPERATOR_SOURCE) - surface_ctx.paint() - reg = Gdk.cairo_region_create_from_surface(surface) - self.input_shape_combine_region(reg) - #self.shape_combine_region(reg) - - def unset_shape(self): - self.get_window().shape_combine_region(None,0,0) - - def force_location(self): - self.set_decorated(False) - self.set_keep_above(True) - display = Gdk.Display.get_default() - monitor = display.get_monitor(self.monitor) - geometry = monitor.get_geometry() - scale_factor = monitor.get_scale_factor() - if not self.floating: - w = scale_factor * geometry.width - h = scale_factor * geometry.height - x = geometry.x - y = geometry.y - self.resize(w, h) - self.move(x,y) - else: - self.move(self.x,self.y) - self.resize(self.w,self.h) - self.redraw() - - def redraw(self): - gdkwin = self.get_window() - - if gdkwin: - if not self.compositing: - (w, h) = self.get_size() - surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) - surface_ctx = cairo.Context(surface) - self.do_draw(surface_ctx) - reg = Gdk.cairo_region_create_from_surface(surface) - gdkwin.shape_combine_region(reg,0,0) - else: - gdkwin.shape_combine_region(None,0,0) - self.queue_draw() - - def set_monitor(self, idx): - self.monitor = idx - self.force_location() - self.redraw() - - def set_align_x(self, b): - self.align_right = b - self.force_location() - self.redraw() - - def set_align_y(self, i): - self.align_vert = i - self.force_location() - self.redraw() - - def col(self,c,a=1.0): - self.context.set_source_rgba(c[0],c[1],c[2],c[3]*a) - - -class TextOverlayWindow(OverlayWindow): - def __init__(self): - OverlayWindow.__init__(self) - self.text_spacing = 4 - self.content = [] - self.text_font=None - self.text_size=13 - - self.connected = True - - def set_text_list(self,tlist, alt): - self.content = tlist - if alt: - self.redraw() - - def set_enabled(self, en): - if en: - self.show_all() - else: - self.hide() - - def set_fg(self, fg_col): - self.fg_col = fg_col - self.redraw() - - def set_bg(self, bg_col): - self.bg_col = bg_col - self.redraw() - - def do_draw(self,context): - self.context = context - context.set_antialias(self.compositing) - (w, h) = self.get_size() - - # Make background transparent - context.set_source_rgba(0.0,0.0,0.0,0.4) - # Don't layer drawing over each other, always replace - self.col(self.bg_col) - context.paint() - context.set_operator(cairo.OPERATOR_OVER) - self.col(self.fg_col) - - if not self.connected: - return - - long_string="" - for line in self.content: - col = "#fff" - if 'nick_col' in line and line['nick_col']: - col = line['nick_col'] - long_string = "%s\n%s: %s" %( - long_string, - self.santize_string(col), - self.santize_string(line["nick"]), - self.santize_string(line["content"])) - layout = self.create_pango_layout(long_string) - layout.set_markup(long_string, -1) - attr = Pango.AttrList() - - layout.set_width(Pango.SCALE *w) - layout.set_spacing(Pango.SCALE * 3) - if(self.text_font): - font = Pango.FontDescription("%s %s" % (self.text_font, self.text_size)) - layout.set_font_description(font) - tw,th =layout.get_pixel_size() - context.move_to(0,-th+h) - PangoCairo.show_layout(context, layout) - - def santize_string(self, string): - # I hate that Pango has nothing for this. - return string.replace("&", "&").replace("<","<").replace(">",">").replace("'", "'").replace("\"", """) - -class VoiceOverlayWindow(OverlayWindow): - def __init__(self): - OverlayWindow.__init__(self) - - self.avatars = {} - - self.avatar_size=48 - self.text_pad=6 - self.text_font=None - self.text_size=13 - self.icon_spacing=8 - self.edge_padding=0 - - self.round_avatar=True - self.talk_col = [0.0,0.6,0.0,0.1] - self.text_col = [1.0,1.0,1.0,1.0] - self.norm_col = [0.0,0.0,0.0,0.5] - self.wind_col = [0.0,0.0,0.0,0.0] - self.mute_col = [0.7,0.0,0.0,1.0] - self.userlist=[] - self.connected=False - self.force_location() - self.def_avatar = self.get_img("https://cdn.discordapp.com/embed/avatars/3.png") - - self.first_draw=True - - def set_bg(self, bg): - self.norm_col = bg - self.redraw() - - def set_fg(self, fg): - self.text_col = fg - self.redraw() - - def set_tk(self, tk): - self.talk_col = tk - self.redraw() - - def set_mt(self, mt): - self.mute_col = mt - self.redraw() - - def set_avatar_size(self, size): - self.avatar_size=size - self.reset_avatar() - self.redraw() - - def set_icon_spacing(self, i): - self.icon_spacing = i - self.redraw() - - def set_text_padding(self, i): - self.text_pad = i - self.redraw() - - def set_edge_padding(self, i): - self.edge_padding=i - self.redraw() - - def set_square_avatar(self, i): - self.round_avatar = not i - self.redraw() - - def set_wind_col(self): - self.col(self.wind_col) - - def set_text_col(self): - self.col(self.text_col) - - def set_norm_col(self): - self.col(self.norm_col) - - def set_talk_col(self,a=1.0): - self.col(self.talk_col,a) - - def set_mute_col(self,a=1.0): - self.col(self.mute_col,a) - - def reset_avatar(self): - self.avatars = {} - self.def_avatar = self.get_img("https://cdn.discordapp.com/embed/avatars/3.png") - - def set_user_list(self, userlist,alt): - self.userlist = userlist - self.userlist.sort(key=lambda x: x["username"]) - screen = self.get_screen() - c = screen.is_composited() - if not self.compositing == c: - alt = True - self.compositing = c - if alt: - self.redraw() - - def set_connection(self, connection): - is_connected = connection == "VOICE_CONNECTED" - if self.connected != is_connected: - self.connected = is_connected - self.redraw() - - def draw(self, widget, context): - # Draw - self.do_draw(context) - - def do_draw(self,context): - self.context = context - context.set_antialias(cairo.ANTIALIAS_GOOD) - - # Make background transparent - self.set_wind_col() - # Don't layer drawing over each other, always replace - context.set_operator(cairo.OPERATOR_SOURCE) - context.paint() - context.set_operator(cairo.OPERATOR_OVER) - if not self.connected: - return - - # Get size of window - (w,h) = self.get_size() - # Calculate height needed to show overlay - height = (len(self.userlist) * self.avatar_size) + (len(self.userlist)+1)*self.icon_spacing - - # Choose where to start drawing - rh = 0 + self.edge_padding - if self.align_vert==1: - # Ignore padding? - rh = (h/2) - (height/2) - elif self.align_vert==2: - rh = h-height - self.edge_padding - # Iterate users in room. - for user in self.userlist: - self.draw_avatar(context, user, rh) - # Shift the relative position down to next location - rh+=self.avatar_size+self.icon_spacing - - # Don't hold a ref - self.context=None - - def get_img(self, url): - req = urllib.request.Request(url) - req.add_header('Referer','https://streamkit.discord.com/overlay/voice') - req.add_header('User-Agent', 'Mozilla/5.0') + self.voice_overlay = VoiceOverlayWindow() + self.text_overlay = TextOverlayWindow() + self.menu = self.make_menu() + self.make_sys_tray_icon(self.menu) + self.voice_settings = VoiceSettingsWindow(self.voice_overlay) + self.text_settings = TextSettingsWindow(self.text_overlay) + + def make_sys_tray_icon(self, menu): + # Create AppIndicator try: - response = urllib.request.urlopen(req) - input_stream = Gio.MemoryInputStream.new_from_data(response.read(), None) - pixbuf = Pixbuf.new_from_stream(input_stream, None) - pixbuf = pixbuf.scale_simple(self.avatar_size, self.avatar_size, - GdkPixbuf.InterpType.BILINEAR) - return pixbuf - except: - print("Could not access : %s"%(url)) - return none - - - def draw_avatar(self, context, user,y): - # Ensure pixbuf for avatar - if user["id"] not in self.avatars and user["avatar"]: - url= "https://cdn.discordapp.com/avatars/%s/%s.jpg" % (user["id"], user["avatar"]) - p = self.get_img(url) - if p: - self.avatars[user["id"]] = p - - (w,h)=self.get_size() - c = None - mute=False - alpha = 1.0 - if "speaking" in user and user["speaking"]: - c = self.talk_col - if "mute" in user and user["mute"]: - mute=True - if "deaf" in user and user["deaf"]: - alpha=0.5 - pix = None - if user["id"] in self.avatars: - pix = self.avatars[user["id"]] - if self.align_right: - self.draw_text(context, user["username"],w-self.avatar_size,y) - self.draw_avatar_pix(context, pix,w-self.avatar_size,y,c,alpha) - if mute: - self.draw_mute(context, w-self.avatar_size, y,alpha) - else: - self.draw_text(context, user["username"],self.avatar_size,y) - self.draw_avatar_pix(context, pix,0,y,c,alpha) - if mute: - self.draw_mute(context, 0,y,alpha) - - def draw_text(self,context, string,x,y): - if self.text_font: - context.set_font_face(cairo.ToyFontFace(self.text_font,cairo.FontSlant.NORMAL,cairo.FontWeight.NORMAL)) - context.set_font_size(self.text_size) - xb, yb, w, h, dx, dy = context.text_extents(string) - ho = (self.avatar_size/2) - (h/2) - if self.align_right: - context.move_to(0,0) - self.set_norm_col() - context.rectangle(x-w-(self.text_pad*2),y+ho-self.text_pad,w+(self.text_pad*4),h+(self.text_pad*2)) - context.fill() - - self.set_text_col() - context.move_to(x-w-self.text_pad,y+ho+h) - context.show_text(string) - else: - context.move_to(0,0) - self.set_norm_col() - context.rectangle(x-(self.text_pad*2),y+ho-self.text_pad,w+(self.text_pad*4),h+(self.text_pad*2)) - context.fill() - - self.set_text_col() - context.move_to(x+self.text_pad,y+ho+h) - context.show_text(string) - - def draw_avatar_pix(self, context, pixbuf,x,y,c,alpha): - context.move_to(x,y) - context.save() - #context.set_source_pixbuf(pixbuf, 0.0, 0.0) - if self.round_avatar: - context.arc(x+(self.avatar_size/2), y+(self.avatar_size/2), self.avatar_size/2,0,2*math.pi) - context.clip() - if not pixbuf: - pixbuf = self.def_avatar - self.set_wind_col() - context.set_operator(cairo.OPERATOR_SOURCE) - context.rectangle(x,y,self.avatar_size,self.avatar_size) - context.fill() - context.set_operator(cairo.OPERATOR_OVER) - Gdk.cairo_set_source_pixbuf(context,pixbuf,x,y) - context.paint_with_alpha(alpha) - context.restore() - if c: - if self.round_avatar: - context.arc(x+(self.avatar_size/2), y+(self.avatar_size/2), self.avatar_size/2, 0, 2*math.pi) - self.col(c) - context.stroke() - else: - context.rectangle(x,y,self.avatar_size,self.avatar_size) - self.col(c) - context.stroke() - - def draw_mute(self, context, x, y, a): - context.save() - context.translate(x,y) - context.scale(self.avatar_size, self.avatar_size) - self.set_mute_col(a) - context.save() - - # Clip Strike-through - context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) - context.set_line_width(0.1) - context.move_to(0.0,0.0) - context.line_to(1.0,0.0) - context.line_to(1.0,1.0) - context.line_to(0.0,1.0) - context.line_to(0.0,0.0) - context.close_path() - context.new_sub_path() - context.arc(0.9,0.1,0.05,1.25*math.pi, 2.25*math.pi) - context.arc(0.1,0.9,0.05,.25*math.pi,1.25*math.pi) - context.close_path() - context.clip() - - # Center - context.set_line_width(0.07) - context.arc(0.5,0.3,0.1,math.pi, 2*math.pi) - context.arc(0.5,0.5,0.1,0, math.pi) - context.close_path() - context.fill() - - context.set_line_width(0.05) - - # Stand rounded - context.arc(0.5,0.5,0.15,0, 1.0*math.pi) - context.stroke() - - # Stand vertical - context.move_to(0.5,0.65) - context.line_to(0.5,0.75) - context.stroke() - - # Stand horizontal - context.move_to(0.35,0.75) - context.line_to(0.65,0.75) - context.stroke() - - context.restore() - # Strike through - context.arc(0.7,0.3,0.035,1.25*math.pi, 2.25*math.pi) - context.arc(0.3,0.7,0.035,.25*math.pi,1.25*math.pi) - context.close_path() - context.fill() - - context.restore() - -def create_gui(): - global win, box, tray, vsettings, tsettings, menu, ind,twin - win = VoiceOverlayWindow() - twin= TextOverlayWindow() - - # Create System Menu - menu = Gtk.Menu() - vsettings_opt = Gtk.MenuItem.new_with_label("Voice Settings") - tsettings_opt = Gtk.MenuItem.new_with_label("Text Settings") - close_opt = Gtk.MenuItem.new_with_label("Close") - - menu.append(vsettings_opt) - menu.append(tsettings_opt) - menu.append(close_opt) - - vsettings_opt.connect("activate", show_vsettings) - tsettings_opt.connect("activate", show_tsettings) - close_opt.connect("activate", close) - menu.show_all() - - # Create AppIndicator - try: - from gi.repository import AppIndicator3 - ind = AppIndicator3.Indicator.new( + gi.require_version('AppIndicator3', '0.1') + from gi.repository import AppIndicator3 + self.ind = AppIndicator3.Indicator.new( "discover", "discover", AppIndicator3.IndicatorCategory.APPLICATION_STATUS) - ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE) - ind.set_menu(menu) - except: + self.ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE) + self.ind.set_menu(menu) + except: - # Create System Tray - tray = Gtk.StatusIcon.new_from_icon_name("discover") - tray.connect('popup-menu', show_menu) + # Create System Tray + self.tray = Gtk.StatusIcon.new_from_icon_name("discover") + self.tray.connect('popup-menu', show_menu) - vsettings = VoiceSettingsWindow(win) - tsettings = TextSettingsWindow(twin) + def make_menu(self): + # Create System Menu + menu = Gtk.Menu() + vsettings_opt = Gtk.MenuItem.new_with_label("Voice Settings") + tsettings_opt = Gtk.MenuItem.new_with_label("Text Settings") + close_opt = Gtk.MenuItem.new_with_label("Close") -def show_menu(obj, button, time): - menu.show_all() - menu.popup(None,None,Gtk.StatusIcon.position_menu,obj,button,time) + menu.append(vsettings_opt) + menu.append(tsettings_opt) + menu.append(close_opt) -def show_vsettings(obj=None, data=None): - global vsettings - vsettings.present() + vsettings_opt.connect("activate", self.show_vsettings) + tsettings_opt.connect("activate", self.show_tsettings) + close_opt.connect("activate", self.close) + menu.show_all() + return menu -def show_tsettings(obj=None, data=None): - global tsettings - tsettings.present() + def show_menu(self, obj, button, time): + self.menu.show_all() + self.menu.popup( + None, None, Gtk.StatusIcon.position_menu, obj, button, time) -def close(a=None, b=None, c=None): - Gtk.main_quit() + def show_vsettings(self, obj=None, data=None): + self.voice_settings.present() -def connect(): - global ws, oauth_token, error_connection - if ws: - return - try: - ws = websocket.create_connection("ws://127.0.0.1:6463/?v=1&client_id=%s" % (oauth_token), - origin="https://streamkit.discord.com") - except Exception as e: - if error_connection: - print(e) - error_connection=False - pass + def show_tsettings(self, obj=None, data=None): + self.text_settings.present() -def main(): - connect() + def close(self, a=None, b=None, c=None): + Gtk.main_quit() - create_gui() - win.show_all() + def main(self): + self.create_gui() + self.connection = DiscordConnector( + self.text_settings, self.voice_settings, + self.text_overlay, self.voice_overlay) + self.connection.connect() + GLib.timeout_add((1000 / 60), self.connection.do_read) - GLib.timeout_add((1000/60), do_read) - - try: - Gtk.main() - except: - pass + try: + Gtk.main() + except: + pass def entrypoint(): - global ws,win,box,tray,vsettings,tsettings,ind,menu,warn_connection,error_connection - ws=None - win=None - box=None - tray=None - vsettings=None - tsettings=None - ind=None - menu=None - warn_connection=True - error_connection=True - main() - -if __name__ == "__main__": - entrypoint() + discover = Discover() + discover.main() diff --git a/discover_overlay/draggable_window.py b/discover_overlay/draggable_window.py new file mode 100644 index 0000000..48a2c5c --- /dev/null +++ b/discover_overlay/draggable_window.py @@ -0,0 +1,124 @@ +import gi +gi.require_version("Gtk", "3.0") +gi.require_version('PangoCairo', '1.0') +gi.require_version('GdkPixbuf', '2.0') +import cairo +from gi.repository.GdkPixbuf import Pixbuf +from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo + + +class DraggableWindow(Gtk.Window): + def __init__(self, x=0, y=0, w=300, h=300, message="Message"): + Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP) + if w < 100: + w = 100 + if h < 100: + h = 100 + self.x = x + self.y = y + self.w = w + self.h = h + self.message = message + self.set_size_request(50, 50) + + self.connect('draw', self.draw) + self.connect('motion-notify-event', self.drag) + self.connect('button-press-event', self.button_press) + self.connect('button-release-event', self.button_release) + + self.compositing = False + # Set RGBA + screen = self.get_screen() + visual = screen.get_rgba_visual() + if visual: + # Set the visual even if we can't use it right now + self.set_visual(visual) + if screen.is_composited(): + self.compositing = True + + self.set_app_paintable(True) + self.monitor = 0 + + self.drag_type = None + self.drag_x = 0 + self.drag_y = 0 + self.force_location() + self.show_all() + + def force_location(self): + self.set_decorated(False) + self.set_keep_above(True) + display = Gdk.Display.get_default() + monitor = display.get_monitor(self.monitor) + geometry = monitor.get_geometry() + scale_factor = monitor.get_scale_factor() + w = scale_factor * geometry.width + h = scale_factor * geometry.height + x = geometry.x + y = geometry.y + #self.resize(400, h) + self.move(self.x, self.y) + self.resize(self.w, self.h) + + def drag(self, w, event): + if event.state & Gdk.ModifierType.BUTTON1_MASK: + if self.drag_type == 1: + # Center is move + self.x = event.x_root - self.drag_x + self.y = event.y_root - self.drag_y + self.force_location() + elif self.drag_type == 2: + # Right edge + self.w += event.x - self.drag_x + self.drag_x = event.x + self.force_location() + elif self.drag_type == 3: + # Bottom edge + self.h += event.y - self.drag_y + self.drag_y = event.y + self.force_location() + else: + # Bottom Right + self.w += event.x - self.drag_x + self.h += event.y - self.drag_y + self.drag_x = event.x + self.drag_y = event.y + self.force_location() + + def button_press(self, w, event): + (w, h) = self.get_size() + if not self.drag_type: + self.drag_type = 1 + # Where in the window did we press? + if event.y > h - 32: + self.drag_type += 2 + if event.x > w - 32: + self.drag_type += 1 + self.drag_x = event.x + self.drag_y = event.y + + def button_release(self, w, event): + self.drag_type = None + + def draw(self, widget, context): + context.set_source_rgba(1.0, 1.0, 0.0, 0.7) + # Don't layer drawing over each other, always replace + context.set_operator(cairo.OPERATOR_SOURCE) + context.paint() + context.set_operator(cairo.OPERATOR_OVER) + # Get size of window + (sw, sh) = self.get_size() + + # Draw text + context.set_source_rgba(0.0, 0.0, 0.0, 1.0) + xb, yb, w, h, dx, dy = context.text_extents(self.message) + context.move_to(sw / 2 - w / 2, sh / 2 - h / 2) + context.show_text(self.message) + + # Draw resizing edges + context.set_source_rgba(0.0, 0.0, 1.0, 0.5) + context.rectangle(sw - 32, 0, 32, sh) + context.fill() + + context.rectangle(0, sh - 32, sw, 32) + context.fill() diff --git a/discover_overlay/overlay.py b/discover_overlay/overlay.py new file mode 100644 index 0000000..42373d6 --- /dev/null +++ b/discover_overlay/overlay.py @@ -0,0 +1,124 @@ +import gi +gi.require_version("Gtk", "3.0") +gi.require_version('PangoCairo', '1.0') +gi.require_version('GdkPixbuf', '2.0') +from gi.repository.GdkPixbuf import Pixbuf +from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo +import cairo + + +class OverlayWindow(Gtk.Window): + def __init__(self): + Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP) + self.set_size_request(50, 50) + + self.connect('draw', self.draw) + + self.compositing = False + # Set RGBA + screen = self.get_screen() + visual = screen.get_rgba_visual() + if not self.get_display().supports_input_shapes(): + print("Input shapes not available. Quitting") + sys.exit(1) + if visual: + # Set the visual even if we can't use it right now + self.set_visual(visual) + if screen.is_composited(): + self.compositing = True + + self.set_app_paintable(True) + self.set_untouchable() + self.set_skip_pager_hint(True) + self.set_skip_taskbar_hint(True) + + self.show_all() + self.monitor = 0 + self.align_right = True + self.align_vert = 1 + self.floating = False + + def draw(self, widget, context): + pass + + def do_draw(self, context): + pass + + def set_font(self, name, size): + self.text_font = name + self.text_size = size + self.redraw() + + def set_floating(self, floating, x, y, w, h): + self.floating = floating + self.x = x + self.y = y + self.w = w + self.h = h + self.force_location() + + def set_untouchable(self): + (w, h) = self.get_size() + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) + surface_ctx = cairo.Context(surface) + surface_ctx.set_source_rgba(0.0, 0.0, 0.0, 0.0) + surface_ctx.set_operator(cairo.OPERATOR_SOURCE) + surface_ctx.paint() + reg = Gdk.cairo_region_create_from_surface(surface) + self.input_shape_combine_region(reg) + # self.shape_combine_region(reg) + + def unset_shape(self): + self.get_window().shape_combine_region(None, 0, 0) + + def force_location(self): + self.set_decorated(False) + self.set_keep_above(True) + display = Gdk.Display.get_default() + monitor = display.get_monitor(self.monitor) + geometry = monitor.get_geometry() + scale_factor = monitor.get_scale_factor() + if not self.floating: + w = scale_factor * geometry.width + h = scale_factor * geometry.height + x = geometry.x + y = geometry.y + self.resize(w, h) + self.move(x, y) + else: + self.move(self.x, self.y) + self.resize(self.w, self.h) + self.redraw() + + def redraw(self): + gdkwin = self.get_window() + + if gdkwin: + if not self.compositing: + (w, h) = self.get_size() + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) + surface_ctx = cairo.Context(surface) + self.do_draw(surface_ctx) + reg = Gdk.cairo_region_create_from_surface(surface) + gdkwin.shape_combine_region(reg, 0, 0) + else: + gdkwin.shape_combine_region(None, 0, 0) + self.queue_draw() + + def set_monitor(self, idx): + self.monitor = idx + self.force_location() + self.redraw() + + def set_align_x(self, b): + self.align_right = b + self.force_location() + self.redraw() + + def set_align_y(self, i): + self.align_vert = i + self.force_location() + self.redraw() + + def col(self, c, a=1.0): + self.context.set_source_rgba(c[0], c[1], c[2], c[3] * a) diff --git a/discover_overlay/settings.py b/discover_overlay/settings.py new file mode 100644 index 0000000..e5c21a2 --- /dev/null +++ b/discover_overlay/settings.py @@ -0,0 +1,48 @@ +import gi +gi.require_version("Gtk", "3.0") +gi.require_version('PangoCairo', '1.0') +gi.require_version('GdkPixbuf', '2.0') +import sys +import os +from gi.repository.GdkPixbuf import Pixbuf +from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo + + +try: + from xdg.BaseDirectory import xdg_config_home +except ModuleNotFoundError: + from xdg import XDG_CONFIG_HOME as xdg_config_home + + +class SettingsWindow(Gtk.Window): + def init_config(self): + self.configDir = os.path.join(xdg_config_home, "discover") + os.makedirs(self.configDir, exist_ok=True) + self.configFile = os.path.join(self.configDir, "discover.ini") + self.read_config() + + def close_window(self, a=None, b=None): + if self.placement_window: + (x, y) = self.placement_window.get_position() + (w, h) = self.placement_window.get_size() + self.floating_x = x + self.floating_y = y + self.floating_w = w + self.floating_h = h + self.overlay.set_floating(True, x, y, w, h) + self.save_config() + self.placement_window.close() + self.placement_window = None + self.hide() + return True + + def get_monitor_index(self, name): + display = Gdk.Display.get_default() + for i in range(0, display.get_n_monitors()): + if display.get_monitor(i).get_model() == name: + return i + print("Could not find monitor : %s" % (name)) + return 0 + + def present(self): + self.show_all() diff --git a/discover_overlay/text_overlay.py b/discover_overlay/text_overlay.py new file mode 100644 index 0000000..9244471 --- /dev/null +++ b/discover_overlay/text_overlay.py @@ -0,0 +1,84 @@ +import gi +gi.require_version("Gtk", "3.0") +gi.require_version('PangoCairo', '1.0') +gi.require_version('GdkPixbuf', '2.0') +import math +from .overlay import OverlayWindow +from gi.repository.GdkPixbuf import Pixbuf +from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo +import cairo + + +class TextOverlayWindow(OverlayWindow): + def __init__(self): + OverlayWindow.__init__(self) + self.text_spacing = 4 + self.content = [] + self.text_font = None + self.text_size = 13 + self.connected = True + self.bg_col = [0.0, 0.6, 0.0, 0.1] + self.fg_col = [1.0, 1.0, 1.0, 1.0] + + def set_text_list(self, tlist, alt): + self.content = tlist + if alt: + self.redraw() + + def set_enabled(self, en): + if en: + self.show_all() + else: + self.hide() + + def set_fg(self, fg_col): + self.fg_col = fg_col + self.redraw() + + def set_bg(self, bg_col): + self.bg_col = bg_col + self.redraw() + + def do_draw(self, context): + self.context = context + context.set_antialias(self.compositing) + (w, h) = self.get_size() + + # Make background transparent + context.set_source_rgba(0.0, 0.0, 0.0, 0.4) + # Don't layer drawing over each other, always replace + self.col(self.bg_col) + context.paint() + context.set_operator(cairo.OPERATOR_OVER) + self.col(self.fg_col) + + if not self.connected: + return + + long_string = "" + for line in self.content: + col = "#fff" + if 'nick_col' in line and line['nick_col']: + col = line['nick_col'] + long_string = "%s\n%s: %s" % ( + long_string, + self.santize_string(col), + self.santize_string(line["nick"]), + self.santize_string(line["content"])) + layout = self.create_pango_layout(long_string) + layout.set_markup(long_string, -1) + attr = Pango.AttrList() + + layout.set_width(Pango.SCALE * w) + layout.set_spacing(Pango.SCALE * 3) + if(self.text_font): + font = Pango.FontDescription( + "%s %s" % (self.text_font, self.text_size)) + layout.set_font_description(font) + tw, th = layout.get_pixel_size() + context.move_to(0, -th + h) + PangoCairo.show_layout(context, layout) + + def santize_string(self, string): + # I hate that Pango has nothing for this. + return string.replace("&", "&").replace("<", "<").replace(">", ">").replace("'", "'").replace("\"", """) diff --git a/discover_overlay/text_settings.py b/discover_overlay/text_settings.py new file mode 100644 index 0000000..47e1fac --- /dev/null +++ b/discover_overlay/text_settings.py @@ -0,0 +1,358 @@ +import gi +gi.require_version("Gtk", "3.0") +gi.require_version('PangoCairo', '1.0') +gi.require_version('GdkPixbuf', '2.0') +import json +from configparser import ConfigParser +from .draggable_window import DraggableWindow +from .settings import SettingsWindow +from gi.repository.GdkPixbuf import Pixbuf +from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo + + +class TextSettingsWindow(SettingsWindow): + def __init__(self, overlay): + Gtk.Window.__init__(self) + self.overlay = overlay + self.set_size_request(400, 200) + self.connect("destroy", self.close_window) + self.connect("delete-event", self.close_window) + self.placement_window = None + self.init_config() + self.list_channels_keys = [] + self.ignore_channel_change = False + self.create_gui() + + def present(self): + self.show_all() + if not self.floating: + self.align_x_widget.show() + self.align_y_widget.show() + self.align_monitor_widget.show() + self.align_placement_widget.hide() + else: + self.align_x_widget.hide() + self.align_y_widget.hide() + self.align_monitor_widget.hide() + self.align_placement_widget.show() + model = monitor_store = Gtk.ListStore(str, bool) + # for c in self.list_channels_keys: + # print(self.list_channels[c]) + # model.append([self.list_channels[c]["name"]]) + self.channel_lookup = [] + for guild in self.guild_list(): + guild_id, guild_name = guild + self.channel_lookup.append('0') + model.append([guild_name, False]) + for c in self.list_channels_keys: + chan = self.list_channels[c] + if chan['guild_id'] == guild_id: + model.append([chan["name"], True]) + self.channel_lookup.append(c) + + self.channel_widget.set_model(model) + self.channel_model = model + + idx = 0 + for c in self.channel_lookup: + if c == self.channel: + self.ignore_channel_change = True + self.channel_widget.set_active(idx) + self.ignore_channel_change = False + break + idx += 1 + + def guild_list(self): + guilds = [] + done = [] + for channel in self.list_channels.values(): + if not channel["guild_id"] in done: + done.append(channel["guild_id"]) + guilds.append([channel["guild_id"], channel["guild_name"]]) + return guilds + + def set_channels(self, in_list): + self.list_channels = in_list + self.list_channels_keys = [] + for key in in_list.keys(): + if in_list[key]["type"] == 0: + self.list_channels_keys.append(key) + self.list_channels_keys.sort() + + def read_config(self): + config = ConfigParser(interpolation=None) + config.read(self.configFile) + self.enabled = config.getboolean("text", "enabled", fallback=False) + self.align_x = config.getboolean("text", "rightalign", fallback=True) + self.align_y = config.getint("text", "topalign", fallback=2) + self.monitor = config.get("text", "monitor", fallback="None") + self.floating = config.getboolean("text", "floating", fallback=True) + self.floating_x = config.getint("text", "floating_x", fallback=0) + self.floating_y = config.getint("text", "floating_y", fallback=0) + self.floating_w = config.getint("text", "floating_w", fallback=400) + self.floating_h = config.getint("text", "floating_h", fallback=400) + self.channel = config.get("text", "channel", fallback="0") + self.font = config.get("text", "font", fallback=None) + self.bg_col = json.loads(config.get( + "text", "bg_col", fallback="[0.0,0.0,0.0,0.5]")) + self.fg_col = json.loads(config.get( + "text", "fg_col", fallback="[1.0,1.0,1.0,1.0]")) + + print("Loading saved channel %s" % (self.channel)) + + # Pass all of our config over to the overlay + self.overlay.set_enabled(self.enabled) + self.overlay.set_align_x(self.align_x) + self.overlay.set_align_y(self.align_y) + self.overlay.set_monitor(self.get_monitor_index(self.monitor)) + self.overlay.set_floating( + self.floating, self.floating_x, self.floating_y, self.floating_w, self.floating_h) + self.overlay.set_bg(self.bg_col) + self.overlay.set_fg(self.fg_col) + + def save_config(self): + config = ConfigParser(interpolation=None) + config.read(self.configFile) + if not config.has_section("text"): + config.add_section("text") + + config.set("text", "rightalign", "%d" % (int(self.align_x))) + config.set("text", "topalign", "%d" % (self.align_y)) + config.set("text", "monitor", self.monitor) + config.set("text", "enabled", "%d" % (int(self.enabled))) + config.set("text", "floating", "%s" % (int(self.floating))) + config.set("text", "floating_x", "%s" % (self.floating_x)) + config.set("text", "floating_y", "%s" % (self.floating_y)) + config.set("text", "floating_w", "%s" % (self.floating_w)) + config.set("text", "floating_h", "%s" % (self.floating_h)) + config.set("text", "channel", self.channel) + config.set("text", "bg_col", json.dumps(self.bg_col)) + config.set("text", "fg_col", json.dumps(self.fg_col)) + + if self.font: + config.set("text", "font", self.font) + + with open(self.configFile, 'w') as file: + config.write(file) + + def create_gui(self): + box = Gtk.Grid() + + # Enabled + enabled_label = Gtk.Label.new("Enable") + enabled = Gtk.CheckButton.new() + enabled.set_active(self.enabled) + enabled.connect("toggled", self.change_enabled) + + # Font chooser + font_label = Gtk.Label.new("Font") + font = Gtk.FontButton() + if self.font: + font.set_font(self.font) + font.connect("font-set", self.change_font) + + # Colours + bg_col_label = Gtk.Label.new("Background colour") + bg_col = Gtk.ColorButton.new_with_rgba( + Gdk.RGBA(self.bg_col[0], self.bg_col[1], self.bg_col[2], self.bg_col[3])) + fg_col_label = Gtk.Label.new("Text colour") + fg_col = Gtk.ColorButton.new_with_rgba( + Gdk.RGBA(self.fg_col[0], self.fg_col[1], self.fg_col[2], self.fg_col[3])) + bg_col.set_use_alpha(True) + fg_col.set_use_alpha(True) + bg_col.connect("color-set", self.change_bg) + fg_col.connect("color-set", self.change_fg) + + # Monitor & Alignment + align_label = Gtk.Label.new("Overlay Location") + + align_type_box = Gtk.HBox() + align_type_edge = Gtk.RadioButton.new_with_label( + None, "Anchor to edge") + align_type_floating = Gtk.RadioButton.new_with_label_from_widget( + align_type_edge, "Floating") + if self.floating: + align_type_floating.set_active(True) + align_type_box.add(align_type_edge) + align_type_box.add(align_type_floating) + + monitor_store = Gtk.ListStore(str) + display = Gdk.Display.get_default() + for i in range(0, display.get_n_monitors()): + monitor_store.append([display.get_monitor(i).get_model()]) + monitor = Gtk.ComboBox.new_with_model(monitor_store) + monitor.set_active(self.get_monitor_index(self.monitor)) + monitor.connect("changed", self.change_monitor) + rt = Gtk.CellRendererText() + monitor.pack_start(rt, True) + monitor.add_attribute(rt, "text", 0) + + align_x_store = Gtk.ListStore(str) + align_x_store.append(["Left"]) + align_x_store.append(["Right"]) + align_x = Gtk.ComboBox.new_with_model(align_x_store) + align_x.set_active(True if self.align_x else False) + align_x.connect("changed", self.change_align_x) + rt = Gtk.CellRendererText() + align_x.pack_start(rt, True) + align_x.add_attribute(rt, "text", 0) + + align_y_store = Gtk.ListStore(str) + align_y_store.append(["Top"]) + align_y_store.append(["Middle"]) + align_y_store.append(["Bottom"]) + align_y = Gtk.ComboBox.new_with_model(align_y_store) + align_y.set_active(self.align_y) + align_y.connect("changed", self.change_align_y) + rt = Gtk.CellRendererText() + align_y.pack_start(rt, True) + align_y.add_attribute(rt, "text", 0) + + align_placement_button = Gtk.Button.new_with_label("Place Window") + + align_type_edge.connect("toggled", self.change_align_type_edge) + align_type_floating.connect("toggled", self.change_align_type_floating) + align_placement_button.connect("pressed", self.change_placement) + + channel_label = Gtk.Label.new("Channel") + channel = Gtk.ComboBox.new() + + channel.connect("changed", self.change_channel) + rt = Gtk.CellRendererText() + #channel.set_row_separator_func(lambda model, path: model[path][1]) + channel.pack_start(rt, True) + channel.add_attribute(rt, "text", 0) + channel.add_attribute(rt, 'sensitive', 1) + + self.align_x_widget = align_x + self.align_y_widget = align_y + self.align_monitor_widget = monitor + self.align_placement_widget = align_placement_button + self.channel_widget = channel + + box.attach(enabled_label, 0, 0, 1, 1) + box.attach(enabled, 1, 0, 1, 1) + box.attach(channel_label, 0, 1, 1, 1) + box.attach(channel, 1, 1, 1, 1) + + box.attach(font_label, 0, 2, 1, 1) + box.attach(font, 1, 2, 1, 1) + box.attach(fg_col_label, 0, 3, 1, 1) + box.attach(fg_col, 1, 3, 1, 1) + box.attach(bg_col_label, 0, 4, 1, 1) + box.attach(bg_col, 1, 4, 1, 1) + box.attach(align_label, 0, 5, 1, 5) + box.attach(align_type_box, 1, 5, 1, 1) + box.attach(monitor, 1, 6, 1, 1) + box.attach(align_x, 1, 7, 1, 1) + box.attach(align_y, 1, 8, 1, 1) + box.attach(align_placement_button, 1, 9, 1, 1) + + self.add(box) + + def change_font(self, button): + font = button.get_font() + desc = Pango.FontDescription.from_string(font) + s = desc.get_size() + if not desc.get_size_is_absolute(): + s = s / Pango.SCALE + self.overlay.set_font(desc.get_family(), s) + + self.font = desc.to_string() + self.save_config() + + def change_channel(self, button): + if self.ignore_channel_change: + return + c = self.channel_lookup[button.get_active()] + self.channel = c + self.save_config() + + def change_placement(self, button): + if self.placement_window: + (x, y) = self.placement_window.get_position() + (w, h) = self.placement_window.get_size() + self.floating_x = x + self.floating_y = y + self.floating_w = w + self.floating_h = h + self.overlay.set_floating(True, x, y, w, h) + self.save_config() + button.set_label("Place Window") + + self.placement_window.close() + self.placement_window = None + else: + self.placement_window = DraggableWindow( + x=self.floating_x, y=self.floating_y, w=self.floating_w, h=self.floating_h, message="Place & resize this window then press Save!") + 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() + mon = display.get_monitor(button.get_active()) + m_s = mon.get_model() + self.overlay.set_monitor(button.get_active()) + + 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 get_channel(self): + return self.channel + + def change_bg(self, button): + c = button.get_rgba() + c = [c.red, c.green, c.blue, c.alpha] + self.overlay.set_bg(c) + + self.bg_col = c + self.save_config() + + def change_fg(self, button): + c = button.get_rgba() + c = [c.red, c.green, c.blue, c.alpha] + self.overlay.set_fg(c) + + self.fg_col = c + self.save_config() diff --git a/discover_overlay/voice_overlay.py b/discover_overlay/voice_overlay.py new file mode 100644 index 0000000..4a501fb --- /dev/null +++ b/discover_overlay/voice_overlay.py @@ -0,0 +1,313 @@ +import gi +gi.require_version("Gtk", "3.0") +gi.require_version('PangoCairo', '1.0') +gi.require_version('GdkPixbuf', '2.0') +import math +from .overlay import OverlayWindow +from gi.repository.GdkPixbuf import Pixbuf +from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo +import cairo +import urllib + + +class VoiceOverlayWindow(OverlayWindow): + def __init__(self): + OverlayWindow.__init__(self) + + self.avatars = {} + + self.avatar_size = 48 + self.text_pad = 6 + self.text_font = None + self.text_size = 13 + self.icon_spacing = 8 + self.edge_padding = 0 + + self.round_avatar = True + self.talk_col = [0.0, 0.6, 0.0, 0.1] + self.text_col = [1.0, 1.0, 1.0, 1.0] + self.norm_col = [0.0, 0.0, 0.0, 0.5] + self.wind_col = [0.0, 0.0, 0.0, 0.0] + self.mute_col = [0.7, 0.0, 0.0, 1.0] + self.userlist = [] + self.connected = False + self.force_location() + self.def_avatar = self.get_img( + "https://cdn.discordapp.com/embed/avatars/3.png") + + self.first_draw = True + + def set_bg(self, bg): + self.norm_col = bg + self.redraw() + + def set_fg(self, fg): + self.text_col = fg + self.redraw() + + def set_tk(self, tk): + self.talk_col = tk + self.redraw() + + def set_mt(self, mt): + self.mute_col = mt + self.redraw() + + def set_avatar_size(self, size): + self.avatar_size = size + self.reset_avatar() + self.redraw() + + def set_icon_spacing(self, i): + self.icon_spacing = i + self.redraw() + + def set_text_padding(self, i): + self.text_pad = i + self.redraw() + + def set_edge_padding(self, i): + self.edge_padding = i + self.redraw() + + def set_square_avatar(self, i): + self.round_avatar = not i + self.redraw() + + def set_wind_col(self): + self.col(self.wind_col) + + def set_text_col(self): + self.col(self.text_col) + + def set_norm_col(self): + self.col(self.norm_col) + + def set_talk_col(self, a=1.0): + self.col(self.talk_col, a) + + def set_mute_col(self, a=1.0): + self.col(self.mute_col, a) + + def reset_avatar(self): + self.avatars = {} + self.def_avatar = self.get_img( + "https://cdn.discordapp.com/embed/avatars/3.png") + + def set_user_list(self, userlist, alt): + self.userlist = userlist + self.userlist.sort(key=lambda x: x["username"]) + screen = self.get_screen() + c = screen.is_composited() + if not self.compositing == c: + alt = True + self.compositing = c + if alt: + self.redraw() + + def set_connection(self, connection): + is_connected = connection == "VOICE_CONNECTED" + if self.connected != is_connected: + self.connected = is_connected + self.redraw() + + def draw(self, widget, context): + # Draw + self.do_draw(context) + + def do_draw(self, context): + self.context = context + context.set_antialias(cairo.ANTIALIAS_GOOD) + + # Make background transparent + self.set_wind_col() + # Don't layer drawing over each other, always replace + context.set_operator(cairo.OPERATOR_SOURCE) + context.paint() + context.set_operator(cairo.OPERATOR_OVER) + if not self.connected: + return + + # Get size of window + (w, h) = self.get_size() + # Calculate height needed to show overlay + height = (len(self.userlist) * self.avatar_size) + \ + (len(self.userlist) + 1) * self.icon_spacing + + # Choose where to start drawing + rh = 0 + self.edge_padding + if self.align_vert == 1: + # Ignore padding? + rh = (h / 2) - (height / 2) + elif self.align_vert == 2: + rh = h - height - self.edge_padding + # Iterate users in room. + for user in self.userlist: + self.draw_avatar(context, user, rh) + # Shift the relative position down to next location + rh += self.avatar_size + self.icon_spacing + + # Don't hold a ref + self.context = None + + def get_img(self, url): + req = urllib.request.Request(url) + req.add_header( + 'Referer', 'https://streamkit.discord.com/overlay/voice') + req.add_header('User-Agent', 'Mozilla/5.0') + try: + response = urllib.request.urlopen(req) + input_stream = Gio.MemoryInputStream.new_from_data( + response.read(), None) + pixbuf = Pixbuf.new_from_stream(input_stream, None) + pixbuf = pixbuf.scale_simple(self.avatar_size, self.avatar_size, + GdkPixbuf.InterpType.BILINEAR) + return pixbuf + except: + print("Could not access : %s" % (url)) + return none + + def draw_avatar(self, context, user, y): + # Ensure pixbuf for avatar + if user["id"] not in self.avatars and user["avatar"]: + url = "https://cdn.discordapp.com/avatars/%s/%s.jpg" % ( + user["id"], user["avatar"]) + p = self.get_img(url) + if p: + self.avatars[user["id"]] = p + + (w, h) = self.get_size() + c = None + mute = False + alpha = 1.0 + if "speaking" in user and user["speaking"]: + c = self.talk_col + if "mute" in user and user["mute"]: + mute = True + if "deaf" in user and user["deaf"]: + alpha = 0.5 + pix = None + if user["id"] in self.avatars: + pix = self.avatars[user["id"]] + if self.align_right: + self.draw_text(context, user["username"], w - self.avatar_size, y) + self.draw_avatar_pix( + context, pix, w - self.avatar_size, y, c, alpha) + if mute: + self.draw_mute(context, w - self.avatar_size, y, alpha) + else: + self.draw_text(context, user["username"], self.avatar_size, y) + self.draw_avatar_pix(context, pix, 0, y, c, alpha) + if mute: + self.draw_mute(context, 0, y, alpha) + + def draw_text(self, context, string, x, y): + if self.text_font: + context.set_font_face(cairo.ToyFontFace( + self.text_font, cairo.FontSlant.NORMAL, cairo.FontWeight.NORMAL)) + context.set_font_size(self.text_size) + xb, yb, w, h, dx, dy = context.text_extents(string) + ho = (self.avatar_size / 2) - (h / 2) + if self.align_right: + context.move_to(0, 0) + self.set_norm_col() + context.rectangle(x - w - (self.text_pad * 2), y + ho - self.text_pad, + w + (self.text_pad * 4), h + (self.text_pad * 2)) + context.fill() + + self.set_text_col() + context.move_to(x - w - self.text_pad, y + ho + h) + context.show_text(string) + else: + context.move_to(0, 0) + self.set_norm_col() + context.rectangle(x - (self.text_pad * 2), y + ho - self.text_pad, + w + (self.text_pad * 4), h + (self.text_pad * 2)) + context.fill() + + self.set_text_col() + context.move_to(x + self.text_pad, y + ho + h) + context.show_text(string) + + def draw_avatar_pix(self, context, pixbuf, x, y, c, alpha): + context.move_to(x, y) + context.save() + #context.set_source_pixbuf(pixbuf, 0.0, 0.0) + if self.round_avatar: + context.arc(x + (self.avatar_size / 2), y + + (self.avatar_size / 2), self.avatar_size / 2, 0, 2 * math.pi) + context.clip() + if not pixbuf: + pixbuf = self.def_avatar + self.set_wind_col() + context.set_operator(cairo.OPERATOR_SOURCE) + context.rectangle(x, y, self.avatar_size, self.avatar_size) + context.fill() + context.set_operator(cairo.OPERATOR_OVER) + Gdk.cairo_set_source_pixbuf(context, pixbuf, x, y) + context.paint_with_alpha(alpha) + context.restore() + if c: + if self.round_avatar: + context.arc(x + (self.avatar_size / 2), y + + (self.avatar_size / 2), self.avatar_size / 2, 0, 2 * math.pi) + self.col(c) + context.stroke() + else: + context.rectangle(x, y, self.avatar_size, self.avatar_size) + self.col(c) + context.stroke() + + def draw_mute(self, context, x, y, a): + context.save() + context.translate(x, y) + context.scale(self.avatar_size, self.avatar_size) + self.set_mute_col(a) + context.save() + + # Clip Strike-through + context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) + context.set_line_width(0.1) + context.move_to(0.0, 0.0) + context.line_to(1.0, 0.0) + context.line_to(1.0, 1.0) + context.line_to(0.0, 1.0) + context.line_to(0.0, 0.0) + context.close_path() + context.new_sub_path() + context.arc(0.9, 0.1, 0.05, 1.25 * math.pi, 2.25 * math.pi) + context.arc(0.1, 0.9, 0.05, .25 * math.pi, 1.25 * math.pi) + context.close_path() + context.clip() + + # Center + context.set_line_width(0.07) + context.arc(0.5, 0.3, 0.1, math.pi, 2 * math.pi) + context.arc(0.5, 0.5, 0.1, 0, math.pi) + context.close_path() + context.fill() + + context.set_line_width(0.05) + + # Stand rounded + context.arc(0.5, 0.5, 0.15, 0, 1.0 * math.pi) + context.stroke() + + # Stand vertical + context.move_to(0.5, 0.65) + context.line_to(0.5, 0.75) + context.stroke() + + # Stand horizontal + context.move_to(0.35, 0.75) + context.line_to(0.65, 0.75) + context.stroke() + + context.restore() + # Strike through + context.arc(0.7, 0.3, 0.035, 1.25 * math.pi, 2.25 * math.pi) + context.arc(0.3, 0.7, 0.035, .25 * math.pi, 1.25 * math.pi) + context.close_path() + context.fill() + + context.restore() diff --git a/discover_overlay/voice_settings.py b/discover_overlay/voice_settings.py new file mode 100644 index 0000000..f55e972 --- /dev/null +++ b/discover_overlay/voice_settings.py @@ -0,0 +1,410 @@ +import gi +gi.require_version("Gtk", "3.0") +gi.require_version('PangoCairo', '1.0') +gi.require_version('GdkPixbuf', '2.0') +import json +from configparser import ConfigParser +from .draggable_window import DraggableWindow +from .settings import SettingsWindow +from gi.repository.GdkPixbuf import Pixbuf +from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo + + +class VoiceSettingsWindow(SettingsWindow): + def __init__(self, overlay): + Gtk.Window.__init__(self) + self.overlay = overlay + self.set_size_request(400, 200) + self.connect("destroy", self.close_window) + self.connect("delete-event", self.close_window) + self.placement_window = None + self.init_config() + + self.create_gui() + + def present(self): + self.show_all() + if not self.floating: + self.align_x_widget.show() + self.align_y_widget.show() + self.align_monitor_widget.show() + self.align_placement_widget.hide() + else: + self.align_x_widget.hide() + self.align_y_widget.hide() + self.align_monitor_widget.hide() + self.align_placement_widget.show() + + def read_config(self): + config = ConfigParser(interpolation=None) + config.read(self.configFile) + self.align_x = config.getboolean("main", "rightalign", fallback=True) + self.align_y = config.getint("main", "topalign", fallback=1) + self.bg_col = json.loads(config.get( + "main", "bg_col", fallback="[0.0,0.0,0.0,0.5]")) + self.fg_col = json.loads(config.get( + "main", "fg_col", fallback="[1.0,1.0,1.0,1.0]")) + self.tk_col = json.loads(config.get( + "main", "tk_col", fallback="[0.0,0.7,0.0,1.0]")) + self.mt_col = json.loads(config.get( + "main", "mt_col", fallback="[0.6,0.0,0.0,1.0]")) + self.avatar_size = config.getint("main", "avatar_size", fallback=48) + self.icon_spacing = config.getint("main", "icon_spacing", fallback=8) + self.text_padding = config.getint("main", "text_padding", fallback=6) + self.font = config.get("main", "font", fallback=None) + self.square_avatar = config.getboolean( + "main", "square_avatar", fallback=False) + self.monitor = config.get("main", "monitor", fallback="None") + self.edge_padding = config.getint("main", "edge_padding", fallback=0) + self.floating = config.getboolean("main", "floating", fallback=False) + self.floating_x = config.getint("main", "floating_x", fallback=0) + self.floating_y = config.getint("main", "floating_y", fallback=0) + self.floating_w = config.getint("main", "floating_w", fallback=400) + self.floating_h = config.getint("main", "floating_h", fallback=400) + + # Pass all of our config over to the overlay + self.overlay.set_align_x(self.align_x) + self.overlay.set_align_y(self.align_y) + self.overlay.set_bg(self.bg_col) + self.overlay.set_fg(self.fg_col) + self.overlay.set_tk(self.tk_col) + self.overlay.set_mt(self.mt_col) + self.overlay.set_avatar_size(self.avatar_size) + self.overlay.set_icon_spacing(self.icon_spacing) + self.overlay.set_text_padding(self.text_padding) + self.overlay.set_square_avatar(self.square_avatar) + self.overlay.set_monitor(self.get_monitor_index(self.monitor)) + self.overlay.set_edge_padding(self.edge_padding) + + self.overlay.set_floating( + self.floating, self.floating_x, self.floating_y, self.floating_w, self.floating_h) + + if self.font: + desc = Pango.FontDescription.from_string(self.font) + s = desc.get_size() + if not desc.get_size_is_absolute(): + s = s / Pango.SCALE + self.overlay.set_font(desc.get_family(), s) + + def save_config(self): + config = ConfigParser(interpolation=None) + config.read(self.configFile) + if not config.has_section("main"): + config.add_section("main") + + config.set("main", "rightalign", "%d" % (int(self.align_x))) + config.set("main", "topalign", "%d" % (self.align_y)) + config.set("main", "bg_col", json.dumps(self.bg_col)) + config.set("main", "fg_col", json.dumps(self.fg_col)) + config.set("main", "tk_col", json.dumps(self.tk_col)) + config.set("main", "mt_col", json.dumps(self.mt_col)) + config.set("main", "avatar_size", "%d" % (self.avatar_size)) + config.set("main", "icon_spacing", "%d" % (self.icon_spacing)) + config.set("main", "text_padding", "%d" % (self.text_padding)) + if self.font: + config.set("main", "font", self.font) + config.set("main", "square_avatar", "%d" % (int(self.square_avatar))) + config.set("main", "monitor", self.monitor) + config.set("main", "edge_padding", "%d" % (self.edge_padding)) + config.set("main", "floating", "%s" % (int(self.floating))) + config.set("main", "floating_x", "%s" % (self.floating_x)) + config.set("main", "floating_y", "%s" % (self.floating_y)) + config.set("main", "floating_w", "%s" % (self.floating_w)) + config.set("main", "floating_h", "%s" % (self.floating_h)) + + with open(self.configFile, 'w') as file: + config.write(file) + + def create_gui(self): + box = Gtk.Grid() + + # Font chooser + font_label = Gtk.Label.new("Font") + font = Gtk.FontButton() + if self.font: + font.set_font(self.font) + font.connect("font-set", self.change_font) + + # Colours + bg_col_label = Gtk.Label.new("Background colour") + bg_col = Gtk.ColorButton.new_with_rgba( + Gdk.RGBA(self.bg_col[0], self.bg_col[1], self.bg_col[2], self.bg_col[3])) + fg_col_label = Gtk.Label.new("Text colour") + fg_col = Gtk.ColorButton.new_with_rgba( + Gdk.RGBA(self.fg_col[0], self.fg_col[1], self.fg_col[2], self.fg_col[3])) + tk_col_label = Gtk.Label.new("Talk colour") + tk_col = Gtk.ColorButton.new_with_rgba( + Gdk.RGBA(self.tk_col[0], self.tk_col[1], self.tk_col[2], self.tk_col[3])) + mt_col_label = Gtk.Label.new("Mute colour") + mt_col = Gtk.ColorButton.new_with_rgba( + Gdk.RGBA(self.mt_col[0], self.mt_col[1], self.mt_col[2], self.mt_col[3])) + bg_col.set_use_alpha(True) + fg_col.set_use_alpha(True) + tk_col.set_use_alpha(True) + mt_col.set_use_alpha(True) + bg_col.connect("color-set", self.change_bg) + fg_col.connect("color-set", self.change_fg) + tk_col.connect("color-set", self.change_tk) + mt_col.connect("color-set", self.change_mt) + + # Avatar size + avatar_size_label = Gtk.Label.new("Avatar size") + avatar_adjustment = Gtk.Adjustment.new( + self.avatar_size, 8, 128, 1, 1, 8) + avatar_size = Gtk.SpinButton.new(avatar_adjustment, 0, 0) + avatar_size.connect("value-changed", self.change_avatar_size) + + # Monitor & Alignment + align_label = Gtk.Label.new("Overlay Location") + + align_type_box = Gtk.HBox() + align_type_edge = Gtk.RadioButton.new_with_label( + None, "Anchor to edge") + align_type_floating = Gtk.RadioButton.new_with_label_from_widget( + align_type_edge, "Floating") + if self.floating: + align_type_floating.set_active(True) + align_type_box.add(align_type_edge) + align_type_box.add(align_type_floating) + + monitor_store = Gtk.ListStore(str) + display = Gdk.Display.get_default() + for i in range(0, display.get_n_monitors()): + monitor_store.append([display.get_monitor(i).get_model()]) + monitor = Gtk.ComboBox.new_with_model(monitor_store) + monitor.set_active(self.get_monitor_index(self.monitor)) + monitor.connect("changed", self.change_monitor) + rt = Gtk.CellRendererText() + monitor.pack_start(rt, True) + monitor.add_attribute(rt, "text", 0) + + align_x_store = Gtk.ListStore(str) + align_x_store.append(["Left"]) + align_x_store.append(["Right"]) + align_x = Gtk.ComboBox.new_with_model(align_x_store) + align_x.set_active(True if self.align_x else False) + align_x.connect("changed", self.change_align_x) + rt = Gtk.CellRendererText() + align_x.pack_start(rt, True) + align_x.add_attribute(rt, "text", 0) + + align_y_store = Gtk.ListStore(str) + align_y_store.append(["Top"]) + align_y_store.append(["Middle"]) + align_y_store.append(["Bottom"]) + align_y = Gtk.ComboBox.new_with_model(align_y_store) + align_y.set_active(self.align_y) + align_y.connect("changed", self.change_align_y) + rt = Gtk.CellRendererText() + align_y.pack_start(rt, True) + align_y.add_attribute(rt, "text", 0) + + align_placement_button = Gtk.Button.new_with_label("Place Window") + + align_type_edge.connect("toggled", self.change_align_type_edge) + align_type_floating.connect("toggled", self.change_align_type_floating) + align_placement_button.connect("pressed", self.change_placement) + + self.align_x_widget = align_x + self.align_y_widget = align_y + self.align_monitor_widget = monitor + self.align_placement_widget = align_placement_button + + # Icon spacing + icon_spacing_label = Gtk.Label.new("Icon Spacing") + icon_spacing_adjustment = Gtk.Adjustment.new( + self.icon_spacing, 0, 64, 1, 1, 0) + icon_spacing = Gtk.SpinButton.new(icon_spacing_adjustment, 0, 0) + icon_spacing.connect("value-changed", self.change_icon_spacing) + + # Text padding + text_padding_label = Gtk.Label.new("Text Padding") + text_padding_adjustment = Gtk.Adjustment.new( + self.text_padding, 0, 64, 1, 1, 0) + text_padding = Gtk.SpinButton.new(text_padding_adjustment, 0, 0) + text_padding.connect("value-changed", self.change_text_padding) + + # Edge padding + edge_padding_label = Gtk.Label.new("Edge Padding") + edge_padding_adjustment = Gtk.Adjustment.new( + self.edge_padding, 0, 1000, 1, 1, 0) + edge_padding = Gtk.SpinButton.new(edge_padding_adjustment, 0, 0) + edge_padding.connect("value-changed", self.change_edge_padding) + + # Avatar shape + square_avatar_label = Gtk.Label.new("Square Avatar") + square_avatar = Gtk.CheckButton.new() + square_avatar.set_active(self.square_avatar) + square_avatar.connect("toggled", self.change_square_avatar) + + box.attach(font_label, 0, 0, 1, 1) + box.attach(font, 1, 0, 1, 1) + box.attach(bg_col_label, 0, 1, 1, 1) + box.attach(bg_col, 1, 1, 1, 1) + box.attach(fg_col_label, 0, 2, 1, 1) + box.attach(fg_col, 1, 2, 1, 1) + box.attach(tk_col_label, 0, 3, 1, 1) + box.attach(tk_col, 1, 3, 1, 1) + box.attach(mt_col_label, 0, 4, 1, 1) + box.attach(mt_col, 1, 4, 1, 1) + box.attach(avatar_size_label, 0, 5, 1, 1) + box.attach(avatar_size, 1, 5, 1, 1) + box.attach(align_label, 0, 6, 1, 5) + box.attach(align_type_box, 1, 6, 1, 1) + box.attach(monitor, 1, 7, 1, 1) + box.attach(align_x, 1, 8, 1, 1) + box.attach(align_y, 1, 9, 1, 1) + box.attach(align_placement_button, 1, 10, 1, 1) + box.attach(icon_spacing_label, 0, 11, 1, 1) + box.attach(icon_spacing, 1, 11, 1, 1) + box.attach(text_padding_label, 0, 12, 1, 1) + box.attach(text_padding, 1, 12, 1, 1) + box.attach(edge_padding_label, 0, 13, 1, 1) + box.attach(edge_padding, 1, 13, 1, 1) + box.attach(square_avatar_label, 0, 14, 1, 1) + box.attach(square_avatar, 1, 14, 1, 1) + + self.add(box) + + pass + + def change_placement(self, button): + if self.placement_window: + (x, y) = self.placement_window.get_position() + (w, h) = self.placement_window.get_size() + self.floating_x = x + self.floating_y = y + self.floating_w = w + self.floating_h = h + self.overlay.set_floating(True, x, y, w, h) + self.save_config + button.set_label("Place Window") + + self.placement_window.close() + self.placement_window = None + else: + self.placement_window = DraggableWindow( + x=self.floating_x, y=self.floating_y, + w=self.floating_w, h=self.floating_h, + message="Place & resize this window then press Save!") + 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 = button.get_font() + desc = Pango.FontDescription.from_string(font) + s = desc.get_size() + if not desc.get_size_is_absolute(): + s = s / Pango.SCALE + self.overlay.set_font(desc.get_family(), s) + + self.font = desc.to_string() + self.save_config() + + def change_bg(self, button): + c = button.get_rgba() + c = [c.red, c.green, c.blue, c.alpha] + self.overlay.set_bg(c) + + self.bg_col = c + self.save_config() + + def change_fg(self, button): + c = button.get_rgba() + c = [c.red, c.green, c.blue, c.alpha] + self.overlay.set_fg(c) + + self.fg_col = c + self.save_config() + + def change_tk(self, button): + c = button.get_rgba() + c = [c.red, c.green, c.blue, c.alpha] + self.overlay.set_tk(c) + + self.tk_col = c + self.save_config() + + def change_mt(self, button): + c = button.get_rgba() + c = [c.red, c.green, c.blue, c.alpha] + self.overlay.set_mt(c) + + self.mt_col = c + self.save_config() + + def change_avatar_size(self, button): + 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() + mon = display.get_monitor(button.get_active()) + m_s = mon.get_model() + self.overlay.set_monitor(button.get_active()) + + 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): + self.overlay.set_icon_spacing(button.get_value()) + + self.icon_spacing = int(button.get_value()) + self.save_config() + + def change_text_padding(self, button): + self.overlay.set_text_padding(button.get_value()) + + self.text_padding = button.get_value() + self.save_config() + + def change_edge_padding(self, button): + self.overlay.set_edge_padding(button.get_value()) + + self.edge_padding = button.get_value() + self.save_config() + + def change_square_avatar(self, button): + self.overlay.set_square_avatar(button.get_active()) + + self.square_avatar = button.get_active() + self.save_config()