From 47c1e6040a9f2baed5f51f1c25969a297ee6730d Mon Sep 17 00:00:00 2001 From: bartool Date: Tue, 14 Oct 2025 09:03:19 +0200 Subject: [PATCH] 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. --- controllers/main_controller.py | 63 ++++++++++++++++------------------ core/camera/states.py | 57 ++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 34 deletions(-) create mode 100644 core/camera/states.py diff --git a/controllers/main_controller.py b/controllers/main_controller.py index d37befa..ee83ab5 100644 --- a/controllers/main_controller.py +++ b/controllers/main_controller.py @@ -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) diff --git a/core/camera/states.py b/core/camera/states.py new file mode 100644 index 0000000..d557e98 --- /dev/null +++ b/core/camera/states.py @@ -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()