diff --git a/controllers/main_controller.py b/controllers/main_controller.py index 6453a84..26068cd 100644 --- a/controllers/main_controller.py +++ b/controllers/main_controller.py @@ -1,100 +1,154 @@ -from PySide6.QtWidgets import QPushButton from pathlib import Path +from PySide6.QtCore import Slot +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QPushButton + from core.database import DatabaseManager from core.media import MediaRepository +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 -# from .camera_controller import CameraController -from core.camera.camera_controller import CameraController -from core.camera.camera_manager import CameraManager +from ui.widgets.split_view_widget import SplitView, CameraPlaceholder -from core.camera.gphoto_camera import GPhotoCamera -from core.camera.camera_controller import CameraController - class MainController: - def __init__(self, view): - self.db = DatabaseManager() - self.db.connect() - self.media_repo = MediaRepository(self.db) - self.media_repo.sync_media() + def __init__(self, view): + self.view = view + self.db = DatabaseManager() + self.media_repo = MediaRepository(self.db) + self.camera_manager = CameraManager() - # camera = GPhotoCamera() - # self.manager = CameraController(camera) - manager = CameraManager() - manager.detect_gphoto() - manager.detect_opencv() + # --- UI Widgets --- + self.color_list: ColorListWidget = view.color_list_widget + self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget + self.split_view: SplitView = view.preview_widget + self.welcome_view: CameraPlaceholder = self.split_view.widget_start + self.photo_button: QPushButton = view.photo_button + self.record_button: QPushButton = view.record_button - # self.camera_controller = CameraController() + self._connect_signals() - self.view = view - self.color_list: ColorListWidget = view.color_list_widget - self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget - self.split_view: SplitView = view.preview_widget + self.db.connect() + self.media_repo.sync_media() + self.camera_manager.detect_cameras() - self.photo_button: QPushButton = view.photo_button - self.photo_button.clicked.connect(self.take_photo) + def _connect_signals(self): + """Connects all signals to slots.""" + # Database and media signals + self.color_list.colorSelected.connect(self.on_color_selected) + self.color_list.editColor.connect(self.on_edit_color) + self.thumbnail_list.selectedThumbnail.connect(self.on_thumbnail_selected) - self.record_button: QPushButton = view.record_button - # self.record_button.clicked.connect(self.fun_test) + # Camera signals + self.camera_manager.cameras_detected.connect(self.on_cameras_detected) + self.camera_manager.frame_ready.connect(self.on_frame_ready) + self.camera_manager.error_occurred.connect(self.on_camera_error) + self.camera_manager.camera_started.connect(self.on_camera_started) + self.camera_manager.camera_stopped.connect(self.on_camera_stopped) - self.color_list.colorSelected.connect(self.on_color_selected) - self.color_list.editColor.connect(self.on_edit_color) - self.thumbnail_list.selectedThumbnail.connect(self.on_thumbnail_selected) + # UI control signals + self.photo_button.clicked.connect(self.take_photo) + # self.record_button.clicked.connect(self.toggle_record) # Placeholder + self.welcome_view.camera_start_btn.clicked.connect(self.start_liveview) + # You will need a way to select a camera, e.g., a combobox. + # self.view.camera_combobox.currentIndexChanged.connect(self.on_camera_selected_in_ui) - # self.camera_controller.errorOccurred.connect(self.split_view.widget_start.set_info_text) - # self.manager.error_occurred.connect(self.split_view.widget_start.set_info_text) - # self.camera_controller.frameReady.connect(self.split_view.set_live_image) - # self.manager.frame_ready.connect(self.split_view.set_live_image) - # self.split_view.widget_start.camera_start_btn.clicked.connect(self.camera_controller.start) - self.split_view.widget_start.camera_start_btn.clicked.connect(self.start_liveview) + def load_colors(self) -> None: + """Loads colors from the database and populates the list.""" + colors = self.db.get_all_colors() + self.color_list.set_colors(colors) + + def shutdown(self): + """Cleans up resources before application exit.""" + self.camera_manager.shutdown() + self.db.disconnect() + + # --- Slots for Database/Media --- + + @Slot(str) + def on_color_selected(self, color_name: str): + color_id = self.db.get_color_id(color_name) + if color_id is not None: + media_items = self.db.get_media_for_color(color_id) + self.thumbnail_list.list_widget.clear() + for media in media_items: + if media['file_type'] == 'photo': + file_name = Path(media['media_path']).name + self.thumbnail_list.add_thumbnail(media['media_path'], file_name, media['id']) + + @Slot(str) + def on_edit_color(self, color_name: str): + print(f"Edycja koloru: {color_name}") # Placeholder + + @Slot(int) + def on_thumbnail_selected(self, media_id: int): + media = self.db.get_media(media_id) + if media: + self.split_view.set_reference_image(media['media_path']) + + # --- Slots for CameraManager --- + + @Slot(list) + def on_cameras_detected(self, cameras: list[dict]): + """Handles the list of detected cameras.""" + print("Detected cameras:", cameras) + self.welcome_view.set_info_text(f"Detected {len(cameras)} cameras.") + # 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']) + + @Slot(QPixmap) + def on_frame_ready(self, pixmap: QPixmap): + """Displays a new frame from the camera.""" + self.split_view.set_live_image(pixmap) + + @Slot(str) + def on_camera_error(self, error_message: str): + """Shows an error message from the camera.""" + print(f"Camera Error: {error_message}") + self.welcome_view.set_error_text(error_message) + + @Slot() + def on_camera_started(self): + """Updates UI when the camera stream starts.""" + self.split_view.toggle_live_view() + self.welcome_view.set_button_text("Stop Camera") + # Re-route button click to stop the camera + self.welcome_view.camera_start_btn.clicked.disconnect() + self.welcome_view.camera_start_btn.clicked.connect(self.stop_liveview) - def start_camera(self): - pass + @Slot() + def on_camera_stopped(self): + """Updates UI when the camera stream stops.""" + # self.split_view.show_placeholder() + self.welcome_view.set_button_text("Start Camera") + # Re-route button click to start the camera + self.welcome_view.camera_start_btn.clicked.disconnect() + self.welcome_view.camera_start_btn.clicked.connect(self.start_liveview) - def load_colors(self) -> None: - colors = self.db.get_all_colors() - print("Loaded colors:", colors) - self.color_list.set_colors(colors) + # --- UI Actions --- + def start_liveview(self): + """Starts the camera feed.""" + detected_cameras = self.camera_manager.get_detected_cameras() + if not detected_cameras: + self.on_camera_error("No cameras detected.") + 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) - def on_color_selected(self, color_name: str): - print(f"Wybrano kolor: {color_name}") - color_id = self.db.get_color_id(color_name) - if color_id is not None: - media_items = self.db.get_media_for_color(color_id) - print(f"Media dla koloru {color_name} (ID: {color_id}):", media_items) + def stop_liveview(self): + """Stops the camera feed.""" + self.camera_manager.stop_camera() - self.thumbnail_list.list_widget.clear() - for media in media_items: - if media['file_type'] == 'photo': - file_name = Path(media['media_path']).name - self.thumbnail_list.add_thumbnail(media['media_path'], file_name, media['id']) - else: - print(f"Nie znaleziono koloru o nazwie: {color_name}") - - def on_edit_color(self, color_name: str): - print(f"Edycja koloru: {color_name}") - - def on_thumbnail_selected(self, media_id: int): - media = self.db.get_media(media_id) - if media: - print(f"Wybrano miniaturę o ID: {media_id}, ścieżka: {media['media_path']}") - self.split_view.set_reference_image(media['media_path']) - else: - print(f"Nie znaleziono mediów o ID: {media_id}") - - def take_photo(self): - print("Robienie zdjęcia...") - self.split_view.toglle_live_view() - - def start_liveview(self): - pass - # self.manager.start_camera() - # self.manager.start_stream() - - def shutdown(self): - pass - # self.manager.stop() \ No newline at end of file + def take_photo(self): + """Takes a photo with the active camera.""" + print("Taking photo...") # Placeholder + # This needs to be implemented in CameraManager and called here. + # e.g., self.camera_manager.take_photo() + self.split_view.toggle_live_view() # This seems like a UI toggle, maybe rename? \ No newline at end of file diff --git a/core/camera/camera_controller.py b/core/camera/camera_controller.py index bb8b3c9..fd39c8b 100644 --- a/core/camera/camera_controller.py +++ b/core/camera/camera_controller.py @@ -1,115 +1,162 @@ -from PySide6.QtCore import QObject, QThread, QTimer, Signal, Slot, QMutex, QMutexLocker +from PySide6.QtCore import QObject, QTimer, Signal, Slot, QMutex, QMutexLocker, QThread from PySide6.QtGui import QImage, QPixmap import cv2 from .base_camera import BaseCamera -class CameraController(QThread): +class CameraWorker(QObject): frame_ready = Signal(QPixmap) photo_ready = Signal(QPixmap) error_occurred = Signal(str) - _enable_timer = Signal(bool) - + camera_ready = Signal(bool) def __init__(self, parent: QObject | None = None) -> None: super().__init__(parent) - self.camera = None - self.timer = None + self.camera: BaseCamera | None = None + self.timer: QTimer | None = None self.fps = 15 self.is_streaming = False self.is_connected = False - self._camera_mutex = QMutex() - self.start() - - def run(self) -> None: + @Slot() + def initialize_worker(self): + """Initializes the timer in the worker's thread.""" self.timer = QTimer() self.timer.timeout.connect(self._update_frame) - self._enable_timer.connect(self._set_timer) - self.exec() - - def stop(self): - self.stop_camera() - self.quit() - self.wait() + @Slot(BaseCamera, int) def set_camera(self, camera: BaseCamera, fps: int = 15) -> None: with QMutexLocker(self._camera_mutex): - self.stop_stream() - self.stop_camera() + if self.is_streaming: + self.stop_stream() + if self.is_connected: + self.stop_camera() self.camera = camera self.fps = fps + @Slot() def start_camera(self) -> None: - if self.camera is None or self.is_connected: - return - - if self.camera.connect(): - self.is_connected = True - else: - self.is_connected = False - self.error_occurred.emit(self.camera.get_error_msg()) - - def stop_camera(self) -> None: - if self.is_streaming: - self.stop_stream() - - if self.camera is not None: - self.camera.disconnect() - - self.is_connected = False - - def start_stream(self): - if not self.is_connected: - return - - if self.is_streaming: - return - - if self.timer: - self.is_streaming = True - # self.timer.start() - self._enable_timer.emit(True) - - def stop_stream(self) -> None: - if self.is_streaming: - self.is_streaming = False - if self.timer: - # self.timer.stop() - self._enable_timer.emit(False) - - def _update_frame(self) -> None: with QMutexLocker(self._camera_mutex): - if self.camera is None or not self.is_connected: + if self.camera is None or self.is_connected: + return + + if self.camera.connect(): + self.is_connected = True + self.camera_ready.emit(True) + else: + self.is_connected = False + self.camera_ready.emit(False) + self.error_occurred.emit(self.camera.get_error_msg()) + + @Slot() + def stop_camera(self) -> None: + with QMutexLocker(self._camera_mutex): + if self.is_streaming: + self.stop_stream() + + if self.camera is not None and self.is_connected: + self.camera.disconnect() + + self.is_connected = False + + @Slot() + def start_stream(self): + if not self.is_connected or self.is_streaming or self.timer is None: + return + + self.is_streaming = True + self.timer.setInterval(int(1000 / self.fps)) + self.timer.start() + + @Slot() + def stop_stream(self) -> None: + if self.is_streaming and self.timer is not None: + self.is_streaming = False + self.timer.stop() + + @Slot() + def _update_frame(self) -> None: + # This method is called by the timer, which is in the same thread. + # A mutex is still good practice for accessing the shared camera object. + with QMutexLocker(self._camera_mutex): + if self.camera is None or not self.is_connected or not self.is_streaming: return - if not self.is_streaming: - return - ret, frame = self.camera.get_frame() if not ret: - self.error_occurred.emit(self.camera.get_error_msg()) + error_msg = self.camera.get_error_msg() + if error_msg: + self.error_occurred.emit(error_msg) return if frame is not None: + # Process the frame and emit it. rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape qimg = QImage(rgb_image.data, w, h, ch * w, QImage.Format.Format_RGB888) pixmap = QPixmap.fromImage(qimg) - self.frame_ready.emit(pixmap) - def _set_timer(self, enable: bool): - if not self.timer: - return - - if enable: - self.timer.setInterval(int(1000 / self.fps)) - self.timer.start() - else: - self.timer.stop() - + + +class CameraController(QObject): + frame_ready = Signal(QPixmap) + photo_ready = Signal(QPixmap) + error_occurred = Signal(str) + camera_ready = Signal(bool) + + # Signals to command the worker + _set_camera_requested = Signal(BaseCamera, int) + _start_camera_requested = Signal() + _stop_camera_requested = Signal() + _start_stream_requested = Signal() + _stop_stream_requested = Signal() + + def __init__(self, parent: QObject | None = None) -> None: + super().__init__(parent) + self._thread = QThread() + self._worker = CameraWorker() + + self._worker.moveToThread(self._thread) + + # Connect worker signals to controller signals + self._worker.frame_ready.connect(self.frame_ready) + self._worker.photo_ready.connect(self.photo_ready) + self._worker.error_occurred.connect(self.error_occurred) + self._worker.camera_ready.connect(self.camera_ready) + + # Connect controller's command signals to worker's slots + self._set_camera_requested.connect(self._worker.set_camera) + self._start_camera_requested.connect(self._worker.start_camera) + self._stop_camera_requested.connect(self._worker.stop_camera) + self._start_stream_requested.connect(self._worker.start_stream) + self._stop_stream_requested.connect(self._worker.stop_stream) + + # Initialize worker when thread starts + self._thread.started.connect(self._worker.initialize_worker) + + self._thread.start() + + def stop(self): + self._thread.quit() + self._thread.wait() + + def set_camera(self, camera: BaseCamera, fps: int = 15) -> None: + self._set_camera_requested.emit(camera, fps) + + def start_camera(self) -> None: + self._start_camera_requested.emit() + + def stop_camera(self) -> None: + self._stop_camera_requested.emit() + + def start_stream(self): + self._start_stream_requested.emit() + + def stop_stream(self) -> None: + self._stop_stream_requested.emit() + \ No newline at end of file diff --git a/core/camera/camera_manager.py b/core/camera/camera_manager.py index 5fbea5c..dc1ed86 100644 --- a/core/camera/camera_manager.py +++ b/core/camera/camera_manager.py @@ -1,20 +1,121 @@ +from PySide6.QtCore import QObject, Signal +from PySide6.QtGui import QPixmap - +from .camera_controller import CameraController from .gphoto_camera import GPhotoCamera from .opencv_camera import OpenCvCamera -from .camera_controller import CameraController +from .base_camera import BaseCamera -class CameraManager: - def __init__(self) -> None: - pass +class CameraManager(QObject): + """ + Zarządza wszystkimi operacjami związanymi z kamerami, + stanowiąc fasadę dla reszty aplikacji. + """ + frame_ready = Signal(QPixmap) + error_occurred = Signal(str) + cameras_detected = Signal(list) + camera_started = Signal() + camera_stopped = Signal() - def detect_gphoto(self): - camera_list = GPhotoCamera.detect() - print(camera_list) - return camera_list + def __init__(self, parent: QObject | None = None) -> None: + super().__init__(parent) + self._camera_controller = CameraController() + self._detected_cameras: list[dict] = [] + self._active_camera: BaseCamera | None = None + self._active_camera_info: dict | None = None - def detect_opencv(self): - camera_list = OpenCvCamera.detect() - print(camera_list) - return camera_list + # Przekazywanie sygnałów z kontrolera kamery na zewnątrz + self._camera_controller.frame_ready.connect(self.frame_ready) + self._camera_controller.error_occurred.connect(self.error_occurred) + self._camera_controller.camera_ready.connect(self.start_liveview) + + def detect_cameras(self) -> None: + """Wykrywa wszystkie dostępne kamery (GPhoto i OpenCV).""" + self._detected_cameras.clear() + + # Wykryj kamery GPhoto + try: + gphoto_cameras = GPhotoCamera.detect() + for index, info in gphoto_cameras.items(): + self._detected_cameras.append({ + "id": f"gphoto_{index}", + "name": f"{info['name']} ({info['port']})", + "type": "gphoto", + "index": index + }) + except Exception as e: + self.error_occurred.emit( + f"Błąd podczas wykrywania kamer GPhoto: {e}") + + # Wykryj kamery OpenCV + try: + opencv_cameras = OpenCvCamera.detect() + for index, info in opencv_cameras.items(): + self._detected_cameras.append({ + "id": f"opencv_{index}", + "name": f"OpenCV: {info['name']}", + "type": "opencv", + "index": index + }) + except Exception as e: + self.error_occurred.emit( + f"Błąd podczas wykrywania kamer OpenCV: {e}") + + self.cameras_detected.emit(self._detected_cameras) + + def get_detected_cameras(self) -> list[dict]: + return self._detected_cameras + + def start_camera(self, camera_id: str, fps: int = 15) -> None: + """Uruchamia wybraną kamerę.""" + if self._active_camera: + self.stop_camera() + + camera_info = next( + (c for c in self._detected_cameras if c['id'] == camera_id), None) + + if not camera_info: + self.error_occurred.emit( + f"Nie znaleziono kamery o ID: {camera_id}") + return + + camera_type = camera_info['type'] + camera_index = camera_info['index'] + + if camera_type == "gphoto": + self._active_camera = GPhotoCamera() + elif camera_type == "opencv": + self._active_camera = OpenCvCamera() + else: + self.error_occurred.emit(f"Nieznany typ kamery: {camera_type}") + return + + self._active_camera_info = camera_info + + self._camera_controller.set_camera(self._active_camera, fps) + self._camera_controller.start_camera() + + def start_liveview(self, connected): + if connected: + self._camera_controller.start_stream() + self.camera_started.emit() + else: + self._active_camera = None + self._active_camera_info = None + + def stop_camera(self) -> None: + """Zatrzymuje aktywną kamerę.""" + if self._active_camera: + self._camera_controller.stop_camera() + self._active_camera = None + self._active_camera_info = None + self.camera_stopped.emit() + + def get_active_camera_info(self) -> dict | None: + return self._active_camera_info + + def shutdown(self) -> None: + """Zamyka kontroler kamery i jego wątek.""" + self.stop_camera() + self._camera_controller.stop()