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:
@@ -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
57
core/camera/states.py
Normal 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()
|
||||
Reference in New Issue
Block a user