feat: Implement State pattern for camera logic

Refactor camera control logic in `MainController` to use the State design pattern.

- Create a new `core/camera/states.py` module with state classes (`NoCamerasState`, `DetectingState`, `ReadyToStreamState`, `StreamingState`).
- `MainController` now acts as a context, delegating actions to the current state object.
- This replaces complex conditional logic with a robust, scalable, and maintainable state machine, making it easier to manage camera behavior and add new states in the future.
This commit is contained in:
2025-10-14 09:03:19 +02:00
parent c8d9029df7
commit 47c1e6040a
2 changed files with 86 additions and 34 deletions

View File

@@ -9,6 +9,7 @@ from core.camera.camera_manager import CameraManager
from ui.widgets.color_list_widget import ColorListWidget
from ui.widgets.thumbnail_list_widget import ThumbnailListWidget
from ui.widgets.split_view_widget import SplitView, CameraPlaceholder, ViewWithOverlay
from core.camera.states import CameraState, NoCamerasState, ReadyToStreamState, StreamingState, DetectingState
class MainController:
@@ -31,7 +32,16 @@ class MainController:
self.db.connect()
self.media_repo.sync_media()
self.camera_manager.detect_cameras()
# Initialize state machine
self.state: CameraState = NoCamerasState()
self.state.enter_state(self)
def transition_to(self, new_state: CameraState):
"""Transitions the controller to a new state."""
print(f"Transitioning to state: {new_state.__class__.__name__}")
self.state = new_state
self.state.enter_state(self)
def _connect_signals(self):
"""Connects all signals to slots."""
@@ -63,15 +73,6 @@ class MainController:
self.camera_manager.shutdown()
self.db.disconnect()
def _update_start_button_state(self):
"""Updates the text and state of the start/stop button."""
if self.camera_manager.get_active_camera_info():
self.welcome_view.set_button_text("Zatrzymaj kamerę")
elif self.camera_manager.get_detected_cameras():
self.welcome_view.set_button_text("Uruchom kamerę")
else:
self.welcome_view.set_button_text("Wykryj kamery")
# --- Slots for Database/Media ---
@Slot(str)
@@ -99,15 +100,12 @@ class MainController:
@Slot(list)
def on_cameras_detected(self, cameras: list[dict]):
"""Handles the list of detected cameras."""
"""Handles the list of detected cameras and transitions state."""
print("Detected cameras:", cameras)
self.welcome_view.set_info_text(f"Detected {len(cameras)} cameras.")
self._update_start_button_state()
# Populate a combobox in the UI here
# self.view.camera_combobox.clear()
# for camera in cameras:
# self.view.camera_combobox.addItem(camera['name'], userData=camera['id'])
if cameras:
self.transition_to(ReadyToStreamState())
else:
self.transition_to(NoCamerasState())
@Slot(QPixmap)
def on_frame_ready(self, pixmap: QPixmap):
@@ -116,36 +114,34 @@ class MainController:
@Slot(str)
def on_camera_error(self, error_message: str):
"""Shows an error message from the camera."""
"""Shows an error message from the camera and transitions to a safe state."""
print(f"Camera Error: {error_message}")
self.welcome_view.set_error_text(error_message)
self._update_start_button_state()
self.transition_to(NoCamerasState())
@Slot()
def on_camera_started(self):
"""Updates UI when the camera stream starts."""
self.split_view.toggle_live_view()
self._update_start_button_state()
"""Transitions to StreamingState when the camera starts."""
self.transition_to(StreamingState())
@Slot()
def on_camera_stopped(self):
"""Updates UI when the camera stream stops."""
"""Transitions to a post-streaming state."""
self.split_view.toggle_live_view()
self._update_start_button_state()
if self.camera_manager.get_detected_cameras():
self.transition_to(ReadyToStreamState())
else:
self.transition_to(NoCamerasState())
# --- UI Actions ---
def _on_start_button_clicked(self):
"""Handles clicks on the main start/stop/detect button."""
if self.camera_manager.get_active_camera_info():
self.stop_liveview()
elif self.camera_manager.get_detected_cameras():
self.start_liveview()
else:
self.camera_detect()
"""Delegates the button click to the current state."""
self.state.handle_start_button(self)
def camera_detect(self):
self.welcome_view.set_info_text("Wykrywanie kamer...")
"""Initiates camera detection and transitions to DetectingState."""
self.transition_to(DetectingState())
self.camera_manager.detect_cameras()
def start_liveview(self):
@@ -156,7 +152,6 @@ class MainController:
return
# For now, just start the first detected camera.
# In a real app, you'd get the selected camera ID from the UI.
camera_id = detected_cameras[0]['id']
self.camera_manager.start_camera(camera_id)

57
core/camera/states.py Normal file
View File

@@ -0,0 +1,57 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from controllers.main_controller import MainController
class CameraState(ABC):
"""Abstract base class for all camera states."""
def enter_state(self, controller: MainController):
"""Called upon entering the state, e.g., to update the UI."""
pass
@abstractmethod
def handle_start_button(self, controller: MainController):
"""Handles the main camera button click."""
pass
class NoCamerasState(CameraState):
def enter_state(self, controller: MainController):
controller.welcome_view.set_button_text("Wykryj kamery")
controller.welcome_view.set_info_text("Nie wykryto kamer. Kliknij, aby rozpocząć.")
controller.welcome_view.camera_start_btn.setEnabled(True)
def handle_start_button(self, controller: MainController):
controller.camera_detect()
class DetectingState(CameraState):
def enter_state(self, controller: MainController):
controller.welcome_view.set_button_text("Wykrywanie...")
controller.welcome_view.set_info_text("Trwa wykrywanie kamer...")
controller.welcome_view.camera_start_btn.setEnabled(False)
def handle_start_button(self, controller: MainController):
# Do nothing while detecting
pass
class ReadyToStreamState(CameraState):
def enter_state(self, controller: MainController):
cameras = controller.camera_manager.get_detected_cameras()
controller.welcome_view.set_button_text("Uruchom kamerę")
controller.welcome_view.set_info_text(f"Wykryto {len(cameras)} kamer(y).")
controller.welcome_view.camera_start_btn.setEnabled(True)
def handle_start_button(self, controller: MainController):
controller.start_liveview()
class StreamingState(CameraState):
def enter_state(self, controller: MainController):
controller.welcome_view.set_button_text("Zatrzymaj kamerę")
controller.welcome_view.camera_start_btn.setEnabled(True)
if controller.split_view.stack.currentWidget() != controller.live_view:
controller.split_view.toggle_live_view()
def handle_start_button(self, controller: MainController):
controller.stop_liveview()