Implement initial structure and core functionality for Duck Preview application
This commit is contained in:
118
duck_preview/main_window.py
Normal file
118
duck_preview/main_window.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtGui import QAction, QCloseEvent
|
||||
from PySide6.QtMultimedia import QCameraDevice, QMediaDevices
|
||||
from PySide6.QtWidgets import QGridLayout, QMainWindow, QWidget
|
||||
|
||||
from duck_preview.camera.service import CameraService
|
||||
from duck_preview.rendering.overlay import OverlayWidget
|
||||
from duck_preview.rendering.video_widget import VideoWidget
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(
|
||||
self,
|
||||
camera_service: CameraService,
|
||||
video_widget: VideoWidget,
|
||||
overlay_widget: OverlayWidget,
|
||||
parent: QWidget | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self._camera = camera_service
|
||||
self._video_widget = video_widget
|
||||
self._overlay = overlay_widget
|
||||
self._media_devices = QMediaDevices()
|
||||
|
||||
self.setWindowTitle("Duck Preview")
|
||||
self.resize(1280, 720)
|
||||
|
||||
central = QWidget()
|
||||
self.setCentralWidget(central)
|
||||
layout = QGridLayout(central)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(video_widget, 0, 0)
|
||||
layout.addWidget(overlay_widget, 0, 0)
|
||||
|
||||
self._setup_menus()
|
||||
|
||||
self._media_devices.videoInputsChanged.connect(self._rebuild_camera_menu)
|
||||
|
||||
def _setup_menus(self) -> None:
|
||||
menu_bar = self.menuBar()
|
||||
|
||||
self._camera_menu = menu_bar.addMenu("Camera")
|
||||
self._rebuild_camera_menu()
|
||||
|
||||
self._resolution_menu = menu_bar.addMenu("Resolution")
|
||||
self._resolution_menu.setEnabled(False)
|
||||
|
||||
self._fps_menu = menu_bar.addMenu("FPS")
|
||||
self._fps_menu.setEnabled(False)
|
||||
|
||||
debug_menu = menu_bar.addMenu("Debug")
|
||||
toggle_overlay = QAction("Show Metrics", self)
|
||||
toggle_overlay.setCheckable(True)
|
||||
toggle_overlay.setChecked(True)
|
||||
toggle_overlay.triggered.connect(self._overlay.set_visible)
|
||||
debug_menu.addAction(toggle_overlay)
|
||||
|
||||
def _rebuild_camera_menu(self) -> None:
|
||||
self._camera_menu.clear()
|
||||
cameras = CameraService.available_cameras()
|
||||
for device in cameras:
|
||||
action = QAction(device.description(), self)
|
||||
action.triggered.connect(lambda checked, d=device: self._on_camera_selected(d))
|
||||
self._camera_menu.addAction(action)
|
||||
if not cameras:
|
||||
action = QAction("No cameras detected", self)
|
||||
action.setEnabled(False)
|
||||
self._camera_menu.addAction(action)
|
||||
|
||||
def _on_camera_selected(self, device: QCameraDevice) -> None:
|
||||
self._camera.start(device)
|
||||
self._rebuild_resolution_menu(device)
|
||||
|
||||
def _rebuild_resolution_menu(self, device: QCameraDevice) -> None:
|
||||
self._resolution_menu.clear()
|
||||
self._resolution_menu.setEnabled(True)
|
||||
formats = device.videoFormats()
|
||||
seen: set[tuple[int, int]] = set()
|
||||
for fmt in formats:
|
||||
res = fmt.resolution()
|
||||
key = (res.width(), res.height())
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
action = QAction(f"{res.width()}x{res.height()}", self)
|
||||
action.triggered.connect(
|
||||
lambda checked, d=device, w=res.width(), h=res.height(): (
|
||||
self._on_resolution_selected(d, w, h)
|
||||
)
|
||||
)
|
||||
self._resolution_menu.addAction(action)
|
||||
|
||||
def _on_resolution_selected(self, device: QCameraDevice, width: int, height: int) -> None:
|
||||
self._rebuild_fps_menu(device, width, height)
|
||||
|
||||
def _rebuild_fps_menu(self, device: QCameraDevice, width: int, height: int) -> None:
|
||||
self._fps_menu.clear()
|
||||
self._fps_menu.setEnabled(True)
|
||||
formats = device.videoFormats()
|
||||
seen_labels: set[str] = set()
|
||||
for fmt in formats:
|
||||
res = fmt.resolution()
|
||||
if res.width() == width and res.height() == height:
|
||||
min_fps = round(fmt.minFrameRate())
|
||||
max_fps = round(fmt.maxFrameRate())
|
||||
label = f"{max_fps} FPS" if min_fps == max_fps else f"{min_fps}-{max_fps} FPS"
|
||||
if label not in seen_labels:
|
||||
seen_labels.add(label)
|
||||
action = QAction(label, self)
|
||||
action.triggered.connect(
|
||||
lambda checked, f=fmt: self._camera.set_camera_format(f)
|
||||
)
|
||||
self._fps_menu.addAction(action)
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
|
||||
self._camera.stop()
|
||||
super().closeEvent(event)
|
||||
Reference in New Issue
Block a user