Merge pull request #2 from trigg/master
Pull latest version into this fork
This commit is contained in:
commit
74f664d0d2
19 changed files with 1654 additions and 448 deletions
25
README.md
25
README.md
|
|
@ -20,28 +20,27 @@ python3 -m pip install .
|
|||
|
||||
## Dependencies
|
||||
|
||||
Requirements should be handled by pip.
|
||||
|
||||
Requires PyGTK3 3.22 or newer, websocket_client
|
||||
Most requirements should be handled by pip.
|
||||
|
||||
A compositor is strongly advised but there is a non-compositor mode optionally
|
||||
|
||||
It is advised to install python-gobject from your own package manager.
|
||||
|
||||
#### Debian/Ubuntu
|
||||
|
||||
`apt install python3-gi`
|
||||
|
||||
#### Arch
|
||||
|
||||
`pacman -S python-gobject`
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
run `discover-overlay` if this fails it is most likely in `~/.local/bin/discover-overlay`
|
||||
|
||||
Comes with sane-enough defaults but has a system tray icon & settings window if needed.
|
||||
Comes with sane-enough default but has a configuration screen to tweak to your own use. Configuration can be reached either via System tray or by running `discover-overlay --configure`
|
||||
|
||||
## Still in progress
|
||||
|
||||
Not all features are at the level I would like.
|
||||
|
||||
To do list:
|
||||
|
||||
- Text channels
|
||||
- Text notifications (different from above)
|
||||
- Open to suggestions
|
||||
|
||||
## Why do you keep making Discord Overlays?
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,14 @@
|
|||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""Entry point for Discover Overlay"""
|
||||
from .discover_overlay import *
|
||||
|
|
|
|||
|
|
@ -1,4 +1,16 @@
|
|||
import sys
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""A class to assist auto-start"""
|
||||
import os
|
||||
import logging
|
||||
try:
|
||||
|
|
@ -9,6 +21,8 @@ except ModuleNotFoundError:
|
|||
|
||||
|
||||
class Autostart:
|
||||
"""A class to assist auto-start"""
|
||||
|
||||
def __init__(self, app_name):
|
||||
if not app_name.endswith(".desktop"):
|
||||
app_name = "%s.desktop" % (app_name)
|
||||
|
|
@ -19,35 +33,37 @@ class Autostart:
|
|||
xdg_data_home, 'applications/'), '/usr/share/applications/']
|
||||
self.auto = self.find_auto()
|
||||
self.desktop = self.find_desktop()
|
||||
logging.info("Autostart info : desktop %s auto %s" %
|
||||
(self.desktop, self.auto))
|
||||
logging.info("Autostart info : desktop %s auto %s",
|
||||
self.desktop, self.auto)
|
||||
|
||||
def find_auto(self):
|
||||
for p in self.auto_locations:
|
||||
file = os.path.join(p, self.app_name)
|
||||
"""Check all known locations for auto-started apps"""
|
||||
for path in self.auto_locations:
|
||||
file = os.path.join(path, self.app_name)
|
||||
if os.path.exists(file):
|
||||
return file
|
||||
return None
|
||||
|
||||
def find_desktop(self):
|
||||
for p in self.desktop_locations:
|
||||
file = os.path.join(p, self.app_name)
|
||||
"""Check all known locations for desktop apps"""
|
||||
for path in self.desktop_locations:
|
||||
file = os.path.join(path, self.app_name)
|
||||
if os.path.exists(file):
|
||||
return file
|
||||
return None
|
||||
|
||||
def set_autostart(self, b):
|
||||
if b and not self.auto:
|
||||
def set_autostart(self, enable):
|
||||
"""Set or Unset auto-start state"""
|
||||
if enable and not self.auto:
|
||||
# Enable
|
||||
d = os.path.join(xdg_config_home, 'autostart')
|
||||
self.auto = os.path.join(d, self.app_name)
|
||||
directory = os.path.join(xdg_config_home, 'autostart')
|
||||
self.auto = os.path.join(directory, self.app_name)
|
||||
os.symlink(self.desktop, self.auto)
|
||||
pass
|
||||
elif not b and self.auto:
|
||||
elif not enable and self.auto:
|
||||
# Disable
|
||||
if os.path.islink(self.auto):
|
||||
os.remove(self.auto)
|
||||
pass
|
||||
|
||||
def is_auto(self):
|
||||
"""Check if it's already set to auto-start"""
|
||||
return True if self.auto else False
|
||||
|
|
|
|||
|
|
@ -1,13 +1,29 @@
|
|||
import websocket
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""The connector for discord. Connects in as if it was Streamkit for OBS or Xsplit and communicates to get voice & text info to display"""
|
||||
import select
|
||||
import time
|
||||
import json
|
||||
import requests
|
||||
import sys
|
||||
import logging
|
||||
import calendar
|
||||
import websocket
|
||||
import requests
|
||||
|
||||
|
||||
class DiscordConnector:
|
||||
"""The connector for discord. Connects in as if it was Streamkit for OBS or Xsplit and communicates to get voice & text info to display"""
|
||||
|
||||
def __init__(self, text_settings, voice_settings, text_overlay, voice_overlay):
|
||||
self.text_settings = text_settings
|
||||
self.text_overlay = text_overlay
|
||||
|
|
@ -15,7 +31,6 @@ class DiscordConnector:
|
|||
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
|
||||
|
|
@ -33,9 +48,9 @@ class DiscordConnector:
|
|||
self.last_connection = ""
|
||||
self.text = []
|
||||
self.authed = False
|
||||
self.last_text_channel = None
|
||||
|
||||
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))
|
||||
|
||||
|
|
@ -45,7 +60,7 @@ class DiscordConnector:
|
|||
x = requests.post(url, json=myobj)
|
||||
try:
|
||||
j = json.loads(x.text)
|
||||
except:
|
||||
except json.JSONDecodeError:
|
||||
j = {}
|
||||
if "access_token" in j:
|
||||
self.access_token = j["access_token"]
|
||||
|
|
@ -60,7 +75,8 @@ class DiscordConnector:
|
|||
if channel != self.current_voice:
|
||||
cn = self.channels[channel]['name']
|
||||
logging.info(
|
||||
"Joined room: %s" % (cn))
|
||||
"Joined room: %s", cn)
|
||||
self.sub_voice_channel(channel)
|
||||
self.current_voice = channel
|
||||
if need_req:
|
||||
self.req_channel_details(channel)
|
||||
|
|
@ -72,7 +88,7 @@ class DiscordConnector:
|
|||
if channel != self.current_text:
|
||||
self.current_text = channel
|
||||
logging.info(
|
||||
"Changing text room: %s" % (channel))
|
||||
"Changing text room: %s", channel)
|
||||
if need_req:
|
||||
self.req_channel_details(channel)
|
||||
|
||||
|
|
@ -87,7 +103,6 @@ class DiscordConnector:
|
|||
def add_text(self, message):
|
||||
utc_time = time.strptime(
|
||||
message["timestamp"], "%Y-%m-%dT%H:%M:%S.%f%z")
|
||||
t = time.time()
|
||||
epoch_time = calendar.timegm(utc_time)
|
||||
un = message["author"]["username"]
|
||||
if "nick" in message and message['nick'] and len(message["nick"]) > 1:
|
||||
|
|
@ -101,6 +116,7 @@ class DiscordConnector:
|
|||
'nick': un,
|
||||
'nick_col': ac,
|
||||
'time': epoch_time,
|
||||
'attach': self.get_attachment_from_message(message),
|
||||
})
|
||||
self.text_altered = True
|
||||
|
||||
|
|
@ -112,13 +128,13 @@ class DiscordConnector:
|
|||
'content': self.get_message_from_message(message_in),
|
||||
'nick': message['nick'],
|
||||
'nick_col': message['nick_col'],
|
||||
'time': message['time']}
|
||||
'time': message['time'],
|
||||
'attach': message['attach']}
|
||||
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']:
|
||||
|
|
@ -127,7 +143,9 @@ class DiscordConnector:
|
|||
return
|
||||
|
||||
def get_message_from_message(self, message):
|
||||
if "content" in message and len(message["content"]) > 0:
|
||||
if "content_parsed" in message:
|
||||
return message["content_parsed"]
|
||||
elif "content" in message and len(message["content"]) > 0:
|
||||
return message["content"]
|
||||
elif len(message["embeds"]) == 1:
|
||||
if "rawDescription" in message["embeds"][0]:
|
||||
|
|
@ -135,20 +153,33 @@ class DiscordConnector:
|
|||
if "author" in message["embeds"][0]:
|
||||
return message["embeds"][0]["author"]["name"]
|
||||
elif len(message["attachments"]) == 1:
|
||||
# Need to care
|
||||
return "-- attachment --"
|
||||
return ""
|
||||
return ""
|
||||
|
||||
def get_attachment_from_message(self, message):
|
||||
if len(message["attachments"]) == 1:
|
||||
return message["attachments"]
|
||||
return None
|
||||
|
||||
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"]
|
||||
if self.userlist[user["id"]]["avatar"] != user["avatar"]:
|
||||
olduser = self.userlist[user["id"]]
|
||||
if "mute" not in user and "mute" in olduser:
|
||||
user["mute"] = olduser["mute"]
|
||||
if "deaf" not in user and "deaf" in olduser:
|
||||
user["deaf"] = olduser["deaf"]
|
||||
if "speaking" not in user and "speaking" in olduser:
|
||||
user["speaking"] = olduser["speaking"]
|
||||
if "nick" not in user and "nick" in olduser:
|
||||
user["nick"] = olduser["nick"]
|
||||
if "lastspoken" not in user and "lastspoken" in olduser:
|
||||
user["lastspoken"] = olduser["lastspoken"]
|
||||
if olduser["avatar"] != user["avatar"]:
|
||||
self.voice_overlay.delete_avatar(user["id"])
|
||||
if "lastspoken" not in user: # Still nothing?
|
||||
user["lastspoken"] = 0 # EEEEPOOCH EEEEEPOCH! BELIEVE MEEEE
|
||||
if "speaking" not in user:
|
||||
user["speaking"] = False
|
||||
self.userlist[user["id"]] = user
|
||||
|
||||
def on_message(self, message):
|
||||
|
|
@ -162,7 +193,8 @@ class DiscordConnector:
|
|||
elif j["evt"] == "VOICE_STATE_UPDATE":
|
||||
self.list_altered = True
|
||||
thisuser = j["data"]["user"]
|
||||
un = j["data"]["user"]["username"]
|
||||
nick = j["data"]["nick"]
|
||||
thisuser["nick"] = nick
|
||||
mute = j["data"]["voice_state"]["mute"] or j["data"]["voice_state"]["self_mute"] or j["data"]["voice_state"]["suppress"]
|
||||
deaf = j["data"]["voice_state"]["deaf"] or j["data"]["voice_state"]["self_deaf"]
|
||||
thisuser["mute"] = mute
|
||||
|
|
@ -170,10 +202,12 @@ class DiscordConnector:
|
|||
self.update_user(thisuser)
|
||||
elif j["evt"] == "VOICE_STATE_CREATE":
|
||||
self.list_altered = True
|
||||
self.update_user(j["data"]["user"])
|
||||
thisuser = j["data"]["user"]
|
||||
nick = j["data"]["nick"]
|
||||
thisuser["nick"] = nick
|
||||
self.update_user(thisuser)
|
||||
# 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":
|
||||
|
|
@ -181,14 +215,14 @@ class DiscordConnector:
|
|||
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"]
|
||||
# self.sub_all_voice()
|
||||
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.userlist[j["data"]["user_id"]]["lastspoken"] = time.time()
|
||||
self.list_altered = True
|
||||
self.set_in_room(j["data"]["user_id"], True)
|
||||
elif j["evt"] == "SPEAKING_STOP":
|
||||
self.list_altered = True
|
||||
|
|
@ -222,9 +256,9 @@ class DiscordConnector:
|
|||
self.req_guilds()
|
||||
self.user = j["data"]["user"]
|
||||
logging.info(
|
||||
"ID is %s" % (self.user["id"]))
|
||||
"ID is %s", self.user["id"])
|
||||
logging.info(
|
||||
"Logged in as %s" % (self.user["username"]))
|
||||
"Logged in as %s", self.user["username"])
|
||||
self.authed = True
|
||||
return
|
||||
elif j["cmd"] == "GET_GUILDS":
|
||||
|
|
@ -241,8 +275,8 @@ class DiscordConnector:
|
|||
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"])
|
||||
# self.sub_all_voice_guild(j["nonce"])
|
||||
# self.sub_all_text_guild(j["nonce"])
|
||||
return
|
||||
elif j["cmd"] == "SUBSCRIBE":
|
||||
return
|
||||
|
|
@ -258,8 +292,11 @@ class DiscordConnector:
|
|||
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)
|
||||
thisuser = voice["user"]
|
||||
if "nick" in j["data"]:
|
||||
thisuser["nick"] = j["data"]["nick"]
|
||||
self.update_user(thisuser)
|
||||
self.set_in_room(thisuser["id"], True)
|
||||
if self.current_text == j["data"]["id"]:
|
||||
self.text = []
|
||||
for message in j["data"]["messages"]:
|
||||
|
|
@ -281,12 +318,14 @@ class DiscordConnector:
|
|||
for channel in guild["channels"]:
|
||||
channels = channels + " " + channel["name"]
|
||||
logging.info(
|
||||
u"%s: %s" % (guild["name"], channels))
|
||||
u"%s: %s", guild["name"], channels)
|
||||
self.sub_server()
|
||||
self.find_user()
|
||||
if self.last_text_channel:
|
||||
self.sub_text_channel(self.last_text_channel)
|
||||
|
||||
def on_error(self, error):
|
||||
logging.error("ERROR : %s" % (error))
|
||||
logging.error("ERROR : %s", error)
|
||||
|
||||
def on_close(self):
|
||||
logging.info("Connection closed")
|
||||
|
|
@ -308,9 +347,12 @@ class DiscordConnector:
|
|||
channel, channel))
|
||||
|
||||
def find_user(self):
|
||||
count = 0
|
||||
for channel in self.channels:
|
||||
if self.channels[channel]["type"] == 2:
|
||||
self.req_channel_details(channel)
|
||||
count += 1
|
||||
logging.warning("Getting %s rooms", count)
|
||||
|
||||
def sub_raw(self, cmd, channel, nonce):
|
||||
self.ws.send("{\"cmd\":\"SUBSCRIBE\",\"args\":{%s},\"evt\":\"%s\",\"nonce\":\"%s\"}" % (
|
||||
|
|
@ -377,7 +419,7 @@ class DiscordConnector:
|
|||
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
|
||||
self.list_altered = False
|
||||
# Update text list
|
||||
if self.text_overlay.popup_style:
|
||||
self.text_altered = True
|
||||
|
|
@ -386,31 +428,38 @@ class DiscordConnector:
|
|||
self.text_altered = False
|
||||
# Update text channels
|
||||
self.text_settings.set_channels(self.channels)
|
||||
# Update guilds
|
||||
self.text_settings.set_guilds(self.guilds)
|
||||
# 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)
|
||||
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:
|
||||
r, _w, _e = select.select((self.ws.sock,), (), (), 0)
|
||||
except websocket.WebSocketConnectionClosedException:
|
||||
self.on_close()
|
||||
return True
|
||||
return True
|
||||
|
||||
def start_listening_text(self, ch):
|
||||
if self.ws:
|
||||
self.sub_text_channel(ch)
|
||||
else:
|
||||
self.last_text_channel = ch
|
||||
|
||||
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:
|
||||
except ConnectionError as e:
|
||||
if self.error_connection:
|
||||
logging.error(e)
|
||||
self.error_connection = False
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -10,48 +10,77 @@
|
|||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
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
|
||||
"""Main application class"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
import cairo
|
||||
import base64
|
||||
import select
|
||||
import urllib
|
||||
import requests
|
||||
import websocket
|
||||
from .voice_settings import VoiceSettingsWindow
|
||||
from .text_settings import TextSettingsWindow
|
||||
import logging
|
||||
import gi
|
||||
import pidfile
|
||||
from .settings_window import MainSettingsWindow
|
||||
from .voice_overlay import VoiceOverlayWindow
|
||||
from .text_overlay import TextOverlayWindow
|
||||
from .discord_connector import DiscordConnector
|
||||
from .autostart import Autostart
|
||||
import logging
|
||||
gi.require_version("Gtk", "3.0")
|
||||
# pylint: disable=wrong-import-position,wrong-import-order
|
||||
from gi.repository import Gtk, GLib, Gio
|
||||
|
||||
try:
|
||||
from xdg.BaseDirectory import xdg_config_home
|
||||
except ModuleNotFoundError:
|
||||
from xdg import XDG_CONFIG_HOME as xdg_config_home
|
||||
|
||||
|
||||
class Discover:
|
||||
def __init__(self):
|
||||
pass
|
||||
"""Main application class"""
|
||||
|
||||
def __init__(self, rpc_file, args):
|
||||
self.ind = None
|
||||
self.tray = None
|
||||
|
||||
self.create_gui()
|
||||
self.connection = DiscordConnector(
|
||||
self.settings.text_settings, self.settings.voice_settings,
|
||||
self.text_overlay, self.voice_overlay)
|
||||
self.settings.text_settings.add_connector(self.connection)
|
||||
self.connection.connect()
|
||||
GLib.timeout_add((1000 / 60), self.connection.do_read)
|
||||
self.rpc_file = rpc_file
|
||||
rpc_file = Gio.File.new_for_path(rpc_file)
|
||||
monitor = rpc_file.monitor_file(0, None)
|
||||
monitor.connect("changed", self.rpc_changed)
|
||||
self.do_args(args)
|
||||
|
||||
Gtk.main()
|
||||
|
||||
def do_args(self, data):
|
||||
if "--help" in data:
|
||||
pass
|
||||
elif "--about" in data:
|
||||
pass
|
||||
elif "--configure" in data:
|
||||
self.show_settings()
|
||||
elif "--close" in data:
|
||||
sys.exit(0)
|
||||
|
||||
def rpc_changed(self, _a=None, _b=None, _c=None, _d=None):
|
||||
with open(self.rpc_file, "r") as tfile:
|
||||
data = tfile.readlines()
|
||||
if len(data) >= 1:
|
||||
self.do_args(data[0])
|
||||
|
||||
def create_gui(self):
|
||||
self.voice_overlay = VoiceOverlayWindow()
|
||||
self.text_overlay = TextOverlayWindow()
|
||||
self.voice_overlay = VoiceOverlayWindow(self)
|
||||
self.text_overlay = TextOverlayWindow(self)
|
||||
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)
|
||||
self.settings = MainSettingsWindow(
|
||||
self.text_overlay, self.voice_overlay)
|
||||
|
||||
def make_sys_tray_icon(self, menu):
|
||||
# Create AppIndicator
|
||||
try:
|
||||
gi.require_version('AppIndicator3', '0.1')
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from gi.repository import AppIndicator3
|
||||
self.ind = AppIndicator3.Indicator.new(
|
||||
"discover_overlay",
|
||||
|
|
@ -59,67 +88,53 @@ class Discover:
|
|||
AppIndicator3.IndicatorCategory.APPLICATION_STATUS)
|
||||
self.ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
|
||||
self.ind.set_menu(menu)
|
||||
except Exception as e:
|
||||
except ImportError as exception:
|
||||
# Create System Tray
|
||||
logging.info("Falling back to Systray : %s" % (e))
|
||||
self.tray = Gtk.StatusIcon.new_from_icon_name("discover_overlay")
|
||||
logging.info("Falling back to Systray : %s", exception)
|
||||
self.tray = Gtk.StatusIcon.new_from_icon_name("discover-overlay")
|
||||
self.tray.connect('popup-menu', self.show_menu)
|
||||
|
||||
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")
|
||||
autostart_opt = Gtk.CheckMenuItem("Start on boot")
|
||||
autostart_opt.set_active(self.a.is_auto())
|
||||
settings_opt = Gtk.MenuItem.new_with_label("Settings")
|
||||
close_opt = Gtk.MenuItem.new_with_label("Close")
|
||||
|
||||
menu.append(vsettings_opt)
|
||||
menu.append(tsettings_opt)
|
||||
menu.append(autostart_opt)
|
||||
menu.append(settings_opt)
|
||||
menu.append(close_opt)
|
||||
|
||||
vsettings_opt.connect("activate", self.show_vsettings)
|
||||
tsettings_opt.connect("activate", self.show_tsettings)
|
||||
autostart_opt.connect("toggled", self.toggle_auto)
|
||||
settings_opt.connect("activate", self.show_settings)
|
||||
close_opt.connect("activate", self.close)
|
||||
menu.show_all()
|
||||
return menu
|
||||
|
||||
def toggle_auto(self, button):
|
||||
self.a.set_autostart(button.get_active())
|
||||
|
||||
def show_menu(self, obj, button, time):
|
||||
self.menu.show_all()
|
||||
self.menu.popup(
|
||||
None, None, Gtk.StatusIcon.position_menu, obj, button, time)
|
||||
|
||||
def show_vsettings(self, obj=None, data=None):
|
||||
self.voice_settings.present()
|
||||
def show_settings(self, _obj=None, _data=None):
|
||||
self.settings.present_settings()
|
||||
|
||||
def show_tsettings(self, obj=None, data=None):
|
||||
self.text_settings.present()
|
||||
|
||||
def close(self, a=None, b=None, c=None):
|
||||
def close(self, _a=None, _b=None, _c=None):
|
||||
Gtk.main_quit()
|
||||
|
||||
def main(self):
|
||||
self.a = Autostart("discover_overlay")
|
||||
# a.set_autostart(True)
|
||||
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)
|
||||
|
||||
try:
|
||||
Gtk.main()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def entrypoint():
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
discover = Discover()
|
||||
discover.main()
|
||||
configDir = os.path.join(xdg_config_home, "discover_overlay")
|
||||
os.makedirs(configDir, exist_ok=True)
|
||||
line = ""
|
||||
for arg in sys.argv[1:]:
|
||||
line = "%s %s" % (line, arg)
|
||||
pid_file = os.path.join(configDir, "discover_overlay.pid")
|
||||
rpc_file = os.path.join(configDir, "discover_overlay.rpc")
|
||||
try:
|
||||
with pidfile.PIDFile(pid_file):
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
Discover(rpc_file, line)
|
||||
except pidfile.AlreadyRunningError:
|
||||
logging.warning("Discover overlay is currently running")
|
||||
|
||||
with open(rpc_file, "w") as tfile:
|
||||
tfile.write(line)
|
||||
logging.warning("Sent RPC command")
|
||||
|
|
|
|||
|
|
@ -1,15 +1,27 @@
|
|||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""An X11 window which can be moved and resized"""
|
||||
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
|
||||
import logging
|
||||
gi.require_version("Gtk", "3.0")
|
||||
# pylint: disable=wrong-import-position
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
|
||||
class DraggableWindow(Gtk.Window):
|
||||
def __init__(self, x=0, y=0, w=300, h=300, message="Message"):
|
||||
"""An X11 window which can be moved and resized"""
|
||||
|
||||
def __init__(self, x=0, y=0, w=300, h=300, message="Message", settings=None):
|
||||
Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP)
|
||||
if w < 100:
|
||||
w = 100
|
||||
|
|
@ -19,10 +31,11 @@ class DraggableWindow(Gtk.Window):
|
|||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.settings = settings
|
||||
self.message = message
|
||||
self.set_size_request(50, 50)
|
||||
|
||||
self.connect('draw', self.draw)
|
||||
self.connect('draw', self.dodraw)
|
||||
self.connect('motion-notify-event', self.drag)
|
||||
self.connect('button-press-event', self.button_press)
|
||||
self.connect('button-release-event', self.button_release)
|
||||
|
|
@ -47,28 +60,15 @@ class DraggableWindow(Gtk.Window):
|
|||
self.show_all()
|
||||
|
||||
def force_location(self):
|
||||
"""Move the window to previously given co-ords. Also double check sanity on layer & decorations"""
|
||||
|
||||
self.set_decorated(False)
|
||||
self.set_keep_above(True)
|
||||
display = Gdk.Display.get_default()
|
||||
if "get_monitor" in dir(display):
|
||||
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
|
||||
else:
|
||||
screen = display.get_default_screen()
|
||||
w = screen.width()
|
||||
h = screen.height()
|
||||
x = 0
|
||||
y = 0
|
||||
#self.resize(400, h)
|
||||
self.move(self.x, self.y)
|
||||
self.resize(self.w, self.h)
|
||||
|
||||
def drag(self, w, event):
|
||||
def drag(self, _w, event):
|
||||
"""Called by GTK while mouse is moving over window. Used to resize and move"""
|
||||
if event.state & Gdk.ModifierType.BUTTON1_MASK:
|
||||
if self.drag_type == 1:
|
||||
# Center is move
|
||||
|
|
@ -94,6 +94,7 @@ class DraggableWindow(Gtk.Window):
|
|||
self.force_location()
|
||||
|
||||
def button_press(self, w, event):
|
||||
"""Called when a mouse button is pressed on this window"""
|
||||
(w, h) = self.get_size()
|
||||
if not self.drag_type:
|
||||
self.drag_type = 1
|
||||
|
|
@ -105,10 +106,12 @@ class DraggableWindow(Gtk.Window):
|
|||
self.drag_x = event.x
|
||||
self.drag_y = event.y
|
||||
|
||||
def button_release(self, w, event):
|
||||
def button_release(self, _w, _event):
|
||||
"""Called when a mouse button is released"""
|
||||
self.drag_type = None
|
||||
|
||||
def draw(self, widget, context):
|
||||
def dodraw(self, _widget, context):
|
||||
"""Draw our window."""
|
||||
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)
|
||||
|
|
@ -119,7 +122,7 @@ class DraggableWindow(Gtk.Window):
|
|||
|
||||
# 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)
|
||||
_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)
|
||||
|
||||
|
|
@ -130,3 +133,9 @@ class DraggableWindow(Gtk.Window):
|
|||
|
||||
context.rectangle(0, sh - 32, sw, 32)
|
||||
context.fill()
|
||||
|
||||
def get_coords(self):
|
||||
"""Return window position and size"""
|
||||
(x, y) = self.get_position()
|
||||
(w, h) = self.get_size()
|
||||
return (x, y, w, h)
|
||||
|
|
|
|||
161
discover_overlay/draggable_window_wayland.py
Normal file
161
discover_overlay/draggable_window_wayland.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""A Wayland full-screen window which can be moved and resized"""
|
||||
import cairo
|
||||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
# pylint: disable=wrong-import-position
|
||||
from gi.repository import Gtk, Gdk
|
||||
try:
|
||||
from gi.repository import GtkLayerShell
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class DraggableWindowWayland(Gtk.Window):
|
||||
"""A Wayland full-screen window which can be moved and resized"""
|
||||
|
||||
def __init__(self, x=0, y=0, w=300, h=300, message="Message", settings=None):
|
||||
Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL)
|
||||
if w < 100:
|
||||
w = 100
|
||||
if h < 100:
|
||||
h = 100
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.settings = settings
|
||||
self.message = message
|
||||
self.set_size_request(50, 50)
|
||||
|
||||
self.connect('draw', self.dodraw)
|
||||
self.connect('motion-notify-event', self.drag)
|
||||
self.connect('button-press-event', self.button_press)
|
||||
self.connect('button-release-event', self.button_release)
|
||||
|
||||
self.set_app_paintable(True)
|
||||
self.monitor = 0
|
||||
|
||||
self.drag_type = None
|
||||
self.drag_x = 0
|
||||
self.drag_y = 0
|
||||
GtkLayerShell.init_for_window(self)
|
||||
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP)
|
||||
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.LEFT, True)
|
||||
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.RIGHT, True)
|
||||
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.BOTTOM, True)
|
||||
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.TOP, True)
|
||||
|
||||
self.show_all()
|
||||
# self.force_location()
|
||||
|
||||
def force_location(self):
|
||||
"""Move the window to previously given co-ords. In wayland just clip to current screen"""
|
||||
(size_x, size_y) = self.get_size()
|
||||
if self.x < 0:
|
||||
self.x = 0
|
||||
if self.y < 0:
|
||||
self.y = 0
|
||||
if self.x + self.w > size_x:
|
||||
self.x = size_x - self.w
|
||||
if self.y + self.h > size_y:
|
||||
self.y = size_y - self.h
|
||||
self.queue_draw()
|
||||
|
||||
def drag(self, _w, event):
|
||||
"""Called by GTK while mouse is moving over window. Used to resize and move"""
|
||||
if event.state & Gdk.ModifierType.BUTTON1_MASK:
|
||||
if self.drag_type == 1:
|
||||
# Center is move
|
||||
self.x += event.x - self.drag_x
|
||||
self.y += event.y - self.drag_y
|
||||
self.drag_x = event.x
|
||||
self.drag_y = event.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):
|
||||
"""Called when a mouse button is pressed on this window"""
|
||||
px = event.x - self.x
|
||||
py = event.y - self.y
|
||||
|
||||
if not self.drag_type:
|
||||
self.drag_type = 1
|
||||
# Where in the window did we press?
|
||||
if px < 20 and py < 20:
|
||||
self.settings.change_placement(None)
|
||||
if py > self.h - 32:
|
||||
self.drag_type += 2
|
||||
if px > self.w - 32:
|
||||
self.drag_type += 1
|
||||
self.drag_x = event.x
|
||||
self.drag_y = event.y
|
||||
|
||||
def button_release(self, _w, _event):
|
||||
"""Called when a mouse button is released"""
|
||||
self.drag_type = None
|
||||
|
||||
def dodraw(self, _widget, context):
|
||||
"""Draw our window. For wayland we're secretly a fullscreen app and need to draw only a single rectangle of the overlay"""
|
||||
context.translate(self.x, self.y)
|
||||
context.save()
|
||||
context.rectangle(0, 0, self.w, self.h)
|
||||
context.clip()
|
||||
|
||||
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()
|
||||
# Get size of window
|
||||
|
||||
# Draw text
|
||||
context.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
_xb, _yb, width, height, _dx, _dy = context.text_extents(self.message)
|
||||
context.move_to(self.w / 2 - width / 2, self.h / 2 - height / 2)
|
||||
context.show_text(self.message)
|
||||
|
||||
# Draw resizing edges
|
||||
context.set_source_rgba(0.0, 0.0, 1.0, 0.5)
|
||||
context.rectangle(self.w - 32, 0, 32, self.h)
|
||||
context.fill()
|
||||
|
||||
context.rectangle(0, self.h - 32, self.w, 32)
|
||||
context.fill()
|
||||
|
||||
# Draw Done!
|
||||
context.set_source_rgba(0.0, 1.0, 0.0, 0.5)
|
||||
context.rectangle(0, 0, 20, 20)
|
||||
context.fill()
|
||||
context.restore()
|
||||
|
||||
def get_coords(self):
|
||||
"""Return the position and size of the window"""
|
||||
return (self.x, self.y, self.w, self.h)
|
||||
91
discover_overlay/general_settings.py
Normal file
91
discover_overlay/general_settings.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""Core Settings Tab"""
|
||||
from configparser import ConfigParser
|
||||
import gi
|
||||
from .settings import SettingsWindow
|
||||
from .autostart import Autostart
|
||||
gi.require_version("Gtk", "3.0")
|
||||
# pylint: disable=wrong-import-position
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class GeneralSettingsWindow(SettingsWindow):
|
||||
"""Core Settings Tab"""
|
||||
|
||||
def __init__(self, overlay, overlay2):
|
||||
SettingsWindow.__init__(self)
|
||||
self.overlay = overlay
|
||||
self.overlay2 = overlay2
|
||||
self.xshape = None
|
||||
self.autostart = None
|
||||
self.set_size_request(400, 200)
|
||||
self.connect("destroy", self.close_window)
|
||||
self.connect("delete-event", self.close_window)
|
||||
self.init_config()
|
||||
self.a = Autostart("discover_overlay")
|
||||
self.placement_window = None
|
||||
|
||||
self.create_gui()
|
||||
|
||||
def read_config(self):
|
||||
config = ConfigParser(interpolation=None)
|
||||
config.read(self.configFile)
|
||||
self.xshape = config.getboolean("general", "xshape", fallback=False)
|
||||
|
||||
# Pass all of our config over to the overlay
|
||||
self.overlay.set_force_xshape(self.xshape)
|
||||
self.overlay2.set_force_xshape(self.xshape)
|
||||
|
||||
def save_config(self):
|
||||
config = ConfigParser(interpolation=None)
|
||||
config.read(self.configFile)
|
||||
if not config.has_section("general"):
|
||||
config.add_section("general")
|
||||
|
||||
config.set("general", "xshape", "%d" % (int(self.xshape)))
|
||||
|
||||
with open(self.configFile, 'w') as file:
|
||||
config.write(file)
|
||||
|
||||
def create_gui(self):
|
||||
box = Gtk.Grid()
|
||||
|
||||
# Auto start
|
||||
autostart_label = Gtk.Label.new("Autostart on boot")
|
||||
autostart = Gtk.CheckButton.new()
|
||||
autostart.set_active(self.a.is_auto())
|
||||
autostart.connect("toggled", self.change_autostart)
|
||||
|
||||
# Force XShape
|
||||
xshape_label = Gtk.Label.new("Force XShape")
|
||||
xshape = Gtk.CheckButton.new()
|
||||
xshape.set_active(self.xshape)
|
||||
xshape.connect("toggled", self.change_xshape)
|
||||
|
||||
box.attach(autostart_label, 0, 0, 1, 1)
|
||||
box.attach(autostart, 1, 0, 1, 1)
|
||||
box.attach(xshape_label, 0, 1, 1, 1)
|
||||
box.attach(xshape, 1, 1, 1, 1)
|
||||
|
||||
self.add(box)
|
||||
|
||||
def change_autostart(self, button):
|
||||
self.autostart = button.get_active()
|
||||
self.a.set_autostart(self.autostart)
|
||||
|
||||
def change_xshape(self, button):
|
||||
self.overlay.set_force_xshape(button.get_active())
|
||||
self.overlay2.set_force_xshape(button.get_active())
|
||||
self.xshape = button.get_active()
|
||||
self.save_config()
|
||||
|
|
@ -1,18 +1,34 @@
|
|||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version('PangoCairo', '1.0')
|
||||
gi.require_version('GdkPixbuf', '2.0')
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""Functions & Classes to assist image loading."""
|
||||
import urllib
|
||||
import threading
|
||||
from gi.repository.GdkPixbuf import Pixbuf
|
||||
from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo
|
||||
import logging
|
||||
import gi
|
||||
import requests
|
||||
import cairo
|
||||
import PIL.Image as Image
|
||||
gi.require_version('GdkPixbuf', '2.0')
|
||||
# pylint: disable=wrong-import-position
|
||||
from gi.repository import Gio, GdkPixbuf
|
||||
|
||||
|
||||
class Image_Getter():
|
||||
def __init__(self, func, url, id, size):
|
||||
class ImageGetter():
|
||||
"""Older attempt. Not advised as it can't manage anything but PNG. Loads to GDK Pixmap"""
|
||||
|
||||
def __init__(self, func, url, identifier, size):
|
||||
self.func = func
|
||||
self.id = id
|
||||
self.id = identifier
|
||||
self.url = url
|
||||
self.size = size
|
||||
|
||||
|
|
@ -25,17 +41,128 @@ class Image_Getter():
|
|||
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.size, self.size,
|
||||
GdkPixbuf.InterpType.BILINEAR)
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_stream(input_stream, None)
|
||||
if self.size:
|
||||
pixbuf = pixbuf.scale_simple(self.size, self.size,
|
||||
GdkPixbuf.InterpType.BILINEAR)
|
||||
|
||||
self.func(self.id, pixbuf)
|
||||
except Exception as e:
|
||||
except urllib.error.URLError as exception:
|
||||
logging.error(
|
||||
"Could not access : %s" % (url))
|
||||
logging.error(e)
|
||||
"Could not access : %s", self.url)
|
||||
logging.error(exception)
|
||||
|
||||
|
||||
def get_image(func, id, ava, size):
|
||||
image_getter = Image_Getter(func, id, ava, size)
|
||||
class SurfaceGetter():
|
||||
"""Download and decode image using PIL and store as a cairo surface"""
|
||||
|
||||
def __init__(self, func, url, identifier, size):
|
||||
self.func = func
|
||||
self.id = identifier
|
||||
self.url = url
|
||||
self.size = size
|
||||
|
||||
def get_url(self):
|
||||
"""Downloads and decodes"""
|
||||
try:
|
||||
resp = requests.get(self.url, stream=True, headers={
|
||||
'Referer': 'https://streamkit.discord.com/overlay/voice', 'User-Agent': 'Mozilla/5.0'})
|
||||
raw = resp.raw
|
||||
im = Image.open(raw)
|
||||
surf = self.from_pil(im)
|
||||
|
||||
self.func(self.id, surf)
|
||||
except requests.HTTPError:
|
||||
logging.error("Unable to open %s", self.url)
|
||||
except requests.TooManyRedirects:
|
||||
logging.error("Unable to open %s - Too many redirects", self.url)
|
||||
except requests.Timeout:
|
||||
logging.error("Unable to open %s - Timeout", self.url)
|
||||
except requests.ConnectionError:
|
||||
logging.error("Unable to open %s - Connection error", self.url)
|
||||
except ValueError:
|
||||
logging.error("Unable to read %s", self.url)
|
||||
except TypeError:
|
||||
logging.error("Unable to read %s", self.url)
|
||||
|
||||
def from_pil(self, im, alpha=1.0):
|
||||
"""
|
||||
:param im: Pillow Image
|
||||
:param alpha: 0..1 alpha to add to non-alpha images
|
||||
:param format: Pixel format for output surface
|
||||
"""
|
||||
if 'A' not in im.getbands():
|
||||
im.putalpha(int(alpha * 256.))
|
||||
arr = bytearray(im.tobytes('raw', 'BGRa'))
|
||||
surface = cairo.ImageSurface.create_for_data(
|
||||
arr, cairo.FORMAT_ARGB32, im.width, im.height)
|
||||
return surface
|
||||
|
||||
|
||||
def get_image(func, identifier, ava, size):
|
||||
"""Download to GDK Pixmap"""
|
||||
image_getter = ImageGetter(func, identifier, ava, size)
|
||||
t = threading.Thread(target=image_getter.get_url, args=())
|
||||
t.start()
|
||||
|
||||
|
||||
def get_surface(func, identifier, ava, size):
|
||||
"""Download to cairo surface"""
|
||||
image_getter = SurfaceGetter(func, identifier, ava, size)
|
||||
t = threading.Thread(target=image_getter.get_url, args=())
|
||||
t.start()
|
||||
|
||||
|
||||
def get_aspected_size(img, w, h, anchor=0, hanchor=0):
|
||||
"""Get dimensions of image keeping current aspect ratio"""
|
||||
px = img.get_width()
|
||||
py = img.get_height()
|
||||
if py < 1 or h < 1:
|
||||
return (0, 0, 0, 0)
|
||||
img_aspect = px / py
|
||||
rect_aspect = w / h
|
||||
|
||||
y = 0
|
||||
x = 0
|
||||
if img_aspect > rect_aspect:
|
||||
oh = h
|
||||
h = w / img_aspect
|
||||
if anchor == 0:
|
||||
y = y + (oh - h)
|
||||
if anchor == 1:
|
||||
y = y + ((oh - h) / 2)
|
||||
elif img_aspect < rect_aspect:
|
||||
ow = w
|
||||
w = h * img_aspect
|
||||
if hanchor == 2:
|
||||
x = x + (ow - w)
|
||||
if hanchor == 1:
|
||||
x = x + ((ow - w) / 2)
|
||||
return (x, y, w, h)
|
||||
|
||||
|
||||
def draw_img_to_rect(img, ctx, x, y, w, h, path=False, aspect=False, anchor=0, hanchor=0):
|
||||
"""Draw cairo surface onto context"""
|
||||
# Path - only add the path do not fill : True/False
|
||||
# Aspect - keep aspect ratio : True/False
|
||||
# Anchor - with aspect : 0=left 1=middle 2=right
|
||||
# HAnchor - with apect : 0=bottom 1=middle 2=top
|
||||
ctx.save()
|
||||
px = img.get_width()
|
||||
py = img.get_height()
|
||||
x_off = 0
|
||||
y_off = 0
|
||||
if aspect:
|
||||
(x_off, y_off, w, h) = get_aspected_size(
|
||||
img, w, h, anchor=anchor, hanchor=hanchor)
|
||||
|
||||
ctx.translate(x + x_off, y + y_off)
|
||||
ctx.scale(w, h)
|
||||
ctx.scale(1 / px, 1 / py)
|
||||
ctx.set_source_surface(img, 0, 0)
|
||||
|
||||
ctx.rectangle(0, 0, px, py)
|
||||
if not path:
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
return (w, h)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,55 @@
|
|||
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
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""Overlay parent class. Helpful if we need more overlay types without copy-and-pasting too much code"""
|
||||
import sys
|
||||
import logging
|
||||
import gi
|
||||
import cairo
|
||||
gi.require_version("Gtk", "3.0")
|
||||
# pylint: disable=wrong-import-position
|
||||
from gi.repository import Gtk, Gdk
|
||||
try:
|
||||
from gi.repository import GtkLayerShell
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class OverlayWindow(Gtk.Window):
|
||||
"""Overlay parent class. Helpful if we need more overlay types without copy-and-pasting too much code"""
|
||||
|
||||
def detect_type(self):
|
||||
window = Gtk.Window()
|
||||
screen = window.get_screen()
|
||||
screen_type = "%s" % (screen)
|
||||
self.is_wayland = False
|
||||
if "Wayland" in screen_type:
|
||||
self.is_wayland = True
|
||||
return Gtk.WindowType.TOPLEVEL
|
||||
return Gtk.WindowType.POPUP
|
||||
|
||||
def __init__(self):
|
||||
Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP)
|
||||
self.set_size_request(50, 50)
|
||||
|
||||
self.connect('draw', self.draw)
|
||||
|
||||
Gtk.Window.__init__(self, type=self.detect_type())
|
||||
screen = self.get_screen()
|
||||
self.compositing = False
|
||||
self.text_font = None
|
||||
self.text_size = None
|
||||
self.x = None
|
||||
self.y = None
|
||||
self.w = None
|
||||
self.h = None
|
||||
|
||||
self.set_size_request(50, 50)
|
||||
self.connect('draw', self.overlay_draw)
|
||||
# Set RGBA
|
||||
screen = self.get_screen()
|
||||
visual = screen.get_rgba_visual()
|
||||
|
|
@ -33,17 +67,29 @@ class OverlayWindow(Gtk.Window):
|
|||
self.set_untouchable()
|
||||
self.set_skip_pager_hint(True)
|
||||
self.set_skip_taskbar_hint(True)
|
||||
self.set_keep_above(True)
|
||||
self.set_decorated(True)
|
||||
self.set_accept_focus(False)
|
||||
self.set_wayland_state()
|
||||
|
||||
self.show_all()
|
||||
self.monitor = 0
|
||||
self.align_right = True
|
||||
self.align_vert = 1
|
||||
self.floating = False
|
||||
self.force_xshape = False
|
||||
self.context = None
|
||||
|
||||
def draw(self, widget, context):
|
||||
pass
|
||||
def set_wayland_state(self):
|
||||
if self.is_wayland:
|
||||
GtkLayerShell.init_for_window(self)
|
||||
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP)
|
||||
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.LEFT, True)
|
||||
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.RIGHT, True)
|
||||
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.BOTTOM, True)
|
||||
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.TOP, True)
|
||||
|
||||
def do_draw(self, context):
|
||||
def overlay_draw(self, _w, context, data=None):
|
||||
pass
|
||||
|
||||
def set_font(self, name, size):
|
||||
|
|
@ -74,53 +120,65 @@ class OverlayWindow(Gtk.Window):
|
|||
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()
|
||||
if "get_monitor" in dir(display):
|
||||
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)
|
||||
if not self.is_wayland:
|
||||
self.set_decorated(False)
|
||||
self.set_keep_above(True)
|
||||
display = Gdk.Display.get_default()
|
||||
if "get_monitor" in dir(display):
|
||||
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)
|
||||
else:
|
||||
self.move(self.x, self.y)
|
||||
self.resize(self.w, self.h)
|
||||
else:
|
||||
if not self.floating:
|
||||
screen = display.get_default_screen()
|
||||
w = screen.width()
|
||||
h = screen.height()
|
||||
x = 0
|
||||
y = 0
|
||||
else:
|
||||
self.move(self.x, self.y)
|
||||
self.resize(self.w, self.h)
|
||||
if not self.floating:
|
||||
screen = display.get_default_screen()
|
||||
w = screen.width()
|
||||
h = screen.height()
|
||||
x = 0
|
||||
y = 0
|
||||
else:
|
||||
self.move(self.x, self.y)
|
||||
self.resize(self.w, self.h)
|
||||
|
||||
if not self.floating:
|
||||
(w, h) = self.get_size()
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.redraw()
|
||||
|
||||
def redraw(self):
|
||||
gdkwin = self.get_window()
|
||||
if not self.floating:
|
||||
(w, h) = self.get_size()
|
||||
self.w = w
|
||||
self.h = h
|
||||
|
||||
if gdkwin:
|
||||
if not self.compositing:
|
||||
if not self.compositing or self.force_xshape:
|
||||
(w, h) = self.get_size()
|
||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h)
|
||||
surface_ctx = cairo.Context(surface)
|
||||
self.do_draw(surface_ctx)
|
||||
self.overlay_draw(None, 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):
|
||||
def set_monitor(self, idx=None, mon=None):
|
||||
self.monitor = idx
|
||||
if self.is_wayland:
|
||||
if mon:
|
||||
GtkLayerShell.set_monitor(self, mon)
|
||||
self.force_location()
|
||||
self.redraw()
|
||||
|
||||
|
|
@ -136,3 +194,6 @@ class OverlayWindow(Gtk.Window):
|
|||
|
||||
def col(self, c, a=1.0):
|
||||
self.context.set_source_rgba(c[0], c[1], c[2], c[3] * a)
|
||||
|
||||
def set_force_xshape(self, force):
|
||||
self.force_xshape = force
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""Settings tab parent class. Helpful if we need more overlay types without copy-and-pasting too much code"""
|
||||
import os
|
||||
import logging
|
||||
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
|
||||
import logging
|
||||
# pylint: disable=wrong-import-position
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
|
||||
try:
|
||||
|
|
@ -15,14 +25,27 @@ except ModuleNotFoundError:
|
|||
from xdg import XDG_CONFIG_HOME as xdg_config_home
|
||||
|
||||
|
||||
class SettingsWindow(Gtk.Window):
|
||||
class SettingsWindow(Gtk.VBox):
|
||||
"""Settings tab parent class. Helpful if we need more overlay types without copy-and-pasting too much code"""
|
||||
|
||||
def __init__(self):
|
||||
Gtk.VBox.__init__(self)
|
||||
self.placement_window = None
|
||||
self.configDir = None
|
||||
self.configFile = None
|
||||
self.overlay = None
|
||||
self.floating_x = None
|
||||
self.floating_y = None
|
||||
self.floating_w = None
|
||||
self.floating_h = None
|
||||
|
||||
def init_config(self):
|
||||
self.configDir = os.path.join(xdg_config_home, "discover_overlay")
|
||||
os.makedirs(self.configDir, exist_ok=True)
|
||||
self.configFile = os.path.join(self.configDir, "config.ini")
|
||||
self.read_config()
|
||||
|
||||
def close_window(self, a=None, b=None):
|
||||
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()
|
||||
|
|
@ -44,8 +67,24 @@ class SettingsWindow(Gtk.Window):
|
|||
if display.get_monitor(i).get_model() == name:
|
||||
return i
|
||||
logging.info(
|
||||
"Could not find monitor : %s" % (name))
|
||||
"Could not find monitor : %s", name)
|
||||
return 0
|
||||
|
||||
def present(self):
|
||||
def get_monitor_obj(self, name):
|
||||
display = Gdk.Display.get_default()
|
||||
if "get_n_monitors" in dir(display):
|
||||
for i in range(0, display.get_n_monitors()):
|
||||
if display.get_monitor(i).get_model() == name:
|
||||
return display.get_monitor(i)
|
||||
logging.info(
|
||||
"Could not find monitor : %s", name)
|
||||
return None
|
||||
|
||||
def present_settings(self):
|
||||
self.show_all()
|
||||
|
||||
def read_config(self):
|
||||
pass
|
||||
|
||||
def save_config(self):
|
||||
pass
|
||||
|
|
|
|||
67
discover_overlay/settings_window.py
Normal file
67
discover_overlay/settings_window.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""Settings window holding all settings tab"""
|
||||
import gi
|
||||
from .voice_settings import VoiceSettingsWindow
|
||||
from .text_settings import TextSettingsWindow
|
||||
from .general_settings import GeneralSettingsWindow
|
||||
gi.require_version("Gtk", "3.0")
|
||||
# pylint: disable=wrong-import-position
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class MainSettingsWindow(Gtk.Window):
|
||||
"""Settings window holding all settings tab"""
|
||||
|
||||
def __init__(self, text_overlay, voice_overlay):
|
||||
Gtk.Window.__init__(self)
|
||||
|
||||
self.connect("destroy", self.close_window)
|
||||
self.connect("delete-event", self.close_window)
|
||||
|
||||
self.text_overlay = text_overlay
|
||||
self.voice_overlay = voice_overlay
|
||||
self.set_title("Discover Overlay Configuration")
|
||||
self.set_icon_name("discover-overlay")
|
||||
self.set_default_size(280, 180)
|
||||
|
||||
# Create
|
||||
nb = Gtk.Notebook()
|
||||
# nb.set_tab_pos(Gtk.POS_TOP)
|
||||
|
||||
self.voice_settings = VoiceSettingsWindow(self.voice_overlay)
|
||||
nb.append_page(self.voice_settings)
|
||||
nb.set_tab_label_text(self.voice_settings, "Voice")
|
||||
self.text_settings = TextSettingsWindow(self.text_overlay)
|
||||
nb.append_page(self.text_settings)
|
||||
nb.set_tab_label_text(self.text_settings, "Text")
|
||||
self.core_settings = GeneralSettingsWindow(
|
||||
self.text_overlay, self.voice_overlay)
|
||||
nb.append_page(self.core_settings)
|
||||
nb.set_tab_label_text(self.core_settings, "Core")
|
||||
self.add(nb)
|
||||
self.nb = nb
|
||||
|
||||
def close_window(self, a=None, b=None):
|
||||
self.text_settings.close_window(a, b)
|
||||
self.voice_settings.close_window(a, b)
|
||||
self.core_settings.close_window(a, b)
|
||||
self.hide()
|
||||
return True
|
||||
|
||||
def present_settings(self):
|
||||
self.voice_settings.present_settings()
|
||||
self.text_settings.present_settings()
|
||||
self.core_settings.present_settings()
|
||||
self.nb.show()
|
||||
self.show()
|
||||
|
|
@ -1,27 +1,55 @@
|
|||
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
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""Overlay window for text"""
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
import cairo
|
||||
import gi
|
||||
from .image_getter import get_surface, draw_img_to_rect, get_aspected_size
|
||||
from .overlay import OverlayWindow
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version('PangoCairo', '1.0')
|
||||
# pylint: disable=wrong-import-position
|
||||
from gi.repository import Pango, PangoCairo
|
||||
|
||||
|
||||
class TextOverlayWindow(OverlayWindow):
|
||||
def __init__(self):
|
||||
"""Overlay window for voice"""
|
||||
|
||||
def __init__(self, discover):
|
||||
OverlayWindow.__init__(self)
|
||||
self.discover = discover
|
||||
self.text_spacing = 4
|
||||
self.content = []
|
||||
self.text_font = None
|
||||
self.text_size = 13
|
||||
self.text_time = None
|
||||
self.show_attach = None
|
||||
self.popup_style = None
|
||||
# 0, 0, self.text_size, self.text_size)
|
||||
self.pango_rect = Pango.Rectangle()
|
||||
self.pango_rect.width = self.text_size * Pango.SCALE
|
||||
self.pango_rect.height = self.text_size * Pango.SCALE
|
||||
|
||||
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]
|
||||
#self.text_time = 30
|
||||
self.attachment = {}
|
||||
|
||||
self.imgList = []
|
||||
self.imgFinder = re.compile(r"`")
|
||||
self.set_title("Discover Text")
|
||||
|
||||
def set_text_time(self, t):
|
||||
self.text_time = t
|
||||
|
|
@ -45,58 +73,194 @@ class TextOverlayWindow(OverlayWindow):
|
|||
self.bg_col = bg_col
|
||||
self.redraw()
|
||||
|
||||
def set_show_attach(self, att):
|
||||
self.show_attach = att
|
||||
self.redraw()
|
||||
|
||||
def set_popup_style(self, b):
|
||||
self.popup_style = b
|
||||
|
||||
def do_draw(self, context):
|
||||
self.context = context
|
||||
context.set_antialias(self.compositing)
|
||||
(w, h) = self.get_size()
|
||||
def set_font(self, name, size):
|
||||
self.text_font = name
|
||||
self.text_size = size
|
||||
self.pango_rect = Pango.Rectangle()
|
||||
self.pango_rect.width = self.text_size * Pango.SCALE
|
||||
self.pango_rect.height = self.text_size * Pango.SCALE
|
||||
self.redraw()
|
||||
|
||||
def make_line(self, msg):
|
||||
ret = ""
|
||||
if isinstance(msg, list):
|
||||
for a in msg:
|
||||
ret = "%s%s" % (ret, self.make_line(a))
|
||||
elif isinstance(msg, str):
|
||||
ret = msg
|
||||
elif msg['type'] == 'strong':
|
||||
ret = "<b>%s</b>" % (self.make_line(msg['content']))
|
||||
elif msg['type'] == 'text':
|
||||
ret = self.santize_string(msg['content'])
|
||||
elif msg['type'] == 'link':
|
||||
ret = "<u>%s</u>" % (self.make_line(msg['content']))
|
||||
elif msg['type'] == 'emoji':
|
||||
if 'surrogate' in msg:
|
||||
# ['src'] is SVG URL
|
||||
#ret = msg
|
||||
ret = msg['surrogate']
|
||||
else:
|
||||
### Add Image ###
|
||||
url = ("https://cdn.discordapp.com/emojis/%s.png?v=1" %
|
||||
(msg['emojiId']))
|
||||
img = {"url": url}
|
||||
self.imgList.append(img)
|
||||
ret = "`"
|
||||
elif msg['type'] == 'inlineCode' or msg['type'] == 'codeBlock' or msg['type'] == 'blockQuote':
|
||||
ret = "<span font_family=\"monospace\" background=\"#0004\">%s</span>" % (
|
||||
self.make_line(msg['content']))
|
||||
elif msg['type'] == 'u':
|
||||
ret = "<u>%s</u>" % (self.make_line(msg['content']))
|
||||
elif msg['type'] == 'em':
|
||||
ret = "<i>%s</i>" % (self.make_line(msg['content']))
|
||||
elif msg['type'] == 's':
|
||||
ret = "<s>%s</s>" % (self.make_line(msg['content']))
|
||||
elif msg['type'] == 'channel':
|
||||
ret = self.make_line(msg['content'])
|
||||
elif msg['type'] == 'mention':
|
||||
ret = self.make_line(msg['content'])
|
||||
elif msg['type'] == 'br':
|
||||
ret = '\n'
|
||||
else:
|
||||
logging.error("Unknown text type : %s", msg["type"])
|
||||
return ret
|
||||
|
||||
def recv_attach(self, identifier, pix):
|
||||
self.attachment[identifier] = pix
|
||||
self.redraw()
|
||||
|
||||
def overlay_draw(self, _w, context, data=None):
|
||||
self.context = context
|
||||
context.set_antialias(cairo.ANTIALIAS_GOOD)
|
||||
(w, h) = self.get_size()
|
||||
# Make background transparent
|
||||
context.set_source_rgba(0.0, 0.0, 0.0, 0.0)
|
||||
context.set_operator(cairo.OPERATOR_SOURCE)
|
||||
context.paint()
|
||||
if not self.connected:
|
||||
return
|
||||
context.save()
|
||||
if self.is_wayland:
|
||||
# Special case!
|
||||
# The window is full-screen regardless of what the user has selected. Because Wayland
|
||||
# We need to set a clip and a transform to imitate original behaviour
|
||||
|
||||
long_string = ""
|
||||
sep = ""
|
||||
w = self.w
|
||||
h = self.h
|
||||
context.translate(self.x, self.y)
|
||||
context.rectangle(0, 0, w, h)
|
||||
context.clip()
|
||||
|
||||
cy = h
|
||||
tnow = time.time()
|
||||
for line in self.content:
|
||||
if not self.popup_style or tnow - line['time'] < self.text_time:
|
||||
col = "#fff"
|
||||
if 'nick_col' in line and line['nick_col']:
|
||||
col = line['nick_col']
|
||||
long_string = "%s%s<span foreground='%s'>%s</span>: %s" % (
|
||||
long_string,
|
||||
sep,
|
||||
self.santize_string(col),
|
||||
self.santize_string(line["nick"]),
|
||||
self.santize_string(line["content"]))
|
||||
sep = '\n'
|
||||
if len(long_string) == 0:
|
||||
return
|
||||
layout = self.create_pango_layout(long_string)
|
||||
layout.set_markup(long_string, -1)
|
||||
attr = Pango.AttrList()
|
||||
for line in reversed(self.content):
|
||||
if self.popup_style and tnow - line['time'] > self.text_time:
|
||||
break
|
||||
out_line = ""
|
||||
self.imgList = []
|
||||
|
||||
layout.set_width(Pango.SCALE * w)
|
||||
col = "#fff"
|
||||
if 'nick_col' in line and line['nick_col']:
|
||||
col = line['nick_col']
|
||||
for in_line in line['content']:
|
||||
out_line = "%s%s" % (out_line, self.make_line(in_line))
|
||||
if line['attach'] and self.show_attach:
|
||||
at = line['attach'][0]
|
||||
url = at['url']
|
||||
if url in self.attachment:
|
||||
cy = self.draw_attach(cy, url)
|
||||
else:
|
||||
get_surface(self.recv_attach,
|
||||
url,
|
||||
url, None)
|
||||
# cy = self.draw_text(cy, "%s" % (line['attach']))
|
||||
cy = self.draw_text(cy, "<span foreground='%s'>%s</span>: %s" % (self.santize_string(col),
|
||||
self.santize_string(line["nick"]), out_line))
|
||||
if cy <= 0:
|
||||
# We've done enough
|
||||
break
|
||||
if self.is_wayland:
|
||||
context.restore()
|
||||
|
||||
def draw_attach(self, y, url):
|
||||
if url in self.attachment and self.attachment[url]:
|
||||
pix = self.attachment[url]
|
||||
iw = pix.get_width()
|
||||
ih = pix.get_height()
|
||||
iw = min(iw, self.w)
|
||||
ih = min(ih, (self.h * .7))
|
||||
(_ax, _ay, _aw, ah) = get_aspected_size(pix, iw, ih)
|
||||
self.col(self.bg_col)
|
||||
self.context.rectangle(0, y - ah, self.w, ah)
|
||||
|
||||
self.context.fill()
|
||||
self.context.set_operator(cairo.OPERATOR_OVER)
|
||||
_new_w, new_h = draw_img_to_rect(
|
||||
pix, self.context, 0, y - ih, iw, ih, aspect=True)
|
||||
return y - new_h
|
||||
return y
|
||||
|
||||
def draw_text(self, y, text):
|
||||
|
||||
layout = self.create_pango_layout(text)
|
||||
layout.set_markup(text, -1)
|
||||
attr = layout.get_attributes()
|
||||
|
||||
layout.set_width(Pango.SCALE * self.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()
|
||||
|
||||
# Don't layer drawing over each other, always replace
|
||||
_tw, th = layout.get_pixel_size()
|
||||
self.col(self.bg_col)
|
||||
context.rectangle(0, -th + h, w, th)
|
||||
context.fill()
|
||||
context.set_operator(cairo.OPERATOR_OVER)
|
||||
self.context.rectangle(0, y - th, self.w, th)
|
||||
self.context.fill()
|
||||
self.context.set_operator(cairo.OPERATOR_OVER)
|
||||
self.col(self.fg_col)
|
||||
|
||||
context.move_to(0, -th + h)
|
||||
PangoCairo.show_layout(context, layout)
|
||||
self.context.move_to(0, y - th)
|
||||
PangoCairo.context_set_shape_renderer(
|
||||
self.get_pango_context(), self.render_custom, None)
|
||||
|
||||
text = layout.get_text()
|
||||
count = 0
|
||||
|
||||
for loc in self.imgFinder.finditer(text):
|
||||
idx = loc.start()
|
||||
|
||||
if len(self.imgList) <= count:
|
||||
break # We fucked up. Who types ` anyway
|
||||
#url = self.imgList[count]
|
||||
|
||||
at = Pango.attr_shape_new_with_data(
|
||||
self.pango_rect, self.pango_rect, count, None)
|
||||
at.start_index = idx
|
||||
at.end_index = idx + 1
|
||||
attr.insert(at)
|
||||
count += 1
|
||||
layout.set_attributes(attr)
|
||||
|
||||
PangoCairo.show_layout(self.context, layout)
|
||||
return y - th
|
||||
|
||||
def render_custom(self, ctx, shape, path, _data):
|
||||
key = self.imgList[shape.data]['url']
|
||||
if key not in self.attachment:
|
||||
get_surface(self.recv_attach,
|
||||
key,
|
||||
key, None)
|
||||
return
|
||||
pix = self.attachment[key]
|
||||
(x, y) = ctx.get_current_point()
|
||||
draw_img_to_rect(pix, ctx, x, y - self.text_size, self.text_size,
|
||||
self.text_size, path=path)
|
||||
return True
|
||||
|
||||
def santize_string(self, string):
|
||||
# I hate that Pango has nothing for this.
|
||||
|
|
|
|||
|
|
@ -1,66 +1,101 @@
|
|||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version('PangoCairo', '1.0')
|
||||
gi.require_version('GdkPixbuf', '2.0')
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""Text setting tab on settings window"""
|
||||
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
|
||||
import logging
|
||||
from configparser import ConfigParser
|
||||
import gi
|
||||
from .draggable_window import DraggableWindow
|
||||
from .draggable_window_wayland import DraggableWindowWayland
|
||||
from .settings import SettingsWindow
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
# pylint: disable=wrong-import-position
|
||||
from gi.repository import Gtk, Gdk, Pango
|
||||
|
||||
|
||||
GUILD_DEFAULT_VALUE = "0"
|
||||
|
||||
|
||||
class TextSettingsWindow(SettingsWindow):
|
||||
"""Text setting tab on settings window"""
|
||||
|
||||
def __init__(self, overlay):
|
||||
Gtk.Window.__init__(self)
|
||||
SettingsWindow.__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.list_channels = {}
|
||||
self.list_guilds_keys = []
|
||||
self.list_guilds = {}
|
||||
self.ignore_channel_change = False
|
||||
self.ignore_guild_change = False
|
||||
self.channel_lookup = None
|
||||
self.channel_model = None
|
||||
self.connector = None
|
||||
self.guild_lookup = None
|
||||
self.guild_model = None
|
||||
self.guild_widget = None
|
||||
self.align_x = None
|
||||
self.align_y = None
|
||||
self.monitor = None
|
||||
self.floating = None
|
||||
self.channel = None
|
||||
self.guild = None
|
||||
self.font = None
|
||||
self.bg_col = None
|
||||
self.fg_col = None
|
||||
self.popup_style = None
|
||||
self.text_time = None
|
||||
self.show_attach = None
|
||||
self.enabled = None
|
||||
|
||||
self.set_size_request(400, 200)
|
||||
self.connect("destroy", self.close_window)
|
||||
self.connect("delete-event", self.close_window)
|
||||
|
||||
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 update_channel_model(self):
|
||||
# potentially organize channels by their group/parent_id
|
||||
# https://discord.com/developers/docs/resources/channel#channel-object-channel-structure
|
||||
c_model = Gtk.ListStore(str, bool)
|
||||
self.channel_lookup = ["0"]
|
||||
|
||||
if self.popup_style:
|
||||
self.text_time_widget.show()
|
||||
self.text_time_label_widget.show()
|
||||
else:
|
||||
self.text_time_widget.hide()
|
||||
self.text_time_label_widget.hide()
|
||||
|
||||
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])
|
||||
# if no guild is specified, populate channel list with every channel from each guild
|
||||
if self.guild == GUILD_DEFAULT_VALUE:
|
||||
c_model.append([guild_name, False])
|
||||
for c in self.list_channels_keys:
|
||||
chan = self.list_channels[c]
|
||||
if chan['guild_id'] == guild_id:
|
||||
c_model.append([chan["name"], True])
|
||||
self.channel_lookup.append(c)
|
||||
|
||||
# if a guild is specified, poulate channel list with every channel from *just that guild*
|
||||
if self.guild != GUILD_DEFAULT_VALUE:
|
||||
for c in self.list_channels_keys:
|
||||
chan = self.list_channels[c]
|
||||
if chan['guild_id'] == guild_id:
|
||||
model.append([chan["name"], True])
|
||||
if chan['guild_id'] == self.guild:
|
||||
c_model.append([chan["name"], True])
|
||||
self.channel_lookup.append(c)
|
||||
|
||||
self.channel_widget.set_model(model)
|
||||
self.channel_model = model
|
||||
self.channel_widget.set_model(c_model)
|
||||
self.channel_model = c_model
|
||||
|
||||
idx = 0
|
||||
for c in self.channel_lookup:
|
||||
|
|
@ -71,23 +106,78 @@ class TextSettingsWindow(SettingsWindow):
|
|||
break
|
||||
idx += 1
|
||||
|
||||
def add_connector(self, conn):
|
||||
self.connector = conn
|
||||
if self.channel:
|
||||
self.connector.start_listening_text(self.channel)
|
||||
|
||||
def present_settings(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.show()
|
||||
self.align_placement_widget.show()
|
||||
|
||||
if self.popup_style:
|
||||
self.text_time_widget.show()
|
||||
self.text_time_label_widget.show()
|
||||
else:
|
||||
self.text_time_widget.hide()
|
||||
self.text_time_label_widget.hide()
|
||||
|
||||
g_model = Gtk.ListStore(str, bool)
|
||||
self.guild_lookup = []
|
||||
|
||||
for guild in self.guild_list():
|
||||
guild_id, guild_name = guild
|
||||
self.guild_lookup.append(guild_id)
|
||||
g_model.append([guild_name, True])
|
||||
|
||||
self.guild_widget.set_model(g_model)
|
||||
self.guild_model = g_model
|
||||
self.update_channel_model()
|
||||
|
||||
idxg = 0
|
||||
for guild_id in self.guild_lookup:
|
||||
if guild_id == self.guild:
|
||||
self.ignore_guild_change = True
|
||||
self.guild_widget.set_active(idxg)
|
||||
self.ignore_guild_change = False
|
||||
break
|
||||
idxg += 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"]])
|
||||
for guild in self.list_guilds.values():
|
||||
if not guild["id"] in done:
|
||||
done.append(guild["id"])
|
||||
guilds.append([guild["id"], 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():
|
||||
# filter for only text channels
|
||||
# https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
if in_list[key]["type"] == 0:
|
||||
self.list_channels_keys.append(key)
|
||||
self.list_channels_keys.sort()
|
||||
|
||||
def set_guilds(self, in_list):
|
||||
self.list_guilds = in_list
|
||||
self.list_guilds_keys = []
|
||||
for key in in_list.keys():
|
||||
self.list_guilds_keys.append(key)
|
||||
self.list_guilds_keys.sort()
|
||||
|
||||
def read_config(self):
|
||||
config = ConfigParser(interpolation=None)
|
||||
config.read(self.configFile)
|
||||
|
|
@ -101,6 +191,7 @@ class TextSettingsWindow(SettingsWindow):
|
|||
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.guild = config.get("text", "guild", fallback=GUILD_DEFAULT_VALUE)
|
||||
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]"))
|
||||
|
|
@ -109,21 +200,25 @@ class TextSettingsWindow(SettingsWindow):
|
|||
self.popup_style = config.getboolean(
|
||||
"text", "popup_style", fallback=False)
|
||||
self.text_time = config.getint("text", "text_time", fallback=30)
|
||||
self.show_attach = config.getboolean(
|
||||
"text", "show_attach", fallback=True)
|
||||
|
||||
logging.info(
|
||||
"Loading saved channel %s" % (self.channel))
|
||||
"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_monitor(self.get_monitor_index(
|
||||
self.monitor), self.get_monitor_obj(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)
|
||||
self.overlay.set_popup_style(self.popup_style)
|
||||
self.overlay.set_text_time(self.text_time)
|
||||
self.overlay.set_show_attach(self.show_attach)
|
||||
|
||||
def save_config(self):
|
||||
config = ConfigParser(interpolation=None)
|
||||
|
|
@ -136,15 +231,17 @@ class TextSettingsWindow(SettingsWindow):
|
|||
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", "floating_x", "%s" % (int(self.floating_x)))
|
||||
config.set("text", "floating_y", "%s" % (int(self.floating_y)))
|
||||
config.set("text", "floating_w", "%s" % (int(self.floating_w)))
|
||||
config.set("text", "floating_h", "%s" % (int(self.floating_h)))
|
||||
config.set("text", "channel", self.channel)
|
||||
config.set("text", "guild", self.guild)
|
||||
config.set("text", "bg_col", json.dumps(self.bg_col))
|
||||
config.set("text", "fg_col", json.dumps(self.fg_col))
|
||||
config.set("text", "popup_style", "%s" % (int(self.popup_style)))
|
||||
config.set("text", "text_time", "%s" % (int(self.text_time)))
|
||||
config.set("text", "show_attach", "%s" % (int(self.show_attach)))
|
||||
|
||||
if self.font:
|
||||
config.set("text", "font", self.font)
|
||||
|
|
@ -250,15 +347,30 @@ class TextSettingsWindow(SettingsWindow):
|
|||
|
||||
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)
|
||||
|
||||
guild_label = Gtk.Label.new("Server")
|
||||
guild = Gtk.ComboBox.new()
|
||||
|
||||
guild.connect("changed", self.change_guild)
|
||||
rt = Gtk.CellRendererText()
|
||||
guild.pack_start(rt, True)
|
||||
guild.add_attribute(rt, "text", 0)
|
||||
guild.add_attribute(rt, 'sensitive', 1)
|
||||
|
||||
# Show Attachments
|
||||
show_attach_label = Gtk.Label.new("Show Attachments")
|
||||
show_attach = Gtk.CheckButton.new()
|
||||
show_attach.set_active(self.show_attach)
|
||||
show_attach.connect("toggled", self.change_show_attach)
|
||||
|
||||
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.guild_widget = guild
|
||||
self.channel_widget = channel
|
||||
self.text_time_widget = text_time
|
||||
self.text_time_label_widget = text_time_label
|
||||
|
|
@ -269,20 +381,25 @@ class TextSettingsWindow(SettingsWindow):
|
|||
box.attach(popup_style, 1, 1, 1, 1)
|
||||
box.attach(text_time_label, 0, 2, 1, 1)
|
||||
box.attach(text_time, 1, 2, 1, 1)
|
||||
box.attach(channel_label, 0, 3, 1, 1)
|
||||
box.attach(channel, 1, 3, 1, 1)
|
||||
box.attach(font_label, 0, 4, 1, 1)
|
||||
box.attach(font, 1, 4, 1, 1)
|
||||
box.attach(fg_col_label, 0, 5, 1, 1)
|
||||
box.attach(fg_col, 1, 5, 1, 1)
|
||||
box.attach(bg_col_label, 0, 6, 1, 1)
|
||||
box.attach(bg_col, 1, 6, 1, 1)
|
||||
box.attach(align_label, 0, 7, 1, 5)
|
||||
#box.attach(align_type_box, 1, 7, 1, 1)
|
||||
box.attach(monitor, 1, 8, 1, 1)
|
||||
box.attach(align_x, 1, 9, 1, 1)
|
||||
box.attach(align_y, 1, 10, 1, 1)
|
||||
box.attach(align_placement_button, 1, 11, 1, 1)
|
||||
box.attach(guild_label, 0, 3, 1, 1)
|
||||
box.attach(guild, 1, 3, 1, 1)
|
||||
|
||||
box.attach(channel_label, 0, 4, 1, 1)
|
||||
box.attach(channel, 1, 4, 1, 1)
|
||||
box.attach(font_label, 0, 5, 1, 1)
|
||||
box.attach(font, 1, 5, 1, 1)
|
||||
box.attach(fg_col_label, 0, 6, 1, 1)
|
||||
box.attach(fg_col, 1, 6, 1, 1)
|
||||
box.attach(bg_col_label, 0, 7, 1, 1)
|
||||
box.attach(bg_col, 1, 7, 1, 1)
|
||||
box.attach(align_label, 0, 8, 1, 5)
|
||||
#box.attach(align_type_box, 1, 8, 1, 1)
|
||||
box.attach(monitor, 1, 9, 1, 1)
|
||||
box.attach(align_x, 1, 10, 1, 1)
|
||||
box.attach(align_y, 1, 11, 1, 1)
|
||||
box.attach(align_placement_button, 1, 12, 1, 1)
|
||||
box.attach(show_attach_label, 0, 13, 1, 1)
|
||||
box.attach(show_attach, 1, 13, 1, 1)
|
||||
|
||||
self.add(box)
|
||||
|
||||
|
|
@ -300,28 +417,47 @@ class TextSettingsWindow(SettingsWindow):
|
|||
def change_channel(self, button):
|
||||
if self.ignore_channel_change:
|
||||
return
|
||||
|
||||
c = self.channel_lookup[button.get_active()]
|
||||
self.connector.start_listening_text(c)
|
||||
self.channel = c
|
||||
self.save_config()
|
||||
|
||||
def change_guild(self, button):
|
||||
if self.ignore_guild_change:
|
||||
return
|
||||
guild_id = self.guild_lookup[button.get_active()]
|
||||
self.guild = guild_id
|
||||
self.save_config()
|
||||
self.update_channel_model()
|
||||
|
||||
def change_placement(self, button):
|
||||
if self.placement_window:
|
||||
(x, y) = self.placement_window.get_position()
|
||||
(w, h) = self.placement_window.get_size()
|
||||
(x, y, w, h) = self.placement_window.get_coords()
|
||||
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")
|
||||
if not self.overlay.is_wayland:
|
||||
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")
|
||||
if self.overlay.is_wayland:
|
||||
self.placement_window = DraggableWindowWayland(
|
||||
x=self.floating_x, y=self.floating_y,
|
||||
w=self.floating_w, h=self.floating_h,
|
||||
message="Place & resize this window then press Green!", settings=self)
|
||||
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!", settings=self)
|
||||
if not self.overlay.is_wayland:
|
||||
button.set_label("Save this position")
|
||||
|
||||
def change_align_type_edge(self, button):
|
||||
if button.get_active():
|
||||
|
|
@ -352,7 +488,7 @@ class TextSettingsWindow(SettingsWindow):
|
|||
if "get_monitor" in dir(display):
|
||||
mon = display.get_monitor(button.get_active())
|
||||
m_s = mon.get_model()
|
||||
self.overlay.set_monitor(button.get_active())
|
||||
self.overlay.set_monitor(button.get_active(), mon)
|
||||
|
||||
self.monitor = m_s
|
||||
self.save_config()
|
||||
|
|
@ -413,3 +549,9 @@ class TextSettingsWindow(SettingsWindow):
|
|||
|
||||
self.fg_col = c
|
||||
self.save_config()
|
||||
|
||||
def change_show_attach(self, button):
|
||||
self.overlay.set_show_attach(button.get_active())
|
||||
|
||||
self.show_attach = button.get_active()
|
||||
self.save_config()
|
||||
|
|
|
|||
|
|
@ -1,20 +1,29 @@
|
|||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version('PangoCairo', '1.0')
|
||||
gi.require_version('GdkPixbuf', '2.0')
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""Overlay window for voice"""
|
||||
import math
|
||||
from .overlay import OverlayWindow
|
||||
from .image_getter import get_image
|
||||
from gi.repository.GdkPixbuf import Pixbuf
|
||||
from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo
|
||||
import cairo
|
||||
import logging
|
||||
from .overlay import OverlayWindow
|
||||
from .image_getter import get_surface, draw_img_to_rect
|
||||
|
||||
|
||||
class VoiceOverlayWindow(OverlayWindow):
|
||||
def __init__(self):
|
||||
"""Overlay window for voice"""
|
||||
|
||||
def __init__(self, discover):
|
||||
OverlayWindow.__init__(self)
|
||||
|
||||
self.discover = discover
|
||||
self.avatars = {}
|
||||
|
||||
self.avatar_size = 48
|
||||
|
|
@ -24,21 +33,26 @@ class VoiceOverlayWindow(OverlayWindow):
|
|||
self.icon_spacing = 8
|
||||
self.vert_edge_padding = 0
|
||||
self.horz_edge_padding = 0
|
||||
self.only_speaking = None
|
||||
self.highlight_self = None
|
||||
self.order = None
|
||||
self.def_avatar = None
|
||||
|
||||
self.round_avatar = True
|
||||
self.icon_only = True
|
||||
self.talk_col = [0.0, 0.6, 0.0, 0.1]
|
||||
self.text_col = [1.0, 1.0, 1.0, 1.0]
|
||||
self.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.users_to_draw = []
|
||||
self.connected = False
|
||||
self.force_location()
|
||||
self.def_avatar = get_image(self.recv_avatar,
|
||||
"https://cdn.discordapp.com/embed/avatars/3.png",
|
||||
'def', self.avatar_size)
|
||||
|
||||
self.first_draw = True
|
||||
get_surface(self.recv_avatar,
|
||||
"https://cdn.discordapp.com/embed/avatars/3.png",
|
||||
'def', self.avatar_size)
|
||||
self.set_title("Discover Voice")
|
||||
|
||||
def set_bg(self, bg):
|
||||
self.norm_col = bg
|
||||
|
|
@ -58,7 +72,6 @@ class VoiceOverlayWindow(OverlayWindow):
|
|||
|
||||
def set_avatar_size(self, size):
|
||||
self.avatar_size = size
|
||||
self.reset_avatar()
|
||||
self.redraw()
|
||||
|
||||
def set_icon_spacing(self, i):
|
||||
|
|
@ -81,6 +94,19 @@ class VoiceOverlayWindow(OverlayWindow):
|
|||
self.round_avatar = not i
|
||||
self.redraw()
|
||||
|
||||
def set_only_speaking(self, only_speaking):
|
||||
self.only_speaking = only_speaking
|
||||
|
||||
def set_highlight_self(self, highlight_self):
|
||||
self.highlight_self = highlight_self
|
||||
|
||||
def set_order(self, i):
|
||||
self.order = i
|
||||
|
||||
def set_icon_only(self, i):
|
||||
self.icon_only = i
|
||||
self.redraw()
|
||||
|
||||
def set_wind_col(self):
|
||||
self.col(self.wind_col)
|
||||
|
||||
|
|
@ -96,15 +122,20 @@ class VoiceOverlayWindow(OverlayWindow):
|
|||
def set_mute_col(self, a=1.0):
|
||||
self.col(self.mute_col, a)
|
||||
|
||||
def reset_avatar(self):
|
||||
self.avatars = {}
|
||||
self.def_avatar = get_image(self.recv_avatar,
|
||||
"https://cdn.discordapp.com/embed/avatars/3.png",
|
||||
'def', self.avatar_size)
|
||||
|
||||
def set_user_list(self, userlist, alt):
|
||||
self.userlist = userlist
|
||||
self.userlist.sort(key=lambda x: x["username"])
|
||||
for user in userlist:
|
||||
if "nick" in user:
|
||||
user["friendlyname"] = user["nick"]
|
||||
else:
|
||||
user["friendlyname"] = user["username"]
|
||||
if self.order == 1: # ID Sort
|
||||
self.userlist.sort(key=lambda x: x["id"])
|
||||
elif self.order == 2: # Spoken sort
|
||||
self.userlist.sort(key=lambda x: x["lastspoken"], reverse=True)
|
||||
self.userlist.sort(key=lambda x: x["speaking"], reverse=True)
|
||||
else: # Name sort
|
||||
self.userlist.sort(key=lambda x: x["friendlyname"])
|
||||
screen = self.get_screen()
|
||||
c = screen.is_composited()
|
||||
if not self.compositing == c:
|
||||
|
|
@ -119,28 +150,64 @@ class VoiceOverlayWindow(OverlayWindow):
|
|||
self.connected = is_connected
|
||||
self.redraw()
|
||||
|
||||
def draw(self, widget, context):
|
||||
# Draw
|
||||
self.do_draw(context)
|
||||
|
||||
def do_draw(self, context):
|
||||
def overlay_draw(self, _w, context, _data=None):
|
||||
self.context = context
|
||||
context.set_antialias(cairo.ANTIALIAS_GOOD)
|
||||
|
||||
# Get size of window
|
||||
(w, h) = self.get_size()
|
||||
# Make background transparent
|
||||
self.set_wind_col()
|
||||
# Don't layer drawing over each other, always replace
|
||||
context.set_operator(cairo.OPERATOR_SOURCE)
|
||||
context.paint()
|
||||
context.save()
|
||||
if self.is_wayland:
|
||||
# Special case!
|
||||
# The window is full-screen regardless of what the user has selected. Because Wayland
|
||||
# We need to set a clip and a transform to imitate original behaviour
|
||||
|
||||
w = self.w
|
||||
h = self.h
|
||||
context.translate(self.x, self.y)
|
||||
context.rectangle(0, 0, w, h)
|
||||
context.clip()
|
||||
|
||||
context.set_operator(cairo.OPERATOR_OVER)
|
||||
if not self.connected:
|
||||
return
|
||||
|
||||
# Get size of window
|
||||
(w, h) = self.get_size()
|
||||
connection = self.discover.connection
|
||||
self_user = connection.user
|
||||
|
||||
# Gather which users to draw
|
||||
self.users_to_draw = self.userlist[:]
|
||||
for user in self.userlist:
|
||||
# Bad object equality here, so we need to reassign
|
||||
if user["id"] == self_user["id"]:
|
||||
self_user = user
|
||||
|
||||
# Update friendly name with nick if possible
|
||||
if "nick" in user:
|
||||
user["friendlyname"] = user["nick"]
|
||||
else:
|
||||
user["friendlyname"] = user["username"]
|
||||
|
||||
# Remove users that arent speaking
|
||||
if self.only_speaking:
|
||||
speaking = "speaking" in user and user["speaking"]
|
||||
if not speaking:
|
||||
self.users_to_draw.remove(user)
|
||||
|
||||
if self.highlight_self:
|
||||
try:
|
||||
self.users_to_draw.remove(self_user)
|
||||
except ValueError:
|
||||
pass # Not in list
|
||||
self.users_to_draw.insert(0, self_user)
|
||||
|
||||
# Calculate height needed to show overlay
|
||||
height = (len(self.userlist) * self.avatar_size) + \
|
||||
(len(self.userlist) + 1) * self.icon_spacing
|
||||
height = (len(self.users_to_draw) * self.avatar_size) + \
|
||||
(len(self.users_to_draw) + 1) * self.icon_spacing
|
||||
|
||||
# Choose where to start drawing
|
||||
rh = 0 + self.vert_edge_padding
|
||||
|
|
@ -149,71 +216,81 @@ class VoiceOverlayWindow(OverlayWindow):
|
|||
rh = (h / 2) - (height / 2)
|
||||
elif self.align_vert == 2:
|
||||
rh = h - height - self.vert_edge_padding
|
||||
# Iterate users in room.
|
||||
for user in self.userlist:
|
||||
|
||||
for user in self.users_to_draw:
|
||||
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
|
||||
if self.is_wayland:
|
||||
context.restore()
|
||||
self.context = None
|
||||
|
||||
def recv_avatar(self, id, pix):
|
||||
if(id == 'def'):
|
||||
def recv_avatar(self, identifier, pix):
|
||||
if identifier == 'def':
|
||||
self.def_avatar = pix
|
||||
else:
|
||||
self.avatars[id] = pix
|
||||
self.avatars[identifier] = pix
|
||||
self.redraw()
|
||||
|
||||
def delete_avatar(self, id):
|
||||
def delete_avatar(self, identifier):
|
||||
if id in self.avatars:
|
||||
del self.avatars[id]
|
||||
del self.avatars[identifier]
|
||||
|
||||
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'])
|
||||
get_image(self.recv_avatar, url, user["id"],
|
||||
self.avatar_size)
|
||||
get_surface(self.recv_avatar, url, user["id"],
|
||||
self.avatar_size)
|
||||
|
||||
# Set the key with no value to avoid spamming requests
|
||||
self.avatars[user["id"]] = None
|
||||
|
||||
(w, h) = self.get_size()
|
||||
c = None
|
||||
mute = False
|
||||
alpha = 1.0
|
||||
if "speaking" in user and user["speaking"]:
|
||||
c = self.talk_col
|
||||
deaf = False
|
||||
|
||||
if "mute" in user and user["mute"]:
|
||||
mute = True
|
||||
if "deaf" in user and user["deaf"]:
|
||||
alpha = 0.5
|
||||
deaf = True
|
||||
if "speaking" in user and user["speaking"] and not deaf and not mute:
|
||||
c = self.talk_col
|
||||
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 - self.horz_edge_padding, y)
|
||||
if not self.icon_only:
|
||||
self.draw_text(
|
||||
context, user["friendlyname"], self.w - self.avatar_size - self.horz_edge_padding, y)
|
||||
self.draw_avatar_pix(
|
||||
context, pix, w - self.avatar_size - self.horz_edge_padding, y, c, alpha)
|
||||
if mute:
|
||||
self.draw_mute(context, w - self.avatar_size -
|
||||
self.horz_edge_padding, y, alpha)
|
||||
context, pix, self.w - self.avatar_size - self.horz_edge_padding, y, c)
|
||||
if deaf:
|
||||
self.draw_deaf(context, self.w - self.avatar_size -
|
||||
self.horz_edge_padding, y)
|
||||
elif mute:
|
||||
self.draw_mute(context, self.w - self.avatar_size -
|
||||
self.horz_edge_padding, y)
|
||||
else:
|
||||
self.draw_text(
|
||||
context, user["username"], self.avatar_size + self.horz_edge_padding, y)
|
||||
if not self.icon_only:
|
||||
self.draw_text(
|
||||
context, user["friendlyname"], self.avatar_size + self.horz_edge_padding, y)
|
||||
self.draw_avatar_pix(
|
||||
context, pix, self.horz_edge_padding, y, c, alpha)
|
||||
if mute:
|
||||
self.draw_mute(context, self.horz_edge_padding, y, alpha)
|
||||
context, pix, self.horz_edge_padding, y, c)
|
||||
if deaf:
|
||||
self.draw_deaf(context, self.horz_edge_padding, y)
|
||||
elif mute:
|
||||
self.draw_mute(context, self.horz_edge_padding, y)
|
||||
|
||||
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)
|
||||
_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)
|
||||
|
|
@ -236,27 +313,24 @@ class VoiceOverlayWindow(OverlayWindow):
|
|||
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):
|
||||
def draw_avatar_pix(self, context, pixbuf, x, y, c):
|
||||
if not pixbuf:
|
||||
pixbuf = self.def_avatar
|
||||
|
||||
if not pixbuf:
|
||||
return
|
||||
if not pixbuf:
|
||||
return
|
||||
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()
|
||||
|
||||
self.set_wind_col()
|
||||
self.set_norm_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)
|
||||
draw_img_to_rect(pixbuf, context, x, y,
|
||||
self.avatar_size, self.avatar_size)
|
||||
context.restore()
|
||||
if c:
|
||||
if self.round_avatar:
|
||||
|
|
@ -269,11 +343,11 @@ class VoiceOverlayWindow(OverlayWindow):
|
|||
self.col(c)
|
||||
context.stroke()
|
||||
|
||||
def draw_mute(self, context, x, y, a):
|
||||
def draw_mute(self, context, x, y):
|
||||
context.save()
|
||||
context.translate(x, y)
|
||||
context.scale(self.avatar_size, self.avatar_size)
|
||||
self.set_mute_col(a)
|
||||
self.set_mute_col()
|
||||
context.save()
|
||||
|
||||
# Clip Strike-through
|
||||
|
|
@ -322,3 +396,50 @@ class VoiceOverlayWindow(OverlayWindow):
|
|||
context.fill()
|
||||
|
||||
context.restore()
|
||||
|
||||
def draw_deaf(self, context, x, y):
|
||||
context.save()
|
||||
context.translate(x, y)
|
||||
context.scale(self.avatar_size, self.avatar_size)
|
||||
self.set_mute_col()
|
||||
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()
|
||||
|
||||
# Top band
|
||||
context.arc(0.5, 0.5, 0.2, 1.0 * math.pi, 0)
|
||||
context.stroke()
|
||||
|
||||
# Left band
|
||||
context.arc(0.28, 0.65, 0.075, 1.5 * math.pi, 0.5 * math.pi)
|
||||
context.move_to(0.3, 0.5)
|
||||
context.line_to(0.3, 0.75)
|
||||
context.stroke()
|
||||
|
||||
# Right band
|
||||
context.arc(0.72, 0.65, 0.075, 0.5 * math.pi, 1.5 * math.pi)
|
||||
context.move_to(0.7, 0.5)
|
||||
context.line_to(0.7, 0.75)
|
||||
context.stroke()
|
||||
|
||||
context.restore()
|
||||
# Strike through
|
||||
context.arc(0.7, 0.3, 0.035, 1.25 * math.pi, 2.25 * math.pi)
|
||||
context.arc(0.3, 0.7, 0.035, .25 * math.pi, 1.25 * math.pi)
|
||||
context.close_path()
|
||||
context.fill()
|
||||
|
||||
context.restore()
|
||||
|
|
|
|||
|
|
@ -1,29 +1,61 @@
|
|||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version('PangoCairo', '1.0')
|
||||
gi.require_version('GdkPixbuf', '2.0')
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""Voice setting tab on settings window"""
|
||||
import json
|
||||
from configparser import ConfigParser
|
||||
import gi
|
||||
from .draggable_window import DraggableWindow
|
||||
from .draggable_window_wayland import DraggableWindowWayland
|
||||
from .settings import SettingsWindow
|
||||
from gi.repository.GdkPixbuf import Pixbuf
|
||||
from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo
|
||||
import logging
|
||||
gi.require_version("Gtk", "3.0")
|
||||
# pylint: disable=wrong-import-position
|
||||
from gi.repository import Gtk, Gdk, Pango
|
||||
|
||||
|
||||
class VoiceSettingsWindow(SettingsWindow):
|
||||
"""Voice setting tab on settings window"""
|
||||
|
||||
def __init__(self, overlay):
|
||||
Gtk.Window.__init__(self)
|
||||
SettingsWindow.__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.align_x = None
|
||||
self.align_y = None
|
||||
self.bg_col = None
|
||||
self.fg_col = None
|
||||
self.tk_col = None
|
||||
self.mt_col = None
|
||||
self.avatar_size = None
|
||||
self.icon_spacing = None
|
||||
self.text_padding = None
|
||||
self.font = None
|
||||
self.square_avatar = None
|
||||
self.only_speaking = None
|
||||
self.highlight_self = None
|
||||
self.icon_only = None
|
||||
self.monitor = None
|
||||
self.vert_edge_padding = None
|
||||
self.horz_edge_padding = None
|
||||
self.floating = None
|
||||
self.order = None
|
||||
self.init_config()
|
||||
|
||||
self.create_gui()
|
||||
|
||||
def present(self):
|
||||
def present_settings(self):
|
||||
self.show_all()
|
||||
if not self.floating:
|
||||
self.align_x_widget.show()
|
||||
|
|
@ -33,7 +65,7 @@ class VoiceSettingsWindow(SettingsWindow):
|
|||
else:
|
||||
self.align_x_widget.hide()
|
||||
self.align_y_widget.hide()
|
||||
self.align_monitor_widget.hide()
|
||||
self.align_monitor_widget.show()
|
||||
self.align_placement_widget.show()
|
||||
|
||||
def read_config(self):
|
||||
|
|
@ -55,6 +87,12 @@ class VoiceSettingsWindow(SettingsWindow):
|
|||
self.font = config.get("main", "font", fallback=None)
|
||||
self.square_avatar = config.getboolean(
|
||||
"main", "square_avatar", fallback=False)
|
||||
self.only_speaking = config.getboolean(
|
||||
"main", "only_speaking", fallback=False)
|
||||
self.highlight_self = config.getboolean(
|
||||
"main", "highlight_self", fallback=False)
|
||||
self.icon_only = config.getboolean(
|
||||
"main", "icon_only", fallback=False)
|
||||
self.monitor = config.get("main", "monitor", fallback="None")
|
||||
self.vert_edge_padding = config.getint(
|
||||
"main", "vert_edge_padding", fallback=0)
|
||||
|
|
@ -65,6 +103,7 @@ class VoiceSettingsWindow(SettingsWindow):
|
|||
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)
|
||||
self.order = config.getint("main", "order", fallback=0)
|
||||
|
||||
# Pass all of our config over to the overlay
|
||||
self.overlay.set_align_x(self.align_x)
|
||||
|
|
@ -77,9 +116,14 @@ class VoiceSettingsWindow(SettingsWindow):
|
|||
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_only_speaking(self.only_speaking)
|
||||
self.overlay.set_highlight_self(self.highlight_self)
|
||||
self.overlay.set_icon_only(self.icon_only)
|
||||
self.overlay.set_monitor(self.get_monitor_index(
|
||||
self.monitor), self.get_monitor_obj(self.monitor))
|
||||
self.overlay.set_vert_edge_padding(self.vert_edge_padding)
|
||||
self.overlay.set_horz_edge_padding(self.horz_edge_padding)
|
||||
self.overlay.set_order(self.order)
|
||||
|
||||
self.overlay.set_floating(
|
||||
self.floating, self.floating_x, self.floating_y, self.floating_w, self.floating_h)
|
||||
|
|
@ -109,6 +153,9 @@ class VoiceSettingsWindow(SettingsWindow):
|
|||
if self.font:
|
||||
config.set("main", "font", self.font)
|
||||
config.set("main", "square_avatar", "%d" % (int(self.square_avatar)))
|
||||
config.set("main", "only_speaking", "%d" % (int(self.only_speaking)))
|
||||
config.set("main", "highlight_self", "%d" % (int(self.highlight_self)))
|
||||
config.set("main", "icon_only", "%d" % (int(self.icon_only)))
|
||||
config.set("main", "monitor", self.monitor)
|
||||
config.set("main", "vert_edge_padding", "%d" %
|
||||
(self.vert_edge_padding))
|
||||
|
|
@ -119,6 +166,7 @@ class VoiceSettingsWindow(SettingsWindow):
|
|||
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))
|
||||
config.set("main", "order", "%s" % (self.order))
|
||||
|
||||
with open(self.configFile, 'w') as file:
|
||||
config.write(file)
|
||||
|
|
@ -256,6 +304,35 @@ class VoiceSettingsWindow(SettingsWindow):
|
|||
square_avatar.set_active(self.square_avatar)
|
||||
square_avatar.connect("toggled", self.change_square_avatar)
|
||||
|
||||
only_speaking_label = Gtk.Label.new("Display Speakers Only")
|
||||
only_speaking = Gtk.CheckButton.new()
|
||||
only_speaking.set_active(self.only_speaking)
|
||||
only_speaking.connect("toggled", self.change_only_speaking)
|
||||
|
||||
highlight_self_label = Gtk.Label.new("Highlight Self")
|
||||
highlight_self = Gtk.CheckButton.new()
|
||||
highlight_self.set_active(self.highlight_self)
|
||||
highlight_self.connect("toggled", self.change_highlight_self)
|
||||
|
||||
# Display icon only
|
||||
icon_only_label = Gtk.Label.new("Display Icon Only")
|
||||
icon_only = Gtk.CheckButton.new()
|
||||
icon_only.set_active(self.icon_only)
|
||||
icon_only.connect("toggled", self.change_icon_only)
|
||||
|
||||
# Order avatars
|
||||
order_label = Gtk.Label.new("Order Avatars By")
|
||||
order_store = Gtk.ListStore(str)
|
||||
order_store.append(["Alphabetically"])
|
||||
order_store.append(["ID"])
|
||||
order_store.append(["Last Spoken"])
|
||||
order = Gtk.ComboBox.new_with_model(order_store)
|
||||
order.set_active(self.order)
|
||||
order.connect("changed", self.change_order)
|
||||
rt = Gtk.CellRendererText()
|
||||
order.pack_start(rt, True)
|
||||
order.add_attribute(rt, "text", 0)
|
||||
|
||||
box.attach(font_label, 0, 0, 1, 1)
|
||||
box.attach(font, 1, 0, 1, 1)
|
||||
box.attach(bg_col_label, 0, 1, 1, 1)
|
||||
|
|
@ -284,31 +361,43 @@ class VoiceSettingsWindow(SettingsWindow):
|
|||
box.attach(horz_edge_padding, 1, 14, 1, 1)
|
||||
box.attach(square_avatar_label, 0, 15, 1, 1)
|
||||
box.attach(square_avatar, 1, 15, 1, 1)
|
||||
box.attach(only_speaking_label, 0, 16, 1, 1)
|
||||
box.attach(only_speaking, 1, 16, 1, 1)
|
||||
box.attach(highlight_self_label, 0, 17, 1, 1)
|
||||
box.attach(highlight_self, 1, 17, 1, 1)
|
||||
box.attach(icon_only_label, 0, 18, 1, 1)
|
||||
box.attach(icon_only, 1, 18, 1, 1)
|
||||
box.attach(order_label, 0, 19, 1, 1)
|
||||
box.attach(order, 1, 19, 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()
|
||||
(x, y, w, h) = self.placement_window.get_coords()
|
||||
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.save_config()
|
||||
if not self.overlay.is_wayland:
|
||||
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")
|
||||
if self.overlay.is_wayland:
|
||||
self.placement_window = DraggableWindowWayland(
|
||||
x=self.floating_x, y=self.floating_y,
|
||||
w=self.floating_w, h=self.floating_h,
|
||||
message="Place & resize this window then press Green!", settings=self)
|
||||
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!", settings=self)
|
||||
if not self.overlay.is_wayland:
|
||||
button.set_label("Save this position")
|
||||
|
||||
def change_align_type_edge(self, button):
|
||||
if button.get_active():
|
||||
|
|
@ -390,7 +479,7 @@ class VoiceSettingsWindow(SettingsWindow):
|
|||
if "get_monitor" in dir(display):
|
||||
mon = display.get_monitor(button.get_active())
|
||||
m_s = mon.get_model()
|
||||
self.overlay.set_monitor(button.get_active())
|
||||
self.overlay.set_monitor(button.get_active(), mon)
|
||||
|
||||
self.monitor = m_s
|
||||
self.save_config()
|
||||
|
|
@ -436,3 +525,27 @@ class VoiceSettingsWindow(SettingsWindow):
|
|||
|
||||
self.square_avatar = button.get_active()
|
||||
self.save_config()
|
||||
|
||||
def change_only_speaking(self, button):
|
||||
self.overlay.set_only_speaking(button.get_active())
|
||||
|
||||
self.only_speaking = button.get_active()
|
||||
self.save_config()
|
||||
|
||||
def change_highlight_self(self, button):
|
||||
self.overlay.set_highlight_self(button.get_active())
|
||||
|
||||
self.highlight_self = button.get_active()
|
||||
self.save_config()
|
||||
|
||||
def change_icon_only(self, button):
|
||||
self.overlay.set_icon_only(button.get_active())
|
||||
|
||||
self.icon_only = button.get_active()
|
||||
self.save_config()
|
||||
|
||||
def change_order(self, button):
|
||||
self.overlay.set_order(button.get_active())
|
||||
|
||||
self.order = button.get_active()
|
||||
self.save_config()
|
||||
|
|
|
|||
8
discover_overlay_close.desktop
Normal file
8
discover_overlay_close.desktop
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[Desktop Entry]
|
||||
Name=Discover Overlay - Close
|
||||
Comment=Close Discover Overlay
|
||||
Exec=discover-overlay --close
|
||||
Icon=discover-overlay
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Catergories=Network;
|
||||
8
discover_overlay_conf.desktop
Normal file
8
discover_overlay_conf.desktop
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[Desktop Entry]
|
||||
Name=Discover Overlay - Config
|
||||
Comment=Open configuration for Discover Overlay
|
||||
Exec=discover-overlay --configure
|
||||
Icon=discover-overlay
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Catergories=Network;
|
||||
7
setup.py
7
setup.py
|
|
@ -9,7 +9,7 @@ setup(
|
|||
name='discover-overlay',
|
||||
author='trigg',
|
||||
author_email='',
|
||||
version='0.2',
|
||||
version='0.3.1',
|
||||
description='Voice chat overlay',
|
||||
long_description=readme(),
|
||||
long_description_content_type='text/markdown',
|
||||
|
|
@ -17,7 +17,8 @@ setup(
|
|||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
data_files=[
|
||||
('share/applications', ['discover_overlay.desktop']),
|
||||
('share/applications', ['discover_overlay.desktop',
|
||||
'discover_overlay_conf.desktop', 'discover_overlay_close.desktop']),
|
||||
('share/icons', ['discover-overlay.png'])
|
||||
],
|
||||
install_requires=[
|
||||
|
|
@ -25,6 +26,8 @@ setup(
|
|||
'websocket-client',
|
||||
'pyxdg',
|
||||
'requests',
|
||||
'python-pidfile>=3.0.0',
|
||||
'pillow',
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue