- Moved some gui logic out of connector

- Added dbus and notification eavesdropping
- Added new overlay to show freedesktop notifications
- Taught text overlay to piggyback
This commit is contained in:
Trigg 2022-03-29 19:19:44 +00:00
parent fdc790155a
commit 1ac75c94f9
9 changed files with 1025 additions and 29 deletions

View file

@ -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 = {

View file

@ -12,14 +12,19 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""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":

View file

@ -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):
"""

View file

@ -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 <https://www.gnu.org/licenses/>.
"""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 = "<span>%s</span>\n%s"
message = m_no_body % (self.sanitize_string(line["title"]),
self.sanitize_string(line['body']))
else:
m_with_body = "<span>%s</span>"
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 = "<span foreground='%s'>%s</span>\n%s"
message = m_no_body % (self.sanitize_string(col),
self.sanitize_string(line["title"]),
self.sanitize_string(line['body']))
else:
m_with_body = "<span foreground='%s'>%s</span>"
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("&", "&amp;")
string = string.replace("<", "&lt;")
string = string .replace(">", "&gt;")
string = string.replace("'", "&#39;")
string = string.replace("\"", "&#34;")
return string
def set_testing(self, testing):
self.testing = testing
self.needsredraw = True
self.get_all_images()

View file

@ -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 <https://www.gnu.org/licenses/>.
"""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()

View file

@ -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()

View file

@ -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()

View file

@ -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!

View file

@ -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):
"""