pax 843d49e4a3 Auto-size video widget to match video aspect ratio
Detects video dimensions from first frame via QVideoSink,
sets max height on the video widget to eliminate black bars.
Resets on each new video. Uses KeepAspectRatio mode.
2026-04-05 20:18:36 -05:00

789 lines
29 KiB
Python

"""Full media preview — image viewer with zoom/pan and video player."""
from __future__ import annotations
from pathlib import Path
from PySide6.QtCore import Qt, QPoint, QPointF, Signal, QUrl
from PySide6.QtGui import QPixmap, QPainter, QWheelEvent, QMouseEvent, QKeyEvent, QColor, QMovie
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QMainWindow,
QStackedWidget, QPushButton, QSlider, QMenu, QInputDialog, QStyle,
)
from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput
from PySide6.QtMultimediaWidgets import QVideoWidget
from ..core.config import MEDIA_EXTENSIONS
VIDEO_EXTENSIONS = (".mp4", ".webm", ".mkv", ".avi", ".mov")
def _is_video(path: str) -> bool:
return Path(path).suffix.lower() in VIDEO_EXTENSIONS
class FullscreenPreview(QMainWindow):
"""Fullscreen media viewer with navigation — images, GIFs, and video."""
navigate = Signal(int) # direction: -1/+1 for left/right, -cols/+cols for up/down
bookmark_requested = Signal()
save_toggle_requested = Signal() # save or unsave depending on state
blacklist_tag_requested = Signal(str) # tag name
blacklist_post_requested = Signal()
privacy_requested = Signal()
closed = Signal()
def __init__(self, grid_cols: int = 3, show_actions: bool = True, monitor: str = "", parent=None) -> None:
super().__init__(parent, Qt.WindowType.Window)
self.setWindowTitle("booru-viewer — Fullscreen")
self.setMinimumSize(640, 480)
self._grid_cols = grid_cols
central = QWidget()
main_layout = QVBoxLayout(central)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# Top toolbar
self._toolbar = QWidget()
toolbar = QHBoxLayout(self._toolbar)
toolbar.setContentsMargins(8, 4, 8, 4)
self._bookmark_btn = QPushButton("Bookmark")
self._bookmark_btn.setFixedWidth(80)
self._bookmark_btn.clicked.connect(self.bookmark_requested)
toolbar.addWidget(self._bookmark_btn)
self._save_btn = QPushButton("Save")
self._save_btn.setFixedWidth(70)
self._save_btn.clicked.connect(self.save_toggle_requested)
toolbar.addWidget(self._save_btn)
self._is_saved = False
self._bl_tag_btn = QPushButton("BL Tag")
self._bl_tag_btn.setFixedWidth(60)
self._bl_tag_btn.setToolTip("Blacklist a tag")
self._bl_tag_btn.clicked.connect(self._show_bl_tag_menu)
toolbar.addWidget(self._bl_tag_btn)
self._bl_post_btn = QPushButton("BL Post")
self._bl_post_btn.setFixedWidth(65)
self._bl_post_btn.setToolTip("Blacklist this post")
self._bl_post_btn.clicked.connect(self.blacklist_post_requested)
toolbar.addWidget(self._bl_post_btn)
if not show_actions:
self._bookmark_btn.hide()
self._save_btn.hide()
self._bl_tag_btn.hide()
self._bl_post_btn.hide()
toolbar.addStretch()
self._info_label = QLabel()
toolbar.addWidget(self._info_label)
main_layout.addWidget(self._toolbar)
# Media stack
self._stack = QStackedWidget()
main_layout.addWidget(self._stack, stretch=1)
self._viewer = ImageViewer()
self._viewer.close_requested.connect(self.close)
self._stack.addWidget(self._viewer)
self._video = VideoPlayer()
self._video.play_next.connect(lambda: self.navigate.emit(1))
self._stack.addWidget(self._video)
self.setCentralWidget(central)
from PySide6.QtWidgets import QApplication
QApplication.instance().installEventFilter(self)
# Pick target monitor
target_screen = None
if monitor and monitor != "Same as app":
for screen in QApplication.screens():
label = f"{screen.name()} ({screen.size().width()}x{screen.size().height()})"
if label == monitor:
target_screen = screen
break
if not target_screen and parent and parent.screen():
target_screen = parent.screen()
if target_screen:
self.setScreen(target_screen)
self.setGeometry(target_screen.geometry())
self.showFullScreen()
_current_tags: dict[str, list[str]] = {}
_current_tag_list: list[str] = []
def set_post_tags(self, tag_categories: dict[str, list[str]], tag_list: list[str]) -> None:
self._current_tags = tag_categories
self._current_tag_list = tag_list
def _show_bl_tag_menu(self) -> None:
menu = QMenu(self)
if self._current_tags:
for category, tags in self._current_tags.items():
cat_menu = menu.addMenu(category)
for tag in tags[:30]:
cat_menu.addAction(tag)
else:
for tag in self._current_tag_list[:30]:
menu.addAction(tag)
action = menu.exec(self._bl_tag_btn.mapToGlobal(self._bl_tag_btn.rect().bottomLeft()))
if action:
self.blacklist_tag_requested.emit(action.text())
def update_state(self, bookmarked: bool, saved: bool) -> None:
self._bookmark_btn.setText("Unbookmark" if bookmarked else "Bookmark")
self._bookmark_btn.setFixedWidth(90 if bookmarked else 80)
self._is_saved = saved
self._save_btn.setText("Unsave" if saved else "Save")
def set_media(self, path: str, info: str = "") -> None:
self._info_label.setText(info)
ext = Path(path).suffix.lower()
if _is_video(path):
self._viewer.clear()
self._video.stop()
self._video.play_file(path, info)
self._stack.setCurrentIndex(1)
elif ext == ".gif":
self._video.stop()
self._viewer.set_gif(path, info)
self._stack.setCurrentIndex(0)
else:
self._video.stop()
pix = QPixmap(path)
if not pix.isNull():
self._viewer.set_image(pix, info)
self._stack.setCurrentIndex(0)
def eventFilter(self, obj, event):
from PySide6.QtCore import QEvent
from PySide6.QtWidgets import QLineEdit, QTextEdit, QSpinBox, QComboBox
if event.type() == QEvent.Type.KeyPress:
# Only intercept when slideshow is the active window
if not self.isActiveWindow():
return super().eventFilter(obj, event)
# Don't intercept keys when typing in text inputs
if isinstance(obj, (QLineEdit, QTextEdit, QSpinBox, QComboBox)):
return super().eventFilter(obj, event)
key = event.key()
mods = event.modifiers()
if key == Qt.Key.Key_P and mods & Qt.KeyboardModifier.ControlModifier:
self.privacy_requested.emit()
return True
elif key == Qt.Key.Key_H and mods & Qt.KeyboardModifier.ControlModifier:
self._toolbar.setVisible(not self._toolbar.isVisible())
# Also hide video controls if showing video
if self._stack.currentIndex() == 1:
for child in self._video.findChildren(QPushButton):
child.setVisible(self._toolbar.isVisible())
for child in self._video.findChildren(QSlider):
child.setVisible(self._toolbar.isVisible())
for child in self._video.findChildren(QLabel):
child.setVisible(self._toolbar.isVisible())
return True
elif key in (Qt.Key.Key_Escape, Qt.Key.Key_Q):
self.close()
return True
elif key in (Qt.Key.Key_Left, Qt.Key.Key_H):
self.navigate.emit(-1)
return True
elif key in (Qt.Key.Key_Right, Qt.Key.Key_L):
self.navigate.emit(1)
return True
elif key in (Qt.Key.Key_Up, Qt.Key.Key_K):
self.navigate.emit(-self._grid_cols)
return True
elif key in (Qt.Key.Key_Down, Qt.Key.Key_J):
self.navigate.emit(self._grid_cols)
return True
elif key == Qt.Key.Key_F11:
if self.isFullScreen():
self.showNormal()
else:
self.showFullScreen()
return True
elif key == Qt.Key.Key_Space and self._stack.currentIndex() == 1:
self._video._toggle_play()
return True
elif key == Qt.Key.Key_Period and self._stack.currentIndex() == 1:
self._video._seek_relative(5000)
return True
elif key == Qt.Key.Key_Comma and self._stack.currentIndex() == 1:
self._video._seek_relative(-5000)
return True
if event.type() == QEvent.Type.Wheel and self._stack.currentIndex() == 1 and self.isActiveWindow():
delta = event.angleDelta().y()
if delta:
vol = self._video._audio.volume()
vol = max(0.0, min(1.0, vol + (0.05 if delta > 0 else -0.05)))
self._video._audio.setVolume(vol)
self._video._vol_slider.setValue(int(vol * 100))
return True
return super().eventFilter(obj, event)
def closeEvent(self, event) -> None:
from PySide6.QtWidgets import QApplication
QApplication.instance().removeEventFilter(self)
self.closed.emit()
self._video.stop()
super().closeEvent(event)
# -- Image Viewer (zoom/pan) --
class ImageViewer(QWidget):
"""Zoomable, pannable image viewer."""
close_requested = Signal()
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self._pixmap: QPixmap | None = None
self._movie: QMovie | None = None
self._zoom = 1.0
self._offset = QPointF(0, 0)
self._drag_start: QPointF | None = None
self._drag_offset = QPointF(0, 0)
self.setMouseTracking(True)
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self._info_text = ""
def set_image(self, pixmap: QPixmap, info: str = "") -> None:
self._stop_movie()
self._pixmap = pixmap
self._zoom = 1.0
self._offset = QPointF(0, 0)
self._info_text = info
self._fit_to_view()
self.update()
def set_gif(self, path: str, info: str = "") -> None:
self._stop_movie()
self._movie = QMovie(path)
self._movie.frameChanged.connect(self._on_gif_frame)
self._movie.start()
self._info_text = info
# Set initial pixmap from first frame
self._pixmap = self._movie.currentPixmap()
self._zoom = 1.0
self._offset = QPointF(0, 0)
self._fit_to_view()
self.update()
def _on_gif_frame(self) -> None:
if self._movie:
self._pixmap = self._movie.currentPixmap()
self.update()
def _stop_movie(self) -> None:
if self._movie:
self._movie.stop()
self._movie = None
def clear(self) -> None:
self._stop_movie()
self._pixmap = None
self._info_text = ""
self.update()
def _fit_to_view(self) -> None:
if not self._pixmap:
return
vw, vh = self.width(), self.height()
pw, ph = self._pixmap.width(), self._pixmap.height()
if pw == 0 or ph == 0:
return
scale_w = vw / pw
scale_h = vh / ph
self._zoom = min(scale_w, scale_h, 1.0)
self._offset = QPointF(
(vw - pw * self._zoom) / 2,
(vh - ph * self._zoom) / 2,
)
def paintEvent(self, event) -> None:
p = QPainter(self)
pal = self.palette()
p.fillRect(self.rect(), pal.color(pal.ColorRole.Window))
if self._pixmap:
p.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
p.translate(self._offset)
p.scale(self._zoom, self._zoom)
p.drawPixmap(0, 0, self._pixmap)
p.resetTransform()
p.end()
def wheelEvent(self, event: QWheelEvent) -> None:
if not self._pixmap:
return
mouse_pos = event.position()
old_zoom = self._zoom
delta = event.angleDelta().y()
factor = 1.15 if delta > 0 else 1 / 1.15
self._zoom = max(0.1, min(self._zoom * factor, 20.0))
ratio = self._zoom / old_zoom
self._offset = mouse_pos - ratio * (mouse_pos - self._offset)
self.update()
def mousePressEvent(self, event: QMouseEvent) -> None:
if event.button() == Qt.MouseButton.MiddleButton:
self._fit_to_view()
self.update()
elif event.button() == Qt.MouseButton.LeftButton:
self._drag_start = event.position()
self._drag_offset = QPointF(self._offset)
self.setCursor(Qt.CursorShape.ClosedHandCursor)
def mouseMoveEvent(self, event: QMouseEvent) -> None:
if self._drag_start is not None:
delta = event.position() - self._drag_start
self._offset = self._drag_offset + delta
self.update()
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
self._drag_start = None
self.setCursor(Qt.CursorShape.ArrowCursor)
def keyPressEvent(self, event: QKeyEvent) -> None:
if event.key() in (Qt.Key.Key_Escape, Qt.Key.Key_Q):
self.close_requested.emit()
elif event.key() == Qt.Key.Key_0:
self._fit_to_view()
self.update()
elif event.key() in (Qt.Key.Key_Plus, Qt.Key.Key_Equal):
self._zoom = min(self._zoom * 1.2, 20.0)
self.update()
elif event.key() == Qt.Key.Key_Minus:
self._zoom = max(self._zoom / 1.2, 0.1)
self.update()
else:
event.ignore()
def resizeEvent(self, event) -> None:
if self._pixmap:
self._fit_to_view()
self.update()
class _ClickSeekSlider(QSlider):
"""Slider that jumps to the clicked position instead of page-stepping."""
clicked_position = Signal(int)
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
val = QStyle.sliderValueFromPosition(
self.minimum(), self.maximum(), int(event.position().x()), self.width()
)
self.setValue(val)
self.clicked_position.emit(val)
super().mousePressEvent(event)
# -- Video Player --
class VideoPlayer(QWidget):
"""Video player with transport controls."""
play_next = Signal() # emitted when video ends in "next" mode
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Video surface
self._video_widget = QVideoWidget()
self._video_widget.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio)
layout.addWidget(self._video_widget, stretch=1)
# Player
self._player = QMediaPlayer()
self._audio = QAudioOutput()
self._player.setAudioOutput(self._audio)
self._player.setVideoOutput(self._video_widget)
self._audio.setVolume(0.5)
# Controls bar
controls = QHBoxLayout()
controls.setContentsMargins(4, 2, 4, 2)
self._play_btn = QPushButton("Play")
self._play_btn.setFixedWidth(65)
self._play_btn.clicked.connect(self._toggle_play)
controls.addWidget(self._play_btn)
self._time_label = QLabel("0:00")
self._time_label.setFixedWidth(45)
controls.addWidget(self._time_label)
self._seek_slider = _ClickSeekSlider(Qt.Orientation.Horizontal)
self._seek_slider.setRange(0, 0)
self._seek_slider.sliderMoved.connect(self._seek)
self._seek_slider.clicked_position.connect(self._seek)
controls.addWidget(self._seek_slider, stretch=1)
self._duration_label = QLabel("0:00")
self._duration_label.setFixedWidth(45)
controls.addWidget(self._duration_label)
self._vol_slider = QSlider(Qt.Orientation.Horizontal)
self._vol_slider.setRange(0, 100)
self._vol_slider.setValue(50)
self._vol_slider.setFixedWidth(80)
self._vol_slider.valueChanged.connect(self._set_volume)
controls.addWidget(self._vol_slider)
self._mute_btn = QPushButton("Mute")
self._mute_btn.setFixedWidth(80)
self._mute_btn.clicked.connect(self._toggle_mute)
controls.addWidget(self._mute_btn)
self._autoplay = True
self._autoplay_btn = QPushButton("Auto")
self._autoplay_btn.setFixedWidth(70)
self._autoplay_btn.setCheckable(True)
self._autoplay_btn.setChecked(True)
self._autoplay_btn.setToolTip("Auto-play videos when selected")
self._autoplay_btn.clicked.connect(self._toggle_autoplay)
self._autoplay_btn.hide()
controls.addWidget(self._autoplay_btn)
self._loop_state = 0 # 0=Loop, 1=Once, 2=Next
self._loop_btn = QPushButton("Loop")
self._loop_btn.setFixedWidth(55)
self._loop_btn.setToolTip("Loop: repeat / Once: stop at end / Next: advance")
self._loop_btn.clicked.connect(self._cycle_loop)
controls.addWidget(self._loop_btn)
layout.addLayout(controls)
# Signals
self._player.positionChanged.connect(self._on_position)
self._player.durationChanged.connect(self._on_duration)
self._player.playbackStateChanged.connect(self._on_state)
self._player.mediaStatusChanged.connect(self._on_media_status)
self._player.errorOccurred.connect(self._on_error)
# Resize video widget to match video aspect ratio
self._video_sized = False
sink = self._player.videoSink()
if sink:
sink.videoFrameChanged.connect(self._on_video_size)
self._current_file: str | None = None
self._error_fired = False
def play_file(self, path: str, info: str = "") -> None:
self._current_file = path
self._error_fired = False
self._ended = False
self._video_sized = False
self._video_widget.setMaximumHeight(16777215) # reset max height
self._last_pos = 0
self._player.setLoops(QMediaPlayer.Loops.Infinite)
self._player.setSource(QUrl.fromLocalFile(path))
if self._autoplay:
self._player.play()
else:
self._player.pause()
def _toggle_autoplay(self, checked: bool = True) -> None:
self._autoplay = self._autoplay_btn.isChecked()
self._autoplay_btn.setText("Autoplay" if self._autoplay else "Manual")
# If turning off autoplay mid-playback, let current video finish then stop
def _cycle_loop(self) -> None:
self._loop_state = (self._loop_state + 1) % 3
labels = ["Loop", "Once", "Next"]
self._loop_btn.setText(labels[self._loop_state])
self._autoplay_btn.setVisible(self._loop_state == 2)
@property
def _loop_mode(self):
return self._loop_state == 0
def stop(self) -> None:
self._player.stop()
self._player.setSource(QUrl())
def _toggle_play(self) -> None:
if self._player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
self._player.pause()
else:
self._player.play()
def _seek(self, pos: int) -> None:
self._player.setPosition(pos)
def _seek_relative(self, ms: int) -> None:
pos = max(0, self._player.position() + ms)
self._player.setPosition(pos)
def _set_volume(self, val: int) -> None:
self._audio.setVolume(val / 100.0)
def _toggle_mute(self) -> None:
self._audio.setMuted(not self._audio.isMuted())
self._mute_btn.setText("Unmute" if self._audio.isMuted() else "Mute")
_last_pos = 0
def _on_position(self, pos: int) -> None:
if not self._seek_slider.isSliderDown():
self._seek_slider.setValue(pos)
self._time_label.setText(self._fmt(pos))
# Detect loop restart: position jumps from near-end back to start
duration = self._player.duration()
if (self._last_pos > 500 and pos < 100 and not self._ended
and duration > 0 and self._last_pos > duration * 0.8):
if self._loop_state == 1: # Once
self._ended = True
self._player.pause()
elif self._loop_state == 2: # Next
self._ended = True
self._player.pause()
self.play_next.emit()
self._last_pos = pos
def _on_duration(self, dur: int) -> None:
self._seek_slider.setRange(0, dur)
self._duration_label.setText(self._fmt(dur))
def _on_state(self, state) -> None:
if state == QMediaPlayer.PlaybackState.PlayingState:
self._play_btn.setText("Pause")
else:
self._play_btn.setText("Play")
def _on_video_size(self, frame) -> None:
"""Resize video widget to match video aspect ratio on first frame."""
if self._video_sized or not frame.isValid():
return
self._video_sized = True
vw = frame.size().width()
vh = frame.size().height()
if vw > 0 and vh > 0:
available_w = self._video_widget.width()
scaled_h = int(available_w * vh / vw)
self._video_widget.setMaximumHeight(scaled_h)
def _on_media_status(self, status) -> None:
pass
def _on_error(self, error, msg: str = "") -> None:
if self._current_file and not self._error_fired:
self._error_fired = True
import logging
logging.getLogger("booru").warning(f"Video playback error: {error} {msg} ({self._current_file})")
@staticmethod
def _fmt(ms: int) -> str:
s = ms // 1000
m = s // 60
return f"{m}:{s % 60:02d}"
# -- Combined Preview (image + video) --
class ImagePreview(QWidget):
"""Combined media preview — auto-switches between image and video."""
close_requested = Signal()
open_in_default = Signal()
open_in_browser = Signal()
save_to_folder = Signal(str)
unsave_requested = Signal()
bookmark_requested = Signal()
navigate = Signal(int) # -1 = prev, +1 = next
fullscreen_requested = Signal()
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self._folders_callback = None
self._current_path: str | None = None
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self._stack = QStackedWidget()
layout.addWidget(self._stack)
# Image viewer (index 0)
self._image_viewer = ImageViewer()
self._image_viewer.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self._image_viewer.close_requested.connect(self.close_requested)
self._stack.addWidget(self._image_viewer)
# Video player (index 1)
self._video_player = VideoPlayer()
self._video_player.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self._video_player.play_next.connect(lambda: self.navigate.emit(1))
self._stack.addWidget(self._video_player)
# Info label
self._info_label = QLabel()
self._info_label.setStyleSheet("padding: 2px 6px;")
layout.addWidget(self._info_label)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self._on_context_menu)
# Keep these for compatibility with app.py accessing them
@property
def _pixmap(self):
return self._image_viewer._pixmap
@property
def _info_text(self):
return self._image_viewer._info_text
def set_folders_callback(self, callback) -> None:
self._folders_callback = callback
def set_image(self, pixmap: QPixmap, info: str = "") -> None:
self._video_player.stop()
self._image_viewer.set_image(pixmap, info)
self._stack.setCurrentIndex(0)
self._info_label.setText(info)
self._current_path = None
def set_media(self, path: str, info: str = "") -> None:
"""Auto-detect and show image or video."""
self._current_path = path
ext = Path(path).suffix.lower()
if _is_video(path):
self._image_viewer.clear()
self._video_player.stop()
self._video_player.play_file(path, info)
self._stack.setCurrentIndex(1)
self._info_label.setText(info)
elif ext == ".gif":
self._video_player.stop()
self._image_viewer.set_gif(path, info)
self._stack.setCurrentIndex(0)
self._info_label.setText(info)
else:
self._video_player.stop()
pix = QPixmap(path)
if not pix.isNull():
self._image_viewer.set_image(pix, info)
self._stack.setCurrentIndex(0)
self._info_label.setText(info)
def clear(self) -> None:
self._video_player.stop()
self._image_viewer.clear()
self._info_label.setText("")
self._current_path = None
def _on_context_menu(self, pos) -> None:
menu = QMenu(self)
fav_action = menu.addAction("Bookmark")
save_menu = menu.addMenu("Save to Library")
save_unsorted = save_menu.addAction("Unsorted")
save_menu.addSeparator()
save_folder_actions = {}
if self._folders_callback:
for folder in self._folders_callback():
a = save_menu.addAction(folder)
save_folder_actions[id(a)] = folder
save_menu.addSeparator()
save_new = save_menu.addAction("+ New Folder...")
menu.addSeparator()
copy_image = menu.addAction("Copy File to Clipboard")
open_action = menu.addAction("Open in Default App")
browser_action = menu.addAction("Open in Browser")
# Image-specific
reset_action = None
if self._stack.currentIndex() == 0:
reset_action = menu.addAction("Reset View")
menu.addSeparator()
unsave_action = menu.addAction("Unsave from Library")
slideshow_action = None
if self._current_path:
slideshow_action = menu.addAction("Slideshow Mode")
clear_action = menu.addAction("Clear Preview")
action = menu.exec(self.mapToGlobal(pos))
if not action:
return
if action == fav_action:
self.bookmark_requested.emit()
elif action == save_unsorted:
self.save_to_folder.emit("")
elif action == save_new:
name, ok = QInputDialog.getText(self, "New Folder", "Folder name:")
if ok and name.strip():
self.save_to_folder.emit(name.strip())
elif id(action) in save_folder_actions:
self.save_to_folder.emit(save_folder_actions[id(action)])
elif action == copy_image:
from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QPixmap as _QP
pix = self._image_viewer._pixmap
if pix and not pix.isNull():
QApplication.clipboard().setPixmap(pix)
elif self._current_path:
pix = _QP(self._current_path)
if not pix.isNull():
QApplication.clipboard().setPixmap(pix)
elif action == open_action:
self.open_in_default.emit()
elif action == browser_action:
self.open_in_browser.emit()
elif action == reset_action:
self._image_viewer._fit_to_view()
self._image_viewer.update()
elif action == unsave_action:
self.unsave_requested.emit()
elif action == slideshow_action:
self.fullscreen_requested.emit()
elif action == clear_action:
self.close_requested.emit()
def mousePressEvent(self, event: QMouseEvent) -> None:
if event.button() == Qt.MouseButton.RightButton:
event.ignore()
else:
super().mousePressEvent(event)
def wheelEvent(self, event) -> None:
if self._stack.currentIndex() == 1:
delta = event.angleDelta().y()
vol = self._video_player._audio.volume()
vol = max(0.0, min(1.0, vol + (0.05 if delta > 0 else -0.05)))
self._video_player._audio.setVolume(vol)
self._video_player._vol_slider.setValue(int(vol * 100))
else:
super().wheelEvent(event)
def keyPressEvent(self, event: QKeyEvent) -> None:
if self._stack.currentIndex() == 0:
self._image_viewer.keyPressEvent(event)
elif event.key() == Qt.Key.Key_Space:
self._video_player._toggle_play()
elif event.key() == Qt.Key.Key_Period:
self._video_player._seek_relative(5000)
elif event.key() == Qt.Key.Key_Comma:
self._video_player._seek_relative(-5000)
elif event.key() in (Qt.Key.Key_Left, Qt.Key.Key_H):
self.navigate.emit(-1)
elif event.key() in (Qt.Key.Key_Right, Qt.Key.Key_L):
self.navigate.emit(1)
def resizeEvent(self, event) -> None:
super().resizeEvent(event)