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': [