Show incoming fight reactions on unfocused visible players

This commit is contained in:
Peter Stockings
2026-10-05 15:57:17 +11:00
parent b5f09a9e9e
commit 75362bfcba
3 changed files with 32 additions and 10 deletions
+2 -1
View File
@@ -36,7 +36,8 @@ stay in chat history; joining or reconnecting does not replay animations.
Quiet sound effects play automatically with visible reactions. Sounds are Quiet sound effects play automatically with visible reactions. Sounds are
original synthesized effects cached locally, with no external downloads. Overlays and sound are original synthesized effects cached locally, with no external downloads. Overlays and sound are
suppressed while the player is hidden, minimized, or another application is active. suppressed while the player is hidden or minimized. Visible players receive
reactions even without keyboard focus, without activating the player window.
Reactions also work while the detached chat window is focused. Reactions also work while the detached chat window is focused.
Robbery votes allow one answer per person, which you can change before the vote Robbery votes allow one answer per person, which you can change before the vote
+5 -7
View File
@@ -6,7 +6,7 @@ import wave
from PyQt6.QtCore import Qt, QTimer, QPropertyAnimation, QVariantAnimation, QPoint, QEasingCurve, QEvent from PyQt6.QtCore import Qt, QTimer, QPropertyAnimation, QVariantAnimation, QPoint, QEasingCurve, QEvent
from PyQt6.QtGui import QPainter, QPainterPath, QFont, QColor, QPen, QLinearGradient, QTransform from PyQt6.QtGui import QPainter, QPainterPath, QFont, QColor, QPen, QLinearGradient, QTransform
from PyQt6.QtWidgets import QApplication, QWidget, QFrame, QLabel, QHBoxLayout, QVBoxLayout from PyQt6.QtWidgets import QWidget, QFrame, QLabel, QHBoxLayout, QVBoxLayout
REACTIONS = { REACTIONS = {
@@ -157,11 +157,7 @@ class RingsideStrip(QWidget):
if kind not in REACTIONS or not self.video_frame.isVisible(): if kind not in REACTIONS or not self.video_frame.isVisible():
return False return False
owner = self.video_frame.window() owner = self.video_frame.window()
# Avoid displaying an always-on-top effect over another application. if owner.isMinimized():
active = QApplication.activeWindow()
while active is not None and active is not owner:
active = active.parentWidget()
if active is None or owner.isMinimized():
return False return False
if self.owner_window is not owner: if self.owner_window is not owner:
if self.owner_window is not None: if self.owner_window is not None:
@@ -211,7 +207,9 @@ class RingsideStrip(QWidget):
def eventFilter(self, watched, event): def eventFilter(self, watched, event):
if self.isVisible(): if self.isVisible():
if event.type() in (QEvent.Type.Hide, QEvent.Type.WindowDeactivate, QEvent.Type.Close): if event.type() in (QEvent.Type.Hide, QEvent.Type.Close):
self.dismiss()
elif event.type() == QEvent.Type.WindowStateChange and self.owner_window.isMinimized():
self.dismiss() self.dismiss()
elif event.type() in (QEvent.Type.Move, QEvent.Type.Resize): elif event.type() in (QEvent.Type.Move, QEvent.Type.Resize):
self.slide.stop() self.slide.stop()
+25 -2
View File
@@ -44,7 +44,7 @@ def test_strip_is_native_positioned_and_does_not_steal_focus(room, qtbot, kind):
qtbot.wait(50) qtbot.wait(50)
bounds = QRect(video.mapToGlobal(QPoint(0, 0)), video.size()) bounds = QRect(video.mapToGlobal(QPoint(0, 0)), video.size())
assert bounds.contains(strip.geometry()) assert bounds.contains(strip.geometry())
QApplication.sendEvent(room, QEvent(QEvent.Type.WindowDeactivate)) QApplication.sendEvent(room, QEvent(QEvent.Type.Hide))
assert not strip.isVisible() assert not strip.isVisible()
@@ -93,7 +93,7 @@ def test_arcade_ko_covers_video_then_restores_strip(room, qtbot):
assert overlay.ko_animation.state() == overlay.ko_animation.State.Stopped assert overlay.ko_animation.state() == overlay.ko_animation.State.Stopped
def test_sound_is_enabled_by_default_and_background_effects_are_suppressed(room, qtbot, monkeypatch): def test_sound_is_enabled_by_default_and_hidden_effects_are_suppressed(room, qtbot, monkeypatch):
play = Mock() play = Mock()
monkeypatch.setattr(room, '_play_reaction_sound', play) monkeypatch.setattr(room, '_play_reaction_sound', play)
room.activateWindow() room.activateWindow()
@@ -109,6 +109,29 @@ def test_sound_is_enabled_by_default_and_background_effects_are_suppressed(room,
assert not room.ringside_strip.isVisible() assert not room.ringside_strip.isVisible()
@pytest.mark.parametrize('kind', ['ko', 'bell'])
def test_receiving_client_shows_reaction_without_focus(room, qtbot, monkeypatch, kind):
from PyQt6.QtWidgets import QLineEdit
other_client = QLineEdit()
qtbot.addWidget(other_client)
other_client.show()
other_client.activateWindow()
other_client.setFocus()
qtbot.wait(30)
assert not room.isActiveWindow()
play = Mock()
monkeypatch.setattr(room, '_play_reaction_sound', play)
room.handle_fight_reaction({'kind': kind, 'username': 'Other client', 'position': 0})
assert room.ringside_strip.isVisible()
play.assert_called_once_with(kind)
assert QApplication.focusWidget() is other_client
QApplication.sendEvent(room, QEvent(QEvent.Type.WindowDeactivate))
assert room.ringside_strip.isVisible()
room.showMinimized()
qtbot.wait(30)
assert not room.ringside_strip.isVisible()
@pytest.mark.parametrize('kind', REACTIONS) @pytest.mark.parametrize('kind', REACTIONS)
def test_original_sound_files_are_valid_and_loadable(qtbot, tmp_path, kind): def test_original_sound_files_are_valid_and_loadable(qtbot, tmp_path, kind):
path = write_reaction_sound(tmp_path, kind) path = write_reaction_sound(tmp_path, kind)