fix: cover the whole display while a disc loads
The loading surface was invisible. GtkWindow only renders its own CSS background when the application is not painting the window itself, and this window was created app-paintable with no draw handler and no child — so it drew nothing at all. The black area during loading was never it: it was VLC's own window, tiled by sway into the usable area, which excludes the strip the dock reserves. That is why the dock and the wallpaper stayed on screen underneath. An earlier headless check missed this because that sway had no wallpaper configured, so an invisible surface and a black one looked identical. Rechecked with the real wallpaper and the real dock underneath. The surface now paints, and carries a spinner and "Loading DVD..." on the same translucent rounded card as the dock. It moves from the top layer to the overlay layer, because the dock is a top-layer surface and DVD mode has to cover it rather than depend on which surface sway created first. Its exclusive zone of -1 keeps sway from moving it out of the way of the dock's reserved strip. It goes up before VLC is started rather than alongside it, so no part of VLC's window is ever seen against the wallpaper, and comes down only once VLC reports playback on screen. The deadline that takes it down regardless is now twenty seconds, since an optical drive can be slow, and a failure to start VLC at all ends the session instead of leaving a cover with nothing behind it. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4a0ff43253
commit
68b6347c05
+19
-2
@@ -45,13 +45,30 @@ disc behind it.
|
|||||||
`classroom-dvd-session` runs the whole thing:
|
`classroom-dvd-session` runs the whole thing:
|
||||||
|
|
||||||
```
|
```
|
||||||
black backdrop layer-shell TOP, covers wallpaper and dock while the
|
loading surface layer-shell OVERLAY, the whole display including the
|
||||||
disc loads, hidden once VLC is on screen
|
dock, taken down once playback is on screen
|
||||||
VLC fullscreen, drawn above the top layer by sway
|
VLC fullscreen, drawn above the top layer by sway
|
||||||
control bar layer-shell OVERLAY, the one layer sway draws above a
|
control bar layer-shell OVERLAY, the one layer sway draws above a
|
||||||
fullscreen window
|
fullscreen window
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Loading
|
||||||
|
|
||||||
|
Pressing DVD covers the entire display immediately — before VLC is even
|
||||||
|
started — with a black surface carrying a spinner and *Loading DVD…* on
|
||||||
|
the same translucent rounded card the dock uses. The wallpaper, the dock
|
||||||
|
and the clock are all gone from the moment the button is pressed.
|
||||||
|
|
||||||
|
It is on the **overlay** layer rather than the top layer, because the
|
||||||
|
dock is a top-layer surface and DVD mode has to cover it. Its exclusive
|
||||||
|
zone is `-1`, which tells sway not to move it out of the way of the
|
||||||
|
strip the dock reserves.
|
||||||
|
|
||||||
|
It comes down when VLC reports playing or paused, meaning the video
|
||||||
|
output exists and has taken the display, plus a moment for the handover.
|
||||||
|
A twenty second deadline takes it down regardless: a cover with nothing
|
||||||
|
behind it is the one outcome DVD mode must never produce.
|
||||||
|
|
||||||
VLC is still the backend because its `dvdnav` module is what decodes
|
VLC is still the backend because its `dvdnav` module is what decodes
|
||||||
disc menus, and nothing else on Debian does that reliably. But nothing
|
disc menus, and nothing else on Debian does that reliably. But nothing
|
||||||
of VLC's own interface is exposed: no menu bar, no panels, no
|
of VLC's own interface is exposed: no menu bar, no panels, no
|
||||||
|
|||||||
+6
-3
@@ -124,9 +124,12 @@ With `DVD_ENABLED=true`:
|
|||||||
|
|
||||||
### DVD mode
|
### DVD mode
|
||||||
|
|
||||||
- [ ] Pressing DVD goes black immediately — no wallpaper, no dock, no
|
- [ ] Pressing DVD covers the **whole** display immediately, dock and
|
||||||
partial VLC window at any point
|
clock included — no wallpaper visible anywhere, at any point
|
||||||
- [ ] Video or the disc menu appears inside that black surface
|
- [ ] A spinner and "Loading DVD…" show while the disc spins up
|
||||||
|
- [ ] No partial VLC window is ever visible against the wallpaper
|
||||||
|
- [ ] Video or the disc menu replaces the loading surface when ready
|
||||||
|
- [ ] A disc that never opens still gives the desktop back (20s)
|
||||||
- [ ] No VLC chrome anywhere: no menu bar, no fullscreen controller,
|
- [ ] No VLC chrome anywhere: no menu bar, no fullscreen controller,
|
||||||
no cone, no sway title bar
|
no cone, no sway title bar
|
||||||
- [ ] Wallpaper is never visible around VLC
|
- [ ] Wallpaper is never visible around VLC
|
||||||
|
|||||||
+115
-42
@@ -10,8 +10,9 @@ this program, styled from the same stylesheet as the dock.
|
|||||||
|
|
||||||
Layout, bottom to top:
|
Layout, bottom to top:
|
||||||
|
|
||||||
black backdrop layer-shell TOP, covers wallpaper and dock while
|
loading surface layer-shell OVERLAY, covers the whole display
|
||||||
the disc loads, hidden once VLC is up
|
including the dock while the disc loads, removed
|
||||||
|
once playback is actually on screen
|
||||||
VLC fullscreen, drawn above the top layer by sway
|
VLC fullscreen, drawn above the top layer by sway
|
||||||
control bar layer-shell OVERLAY, drawn above fullscreen video
|
control bar layer-shell OVERLAY, drawn above fullscreen video
|
||||||
"""
|
"""
|
||||||
@@ -76,26 +77,56 @@ HOVER_STRIP = 14
|
|||||||
IDLE_HIDE_SECONDS = 4
|
IDLE_HIDE_SECONDS = 4
|
||||||
LEAVE_HIDE_SECONDS = 1
|
LEAVE_HIDE_SECONDS = 1
|
||||||
|
|
||||||
# The backdrop goes away as soon as VLC is up. The deadline is a safety
|
# The loading surface goes away as soon as playback is on screen. The
|
||||||
# net: whatever happens, DVD mode must never leave a black screen with
|
# deadline is a safety net: whatever happens, DVD mode must never leave a
|
||||||
# nothing behind it.
|
# cover over a screen with nothing behind it. Generous, because an
|
||||||
|
# optical drive can take a while to spin a disc up.
|
||||||
WINDOW_POLL_MS = 500
|
WINDOW_POLL_MS = 500
|
||||||
BACKDROP_DEADLINE_SECONDS = 10
|
LOADING_DEADLINE_SECONDS = 20
|
||||||
|
|
||||||
|
# Long enough for the cover to be on screen before VLC is even started,
|
||||||
|
# so no part of VLC's window is ever visible against the wallpaper.
|
||||||
|
LAUNCH_DELAY_MS = 120
|
||||||
|
|
||||||
STATUS_POLL_SECONDS = 1
|
STATUS_POLL_SECONDS = 1
|
||||||
|
|
||||||
EXTRA_CSS = """
|
def extra_css(scale):
|
||||||
window#classroom-dvd-backdrop {
|
"""Styling for the two surfaces this program owns.
|
||||||
|
|
||||||
|
The loading card reuses the dock's own .dock class, so it is the same
|
||||||
|
translucent surface, border and corner radius as everything else in
|
||||||
|
the room.
|
||||||
|
"""
|
||||||
|
return f"""
|
||||||
|
window#classroom-dvd-loading {{
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
background-image: none;
|
background-image: none;
|
||||||
}
|
}}
|
||||||
|
|
||||||
window#classroom-dvd-bar {
|
.dvd-loading {{
|
||||||
|
padding: {scaled(16, scale)}px {scaled(22, scale)}px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.dvd-loading label {{
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: {scaled(17, scale)}px;
|
||||||
|
font-weight: 600;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.dvd-loading spinner,
|
||||||
|
.dvd-loading spinner:checked {{
|
||||||
|
color: #ffffff;
|
||||||
|
opacity: 1;
|
||||||
|
min-width: {scaled(28, scale)}px;
|
||||||
|
min-height: {scaled(28, scale)}px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
window#classroom-dvd-bar {{
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
background-image: none;
|
background-image: none;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -161,24 +192,35 @@ class VlcControl:
|
|||||||
return found.group(1) if found else None
|
return found.group(1) if found else None
|
||||||
|
|
||||||
|
|
||||||
class Backdrop(Gtk.Window):
|
class LoadingSurface(Gtk.Window):
|
||||||
"""Full-screen black, above the wallpaper and the dock."""
|
"""The whole display, while the disc spins up.
|
||||||
|
|
||||||
def __init__(self):
|
Entering DVD mode has to look like entering DVD mode, not like an
|
||||||
|
application opening on the classroom desktop, so this goes up before
|
||||||
|
VLC is even started and covers everything until there is something
|
||||||
|
to show.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, scale):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
screen = self.get_screen()
|
self.set_name("classroom-dvd-loading")
|
||||||
visual = screen.get_rgba_visual()
|
|
||||||
|
|
||||||
if visual is not None:
|
|
||||||
self.set_visual(visual)
|
|
||||||
|
|
||||||
self.set_app_paintable(True)
|
|
||||||
self.set_name("classroom-dvd-backdrop")
|
|
||||||
self.set_decorated(False)
|
self.set_decorated(False)
|
||||||
|
|
||||||
|
# Deliberately NOT app-paintable, and deliberately no RGBA visual.
|
||||||
|
# A GtkWindow only renders its own CSS background when the
|
||||||
|
# application is not painting the window itself, and this surface
|
||||||
|
# is nothing but background. Setting app_paintable here is what
|
||||||
|
# made an earlier version of this window invisible.
|
||||||
|
|
||||||
GtkLayerShell.init_for_window(self)
|
GtkLayerShell.init_for_window(self)
|
||||||
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP)
|
|
||||||
|
# OVERLAY rather than TOP. The dock is a TOP layer surface, and
|
||||||
|
# DVD mode has to cover it; OVERLAY is above it without depending
|
||||||
|
# on which surface sway happens to have created first. It is
|
||||||
|
# taken down the moment playback is on screen, so it never sits
|
||||||
|
# over the video.
|
||||||
|
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.OVERLAY)
|
||||||
|
|
||||||
for edge in (
|
for edge in (
|
||||||
GtkLayerShell.Edge.TOP,
|
GtkLayerShell.Edge.TOP,
|
||||||
@@ -188,19 +230,33 @@ class Backdrop(Gtk.Window):
|
|||||||
):
|
):
|
||||||
GtkLayerShell.set_anchor(self, edge, True)
|
GtkLayerShell.set_anchor(self, edge, True)
|
||||||
|
|
||||||
# -1 means "ignore everyone else's reserved space": DVD mode is
|
# -1 means "do not move me out of anyone's way": the strip the
|
||||||
# the whole display, including the strip the dock reserves.
|
# dock reserves is part of the display like any other.
|
||||||
GtkLayerShell.set_exclusive_zone(self, -1)
|
GtkLayerShell.set_exclusive_zone(self, -1)
|
||||||
GtkLayerShell.set_keyboard_interactivity(self, False)
|
GtkLayerShell.set_keyboard_interactivity(self, False)
|
||||||
|
|
||||||
self.connect("realize", self.ignore_pointer)
|
card = Gtk.Box(
|
||||||
|
orientation=Gtk.Orientation.HORIZONTAL,
|
||||||
|
spacing=scaled(14, scale),
|
||||||
|
)
|
||||||
|
card.get_style_context().add_class("dock")
|
||||||
|
card.get_style_context().add_class("dvd-loading")
|
||||||
|
|
||||||
def ignore_pointer(self, *_):
|
self.spinner = Gtk.Spinner()
|
||||||
# Scenery, never a target.
|
self.spinner.set_size_request(scaled(28, scale), scaled(28, scale))
|
||||||
if cairo is None:
|
card.pack_start(self.spinner, False, False, 0)
|
||||||
return
|
|
||||||
|
|
||||||
self.get_window().input_shape_combine_region(cairo.Region(), 0, 0)
|
label = Gtk.Label(label="Loading DVD\u2026")
|
||||||
|
card.pack_start(label, False, False, 0)
|
||||||
|
|
||||||
|
centred = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0, yscale=0)
|
||||||
|
centred.add(card)
|
||||||
|
|
||||||
|
self.add(centred)
|
||||||
|
|
||||||
|
def show_all(self):
|
||||||
|
super().show_all()
|
||||||
|
self.spinner.start()
|
||||||
|
|
||||||
|
|
||||||
class ControlBar(Gtk.Window):
|
class ControlBar(Gtk.Window):
|
||||||
@@ -404,7 +460,7 @@ class DvdSession:
|
|||||||
self.eject_on_exit = False
|
self.eject_on_exit = False
|
||||||
self.elapsed = 0
|
self.elapsed = 0
|
||||||
|
|
||||||
self.backdrop = Backdrop()
|
self.loading = LoadingSurface(self.scale)
|
||||||
self.bar = ControlBar(
|
self.bar = ControlBar(
|
||||||
self.scale,
|
self.scale,
|
||||||
[
|
[
|
||||||
@@ -473,49 +529,64 @@ class DvdSession:
|
|||||||
# -- the player -------------------------------------------------
|
# -- the player -------------------------------------------------
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.backdrop.show_all()
|
self.loading.show_all()
|
||||||
self.bar.show_all()
|
self.bar.show_all()
|
||||||
|
|
||||||
|
# The cover goes up first. VLC follows once it is actually on
|
||||||
|
# screen, so no part of its window is ever seen against the
|
||||||
|
# wallpaper.
|
||||||
|
GLib.timeout_add(LAUNCH_DELAY_MS, self.launch)
|
||||||
|
|
||||||
|
def launch(self):
|
||||||
command = vlc.player_command(self.device, self.port, self.password)
|
command = vlc.player_command(self.device, self.port, self.password)
|
||||||
|
|
||||||
|
try:
|
||||||
self.pid, _, _, _ = GLib.spawn_async(
|
self.pid, _, _, _ = GLib.spawn_async(
|
||||||
argv=command,
|
argv=command,
|
||||||
flags=GLib.SpawnFlags.SEARCH_PATH
|
flags=GLib.SpawnFlags.SEARCH_PATH
|
||||||
| GLib.SpawnFlags.DO_NOT_REAP_CHILD,
|
| GLib.SpawnFlags.DO_NOT_REAP_CHILD,
|
||||||
)
|
)
|
||||||
|
except GLib.Error as error:
|
||||||
|
# A cover with nothing behind it is the one outcome DVD mode
|
||||||
|
# must never produce.
|
||||||
|
print(f"classroom-dvd-session: {error}", file=sys.stderr)
|
||||||
|
Gtk.main_quit()
|
||||||
|
return GLib.SOURCE_REMOVE
|
||||||
|
|
||||||
GLib.child_watch_add(GLib.PRIORITY_DEFAULT, self.pid, self.on_exit)
|
GLib.child_watch_add(GLib.PRIORITY_DEFAULT, self.pid, self.on_exit)
|
||||||
|
|
||||||
GLib.timeout_add(WINDOW_POLL_MS, self.watch_startup)
|
GLib.timeout_add(WINDOW_POLL_MS, self.watch_startup)
|
||||||
GLib.timeout_add_seconds(STATUS_POLL_SECONDS, self.watch_state)
|
GLib.timeout_add_seconds(STATUS_POLL_SECONDS, self.watch_state)
|
||||||
|
|
||||||
|
return GLib.SOURCE_REMOVE
|
||||||
|
|
||||||
def on_exit(self, pid, status, *_):
|
def on_exit(self, pid, status, *_):
|
||||||
GLib.spawn_close_pid(pid)
|
GLib.spawn_close_pid(pid)
|
||||||
self.pid = None
|
self.pid = None
|
||||||
Gtk.main_quit()
|
Gtk.main_quit()
|
||||||
|
|
||||||
def watch_startup(self):
|
def watch_startup(self):
|
||||||
"""Keep the black surface until VLC is actually covering the screen.
|
"""Hold the cover until playback is genuinely on screen.
|
||||||
|
|
||||||
Playing or paused means the video output exists, which is also
|
Playing or paused means the video output exists, which is also
|
||||||
when VLC takes the screen. The deadline is the safety net: DVD
|
when VLC takes the display. The deadline is the safety net: DVD
|
||||||
mode must never leave a black screen with nothing behind it, even
|
mode must never leave a cover over a screen with nothing behind
|
||||||
if the disc never opens.
|
it, even if the disc never opens.
|
||||||
"""
|
"""
|
||||||
self.elapsed += WINDOW_POLL_MS / 1000
|
self.elapsed += WINDOW_POLL_MS / 1000
|
||||||
|
|
||||||
ready = self.control.state() in ("playing", "paused")
|
ready = self.control.state() in ("playing", "paused")
|
||||||
|
|
||||||
if ready or self.elapsed >= BACKDROP_DEADLINE_SECONDS:
|
if ready or self.elapsed >= LOADING_DEADLINE_SECONDS:
|
||||||
# A moment for the video window to actually take the screen,
|
# A moment for the video window to actually take the screen,
|
||||||
# so the handover has no frame of wallpaper in it.
|
# so the handover has no frame of wallpaper in it.
|
||||||
GLib.timeout_add(400, self.drop_backdrop)
|
GLib.timeout_add(400, self.drop_loading)
|
||||||
return GLib.SOURCE_REMOVE
|
return GLib.SOURCE_REMOVE
|
||||||
|
|
||||||
return GLib.SOURCE_CONTINUE
|
return GLib.SOURCE_CONTINUE
|
||||||
|
|
||||||
def drop_backdrop(self):
|
def drop_loading(self):
|
||||||
self.backdrop.hide()
|
self.loading.hide()
|
||||||
return GLib.SOURCE_REMOVE
|
return GLib.SOURCE_REMOVE
|
||||||
|
|
||||||
def watch_state(self):
|
def watch_state(self):
|
||||||
@@ -530,7 +601,9 @@ class DvdSession:
|
|||||||
def load_css(scale):
|
def load_css(scale):
|
||||||
try:
|
try:
|
||||||
provider = Gtk.CssProvider()
|
provider = Gtk.CssProvider()
|
||||||
provider.load_from_data((style.render(scale) + EXTRA_CSS).encode("utf-8"))
|
provider.load_from_data(
|
||||||
|
(style.render(scale) + extra_css(scale)).encode("utf-8")
|
||||||
|
)
|
||||||
except (OSError, GLib.Error) as error:
|
except (OSError, GLib.Error) as error:
|
||||||
print(f"classroom-dvd-session: {error}", file=sys.stderr)
|
print(f"classroom-dvd-session: {error}", file=sys.stderr)
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user