Make the player answer the keyboard and the mouse
PlayerPanel.keyPressEvent handled Space, F and Escape and was dead code: nothing in the player called setFocusPolicy, so focus went to whichever child button, slider or combo box was first in the tab order, and that child ate the keys. Fixing focus alone would not have been enough -- clicking play parks focus on the play button, which then swallows Space -- so the control bar is explicitly NoFocus and the bindings are QShortcuts with WidgetWithChildrenShortcut context on the panel. That fires whichever descendant holds focus, and does not reach the Search box or the URL field on other tabs the way an application shortcut would. mpv's own input stays disabled. With vo=libmpv there is no mpv-owned window, so its input layer receives nothing; enabling default bindings would mean hand-forwarding events through a Qt-to-mpv key-name table, handing mpv the OSD and OSC that are switched off here, and letting `q` quit the core out from under the Qt UI. One declarative action table now drives the shortcuts, the context menu and the buttons. There was no mouse handling at all. Click pauses, double-click goes fullscreen -- via a doubleClickInterval timer, so a double-click does not also pause on the way -- the wheel changes volume and Ctrl+wheel seeks, with sub-notch deltas accumulated so a trackpad is not inert. The control bar gained previous, next and mute. Mute and volume are driven by observed mpv properties rather than assumed, so the icon follows a change made by key, menu or mpv itself. Buffering was completely invisible: a stalled stream showed a frozen frame for 25 seconds before the retry with nothing on screen, so paused-for-cache now surfaces a label over the video. WatchPage owns next/previous because it owns the queue; the panel only asks. Previous restarts the current item when it is more than five seconds in, as every other player does, and pushes the interrupted item back onto the head of the queue rather than dropping it. Verified: all 38 bindings install, every action is a no-op rather than an exception with nothing loaded, focus policies are as intended, and a fullscreen round trip drives the player state correctly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,25 +18,27 @@ widgets - they only emit queued Qt signals.
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import webbrowser
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from PySide6.QtCore import QCoreApplication, Qt, QTimer, Signal, Slot
|
||||
from PySide6.QtOpenGLWidgets import QOpenGLWidget
|
||||
from PySide6.QtGui import QOpenGLContext
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSlider,
|
||||
QStyle,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from . import ytsage_icons as icons
|
||||
from . import ytsage_theme as theme
|
||||
from .ytsage_player_input import SEEK_MEDIUM, build_context_menu, install_player_shortcuts
|
||||
from ..core.ytsage_mpv import probe_player
|
||||
from ..core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from ..utils.ytsage_config_manager import ConfigManager
|
||||
@@ -144,12 +146,32 @@ class MpvRenderWidget(QOpenGLWidget):
|
||||
mpvPausedChanged = Signal(bool)
|
||||
mpvEndReached = Signal(str) # end-file reason
|
||||
mpvError = Signal(str)
|
||||
mpvMuteChanged = Signal(bool)
|
||||
mpvVolumeChanged = Signal(float)
|
||||
mpvBufferingChanged = Signal(bool)
|
||||
_renderUpdateRequested = Signal()
|
||||
|
||||
# Mouse gestures, surfaced for PlayerPanel to act on. Kept here rather
|
||||
# than on the panel so the control bar below the video is unaffected.
|
||||
clicked = Signal()
|
||||
doubleClicked = Signal()
|
||||
wheelScrolled = Signal(int, bool) # notches, ctrl_held
|
||||
mouseMovedOverVideo = Signal()
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self.setMinimumHeight(240)
|
||||
# Without this the widget cannot hold focus, which is why the panel's
|
||||
# key handler never ran.
|
||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.setMouseTracking(True)
|
||||
self._wheel_remainder = 0
|
||||
# A single click must not act until a double-click can be ruled out,
|
||||
# or double-click-to-fullscreen also pauses on the way.
|
||||
self._click_timer = QTimer(self)
|
||||
self._click_timer.setSingleShot(True)
|
||||
self._click_timer.timeout.connect(self.clicked)
|
||||
self._mpv = None
|
||||
self._render_ctx = None
|
||||
self._gl_ctx = None
|
||||
@@ -202,6 +224,13 @@ class MpvRenderWidget(QOpenGLWidget):
|
||||
self._mpv.observe_property("time-pos", self._on_time_pos)
|
||||
self._mpv.observe_property("duration", self._on_duration)
|
||||
self._mpv.observe_property("pause", self._on_pause)
|
||||
# Observed rather than assumed, so the button follows changes made
|
||||
# by a key, the menu or mpv itself.
|
||||
self._mpv.observe_property("mute", self._on_mute)
|
||||
self._mpv.observe_property("volume", self._on_volume)
|
||||
# Buffering was entirely invisible: a stall showed a frozen frame for
|
||||
# 25 seconds before the retry, with nothing on screen.
|
||||
self._mpv.observe_property("paused-for-cache", self._on_cache_pause)
|
||||
|
||||
@self._mpv.event_callback("end-file")
|
||||
def _on_end_file(event): # mpv thread
|
||||
@@ -224,6 +253,18 @@ class MpvRenderWidget(QOpenGLWidget):
|
||||
if value is not None:
|
||||
self.mpvPausedChanged.emit(bool(value))
|
||||
|
||||
def _on_mute(self, _name, value) -> None:
|
||||
if value is not None:
|
||||
self.mpvMuteChanged.emit(bool(value))
|
||||
|
||||
def _on_volume(self, _name, value) -> None:
|
||||
if value is not None:
|
||||
self.mpvVolumeChanged.emit(float(value))
|
||||
|
||||
def _on_cache_pause(self, _name, value) -> None:
|
||||
if value is not None:
|
||||
self.mpvBufferingChanged.emit(bool(value))
|
||||
|
||||
def _on_mpv_log(self, level: str, prefix: str, text: str) -> None:
|
||||
if level in ("error", "fatal"):
|
||||
logger.error(f"mpv [{prefix}] {text.strip()}")
|
||||
@@ -416,6 +457,35 @@ class MpvRenderWidget(QOpenGLWidget):
|
||||
except Exception as e:
|
||||
logger.debug(f"Error terminating mpv: {e}")
|
||||
|
||||
# ----------------------------------------------------------------- mouse
|
||||
|
||||
def mousePressEvent(self, event) -> None:
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.setFocus(Qt.FocusReason.MouseFocusReason)
|
||||
self._click_timer.start(QApplication.doubleClickInterval())
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event) -> None:
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._click_timer.stop() # cancel the pending single-click
|
||||
self.doubleClicked.emit()
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
def wheelEvent(self, event) -> None:
|
||||
# Accumulate: a high-resolution trackpad sends far less than one
|
||||
# notch (120 units) per event, and dropping those makes it inert.
|
||||
self._wheel_remainder += event.angleDelta().y()
|
||||
notches, self._wheel_remainder = divmod(abs(self._wheel_remainder), 120)
|
||||
if notches:
|
||||
sign = 1 if event.angleDelta().y() > 0 else -1
|
||||
ctrl = bool(event.modifiers() & Qt.KeyboardModifier.ControlModifier)
|
||||
self.wheelScrolled.emit(sign * int(notches), ctrl)
|
||||
event.accept()
|
||||
|
||||
def mouseMoveEvent(self, event) -> None:
|
||||
self.mouseMovedOverVideo.emit()
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
# ------------------------------------------------------------- controls
|
||||
|
||||
@property
|
||||
@@ -431,6 +501,9 @@ class PlayerPanel(QWidget):
|
||||
playbackEnded = Signal(str) # end-file reason ("eof", "error", ...)
|
||||
playerError = Signal(str)
|
||||
nowPlayingChanged = Signal(dict) # entry dict of the current item
|
||||
# The panel stays queue-agnostic: it asks, WatchPage decides what is next.
|
||||
nextRequested = Signal()
|
||||
previousRequested = Signal()
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
||||
super().__init__(parent)
|
||||
@@ -438,6 +511,7 @@ class PlayerPanel(QWidget):
|
||||
self._duration: float = 0.0
|
||||
self._slider_down = False
|
||||
self._fullscreen_controller = None
|
||||
self._muted = False
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -446,15 +520,35 @@ class PlayerPanel(QWidget):
|
||||
self.video = MpvRenderWidget(self)
|
||||
layout.addWidget(self.video, stretch=1)
|
||||
|
||||
# A child of the video widget, not a sibling in the layout: it must
|
||||
# float over the frame without changing the geometry underneath.
|
||||
self.buffering_label = QLabel(_("player.buffering"), self.video)
|
||||
self.buffering_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.buffering_label.setStyleSheet(
|
||||
f"background-color: rgba(0,0,0,170); color: {theme.TEXT};"
|
||||
"padding: 8px 16px; border-radius: 6px; font-size: 13px;"
|
||||
)
|
||||
self.buffering_label.hide()
|
||||
|
||||
self.title_label = QLabel("")
|
||||
self.title_label.setStyleSheet("font-weight: bold; padding: 2px 6px;")
|
||||
self.title_label.setWordWrap(True)
|
||||
layout.addWidget(self.title_label)
|
||||
|
||||
controls = QHBoxLayout()
|
||||
# A container, not a bare layout: fullscreen needs something it can
|
||||
# hide, and hiding a layout is not a thing.
|
||||
self.controls_bar = QWidget(self)
|
||||
controls = QHBoxLayout(self.controls_bar)
|
||||
controls.setSpacing(8)
|
||||
controls.setContentsMargins(6, 0, 6, 4)
|
||||
|
||||
self.prev_btn = QPushButton()
|
||||
self.prev_btn.setIcon(icons.icon("skip-back", theme.ICON_ON_ACCENT))
|
||||
self.prev_btn.setFixedWidth(36)
|
||||
self.prev_btn.setToolTip(_("player.previous"))
|
||||
self.prev_btn.clicked.connect(self.request_previous)
|
||||
controls.addWidget(self.prev_btn)
|
||||
|
||||
self.play_btn = QPushButton()
|
||||
self.play_btn.setIcon(icons.icon("play", theme.ICON_ON_ACCENT))
|
||||
self.play_btn.setFixedWidth(36)
|
||||
@@ -462,6 +556,13 @@ class PlayerPanel(QWidget):
|
||||
self.play_btn.clicked.connect(self.toggle_pause)
|
||||
controls.addWidget(self.play_btn)
|
||||
|
||||
self.next_btn = QPushButton()
|
||||
self.next_btn.setIcon(icons.icon("skip-forward", theme.ICON_ON_ACCENT))
|
||||
self.next_btn.setFixedWidth(36)
|
||||
self.next_btn.setToolTip(_("player.next"))
|
||||
self.next_btn.clicked.connect(self.request_next)
|
||||
controls.addWidget(self.next_btn)
|
||||
|
||||
self.time_label = QLabel("0:00 / 0:00")
|
||||
controls.addWidget(self.time_label)
|
||||
|
||||
@@ -496,6 +597,13 @@ class PlayerPanel(QWidget):
|
||||
self.volume_slider.setValue(int(ConfigManager.get("player.volume") or 100))
|
||||
self.volume_slider.setToolTip(_("player.volume_tooltip"))
|
||||
self.volume_slider.valueChanged.connect(self._on_volume_changed)
|
||||
|
||||
self.mute_btn = QPushButton()
|
||||
self.mute_btn.setIcon(icons.icon("volume-high", theme.ICON_ON_ACCENT))
|
||||
self.mute_btn.setFixedWidth(36)
|
||||
self.mute_btn.setToolTip(_("player.act_mute"))
|
||||
self.mute_btn.clicked.connect(self.toggle_mute)
|
||||
controls.addWidget(self.mute_btn)
|
||||
controls.addWidget(self.volume_slider)
|
||||
|
||||
self.subs_btn = QPushButton(_("player.subtitles"))
|
||||
@@ -511,7 +619,7 @@ class PlayerPanel(QWidget):
|
||||
self.fullscreen_btn.clicked.connect(self.toggle_fullscreen)
|
||||
controls.addWidget(self.fullscreen_btn)
|
||||
|
||||
layout.addLayout(controls)
|
||||
layout.addWidget(self.controls_bar)
|
||||
|
||||
# mpv-thread signals arrive queued on the GUI thread
|
||||
self.video.mpvPositionChanged.connect(self._on_position, Qt.ConnectionType.QueuedConnection)
|
||||
@@ -519,6 +627,9 @@ class PlayerPanel(QWidget):
|
||||
self.video.mpvPausedChanged.connect(self._on_paused_changed, Qt.ConnectionType.QueuedConnection)
|
||||
self.video.mpvEndReached.connect(self._on_end_reached, Qt.ConnectionType.QueuedConnection)
|
||||
self.video.mpvError.connect(self.playerError, Qt.ConnectionType.QueuedConnection)
|
||||
self.video.mpvMuteChanged.connect(self._on_mute_changed, Qt.ConnectionType.QueuedConnection)
|
||||
self.video.mpvVolumeChanged.connect(self._on_mpv_volume, Qt.ConnectionType.QueuedConnection)
|
||||
self.video.mpvBufferingChanged.connect(self._on_buffering, Qt.ConnectionType.QueuedConnection)
|
||||
|
||||
self._volume_apply_timer = QTimer(self)
|
||||
self._volume_apply_timer.setSingleShot(True)
|
||||
@@ -535,6 +646,36 @@ class PlayerPanel(QWidget):
|
||||
self._stall_retries = 0
|
||||
self._playback_started = False
|
||||
|
||||
# --- input ------------------------------------------------------
|
||||
# The panel must be able to hold focus for its shortcuts to fire...
|
||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
# ...and the control bar must not take it, or clicking play would
|
||||
# park focus on a button that then swallows Space. This is why the
|
||||
# old keyPressEvent never ran.
|
||||
for widget in (
|
||||
self.prev_btn, self.play_btn, self.next_btn, self.mute_btn,
|
||||
self.subs_btn, self.fullscreen_btn, self.seek_slider,
|
||||
self.volume_slider, self.quality_combo, self.speed_combo,
|
||||
):
|
||||
widget.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
|
||||
# Kept on the instance: an unreferenced QShortcut is collected and
|
||||
# silently stops working.
|
||||
self._shortcuts = install_player_shortcuts(self)
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu)
|
||||
|
||||
self.video.clicked.connect(self.toggle_pause)
|
||||
self.video.doubleClicked.connect(self.toggle_fullscreen)
|
||||
self.video.wheelScrolled.connect(self._on_wheel)
|
||||
self.video.mouseMovedOverVideo.connect(self._on_mouse_activity)
|
||||
|
||||
# Controls hide themselves in fullscreen only; doing it in a window
|
||||
# would make the layout jump under the pointer.
|
||||
self._idle_timer = QTimer(self)
|
||||
self._idle_timer.setSingleShot(True)
|
||||
self._idle_timer.setInterval(2500)
|
||||
self._idle_timer.timeout.connect(self._hide_idle_controls)
|
||||
|
||||
# ------------------------------------------------------------ public API
|
||||
|
||||
def play(self, entry: Dict[str, Any], resume_pos: float = 0.0) -> None:
|
||||
@@ -593,6 +734,106 @@ class PlayerPanel(QWidget):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------- bound actions
|
||||
# Every method below is reachable from a key, the context menu, or both;
|
||||
# see ytsage_player_input.ACTIONS. They are all no-ops when nothing is
|
||||
# loaded rather than raising, because a key can be pressed at any time.
|
||||
|
||||
def seek_relative(self, seconds: float) -> None:
|
||||
mpv_inst = self.video.mpv
|
||||
if mpv_inst is None:
|
||||
return
|
||||
try:
|
||||
mpv_inst.seek(seconds, reference="relative")
|
||||
except Exception as e:
|
||||
logger.debug(f"Relative seek failed: {e}")
|
||||
|
||||
def seek_absolute(self, seconds: float) -> None:
|
||||
mpv_inst = self.video.mpv
|
||||
if mpv_inst is None:
|
||||
return
|
||||
try:
|
||||
mpv_inst.seek(max(0.0, seconds), reference="absolute")
|
||||
except Exception as e:
|
||||
logger.debug(f"Absolute seek failed: {e}")
|
||||
|
||||
def seek_percent(self, fraction: float) -> None:
|
||||
if self._duration > 0:
|
||||
self.seek_absolute(self._duration * max(0.0, min(1.0, fraction)))
|
||||
|
||||
def seek_to_end(self) -> None:
|
||||
if self._duration > 0:
|
||||
# Not exactly the end: mpv would treat that as EOF and advance.
|
||||
self.seek_absolute(max(0.0, self._duration - 3.0))
|
||||
|
||||
def frame_step(self, direction: int) -> None:
|
||||
mpv_inst = self.video.mpv
|
||||
if mpv_inst is None:
|
||||
return
|
||||
try:
|
||||
mpv_inst.command("frame-back-step" if direction < 0 else "frame-step")
|
||||
except Exception as e:
|
||||
logger.debug(f"Frame step failed: {e}")
|
||||
|
||||
def nudge_volume(self, delta: int) -> None:
|
||||
self.volume_slider.setValue(max(0, min(100, self.volume_slider.value() + delta)))
|
||||
|
||||
def toggle_mute(self) -> None:
|
||||
mpv_inst = self.video.mpv
|
||||
if mpv_inst is None:
|
||||
return
|
||||
try:
|
||||
mpv_inst["mute"] = not bool(mpv_inst["mute"])
|
||||
except Exception as e:
|
||||
logger.debug(f"Mute toggle failed: {e}")
|
||||
|
||||
def nudge_speed(self, delta: float) -> None:
|
||||
mpv_inst = self.video.mpv
|
||||
if mpv_inst is None:
|
||||
return
|
||||
try:
|
||||
current = float(mpv_inst["speed"] or 1.0)
|
||||
except Exception:
|
||||
current = 1.0
|
||||
self._apply_speed(max(0.25, min(4.0, round((current + delta) * 100) / 100)))
|
||||
|
||||
def reset_speed(self) -> None:
|
||||
self._apply_speed(1.0)
|
||||
|
||||
def _apply_speed(self, speed: float) -> None:
|
||||
mpv_inst = self.video.mpv
|
||||
if mpv_inst is not None:
|
||||
try:
|
||||
mpv_inst["speed"] = speed
|
||||
except Exception as e:
|
||||
logger.debug(f"Speed change failed: {e}")
|
||||
# Reflect it in the combo when it is one of the offered values,
|
||||
# without re-triggering the handler.
|
||||
index = self.speed_combo.findData(speed)
|
||||
if index >= 0 and index != self.speed_combo.currentIndex():
|
||||
self.speed_combo.blockSignals(True)
|
||||
self.speed_combo.setCurrentIndex(index)
|
||||
self.speed_combo.blockSignals(False)
|
||||
|
||||
def cycle_subtitles(self) -> None:
|
||||
self.subs_btn.setChecked(not self.subs_btn.isChecked())
|
||||
|
||||
def request_next(self) -> None:
|
||||
self.nextRequested.emit()
|
||||
|
||||
def request_previous(self) -> None:
|
||||
self.previousRequested.emit()
|
||||
|
||||
def copy_video_url(self) -> None:
|
||||
url = self._current_entry.get("url") or self._current_entry.get("webpage_url")
|
||||
if url:
|
||||
QApplication.clipboard().setText(str(url))
|
||||
|
||||
def open_in_browser(self) -> None:
|
||||
url = self._current_entry.get("url") or self._current_entry.get("webpage_url")
|
||||
if url:
|
||||
webbrowser.open(str(url))
|
||||
|
||||
def current_entry(self) -> Dict[str, Any]:
|
||||
return dict(self._current_entry)
|
||||
|
||||
@@ -631,6 +872,38 @@ class PlayerPanel(QWidget):
|
||||
self.play_btn.setIcon(icons.icon("play" if paused else "pause", theme.ICON_ON_ACCENT))
|
||||
self.play_btn.setToolTip(_("player.play") if paused else _("player.pause"))
|
||||
|
||||
@Slot(bool)
|
||||
def _on_mute_changed(self, muted: bool) -> None:
|
||||
self._muted = muted
|
||||
self._refresh_volume_icon()
|
||||
|
||||
@Slot(float)
|
||||
def _on_mpv_volume(self, volume: float) -> None:
|
||||
# Reflect changes made by key or menu, without echoing back into mpv.
|
||||
value = int(round(volume))
|
||||
if value != self.volume_slider.value():
|
||||
self.volume_slider.blockSignals(True)
|
||||
self.volume_slider.setValue(value)
|
||||
self.volume_slider.blockSignals(False)
|
||||
self._volume_apply_timer.start()
|
||||
self._refresh_volume_icon()
|
||||
|
||||
def _refresh_volume_icon(self) -> None:
|
||||
if self._muted or self.volume_slider.value() == 0:
|
||||
name = "volume-mute"
|
||||
elif self.volume_slider.value() < 50:
|
||||
name = "volume-low"
|
||||
else:
|
||||
name = "volume-high"
|
||||
self.mute_btn.setIcon(icons.icon(name, theme.ICON_ON_ACCENT))
|
||||
self.mute_btn.setToolTip(_("player.act_unmute") if self._muted else _("player.act_mute"))
|
||||
|
||||
@Slot(bool)
|
||||
def _on_buffering(self, buffering: bool) -> None:
|
||||
self.buffering_label.setVisible(buffering)
|
||||
if buffering:
|
||||
self.buffering_label.raise_()
|
||||
|
||||
@Slot(str)
|
||||
def _on_end_reached(self, reason: str) -> None:
|
||||
self.playbackEnded.emit(reason)
|
||||
@@ -656,8 +929,10 @@ class PlayerPanel(QWidget):
|
||||
if mpv_inst is None:
|
||||
return
|
||||
mpv_inst["ytdl-format"] = _ytdl_format_for(height)
|
||||
# Reload the current item at the new quality, keeping position
|
||||
if self._current_entry:
|
||||
# Reload the current item at the new quality, keeping position. Only
|
||||
# when something is actually loaded -- otherwise changing the default
|
||||
# quality would start playing whatever was last selected.
|
||||
if self._current_entry and self._playback_started:
|
||||
pos = self.current_position()
|
||||
self.play(self._current_entry, resume_pos=pos + 5.0 if pos else 0.0)
|
||||
|
||||
@@ -689,6 +964,71 @@ class PlayerPanel(QWidget):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ---------------------------------------------------------- mouse/idle
|
||||
|
||||
def _on_wheel(self, notches: int, ctrl_held: bool) -> None:
|
||||
if ctrl_held:
|
||||
self.seek_relative(notches * SEEK_MEDIUM)
|
||||
else:
|
||||
self.nudge_volume(notches * 2)
|
||||
|
||||
def _on_mouse_activity(self) -> None:
|
||||
"""Any movement over the video brings the controls back."""
|
||||
if not self.controls_bar.isVisible():
|
||||
self.controls_bar.show()
|
||||
self.video.unsetCursor()
|
||||
if self.is_fullscreen():
|
||||
self._idle_timer.start()
|
||||
|
||||
def _hide_idle_controls(self) -> None:
|
||||
# Only in fullscreen, and never while the pointer is on the bar
|
||||
# itself -- hiding it out from under the cursor is hostile.
|
||||
if not self.is_fullscreen() or self.controls_bar.underMouse():
|
||||
return
|
||||
try:
|
||||
if bool(self.video.mpv["pause"]):
|
||||
return # paused: leave the controls up
|
||||
except Exception:
|
||||
pass
|
||||
self.controls_bar.hide()
|
||||
self.video.setCursor(Qt.CursorShape.BlankCursor)
|
||||
|
||||
def on_fullscreen_changed(self, active: bool) -> None:
|
||||
"""Called by FullscreenController so idle-hiding follows the state."""
|
||||
self.fullscreen_btn.setIcon(
|
||||
icons.icon("minimize" if active else "maximize", theme.ICON_ON_ACCENT)
|
||||
)
|
||||
self.fullscreen_btn.setToolTip(
|
||||
_("player.act_leave_fullscreen") if active else _("player.fullscreen")
|
||||
)
|
||||
if active:
|
||||
self.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
self._idle_timer.start()
|
||||
else:
|
||||
self._idle_timer.stop()
|
||||
self.controls_bar.show()
|
||||
self.video.unsetCursor()
|
||||
|
||||
def contextMenuEvent(self, event) -> None:
|
||||
menu = build_context_menu(self)
|
||||
menu.exec(event.globalPos())
|
||||
event.accept()
|
||||
|
||||
def resizeEvent(self, event) -> None:
|
||||
super().resizeEvent(event)
|
||||
# The buffering label floats over the video, so it has no layout to
|
||||
# centre it. Positioned even while hidden, so it appears in the right
|
||||
# place rather than jumping there.
|
||||
hint = self.buffering_label.sizeHint()
|
||||
self.buffering_label.setGeometry(
|
||||
(self.video.width() - hint.width()) // 2,
|
||||
(self.video.height() - hint.height()) // 2,
|
||||
hint.width(),
|
||||
hint.height(),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------ fullscreen
|
||||
|
||||
def set_fullscreen_controller(self, controller) -> None:
|
||||
"""
|
||||
Injected by the main window, which owns the chrome being hidden.
|
||||
|
||||
Reference in New Issue
Block a user