~comcloudway/melon

c9e1e0df7be4c644478ef05de2edf105bd66a97c — Jakob Meier 6 months ago a92077c
add popout/floating and fullscreen feature to video player
2 files changed, 215 insertions(+), 7 deletions(-)

M melon/widgets/iconbutton.py
M melon/widgets/player.py
M melon/widgets/iconbutton.py => melon/widgets/iconbutton.py +11 -5
@@ 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)

M melon/widgets/player.py => melon/widgets/player.py +204 -2
@@ 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