From c9e1e0df7be4c644478ef05de2edf105bd66a97c Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Sat, 16 Mar 2024 17:56:52 +0100 Subject: [PATCH] add popout/floating and fullscreen feature to video player --- melon/widgets/iconbutton.py | 16 ++- melon/widgets/player.py | 206 +++++++++++++++++++++++++++++++++++- 2 files changed, 215 insertions(+), 7 deletions(-) diff --git a/melon/widgets/iconbutton.py b/melon/widgets/iconbutton.py index 6964aed..f0b4b12 100644 --- a/melon/widgets/iconbutton.py +++ b/melon/widgets/iconbutton.py @@ -5,15 +5,21 @@ gi.require_version('Adw', '1') from gi.repository import Gtk, Adw class IconButton(Gtk.Button): - def __init__(self, name, icon, tooltip="", *args, **kwargs): + def __init__(self, name, icon, tooltip=None, *args, **kwargs): super().__init__(*args, **kwargs) self.inner = Adw.ButtonContent() self.inner.set_can_shrink(True) self.set_can_shrink(True) self.set_child(self.inner) self.update(name, icon, tooltip) - def update(self, name, icon, tooltip=""): - self.inner.set_label(name) + def set_icon(self, icon): self.inner.set_icon_name(icon) - if tooltip != "": - self.set_tooltip_text(tooltip) + def set_name(self, name): + self.inner.set_label(name) + def set_tooltip(self, tooltip): + self.set_tooltip_text(tooltip) + def update(self, name, icon, tooltip=None): + self.set_name(name) + self.set_icon(icon) + if not tooltip is None: + self.set_tooltip(tooltip) diff --git a/melon/widgets/player.py b/melon/widgets/player.py index 502d66e..da5ad01 100644 --- a/melon/widgets/player.py +++ b/melon/widgets/player.py @@ -8,6 +8,8 @@ from unidecode import unidecode from gettext import gettext as _ from datetime import datetime +from enum import Enum, auto + from melon.servers import Stream from melon.widgets.iconbutton import IconButton from melon.widgets.preferencerow import PreferenceRow, PreferenceType, Preference @@ -79,7 +81,7 @@ class VolumeDisplay(OverlayDispay): def get_text_for(self, value:float) -> str: return f"{int(value * 100)}%" -class VideoPlayer(Gtk.Overlay): +class VideoPlayerBase(Gtk.Overlay): volume = 1.0 # inverse opacity of overlay brightness = 1.0 @@ -94,6 +96,8 @@ class VideoPlayer(Gtk.Overlay): self.stopped = True self.update_callback=None self.ended_callback=None + self.toggle_fullscreen = None + self.toggle_popout = None overlay = Gtk.Overlay() self.set_child(overlay) @@ -126,6 +130,7 @@ class VideoPlayer(Gtk.Overlay): # this makes the player full-width self.picture.set_can_shrink(True) self.picture.set_keep_aspect_ratio(True) + self.picture.set_hexpand(True) # Set the paintable on the picture self.picture.set_paintable(paintable) @@ -144,6 +149,30 @@ class VideoPlayer(Gtk.Overlay): overlay.add_overlay(self.volume_display) self.volume_display.hide() + self.win_controls = Gtk.Box() + overlay.add_overlay(self.win_controls) + # used for flat button styles + self.win_controls.add_css_class("toolbar") + # add padding to box + self.win_controls.add_css_class("osd") + # distance control box from edges + margin = 4 + self.win_controls.set_margin_start(margin) + self.win_controls.set_margin_end(margin) + self.win_controls.set_margin_top(margin) + self.win_controls.set_margin_bottom(margin) + # add player window control buttons on top right + self.win_controls.set_halign(Gtk.Align.END) + self.win_controls.set_valign(Gtk.Align.START) + + self.popout_ctr = IconButton("", "view-paged-symbolic", tooltip=_("Toggle floating window")) + self.popout_ctr.connect("clicked", lambda _: self._toggle_popout()) + self.win_controls.append(self.popout_ctr) + + self.fullscreen_ctr = IconButton("", "view-fullscreen-symbolic", tooltip=_("Toggle fullscreen")) + self.fullscreen_ctr.connect("clicked", lambda _: self._toggle_fullscreen()) + self.win_controls.append(self.fullscreen_ctr) + self.controls = Gtk.Box() overlay.add_overlay(self.controls) # used for flat button styles @@ -151,7 +180,6 @@ class VideoPlayer(Gtk.Overlay): # add padding to box self.controls.add_css_class("osd") # distance control box from edges - margin = 4 self.controls.set_margin_start(margin) self.controls.set_margin_end(margin) self.controls.set_margin_top(margin) @@ -251,14 +279,40 @@ class VideoPlayer(Gtk.Overlay): def connect_ended(self, callback=None): self.ended_callback = callback + def _toggle_fullscreen(self): + if not self.toggle_fullscreen is None: + state = self.toggle_fullscreen() + def _toggle_popout(self): + if not self.toggle_popout is None: + state = self.toggle_popout() + + def set_toggle_states(self, popout:bool, fullscreen:bool): + if popout: + # window now popped out + self.popout_ctr.set_icon("view-dual-symbolic") + else: + self.popout_ctr.set_icon("view-paged-symbolic") + if fullscreen: + # now fullscreen + self.fullscreen_ctr.set_icon("view-restore-symbolic") + else: + self.fullscreen_ctr.set_icon("view-fullscreen-symbolic") + + def connect_toggle_fullscreen(self, callback=None): + self.toggle_fullscreen = callback + def connect_toggle_popout(self, callback=None): + self.toggle_popout = callback + def _show_controls(self): self.controls.set_opacity(1) + self.win_controls.set_opacity(1) def _hide_controls(self, force=False): if self.hover_ctr.get_property("contains-pointer") and not force: # mouse is still hovering on player # do not hide - try again later return True self.controls.set_opacity(0) + self.win_controls.set_opacity(0) def _schedule_hide_controls(self, timeout=2): GLib.timeout_add(timeout*1000, self._hide_controls) @@ -519,3 +573,151 @@ def format_seconds(secs:float) -> str: m = f"{minutes:02}" s = f"{seconds:02}" return f"{h}{m}:{s}" + +class VideoPlayer(Gtk.Stack): + def __init__(self, streams: list[Stream], *args, **kwargs): + super().__init__(*args, **kwargs) + self.window = None + self.player = VideoPlayerBase(streams) + self.player.connect_toggle_fullscreen(self.toggle_fullscreen) + self.player.connect_toggle_popout(self.toggle_popout) + placeholder = Gtk.Label() + placeholder.set_label(_("The video is playing in separate window")) + self.add_named(placeholder, "placeholder") + self._show_player() + def _show_player(self): + has_player = self.get_child_by_name("player") + if not has_player is None: + # has player -> remove it + self.remove(has_player) + else: + self.player.unparent() + self.add_named(self.player, "player") + self.set_visible_child_name("player") + def _popout_player(self): + has_player = self.get_child_by_name("player") + if not has_player is None: + self.remove(has_player) + self.set_visible_child_name("placeholder") + # bridge methods + def connect_update(self, callback=None): + self.player.connect_update(callback) + def connect_ended(self, callback=None): + self.player.connect_ended(callback) + def change_volume(self, vol): + self.player.change_volume(vol) + def change_brightness(self, bright): + self.player.change_brightness(bright) + def seek(self, delta): + self.player.seek(delta) + def seek_forwards(self, delta=30): + self.player.seek_forwards(delta) + def seek_backwards(self, delta=30): + self.player.seek_backwards(delta) + def goto(self, position): + self.player.goto(position) + def play(self): + self.player.play() + def pause(self): + self.player.pause() + def stop(self): + self.player.stop() + def select_stream(self, stream): + self.player.select_stream(stream) + + def _create_window(self): + # remove the player from old tree + self._popout_player() + # create a new window + self.window = VideoPlayerWindow(self.player) + self.window.connect_close(self._show_player) + self.window.conenct_full(lambda s: self.player.set_toggle_states(True, s)) + + _only_fullscreen = False + def toggle_fullscreen(self) -> bool: + if self.window is None: + # currently fixed + self._only_fullscreen = True + self._create_window() + self.window.fullscreen() + # Note that you shouldn’t assume the window is definitely fullscreen afterward + res = self.window.is_fullscreen() + self.player.set_toggle_states(True, res) + return res + else: + # already popped out + if self.window.is_fullscreen(): + # currently fullscreened so we want to exit that + self.window.unfullscreen() + # video was triggert from fixed mode + if self._only_fullscreen: + # we also want to close the window + # request close first to properly detatch the player + self.window.close() + # this force closes the window afterwards + self.window.destroy() + self.window = None + self.player.set_toggle_states(False, False) + return False + # Note that you shouldn’t assume the window is definitely fullscreen afterward + res = self.window.is_fullscreen() + self.player.set_toggle_states(True, res) + return res + else: + # in floating mode + self.window.fullscreen() + self._only_fullscreen = False + # Note that you shouldn’t assume the window is definitely fullscreen afterward + res = self.window.is_fullscreen() + self.player.set_toggle_states(True, res) + return res + def toggle_popout(self) -> bool: + if self.window is None: + # currently fixed + self._create_window() + self.player.set_toggle_states(True, False) + # Note that you shouldn’t assume the window is definitely fullscreen afterward + return True + else: + # we also want to close the window + # request close first to properly detatch the player + self.window.close() + # this force closes the window afterwards + self.window.destroy() + self.window = None + self.player.set_toggle_states(False, False) + return False + + +class VideoPlayerWindow(Adw.Window): + def __init__(self, player:VideoPlayerBase, *args, **kwargs): + super().__init__(*args, **kwargs) + self.player = player + self.onclose = None + self.onfull = None + self._lastfull = None + # connect to new parent + self.set_content(self.player) + # this should block the application + self.set_modal(True) + self.connect("close-request", lambda _: self.close()) + self.connect("notify", lambda a,b: self._notify()) + self.set_visible(True) + self.set_title(_("Player")) + + def _notify(self): + curr = self.is_fullscreen() + if curr == self._lastfull: + return + self._lastfull = curr + if not self.onfull is None: + self.onfull(curr) + + def close(self): + if not self.onclose is None: + self.onclose() + + def connect_close(self, callback=None): + self.onclose = callback + def conenct_full(self, callback=None): + self.onfull = callback -- 2.38.5