Source code for rekordbox_set_list_manager.controllers.file_controller

"""File operations controller: new, open, save, save-as, open recent."""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QFileDialog, QMessageBox, QWidget

from rekordbox_set_list_manager.services import telemetry
from rekordbox_set_list_manager.services.project_io import ProjectIOError
from rekordbox_set_list_manager.utils.config import add_recent_file
from rekordbox_set_list_manager.utils.constants import PROJECT_FILE_EXTENSION

if TYPE_CHECKING:
    from rekordbox_set_list_manager.controllers.project_controller import ProjectController

_DEFAULT_PROJECT_NAME = "Untitled"


[docs] class FileController(QObject): """Handles file operations: new, open, save, save-as, open-recent. Emits signals that MainWindow uses to trigger purely UI side effects (clear undo history, reset transition note, rebuild recent-file menu). """
[docs] recent_changed = Signal() # recent-files list changed → rebuild menu
[docs] note_cleared = Signal() # project loaded/created → clear transition note
[docs] undo_cleared = Signal() # project loaded/created → clear undo history
def __init__( self, ctrl: ProjectController, parent_widget: QWidget, parent: QObject | None = None, ) -> None: """Initialise the file controller backed by *ctrl*.""" super().__init__(parent) self._ctrl = ctrl self._w = parent_widget # ------------------------------------------------------------------ # Public API # ------------------------------------------------------------------
[docs] def confirm_discard(self) -> bool: """Return True if it is safe to proceed (saved or discarded), False to cancel.""" if not self._ctrl.dirty: return True msg = QMessageBox(self._w) msg.setWindowTitle("Unsaved changes") msg.setText("You have unsaved changes.") msg.setIcon(QMessageBox.Icon.Warning) save_btn = msg.addButton("Save", QMessageBox.ButtonRole.AcceptRole) msg.addButton("Discard", QMessageBox.ButtonRole.DestructiveRole) msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole) msg.setDefaultButton(save_btn) msg.exec() clicked = msg.clickedButton() if clicked is save_btn: self.save() # If save was cancelled (e.g. Save As dialog dismissed), still dirty → abort return not self._ctrl.dirty return ( clicked is not None and msg.buttonRole(clicked) == QMessageBox.ButtonRole.DestructiveRole )
[docs] def new_project(self) -> None: """Create a new empty project, prompting to discard unsaved changes.""" if not self.confirm_discard(): return self._ctrl.new() self.note_cleared.emit() self.undo_cleared.emit()
[docs] def open(self) -> None: """Prompt the user to select and open a project file.""" if not self.confirm_discard(): return path_str, _ = QFileDialog.getOpenFileName( self._w, "Open Project", "", f"Rekordbox Set List Manager files (*{PROJECT_FILE_EXTENSION})", ) if not path_str: return self._load(Path(path_str))
[docs] def open_recent(self, path_str: str) -> None: """Open a recently-used project by file path string.""" path = Path(path_str) if not path.exists(): QMessageBox.warning( self._w, "File not found", f"{path_str}\n\nThe file no longer exists.", ) add_recent_file(path_str) # keep in list (existing behaviour) self.recent_changed.emit() return if not self.confirm_discard(): return self._load(path)
[docs] def save(self) -> None: """Save the current project, prompting for a path if not yet set.""" if self._ctrl.save_path is None: self.save_as() else: self._do_save(self._ctrl.save_path)
[docs] def save_as(self) -> None: """Prompt the user for a new file path and save the current project.""" project = self._ctrl.project default = project.name if project else _DEFAULT_PROJECT_NAME path_str, _ = QFileDialog.getSaveFileName( self._w, "Save Project", default, f"Rekordbox Set List Manager files (*{PROJECT_FILE_EXTENSION})", ) if not path_str: return path = Path(path_str) if path.suffix.lower() != PROJECT_FILE_EXTENSION: path = path.with_suffix(PROJECT_FILE_EXTENSION) self._do_save(path)
# ------------------------------------------------------------------ # Private helpers # ------------------------------------------------------------------ def _load(self, path: Path) -> None: try: self._ctrl.load(path) except ProjectIOError as exc: QMessageBox.critical(self._w, "Open failed", str(exc)) return telemetry.record("project_open") self.note_cleared.emit() self.undo_cleared.emit() self.recent_changed.emit() def _do_save(self, path: Path) -> None: try: self._ctrl.save(path) except ProjectIOError as exc: QMessageBox.critical(self._w, "Save failed", str(exc)) return telemetry.record("project_save") self.undo_cleared.emit() self.recent_changed.emit()