diff --git a/README.md b/README.md
index 708935f..854db0c 100644
--- a/README.md
+++ b/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?
diff --git a/discover_overlay/__init__.py b/discover_overlay/__init__.py
index 7ef8049..7ee925a 100644
--- a/discover_overlay/__init__.py
+++ b/discover_overlay/__init__.py
@@ -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 .
+"""Entry point for Discover Overlay"""
from .discover_overlay import *
diff --git a/discover_overlay/autostart.py b/discover_overlay/autostart.py
index 621343c..d5aad74 100644
--- a/discover_overlay/autostart.py
+++ b/discover_overlay/autostart.py
@@ -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 .
+"""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
diff --git a/discover_overlay/discord_connector.py b/discover_overlay/discord_connector.py
index b54f4c6..b27c2d2 100644
--- a/discover_overlay/discord_connector.py
+++ b/discover_overlay/discord_connector.py
@@ -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 .
+"""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
diff --git a/discover_overlay/discover_overlay.py b/discover_overlay/discover_overlay.py
index 9c50c31..f66ea20 100755
--- a/discover_overlay/discover_overlay.py
+++ b/discover_overlay/discover_overlay.py
@@ -10,48 +10,77 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import gi
-gi.require_version("Gtk", "3.0")
-gi.require_version('PangoCairo', '1.0')
-gi.require_version('GdkPixbuf', '2.0')
-from gi.repository.GdkPixbuf import Pixbuf
-from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo
+"""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")
diff --git a/discover_overlay/draggable_window.py b/discover_overlay/draggable_window.py
index f2da1b9..cf82e83 100644
--- a/discover_overlay/draggable_window.py
+++ b/discover_overlay/draggable_window.py
@@ -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 .
+"""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)
diff --git a/discover_overlay/draggable_window_wayland.py b/discover_overlay/draggable_window_wayland.py
new file mode 100644
index 0000000..c587381
--- /dev/null
+++ b/discover_overlay/draggable_window_wayland.py
@@ -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 .
+"""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)
diff --git a/discover_overlay/general_settings.py b/discover_overlay/general_settings.py
new file mode 100644
index 0000000..3a6654d
--- /dev/null
+++ b/discover_overlay/general_settings.py
@@ -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 .
+"""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()
diff --git a/discover_overlay/image_getter.py b/discover_overlay/image_getter.py
index a9a11ae..73c7632 100644
--- a/discover_overlay/image_getter.py
+++ b/discover_overlay/image_getter.py
@@ -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 .
+"""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)
diff --git a/discover_overlay/overlay.py b/discover_overlay/overlay.py
index c7b8953..5639462 100644
--- a/discover_overlay/overlay.py
+++ b/discover_overlay/overlay.py
@@ -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 .
+"""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
diff --git a/discover_overlay/settings.py b/discover_overlay/settings.py
index 0443a76..38c5387 100644
--- a/discover_overlay/settings.py
+++ b/discover_overlay/settings.py
@@ -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 .
+"""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
diff --git a/discover_overlay/settings_window.py b/discover_overlay/settings_window.py
new file mode 100644
index 0000000..ec7bc05
--- /dev/null
+++ b/discover_overlay/settings_window.py
@@ -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 .
+"""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()
diff --git a/discover_overlay/text_overlay.py b/discover_overlay/text_overlay.py
index 77af430..654978a 100644
--- a/discover_overlay/text_overlay.py
+++ b/discover_overlay/text_overlay.py
@@ -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 .
+"""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 = "%s" % (self.make_line(msg['content']))
+ elif msg['type'] == 'text':
+ ret = self.santize_string(msg['content'])
+ elif msg['type'] == 'link':
+ ret = "%s" % (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 = "%s" % (
+ self.make_line(msg['content']))
+ elif msg['type'] == 'u':
+ ret = "%s" % (self.make_line(msg['content']))
+ elif msg['type'] == 'em':
+ ret = "%s" % (self.make_line(msg['content']))
+ elif msg['type'] == 's':
+ ret = "%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%s: %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, "%s: %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.
diff --git a/discover_overlay/text_settings.py b/discover_overlay/text_settings.py
index 7df0a5f..9256ace 100644
--- a/discover_overlay/text_settings.py
+++ b/discover_overlay/text_settings.py
@@ -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 .
+"""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()
diff --git a/discover_overlay/voice_overlay.py b/discover_overlay/voice_overlay.py
index dc8e610..582270c 100644
--- a/discover_overlay/voice_overlay.py
+++ b/discover_overlay/voice_overlay.py
@@ -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 .
+"""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()
diff --git a/discover_overlay/voice_settings.py b/discover_overlay/voice_settings.py
index 6825fbb..e4a98cd 100644
--- a/discover_overlay/voice_settings.py
+++ b/discover_overlay/voice_settings.py
@@ -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 .
+"""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()
diff --git a/discover_overlay_close.desktop b/discover_overlay_close.desktop
new file mode 100644
index 0000000..e49e3a0
--- /dev/null
+++ b/discover_overlay_close.desktop
@@ -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;
diff --git a/discover_overlay_conf.desktop b/discover_overlay_conf.desktop
new file mode 100644
index 0000000..cebbbd7
--- /dev/null
+++ b/discover_overlay_conf.desktop
@@ -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;
diff --git a/setup.py b/setup.py
index 6ef1a51..bddd809 100644
--- a/setup.py
+++ b/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': [