From 1b6397fd8edc044b5883e671fd6f2808d8c3c835 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Sat, 16 Mar 2024 11:36:56 +0100 Subject: [PATCH] add volume & brightness control using slide gestures (these are currently not saved) --- melon/widgets/player.py | 182 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 164 insertions(+), 18 deletions(-) diff --git a/melon/widgets/player.py b/melon/widgets/player.py index 1623623..0853099 100644 --- a/melon/widgets/player.py +++ b/melon/widgets/player.py @@ -12,10 +12,77 @@ from melon.widgets.iconbutton import IconButton from melon.widgets.preferencerow import PreferenceRow, PreferenceType, Preference from melon.utils import pass_me +def clamp(lower, value, upper): + return max(lower, min(value, upper)) + +class OverlayDispay(Gtk.Box): + def __init__(self, icon, text, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_halign(Gtk.Align.CENTER) + self.set_valign(Gtk.Align.CENTER) + self.add_css_class("osd") + self.add_css_class("toolbar") + self._icon = Gtk.Image() + self.append(self._icon) + self._label = Gtk.Label() + self.append(self._label) + self.set_icon(icon) + self.set_text(text) + self.show() + def show(self): + self.set_opacity(1.0) + def hide(self): + self.set_opacity(0.0) + def set_icon(self, icon:str): + self._icon.set_from_icon_name(icon) + def set_text(self, text:str): + self._label.set_label(text) + +class BrightnessDisplay(OverlayDispay): + def __init__(self, value:float, *args, **kwargs): + initial_icon = self.get_icon_for(value) + initial_text = self.get_text_for(value) + super().__init__(initial_icon, initial_text, *args, **kwargs) + def update(self, value:float): + icon = self.get_icon_for(value) + text = self.get_text_for(value) + self.set_icon(icon) + self.set_text(text) + def get_icon_for(self, value:float) -> str: + if value < 0.4: + return "night-light-symbolic" + if value < 0.8: + return "weather-clear-symbolic" + return "display-brightness-symbolic" + def get_text_for(self, value:float) -> str: + return f"{int(value * 100)}%" + +class VolumeDisplay(OverlayDispay): + def __init__(self, value:float, *args, **kwargs): + initial_icon = self.get_icon_for(value) + initial_text = self.get_text_for(value) + super().__init__(initial_icon, initial_text, *args, **kwargs) + def update(self, value:float): + icon = self.get_icon_for(value) + text = self.get_text_for(value) + self.set_icon(icon) + self.set_text(text) + def get_icon_for(self, value:float) -> str: + if value == 0.0: + return "audio-volume-muted-symbolic" + if value < 0.4: + return "audio-volume-low-symbolic" + if value < 0.8: + return "audio-volume-medium-symbolic" + return "audio-volume-high-symbolic" + def get_text_for(self, value:float) -> str: + return f"{int(value * 100)}%" + class VideoPlayer(Gtk.Overlay): - # TODO: consider adding volume + brightness control - # place volume + brightness here so they are static - # and the values are hopefully remembered for the app lifetime + volume = 1.0 + # inverse opacity of overlay + brightness = 1.0 + def __init__(self, streams: list[Stream], *args, **kwargs): super().__init__(*args, **kwargs) self.streams = streams @@ -61,6 +128,23 @@ class VideoPlayer(Gtk.Overlay): # Set the paintable on the picture self.picture.set_paintable(paintable) + self.brightness_overlay = Gtk.Box() + # make full size + self.brightness_overlay.set_hexpand(True) + self.brightness_overlay.set_vexpand(True) + # add background color (this is semi-opaque, so it won't ever completely hide the video) + self.brightness_overlay.add_css_class("osd") + overlay.add_overlay(self.brightness_overlay) + + self.brightness_display = BrightnessDisplay(self.brightness) + overlay.add_overlay(self.brightness_display) + self.brightness_display.hide() + self.volume_display = VolumeDisplay(self.volume) + overlay.add_overlay(self.volume_display) + self.volume_display.hide() + self.change_brightness(self.brightness) + self.change_volume(self.volume) + self.controls = Gtk.Box() overlay.add_overlay(self.controls) # used for flat button styles @@ -121,11 +205,14 @@ class VideoPlayer(Gtk.Overlay): self.add_controller(self.hover_ctr) self.hover_ctr.connect("enter", self._mouse_enter) self.hover_ctr.connect("leave", self._mouse_leave) - # listen to click (for mobile support) - # functions as a toggle - self.click_ctr = Gtk.GestureClick() - self.picture.add_controller(self.click_ctr) - self.click_ctr.connect("pressed", self._onclick) + # swipe handler + self.swipe_ctr = Gtk.GestureSwipe() + # only called for valid swipe input, while it is happening + self.swipe_ctr.connect("update", self._update_swipe) + # also calls swipe for clicks (and on swipe end) + # so we use this to handle the onclick event to toggle the controls (mobile) + self.swipe_ctr.connect("swipe", self._end_swipe) + self.add_controller(self.swipe_ctr) def connect_update(self, callback=None): self.update_callback = callback @@ -143,20 +230,79 @@ class VideoPlayer(Gtk.Overlay): def _schedule_hide_controls(self, timeout=2): GLib.timeout_add(timeout*1000, self._hide_controls) + def _update_swipe(self, e, s): + bounds = self.swipe_ctr.get_bounding_box_center() + if not bounds[0]: + return + width = self.picture.get_width() + left_bound = width*(2/5) + right_bound = width - left_bound + + vel = self.swipe_ctr.get_velocity() + if not vel[0]: + return + + # randomly selected values to make it feel responsive + fact = vel[2]/10000 + fact = clamp(-0.2, fact, 0.2) + + if bounds[1] >= right_bound: + # volume control + # update the value before showing the overlay + # invert the value so it goes in the right direction + self.change_volume(self.volume - fact) + # make sure the display is shown + self.volume_display.show() + elif bounds[1] <= left_bound: + # brightness controll + # update the value before showing the overlay + # invert the value so it goes in the right direction + self.change_brightness(self.brightness - fact) + self.brightness_display.show() + else: + # mixed controls + # hide all + self.brightness_display.hide() + self.volume_display.hide() + + def change_volume(self, vol): + self.volume = clamp(0.0, vol, 1.0) + self.volume_display.update(self.volume) + # TODO: update player volume + self.source.set_property("volume", self.volume) + # TODO: update volume button value (once implemented) + + def change_brightness(self, bright): + self.brightness = clamp(0.0, bright, 1.0) + self.brightness_display.update(self.brightness) + self.brightness_overlay.set_opacity(1.0 - self.brightness) + + def _end_swipe(self, e, vx, vy): + # no swipe movement + # just a click + if vx==0.0 and vy == 0.0: + self._onclick() + return + # hide overlay + # always hide both overlays just to make sure + self.brightness_display.hide() + self.volume_display.hide() + def _mouse_enter(self, w, x, y): self._show_controls() def _mouse_leave(self, w): self._schedule_hide_controls() - def _onclick(self, w, n, x, y): - if n == 1: - # single click toggles focus - # because floats are a thing we can't use == - # however given that we switch between 0 and 1 - # we can use 0.9 or 0.1 as breakpoints - if self.controls.get_opacity() >= 0.9: - self._hide_controls(force=True) - else: - self._show_controls() + + offset = 50 + def _onclick(self): + # single click toggles focus + # because floats are a thing we can't use == + # however given that we switch between 0 and 1 + # we can use 0.9 or 0.1 as breakpoints + if self.controls.get_opacity() >= 0.9: + self._hide_controls(force=True) + else: + self._show_controls() def _update_quality_list(self): # TODO consider making this scrollable -- 2.38.5