discover-desktop/discover_overlay/overlay.py
trigg 02e4a455a0 - Add new config variables
- - fade out when inactive
- - inactive time (in seconds) before fade out
- - fade out duration (seconds)
- - fade out lower limit. Lowest opacity at which to stop fading out
- Add activity tracker to voice overlay
- Remove old avatar alpha transparency which reloaded file each time it changed
- Convert to images to PIL and back when needed to lower alpha level
- - This was much less than ideal, but alternatives failed
- Fix issue with premultiplied alpha in image loading
- Lowered minimum avatar opacity
- Fixed an edge case where overlay windows were closed
- Added inactivity variables to config window
- Updated Welsh translation - poorly
2024-03-22 17:37:09 +00:00

447 lines
No EOL
15 KiB
Python

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Overlay parent class. Helpful if we need more overlay
types without copy-and-pasting too much code
"""
import os
import sys
import logging
import gi
import cairo
import Xlib
from Xlib.display import Display
from Xlib import X, Xatom
gi.require_version("Gtk", "3.0")
# pylint: disable=wrong-import-position,wrong-import-order
from gi.repository import Gtk, Gdk, GLib # nopep8
try:
gi.require_version('GtkLayerShell', '0.1')
from gi.repository import GtkLayerShell
except (ImportError, ValueError):
pass
log = logging.getLogger(__name__)
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):
"""
Helper function to determine if Wayland is being used and return the Window type needed
"""
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, discover, piggyback=None):
Gtk.Window.__init__(self, type=self.detect_type())
self.discover = discover
screen = self.get_screen()
self.text_font = None
self.text_size = None
self.pos_x = None
self.pos_y = None
self.width = None
self.height = None
self.hidden = False
self.enabled = False
self.set_size_request(50, 50)
self.hide_on_mouseover = True
self.connect('draw', self.overlay_draw_pre)
# Set RGBA
screen = self.get_screen()
visual = screen.get_rgba_visual()
if not self.get_display().supports_input_shapes():
log.info(
"Input shapes not available. Quitting")
sys.exit(1)
if visual:
# Set the visual even if we can't use it right now
self.set_visual(visual)
self.set_app_paintable(True)
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.piggyback = None
self.piggyback_parent = None
if not piggyback:
self.show_all()
if discover.steamos:
self.set_gamescope_xatom(1)
self.monitor = 0
self.align_right = True
self.align_vert = 1
self.floating = False
self.force_xshape = False
self.context = None
self.redraw_id = None
self.draw_blank = False
self.timeout_mouse_over = 1
self.timer_after_draw = None
if piggyback:
self.set_piggyback(piggyback)
self.get_screen().connect("composited-changed", self.check_composite)
self.get_screen().connect("monitors-changed", self.screen_changed)
self.get_screen().connect("size-changed", self.screen_changed)
self.get_window().set_events(self.get_window().get_events() | Gdk.EventMask.ENTER_NOTIFY_MASK)
self.connect("enter-notify-event", self.mouseover)
self.connect("leave-notify-event", self.mouseout)
self.mouse_over_timer = None
# It shouldn't be possible, but let's not leave
# this process hanging if it happens
self.connect('destroy', self.window_exited)
def window_exited(self, window=None):
sys.exit(1)
def set_gamescope_xatom(self, enabled):
if self.piggyback_parent:
return
display = Display()
atom = display.intern_atom("GAMESCOPE_EXTERNAL_OVERLAY")
opaq = display.intern_atom("_NET_WM_WINDOW_OPACITY")
if self.get_toplevel().get_window():
topw = display.create_resource_object(
"window", self.get_toplevel().get_window().get_xid())
topw.change_property(atom,
Xatom.CARDINAL, 32,
[enabled], X.PropModeReplace)
log.info("Setting GAMESCOPE_EXTERNAL_OVERLAY to %s", enabled)
display.sync()
else:
log.warn("Unable to set GAMESCOPE_EXTERNAL_OVERLAY")
def set_wayland_state(self):
"""
If wayland is in use then attempt to set up a GtkLayerShell
"""
if self.is_wayland:
if not GtkLayerShell.is_supported():
log.info(
"GTK Layer Shell is not supported on this wayland compositor")
log.info("Currently not possible: Gnome, Weston")
sys.exit(0)
if not GtkLayerShell.is_layer_window(self):
GtkLayerShell.init_for_window(self)
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.OVERLAY)
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 set_piggyback(self, other_overlay):
other_overlay.piggyback = self
self.piggyback_parent = other_overlay
def has_content(self):
return False
def overlay_draw_pre(self, _w, context, data=None):
content = self.has_content()
if self.piggyback and self.piggyback.has_content():
content = True
if self.discover.steamos:
if not content:
self.set_gamescope_xatom(0)
else:
if not self.hidden and self.enabled:
self.set_gamescope_xatom(1)
# If we're hiding on mouseover, allow mouse-in
if self.hide_on_mouseover:
# We've mouse-overed
if self.draw_blank:
self.set_untouchable()
context.set_source_rgba(0.0, 0.0, 0.0, 0.0)
context.set_operator(cairo.OPERATOR_SOURCE)
context.paint()
return
else:
(width, height) = self.get_size()
surface = cairo.ImageSurface(
cairo.FORMAT_ARGB32, width, height)
surface_ctx = cairo.Context(surface)
self.overlay_draw(None, surface_ctx)
reg = Gdk.cairo_region_create_from_surface(surface)
self.input_shape_combine_region(reg)
self.overlay_draw(_w, context, data)
def overlay_draw(self, _w, context, data=None):
"""
Draw overlay
"""
def set_font(self, font):
"""
Set the font used by the overlay
"""
self.text_font = font
self.set_needs_redraw()
def set_floating(self, floating, pos_x, pos_y, width, height):
"""
Set if the window is floating and what dimensions to use
"""
# Special case for Cinnamon desktop : see https://github.com/trigg/Discover/issues/322
if 'XDG_SESSION_DESKTOP' in os.environ and os.environ['XDG_SESSION_DESKTOP']=='cinnamon':
floating = True
self.floating = floating
self.pos_x = pos_x
self.pos_y = pos_y
self.width = width
self.height = height
self.force_location()
def set_untouchable(self):
"""
Create a custom input shape and tell it that all of the window is a cut-out
This allows us to have a window above everything but that never gets clicked on
"""
(width, height) = self.get_size()
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
surface_ctx = cairo.Context(surface)
surface_ctx.set_source_rgba(0.0, 0.0, 0.0, 0.0)
surface_ctx.set_operator(cairo.OPERATOR_SOURCE)
surface_ctx.paint()
reg = Gdk.cairo_region_create_from_surface(surface)
self.input_shape_combine_region(reg)
def set_hide_on_mouseover(self, hide):
self.hide_on_mouseover = hide
if self.hide_on_mouseover:
self.set_needs_redraw()
else:
self.set_untouchable()
def set_mouseover_timer(self, time):
self.timeout_mouse_over = time
def unset_shape(self):
"""
Remove XShape (not input shape)
"""
self.get_window().shape_combine_region(None, 0, 0)
def force_location(self):
"""
On X11 enforce the location and sane defaults
On Wayland just store for later
On Gamescope enforce size of display but only if it's the primary overlay
"""
if self.discover.steamos and not self.piggyback_parent:
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()
width = geometry.width
height = geometry.height
self.resize(width, height)
self.set_needs_redraw()
return
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)
if monitor:
geometry = monitor.get_geometry()
scale_factor = monitor.get_scale_factor()
if not self.floating:
width = geometry.width
height = geometry.height
pos_x = geometry.x
pos_y = geometry.y
self.resize(width, height)
self.move(pos_x, pos_y)
else:
self.move(self.pos_x, self.pos_y)
self.resize(self.width, self.height)
else:
if self.floating:
self.move(self.pos_x, self.pos_y)
self.resize(self.width, self.height)
if not self.floating:
(width, height) = self.get_size()
self.width = width
self.height = height
self.set_needs_redraw()
def set_needs_redraw(self):
if not self.hidden and self.enabled:
if self.piggyback_parent:
self.piggyback_parent.set_need_redraw()
if self.redraw_id == None:
self.redraw_id = GLib.idle_add(self.redraw)
else:
log.debug("Already awaiting paint")
# If this overlay has data that expires after draw, plan for that here
if self.timer_after_draw != None:
GLib.timeout_add_seconds(self.timer_after_draw, self.redraw)
def redraw(self):
"""
Request a redraw.
If we're using XShape (optionally or forcibly) then render the image into the shape
so that we only cut out clear sections
"""
self.redraw_id = None
gdkwin = self.get_window()
if self.piggyback_parent:
self.piggyback_parent.redraw()
return
if not self.floating:
(width, height) = self.get_size()
self.width = width
self.height = height
if gdkwin:
compositing = self.get_screen().is_composited()
if not compositing or self.force_xshape:
(width, height) = self.get_size()
surface = cairo.ImageSurface(
cairo.FORMAT_ARGB32, width, height)
surface_ctx = cairo.Context(surface)
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()
self.redraw_id = None
return False
def set_hidden(self, hidden):
self.hidden = hidden
self.set_enabled(self.enabled)
def set_monitor(self, idx=None):
"""
Set the monitor this overlay should display on.
"""
if type(idx) is str:
idx = 0
self.monitor = idx
if self.is_wayland:
display = Gdk.Display.get_default()
if "get_monitor" in dir(display):
monitor = display.get_monitor(self.monitor)
if monitor:
GtkLayerShell.set_monitor(self, monitor)
else:
self.hide()
self.set_wayland_state()
self.show()
else:
log.error("No get_monitor in display")
self.set_untouchable()
self.force_location()
self.set_needs_redraw()
def set_align_x(self, align_right):
"""
Set the alignment (True for right, False for left)
"""
self.align_right = align_right
self.force_location()
self.set_needs_redraw()
def set_align_y(self, align_vert):
"""
Set the veritcal alignment
"""
self.align_vert = align_vert
self.force_location()
self.set_needs_redraw()
def col(self, col, alpha=1.0):
"""
Convenience function to set the cairo context next colour
"""
self.context.set_source_rgba(col[0], col[1], col[2], col[3] * alpha)
def set_force_xshape(self, force):
"""
Set if XShape should be forced
"""
self.force_xshape = force
if self.is_wayland or self.discover.steamos:
# Wayland and XShape are a bad idea unless you're a fan on artifacts
self.force_xshape = False
def set_enabled(self, enabled):
"""
Set if this overlay should be visible
"""
self.enabled = enabled
if enabled and not self.hidden and not self.piggyback_parent:
self.show_all()
self.set_untouchable()
if self.discover.steamos:
self.set_gamescope_xatom(1)
else:
if self.discover.steamos:
self.set_gamescope_xatom(0)
self.hide()
def set_task(self, visible):
self.set_skip_pager_hint(not visible)
self.set_skip_taskbar_hint(not visible)
def check_composite(self, _a=None, _b=None):
self.redraw()
def screen_changed(self, screen=None):
self.set_monitor(self.monitor)
def mouseover(self, a=None, b=None):
self.draw_blank = True
self.set_needs_redraw()
return True
def mouseout(self, a=None, b=None):
GLib.timeout_add_seconds(self.timeout_mouse_over, self.mouseout_timed)
return True
def mouseout_timed(self, a=None, b=None):
self.draw_blank = False
self.set_needs_redraw()