From 1ac75c94f9100994d4ac6e6eee98ef1e6ac18614 Mon Sep 17 00:00:00 2001 From: Trigg Date: Tue, 29 Mar 2022 19:19:44 +0000 Subject: [PATCH] - Moved some gui logic out of connector - Added dbus and notification eavesdropping - Added new overlay to show freedesktop notifications - Taught text overlay to piggyback --- discover_overlay/discord_connector.py | 11 +- discover_overlay/discover_overlay.py | 58 ++- discover_overlay/image_getter.py | 25 +- discover_overlay/notification_overlay.py | 422 ++++++++++++++++++ discover_overlay/notification_settings.py | 510 ++++++++++++++++++++++ discover_overlay/overlay.py | 1 + discover_overlay/settings_window.py | 10 +- discover_overlay/text_overlay.py | 14 +- discover_overlay/voice_overlay.py | 3 +- 9 files changed, 1025 insertions(+), 29 deletions(-) create mode 100644 discover_overlay/notification_overlay.py create mode 100644 discover_overlay/notification_settings.py diff --git a/discover_overlay/discord_connector.py b/discover_overlay/discord_connector.py index 6984e1f..9fc56f1 100644 --- a/discover_overlay/discord_connector.py +++ b/discover_overlay/discord_connector.py @@ -382,10 +382,11 @@ class DiscordConnector: self.set_in_room(thisuser["id"], True) elif j["data"]["type"] == 0: # Text channel if self.request_text_rooms_response is not None: - if j['data']['position'] >= len(self.request_text_rooms_response) : + if j['data']['position'] >= len(self.request_text_rooms_response): # Error. The list of channels has changed since we requested last self.needs_guild_rerequest = 60 * 30 - logging.error("IndexError getting channel information. Starting again in 30 seconds") + logging.error( + "IndexError getting channel information. Starting again in 30 seconds") pass else: self.request_text_rooms_response[j['data'] @@ -629,12 +630,6 @@ class DiscordConnector: if self.authed: self.set_text_channel(self.text_settings.get_channel()) - if self.voice_overlay.needsredraw: - self.voice_overlay.redraw() - - if self.text_overlay and self.text_overlay.needsredraw: - self.text_overlay.redraw() - if len(self.rate_limited_channels) > 0: guild = self.rate_limited_channels.pop() cmd = { diff --git a/discover_overlay/discover_overlay.py b/discover_overlay/discover_overlay.py index 5abdaed..d36f754 100755 --- a/discover_overlay/discover_overlay.py +++ b/discover_overlay/discover_overlay.py @@ -12,14 +12,19 @@ # along with this program. If not, see . """Main application class""" import os +import time import sys +import dbus import logging import gi import pidfile from .settings_window import MainSettingsWindow from .voice_overlay import VoiceOverlayWindow from .text_overlay import TextOverlayWindow +from .notification_overlay import NotificationOverlayWindow from .discord_connector import DiscordConnector +from dbus.mainloop.glib import DBusGMainLoop # integration into the main loop + gi.require_version("Gtk", "3.0") # pylint: disable=wrong-import-position,wrong-import-order from gi.repository import Gtk, GLib, Gio # nopep8 @@ -39,6 +44,9 @@ class Discover: self.steamos = False self.show_settings_delay = False self.settings = None + self.notification_messages = [] + self.dbus_notification = None + self.bus = None self.debug_file = debug_file @@ -66,6 +74,7 @@ class Discover: self.settings.text_settings.add_connector(self.connection) self.connection.connect() GLib.timeout_add((1000 / 60), self.connection.do_read) + GLib.timeout_add((1000 / 20), self.periodic_run) self.rpc_file = rpc_file rpc_file = Gio.File.new_for_path(rpc_file) monitor = rpc_file.monitor_file(0, None) @@ -73,6 +82,51 @@ class Discover: Gtk.main() + def set_dbus_notifications(self, enabled=False): + if not self.bus: + DBusGMainLoop(set_as_default=True) + self.bus = dbus.SessionBus() + self.bus.add_match_string_non_blocking( + "eavesdrop=true, interface='org.freedesktop.Notifications', member='Notify'") + if enabled: + if not self.dbus_notification: + self.bus.add_message_filter(self.add_notification_message) + self.dbus_notification = True + + def periodic_run(self, data=None): + if self.voice_overlay.needsredraw: + self.voice_overlay.redraw() + + if self.text_overlay and self.text_overlay.needsredraw: + self.text_overlay.redraw() + + if self.notification_overlay: + if self.notification_overlay.enabled: + # This doesn't really belong in overlay or settings + now = time.time() + newlist = [] + oldsize = len(self.notification_messages) + # Iterate over and remove messages older than 30s + for message in self.notification_messages: + if message['time'] + self.settings.notification_settings.text_time > now: + newlist.append(message) + self.notification_messages = newlist + # If the list is different than before + if oldsize != len(newlist): + self.notification_overlay.set_content( + self.notification_messages, True) + if self.notification_overlay.needsredraw: + self.notification_overlay.redraw() + return True + + def add_notification_message(self, bus, message): + args = message.get_args_list() + logging.warning(args) + noti = {"title": "%s" % (args[3]), "body": "%s" % (args[4]), + "icon": "%s" % (args[2]), "cmd": "%s" % (args[0]), "time": time.time()} + self.notification_messages.append(noti) + self.notification_overlay.set_content(self.notification_messages, True) + def do_args(self, data, normal_close): """ Read in arg list from command or RPC and act accordingly @@ -133,8 +187,11 @@ class Discover: if self.steamos: self.text_overlay = TextOverlayWindow(self, self.voice_overlay) + self.notification_overlay = NotificationOverlayWindow( + self, self.text_overlay) else: self.text_overlay = TextOverlayWindow(self) + self.notification_overlay = NotificationOverlayWindow(self) self.menu = self.make_menu() self.make_sys_tray_icon(self.menu) self.settings = MainSettingsWindow(self) @@ -262,7 +319,6 @@ def entrypoint(): rpc_file = os.path.join(config_dir, "discover_overlay.rpc") debug_file = os.path.join(config_dir, "output.txt") - # Flatpak compat mode if "container" in os.environ and os.environ["container"] == "flatpak": diff --git a/discover_overlay/image_getter.py b/discover_overlay/image_getter.py index 7673813..b1b49e1 100644 --- a/discover_overlay/image_getter.py +++ b/discover_overlay/image_getter.py @@ -97,21 +97,24 @@ class SurfaceGetter(): logging.error("Unknown image type") def get_file(self): - locations = [os.path.expanduser('~/.local/'), '/usr/'] + locations = [os.path.expanduser('~/.local/'), '/usr/', '/'] for prefix in locations: + mixpath = os.path.join(prefix, self.url) + image = None try: - image = Image.open(os.path.join(prefix, self.url)) - surface = self.from_pil(image) + image = Image.open(mixpath) + except ValueError: + logging.error("Value Erorr - Unable to read %s", mixpath) + except TypeError: + logging.error("Type Error - Unable to read %s", mixpath) + except PIL.UnidentifiedImageError: + logging.error("Unknown image type: %s", mixpath) + except FileNotFoundError: + logging.error("File not found: %s", mixpath) + surface = self.from_pil(image) + if surface: self.func(self.identifier, surface) return - except ValueError: - logging.error("Unable to read %s", self.url) - except TypeError: - logging.error("Unable to read %s", self.url) - except PIL.UnidentifiedImageError: - logging.error("Unknown image type") - except FileNotFoundError: - logging.error("Unable to load file %s", self.url) def from_pil(self, image, alpha=1.0): """ diff --git a/discover_overlay/notification_overlay.py b/discover_overlay/notification_overlay.py new file mode 100644 index 0000000..523e4ab --- /dev/null +++ b/discover_overlay/notification_overlay.py @@ -0,0 +1,422 @@ +# 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 . +"""Notification 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,wrong-import-order +from gi.repository import Gtk, Pango, PangoCairo # nopep8 + + +class NotificationOverlayWindow(OverlayWindow): + """Overlay window for notifications""" + + def __init__(self, discover, piggyback=None): + OverlayWindow.__init__(self, discover, piggyback) + self.text_spacing = 4 + self.content = [] + self.test_content = [{"icon": "next", "title": "Title1"}, + {"title": "Title2", "body": "Body", "icon": None}, + {"icon": "discord", "title": "Title 3", "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}, + {"icon": None, "title": "Title 3", "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}] + self.text_font = None + self.text_size = 13 + self.text_time = None + self.show_icon = None + 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.icons = [] + self.reverse_order = False + self.padding = 10 + self.border_radius = 5 + self.limit_width = 100 + self.testing = False + self.icon_size = 64 + self.icon_pad = 16 + self.icon_left = True + + self.image_list = {} + self.warned_filetypes = [] + self.set_title("Discover Text") + self.redraw() + + def set_padding(self, padding): + """ + Set the padding between notifications + """ + self.padding = padding + self.needsredraw = True + + def set_border_radius(self, radius): + """ + Set the radius of the border + """ + self.border_radius = radius + self.needsredraw = True + + def set_icon_size(self, size): + """ + Set Icon size + """ + self.icon_size = size + self.image_list = {} + self.get_all_images() + + def set_icon_pad(self, pad): + """ + Set padding between icon and message + """ + self.icon_pad = pad + self.needsredraw = True + + def set_icon_left(self, left): + self.icon_left = left + self.needsredraw = True + + def set_text_time(self, timer): + """ + Set the duration that a message will be visible for. + """ + self.text_time = timer + + def set_limit_width(self, limit): + """ + Set the word wrap limit in pixels + """ + self.limit_width = limit + self.needsredraw = True + + def set_content(self, content, altered): + """ + Update the list of text messages to show + """ + self.content = content + if altered: + self.needsredraw = True + self.get_all_images() + + def get_all_images(self): + the_list = self.content + if self.testing: + the_list = self.test_content + for line in the_list: + icon = line["icon"] + if icon and icon not in self.image_list: + icon_theme = Gtk.IconTheme.get_default() + icon_info = icon_theme.lookup_icon( + icon, self.icon_size, Gtk.IconLookupFlags.FORCE_SIZE) + if icon_info: + get_surface(self.recv_icon, + icon_info.get_filename(), icon, self.icon_size) + + def recv_icon(self, identifier, pix): + """ + Called when image_getter has downloaded an image + """ + self.image_list[identifier] = pix + self.needsredraw = True + + def set_fg(self, fg_col): + """ + Set default text colour + """ + self.fg_col = fg_col + self.needsredraw = True + + def set_bg(self, bg_col): + """ + Set background colour + """ + self.bg_col = bg_col + self.needsredraw = True + + def set_show_icon(self, icon): + """ + Set if icons should be shown inline + """ + self.show_icon = icon + self.needsredraw = True + self.get_all_images() + + def set_reverse_order(self, rev): + self.reverse_order = rev + self.needsredraw = True + + def set_font(self, font): + """ + Set font used to render text + """ + self.text_font = font + + self.pango_rect = Pango.Rectangle() + font = Pango.FontDescription(self.text_font) + self.pango_rect.width = font.get_size() * Pango.SCALE + self.pango_rect.height = font.get_size() * Pango.SCALE + self.needsredraw = True + + def recv_attach(self, identifier, pix): + """ + Called when an image has been downloaded by image_getter + """ + self.icons[identifier] = pix + self.needsredraw = True + + def calc_all_height(self): + h = 0 + my_list = self.content + if self.testing: + my_list = self.test_content + for line in my_list: + h += self.calc_height(line) + if h > 0: + h -= self.padding # Remove one unneeded padding + return h + + def calc_height(self, line): + icon_width = 0 + icon_pad = 0 + icon = line['icon'] + if self.show_icon and icon and icon in self.image_list and self.image_list[icon]: + icon_width = self.icon_size + icon_pad = self.icon_pad + message = "" + if 'body' in line and len(line['body']) > 0: + m_no_body = "%s\n%s" + message = m_no_body % (self.sanitize_string(line["title"]), + self.sanitize_string(line['body'])) + else: + m_with_body = "%s" + message = m_with_body % (self.sanitize_string(line["title"])) + layout = self.create_pango_layout(message) + layout.set_auto_dir(True) + layout.set_markup(message, -1) + attr = layout.get_attributes() + width = self.limit_width if self.width > self.limit_width else self.width + layout.set_width((Pango.SCALE * (width - + (self.border_radius * 4 + icon_width + icon_pad)))) + layout.set_spacing(Pango.SCALE * 3) + if self.text_font: + font = Pango.FontDescription(self.text_font) + layout.set_font_description(font) + text_width, text_height = layout.get_pixel_size() + if text_height < icon_width: + text_height = icon_width + return text_height + (self.border_radius*4) + self.padding + + def overlay_draw(self, _w, context, data=None): + """ + Draw the overlay + """ + if self.piggyback: + self.piggyback.overlay_draw(w, context, data) + if not self.enabled: + return + self.context = context + (width, height) = self.get_size() + if not self.piggyback_parent: + context.set_antialias(cairo.ANTIALIAS_GOOD) + + # Make background transparent + context.set_source_rgba(0.0, 0.0, 0.0, 0.0) + context.set_operator(cairo.OPERATOR_SOURCE) + context.paint() + + context.save() + if self.is_wayland or self.piggyback_parent or self.discover.steamos: + # Special case! + # The window is full-screen regardless of what the user has selected. + # We need to set a clip and a transform to imitate original behaviour + # Used in wlroots & gamescope + width = self.width + height = self.height + context.translate(self.pos_x, self.pos_y) + context.rectangle(0, 0, width, height) + context.clip() + + current_y = height + if self.align_vert == 0: + current_y = 0 + if self.align_vert == 1: # Center. Oh god why + current_y = (height/2.0) - (self.calc_all_height() / 2.0) + tnow = time.time() + if self.testing: + the_list = self.test_content + else: + the_list = self.content + if self.reverse_order: + the_list = reversed(the_list) + for line in the_list: + col = "#fff" + if 'body' in line and len(line['body']) > 0: + m_no_body = "%s\n%s" + message = m_no_body % (self.sanitize_string(col), + self.sanitize_string(line["title"]), + self.sanitize_string(line['body'])) + else: + m_with_body = "%s" + message = m_with_body % (self.sanitize_string(col), + self.sanitize_string(line["title"])) + current_y = self.draw_text(current_y, message, line["icon"]) + if current_y <= 0: + # We've done enough + break + context.restore() + self.context = None + + def draw_text(self, pos_y, text, icon): + """ + Draw a text message, returning the Y position of the next message + """ + + icon_width = 0 + icon_pad = 0 + if self.show_icon and icon and icon in self.image_list and self.image_list[icon]: + icon = self.image_list[icon] + icon_width = self.icon_size + icon_pad = self.icon_pad + else: + icon = None + + layout = self.create_pango_layout(text) + layout.set_auto_dir(True) + layout.set_markup(text, -1) + attr = layout.get_attributes() + + width = self.limit_width if self.width > self.limit_width else self.width + layout.set_width((Pango.SCALE * (width - + (self.border_radius * 4 + icon_width + icon_pad)))) + layout.set_spacing(Pango.SCALE * 3) + if self.text_font: + font = Pango.FontDescription(self.text_font) + layout.set_font_description(font) + text_width, text_height = layout.get_pixel_size() + self.col(self.bg_col) + top = 0 + if self.align_vert == 2: # Bottom align + top = pos_y - (text_height + self.border_radius * 4) + else: # Top align + top = pos_y + if text_height < icon_width: + text_height = icon_width + shape_height = text_height + self.border_radius * 4 + shape_width = text_width + self.border_radius*4 + icon_width + icon_pad + + left = 0 + if self.align_right: + left = self.width - shape_width + + self.context.save() + # Draw Background + self.context.translate(left, top) + self.context.rectangle(self.border_radius, 0, + shape_width - (self.border_radius*2), shape_height) + self.context.rectangle(0, self.border_radius, + shape_width, shape_height - (self.border_radius * 2)) + self.context.fill() + self.context.set_operator(cairo.OPERATOR_OVER) + # Draw Image + if icon: + self.context.save() + if self.icon_left: + self.context.translate( + self.border_radius*2, self.border_radius*2) + draw_img_to_rect(icon, self.context, 0, 0, + icon_width, icon_width) + else: + self.context.translate( + self.border_radius*2 + text_width + icon_pad, self.border_radius*2) + draw_img_to_rect(icon, self.context, 0, 0, + icon_width, icon_width) + self.context.restore() + + self.col(self.fg_col) + + if self.icon_left: + self.context.translate( + self.border_radius*2 + icon_width + icon_pad, self.border_radius*2) + PangoCairo.context_set_shape_renderer( + self.get_pango_context(), self.render_custom, None) + + text = layout.get_text() + count = 0 + + layout.set_attributes(attr) + + PangoCairo.show_layout(self.context, layout) + else: + self.context.translate(self.border_radius*2, self.border_radius*2) + PangoCairo.context_set_shape_renderer( + self.get_pango_context(), self.render_custom, None) + + text = layout.get_text() + count = 0 + + layout.set_attributes(attr) + + PangoCairo.show_layout(self.context, layout) + + self.context.restore() + next_y = 0 + if self.align_vert == 2: + next_y = pos_y - (shape_height + self.padding) + else: + next_y = pos_y + shape_height + self.padding + return next_y + + def render_custom(self, ctx, shape, path, _data): + """ + Draw an inline image as a custom emoticon + """ + # print(shape.data, self.image_list, self.attachment) + if shape.data >= len(self.image_list): + logging.warning(f"{shape.data} >= {len(self.image_list)}") + return + # key is the url to the image + key = self.image_list[shape.data] + if key not in self.attachment: + get_surface(self.recv_attach, + key, + key, None) + return + pix = self.attachment[key] + (pos_x, pos_y) = ctx.get_current_point() + draw_img_to_rect(pix, ctx, pos_x, pos_y - self.text_size, self.text_size, + self.text_size, path=path) + return True + + def sanitize_string(self, string): + """ + Sanitize a text message so that it doesn't intefere with Pango's XML format + """ + string = string.replace("&", "&") + string = string.replace("<", "<") + string = string .replace(">", ">") + string = string.replace("'", "'") + string = string.replace("\"", """) + return string + + def set_testing(self, testing): + self.testing = testing + self.needsredraw = True + self.get_all_images() diff --git a/discover_overlay/notification_settings.py b/discover_overlay/notification_settings.py new file mode 100644 index 0000000..d4b4000 --- /dev/null +++ b/discover_overlay/notification_settings.py @@ -0,0 +1,510 @@ +# 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 . +"""Notification setting tab on settings window""" +import json +import logging +from configparser import ConfigParser +import gi +from .settings import SettingsWindow + +gi.require_version("Gtk", "3.0") +# pylint: disable=wrong-import-position,wrong-import-order +from gi.repository import Gtk, Gdk # nopep8 + + +GUILD_DEFAULT_VALUE = "0" + + +class NotificationSettingsWindow(SettingsWindow): + """Notification setting tab on settings window""" + + def __init__(self, overlay, discover): + SettingsWindow.__init__(self, discover) + self.overlay = overlay + + self.placement_window = None + self.init_config() + self.align_x = None + self.align_y = None + self.monitor = None + self.floating = None + self.font = None + self.bg_col = None + self.fg_col = None + self.text_time = None + self.show_icon = None + self.enabled = None + self.limit_width = None + self.icon_left = None + self.padding = None + self.icon_padding = None + self.icon_size = None + self.border_radius = None + + self.set_size_request(400, 200) + self.connect("destroy", self.close_window) + self.connect("delete-event", self.close_window) + if overlay: + self.init_config() + self.create_gui() + + def present_settings(self): + """ + Show contents of tab and update lists + """ + if not self.overlay: + return + 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() + + def read_config(self): + """ + Read in the 'text' section of the config + """ + if not self.overlay: + return + config = ConfigParser(interpolation=None) + config.read(self.config_file) + self.enabled = config.getboolean( + "notification", "enabled", fallback=False) + self.align_x = config.getboolean( + "notification", "rightalign", fallback=True) + self.align_y = config.getint("notification", "topalign", fallback=2) + self.monitor = config.get("notification", "monitor", fallback="None") + self.floating = config.getboolean( + "notification", "floating", fallback=False) + self.floating_x = config.getint( + "notification", "floating_x", fallback=0) + self.floating_y = config.getint( + "notification", "floating_y", fallback=0) + self.floating_w = config.getint( + "notification", "floating_w", fallback=400) + self.floating_h = config.getint( + "notification", "floating_h", fallback=400) + self.font = config.get("notification", "font", fallback=None) + self.bg_col = json.loads(config.get( + "notification", "bg_col", fallback="[0.0,0.0,0.0,0.5]")) + self.fg_col = json.loads(config.get( + "notification", "fg_col", fallback="[1.0,1.0,1.0,1.0]")) + self.text_time = config.getint( + "notification", "text_time", fallback=30) + self.show_icon = config.getboolean( + "notification", "show_icon", fallback=True) + self.reverse_order = config.getboolean( + "notification", "rev", fallback=False) + self.limit_width = config.getint( + "notification", "limit_width", fallback=100) + self.icon_left = config.getboolean( + "notification", "icon_left", fallback=True) + self.icon_padding = config.getint( + "notification", "icon_padding", fallback=8) + self.icon_size = config.getint( + "notification", "icon_size", fallback=32) + self.padding = config.getint( + "notification", "padding", fallback=8) + self.border_radius = config.getint( + "notification", "border_radius", fallback=8) + + # 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.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_text_time(self.text_time) + self.overlay.set_show_icon(self.show_icon) + self.overlay.set_reverse_order(self.reverse_order) + self.overlay.set_limit_width(self.limit_width) + self.overlay.set_icon_left(self.icon_left) + self.overlay.set_icon_size(self.icon_size) + self.overlay.set_icon_pad(self.icon_padding) + self.overlay.set_padding(self.padding) + self.overlay.set_border_radius(self.border_radius) + self.discover.set_dbus_notifications(self.enabled) + + def save_config(self): + """ + Save the current settings to the 'text' section of the config + """ + config = ConfigParser(interpolation=None) + config.read(self.config_file) + if not config.has_section("notification"): + config.add_section("notification") + + config.set("notification", "rightalign", "%d" % (int(self.align_x))) + config.set("notification", "topalign", "%d" % (self.align_y)) + config.set("notification", "monitor", self.monitor) + config.set("notification", "enabled", "%d" % (int(self.enabled))) + config.set("notification", "floating", "%s" % (int(self.floating))) + config.set("notification", "floating_x", "%s" % (int(self.floating_x))) + config.set("notification", "floating_y", "%s" % (int(self.floating_y))) + config.set("notification", "floating_w", "%s" % (int(self.floating_w))) + config.set("notification", "floating_h", "%s" % (int(self.floating_h))) + config.set("notification", "bg_col", json.dumps(self.bg_col)) + config.set("notification", "fg_col", json.dumps(self.fg_col)) + config.set("notification", "text_time", "%s" % (int(self.text_time))) + config.set("notification", "show_icon", "%s" % + (int(self.show_icon))) + config.set("notification", "rev", "%s" % + (int(self.reverse_order))) + config.set("notification", "limit_width", "%d" % + (int(self.limit_width))) + config.set("notification", "icon_left", "%d" % (int(self.icon_left))) + config.set("notification", "icon_padding", "%d" % + (int(self.icon_padding))) + config.set("notification", "icon_size", "%d" % (int(self.icon_size))) + config.set("notification", "padding", "%d" % (int(self.padding))) + config.set("notification", "border_radius", "%d" % + (int(self.border_radius))) + + if self.font: + config.set("notification", "font", self.font) + + with open(self.config_file, 'w') as file: + config.write(file) + + def create_gui(self): + """ + Prepare the gui + """ + box = Gtk.Grid() + + # Enabled + enabled_label = Gtk.Label.new("Enable") + enabled = Gtk.CheckButton.new() + enabled.set_active(self.enabled) + enabled.connect("toggled", self.change_enabled) + + # Enabled + testing_label = Gtk.Label.new("Show test content") + testing = Gtk.CheckButton.new() + testing.connect("toggled", self.change_testing) + + # Order + reverse_label = Gtk.Label.new("Reverse Order") + reverse = Gtk.CheckButton.new() + reverse.set_active(self.reverse_order) + reverse.connect("toggled", self.change_reverse_order) + + # Popup timer + text_time_label = Gtk.Label.new("Popup timer") + text_time_adjustment = Gtk.Adjustment.new( + self.text_time, 8, 9000, 1, 1, 8) + text_time = Gtk.SpinButton.new(text_time_adjustment, 0, 0) + text_time.connect("value-changed", self.change_text_time) + + # Limit width + limit_width_label = Gtk.Label.new("Limit popup width") + limit_width_adjustment = Gtk.Adjustment.new( + self.limit_width, 100, 9000, 1, 1, 8) + limit_width = Gtk.SpinButton.new(limit_width_adjustment, 0, 0) + limit_width.connect("value-changed", self.change_limit_width) + + # Font chooser + font_label = Gtk.Label.new("Font") + font = Gtk.FontButton() + if self.font: + font.set_font(self.font) + font.connect("font-set", self.change_font) + + # Icon alignment + align_icon_label = Gtk.Label.new("Icon position") + align_icon_store = Gtk.ListStore(str) + align_icon_store.append(["Left"]) + align_icon_store.append(["Right"]) + align_icon = Gtk.ComboBox.new_with_model(align_icon_store) + align_icon.set_active(not self.icon_left) + align_icon.connect("changed", self.change_icon_left) + renderer_text = Gtk.CellRendererText() + align_icon.pack_start(renderer_text, True) + align_icon.add_attribute(renderer_text, "text", 0) + + # Colours + bg_col_label = Gtk.Label.new("Background colour") + bg_col = Gtk.ColorButton.new_with_rgba( + Gdk.RGBA(self.bg_col[0], self.bg_col[1], self.bg_col[2], self.bg_col[3])) + fg_col_label = Gtk.Label.new("Text colour") + fg_col = Gtk.ColorButton.new_with_rgba( + Gdk.RGBA(self.fg_col[0], self.fg_col[1], self.fg_col[2], self.fg_col[3])) + bg_col.set_use_alpha(True) + fg_col.set_use_alpha(True) + bg_col.connect("color-set", self.change_bg) + fg_col.connect("color-set", self.change_fg) + + # Monitor & Alignment + align_label = Gtk.Label.new("Overlay Location") + + align_type_box = Gtk.HBox() + align_type_edge = Gtk.RadioButton.new_with_label( + None, "Anchor to edge") + align_type_floating = Gtk.RadioButton.new_with_label_from_widget( + align_type_edge, "Floating") + if self.floating: + align_type_floating.set_active(True) + align_type_box.add(align_type_edge) + align_type_box.add(align_type_floating) + + monitor_store = Gtk.ListStore(str) + display = Gdk.Display.get_default() + if "get_n_monitors" in dir(display): + for i in range(0, display.get_n_monitors()): + monitor_store.append([display.get_monitor(i).get_model()]) + monitor = Gtk.ComboBox.new_with_model(monitor_store) + monitor.set_active(self.get_monitor_index(self.monitor)) + monitor.connect("changed", self.change_monitor) + renderer_text = Gtk.CellRendererText() + monitor.pack_start(renderer_text, True) + monitor.add_attribute(renderer_text, "text", 0) + + align_x_store = Gtk.ListStore(str) + align_x_store.append(["Left"]) + align_x_store.append(["Right"]) + align_x = Gtk.ComboBox.new_with_model(align_x_store) + align_x.set_active(True if self.align_x else False) + align_x.connect("changed", self.change_align_x) + renderer_text = Gtk.CellRendererText() + align_x.pack_start(renderer_text, True) + align_x.add_attribute(renderer_text, "text", 0) + + align_y_store = Gtk.ListStore(str) + align_y_store.append(["Top"]) + align_y_store.append(["Middle"]) + align_y_store.append(["Bottom"]) + align_y = Gtk.ComboBox.new_with_model(align_y_store) + align_y.set_active(self.align_y) + align_y.connect("changed", self.change_align_y) + renderer_text = Gtk.CellRendererText() + align_y.pack_start(renderer_text, True) + align_y.add_attribute(renderer_text, "text", 0) + + align_placement_button = Gtk.Button.new_with_label("Place Window") + + align_type_edge.connect("toggled", self.change_align_type_edge) + align_type_floating.connect("toggled", self.change_align_type_floating) + align_placement_button.connect("pressed", self.change_placement) + + # Show Icons + show_icon_label = Gtk.Label.new("Show Icon") + show_icon = Gtk.CheckButton.new() + show_icon.set_active(self.show_icon) + show_icon.connect("toggled", self.change_show_icon) + + # Icon Padding + icon_padding_label = Gtk.Label.new("Icon padding") + icon_padding_adjustment = Gtk.Adjustment.new( + self.icon_padding, 0, 150, 1, 1, 8) + icon_padding = Gtk.SpinButton.new(icon_padding_adjustment, 0, 0) + icon_padding.connect("value-changed", self.change_icon_pad) + + # Icon Size + icon_size_label = Gtk.Label.new("Icon size") + icon_size_adjustment = Gtk.Adjustment.new( + self.icon_size, 0, 128, 1, 1, 8) + icon_size = Gtk.SpinButton.new(icon_size_adjustment, 0, 0) + icon_size.connect("value-changed", self.change_icon_size) + + # Padding + padding_label = Gtk.Label.new("Notification padding") + padding_adjustment = Gtk.Adjustment.new( + self.padding, 0, 150, 1, 1, 8) + padding = Gtk.SpinButton.new(padding_adjustment, 0, 0) + padding.connect("value-changed", self.change_padding) + + # Border Radius + border_radius_label = Gtk.Label.new("Border radius") + border_radius_adjustment = Gtk.Adjustment.new( + self.border_radius, 0, 50, 1, 1, 8) + border_radius = Gtk.SpinButton.new(border_radius_adjustment, 0, 0) + border_radius.connect( + "value-changed", self.change_border_radius) + + 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.text_time_widget = text_time + self.text_time_label_widget = text_time_label + + box.attach(enabled_label, 0, 0, 1, 1) + box.attach(enabled, 1, 0, 1, 1) + box.attach(reverse_label, 0, 1, 1, 1) + box.attach(reverse, 1, 1, 1, 1) + box.attach(text_time_label, 0, 3, 1, 1) + box.attach(text_time, 1, 3, 1, 1) + box.attach(limit_width_label, 0, 4, 1, 1) + box.attach(limit_width, 1, 4, 1, 1) + + box.attach(font_label, 0, 6, 1, 1) + box.attach(font, 1, 6, 1, 1) + box.attach(fg_col_label, 0, 7, 1, 1) + box.attach(fg_col, 1, 7, 1, 1) + box.attach(bg_col_label, 0, 8, 1, 1) + box.attach(bg_col, 1, 8, 1, 1) + box.attach(align_label, 0, 9, 1, 5) + #box.attach(align_type_box, 1, 8, 1, 1) + box.attach(monitor, 1, 10, 1, 1) + box.attach(align_x, 1, 11, 1, 1) + box.attach(align_y, 1, 12, 1, 1) + box.attach(align_placement_button, 1, 13, 1, 1) + box.attach(show_icon_label, 0, 14, 1, 1) + box.attach(show_icon, 1, 14, 1, 1) + box.attach(align_icon_label, 0, 15, 1, 1) + box.attach(align_icon, 1, 15, 1, 1) + box.attach(icon_padding_label, 0, 16, 1, 1) + box.attach(icon_padding, 1, 16, 1, 1) + box.attach(icon_size_label, 0, 17, 1, 1) + box.attach(icon_size, 1, 17, 1, 1) + box.attach(padding_label, 0, 18, 1, 1) + box.attach(padding, 1, 18, 1, 1) + + box.attach(testing_label, 0, 20, 1, 1) + box.attach(testing, 1, 20, 1, 1) + + self.add(box) + + def change_padding(self, button): + """ + Padding between notifications changed + """ + self.overlay.set_padding(button.get_value()) + self.padding = button.get_value() + + self.save_config() + + def change_border_radius(self, button): + """ + Border radius changed + """ + self.overlay.set_border_radius(button.get_value()) + self.padding = button.get_value() + + self.save_config() + + def change_icon_size(self, button): + """ + Icon size changed + """ + self.overlay.set_icon_size(button.get_value()) + self.icon_size = button.get_value() + + self.save_config() + + def change_icon_pad(self, button): + """ + Icon padding changed + """ + self.overlay.set_icon_pad(button.get_value()) + self.icon_pad = button.get_value() + + self.save_config() + + def change_icon_left(self, button): + """ + Icon alignment changed + """ + self.overlay.set_icon_left(button.get_active() != 1) + self.icon_left = (button.get_active() != 1) + + self.save_config() + + def change_font(self, button): + """ + Font settings changed + """ + font = button.get_font() + self.overlay.set_font(font) + + self.font = font + self.save_config() + + def change_text_time(self, button): + """ + Popup style setting changed + """ + self.overlay.set_text_time(button.get_value()) + + self.text_time = button.get_value() + self.save_config() + + def change_limit_width(self, button): + """ + Popup width limiter + """ + self.overlay.set_limit_width(button.get_value()) + self.limit_width = button.get_value() + self.save_config() + + def change_bg(self, button): + """ + Background colour changed + """ + colour = button.get_rgba() + colour = [colour.red, colour.green, colour.blue, colour.alpha] + self.overlay.set_bg(colour) + + self.bg_col = colour + self.save_config() + + def change_fg(self, button): + """ + Foreground colour changed + """ + colour = button.get_rgba() + colour = [colour.red, colour.green, colour.blue, colour.alpha] + self.overlay.set_fg(colour) + + self.fg_col = colour + self.save_config() + + def change_show_icon(self, button): + """ + Icon setting changed + """ + self.overlay.set_show_icon(button.get_active()) + + self.show_icon = button.get_active() + self.save_config() + + def change_reverse_order(self, button): + """ + Reverse Order changed + """ + self.overlay.set_reverse_order(button.get_active()) + + self.reverse_order = button.get_active() + self.save_config() + + def change_testing(self, button): + self.overlay.set_testing(button.get_active()) + + def change_enabled(self, button): + """ + Overlay active state toggled + """ + self.overlay.set_enabled(button.get_active()) + self.enabled = button.get_active() + self.discover.set_dbus_notifications(self.enabled) + self.save_config() diff --git a/discover_overlay/overlay.py b/discover_overlay/overlay.py index 31bbdb6..f8f63e3 100644 --- a/discover_overlay/overlay.py +++ b/discover_overlay/overlay.py @@ -318,6 +318,7 @@ class OverlayWindow(Gtk.Window): self.enabled = enabled if enabled and not self.hidden and not self.piggyback_parent: self.show_all() + self.set_untouchable() else: self.hide() diff --git a/discover_overlay/settings_window.py b/discover_overlay/settings_window.py index fd47a68..0faddd2 100644 --- a/discover_overlay/settings_window.py +++ b/discover_overlay/settings_window.py @@ -16,6 +16,7 @@ from .voice_settings import VoiceSettingsWindow from .text_settings import TextSettingsWindow from .general_settings import GeneralSettingsWindow from .about_settings import AboutSettingsWindow +from .notification_settings import NotificationSettingsWindow gi.require_version("Gtk", "3.0") # pylint: disable=wrong-import-position,wrong-import-order from gi.repository import Gtk # nopep8 @@ -33,6 +34,7 @@ class MainSettingsWindow(Gtk.Window): self.discover = discover self.text_overlay = discover.text_overlay self.voice_overlay = discover.voice_overlay + self.notification_overlay = discover.notification_overlay self.set_title("Discover Overlay Configuration") self.set_icon_name("discover-overlay") self.set_default_size(280, 180) @@ -56,10 +58,15 @@ class MainSettingsWindow(Gtk.Window): self.text_settings = TextSettingsWindow(self.text_overlay, discover) text_label = Gtk.Label.new("Text") - notebook.append_page(self.text_settings) notebook.set_tab_label(self.text_settings, text_label) + self.notification_settings = NotificationSettingsWindow( + self.notification_overlay, discover) + notification_label = Gtk.Label.new("Notifications") + notebook.append_page(self.notification_settings) + notebook.set_tab_label(self.notification_settings, notification_label) + self.core_settings = GeneralSettingsWindow(self.discover) core_label = Gtk.Label.new("Core") notebook.append_page(self.core_settings) @@ -84,6 +91,7 @@ class MainSettingsWindow(Gtk.Window): self.about_settings.present_settings() self.voice_settings.present_settings() self.text_settings.present_settings() + self.notification_settings.present_settings() self.core_settings.present_settings() self.notebook.show() self.show() diff --git a/discover_overlay/text_overlay.py b/discover_overlay/text_overlay.py index 7f7fc73..0257378 100644 --- a/discover_overlay/text_overlay.py +++ b/discover_overlay/text_overlay.py @@ -168,19 +168,21 @@ class TextOverlayWindow(OverlayWindow): self.needsredraw = True logging.info("recv_attach redraw") - def overlay_draw(self, _w, context, data=None): + def overlay_draw(self, w, context, data=None): """ Draw the overlay """ + if self.piggyback: + self.piggyback.overlay_draw(w, context, data) if not self.enabled: return self.context = context - context.set_antialias(cairo.ANTIALIAS_GOOD) (width, height) = 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.piggyback_parent: + context.set_antialias(cairo.ANTIALIAS_GOOD) + context.set_source_rgba(0.0, 0.0, 0.0, 0.0) + context.set_operator(cairo.OPERATOR_SOURCE) + context.paint() context.save() if self.is_wayland or self.piggyback_parent or self.discover.steamos: # Special case! diff --git a/discover_overlay/voice_overlay.py b/discover_overlay/voice_overlay.py index 7142bcf..4780211 100644 --- a/discover_overlay/voice_overlay.py +++ b/discover_overlay/voice_overlay.py @@ -357,8 +357,7 @@ class VoiceOverlayWindow(OverlayWindow): self.def_avatar = pix else: self.avatars[identifier] = pix - if self.context: - self.needsredraw = True + self.needsredraw = True def delete_avatar(self, identifier): """