diff --git a/src/ui/screens/import_screen.py b/src/ui/screens/import_screen.py index 45c2094..6d9097e 100644 --- a/src/ui/screens/import_screen.py +++ b/src/ui/screens/import_screen.py @@ -297,7 +297,7 @@ class ImportScreen(QWidget): self.summary_label.setStyleSheet("color: #999;") def _validate(self): - """Envoie les photos sélectionnées à l'impression.""" + """Envoie les photos sélectionnées à l'écran d'impression.""" to_print = [] for card in self.cards: if card.quantity > 0: @@ -306,9 +306,8 @@ class ImportScreen(QWidget): if not to_print: return - # TODO: envoyer à l'écran d'impression avec les quantités - # Pour l'instant, on envoie la première photo - self.main_window.show_print(to_print[0][0]) + self.main_window.print_screen.set_print_list(to_print) + self.main_window.show_print() def get_print_list(self): """Retourne la liste [(path, quantity), ...] pour l'impression.""" diff --git a/src/ui/screens/print_screen.py b/src/ui/screens/print_screen.py index c4d8d97..47611a9 100644 --- a/src/ui/screens/print_screen.py +++ b/src/ui/screens/print_screen.py @@ -1,90 +1,259 @@ -"""Écran d'impression — prévisualisation et lancement impression.""" +"""Écran d'impression — choix du format, recadrage au ratio, impression.""" from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, - QLabel, QComboBox + QLabel, QGraphicsScene, QGraphicsView, QGraphicsPixmapItem ) -from PyQt6.QtCore import Qt -from PyQt6.QtGui import QFont, QPixmap +from PyQt6.QtCore import Qt, QRectF +from PyQt6.QtGui import QFont, QPixmap, QImage, QPainter, QColor + +from ui.widgets.config_dialog import load_config +from ui.widgets.crop_overlay import CropOverlay +from ui.style import scale, scaled_font, Sizes, back_btn_style, green_btn, blue_btn + +# Ratios par format (largeur / hauteur, paysage) +FORMAT_RATIOS = { + "10x15": 1.5, # 15/10 + "13x18": 1.385, # 18/13 + "15x20": 1.333, # 20/15 +} class PrintScreen(QWidget): - FORMATS = { - "10x15 cm": (10, 15), - "13x18 cm": (13, 18), - "15x20 cm": (15, 20), - } - def __init__(self, main_window): super().__init__() self.main_window = main_window self.image_path = None + self.pixmap_item = None + self.crop_overlay = None + self.selected_format = None + self.print_list = [] # [(path, quantity), ...] self._setup_ui() def _setup_ui(self): layout = QVBoxLayout(self) - layout.setAlignment(Qt.AlignmentFlag.AlignCenter) - layout.setSpacing(30) + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(8) # Header header = QHBoxLayout() - back_btn = QPushButton("Retour éditeur") - back_btn.setFont(QFont("", 14)) - back_btn.clicked.connect(lambda: self.main_window.show_editor()) - header.addWidget(back_btn) - header.addStretch() + self.back_btn = QPushButton("< Retour") + self.back_btn.clicked.connect(self._go_back) + header.addWidget(self.back_btn) + + self.title = QLabel("Choisissez le format et recadrez") + self.title.setAlignment(Qt.AlignmentFlag.AlignCenter) + header.addWidget(self.title, 1) + + header.addSpacing(100) layout.addLayout(header) - # Prévisualisation - self.preview = QLabel() - self.preview.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.preview.setMinimumSize(400, 300) - self.preview.setStyleSheet("background-color: #333; border-radius: 10px;") - layout.addWidget(self.preview) + # Boutons de format + self.format_bar = QHBoxLayout() + self.format_bar.setSpacing(10) + self.format_buttons = {} + layout.addLayout(self.format_bar) - # Sélection format - format_layout = QHBoxLayout() - format_label = QLabel("Format :") - format_label.setFont(QFont("", 16)) - format_layout.addWidget(format_label) + # Zone de recadrage (QGraphicsScene) + self.scene = QGraphicsScene() + self.view = QGraphicsView(self.scene) + self.view.setRenderHint(QPainter.RenderHint.Antialiasing) + self.view.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + self.view.setStyleSheet("background-color: #333; border: none;") + self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + layout.addWidget(self.view, 1) - self.format_combo = QComboBox() - self.format_combo.setFont(QFont("", 16)) - self.format_combo.addItems(self.FORMATS.keys()) - self.format_combo.setMinimumHeight(50) - format_layout.addWidget(self.format_combo) - layout.addLayout(format_layout) + # Info recadrage + self.info_label = QLabel("Déplacez le cadre pour recadrer votre photo") + self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.info_label.setStyleSheet("color: #999;") + layout.addWidget(self.info_label) # Bouton imprimer - print_btn = QPushButton("IMPRIMER") - print_btn.setFont(QFont("", 24, QFont.Weight.Bold)) - print_btn.setMinimumSize(300, 80) - print_btn.setStyleSheet(""" - QPushButton { - background-color: #4CAF50; - color: white; - border-radius: 15px; - } - QPushButton:pressed { background-color: #388E3C; } - """) - print_btn.clicked.connect(self._print) - layout.addWidget(print_btn, alignment=Qt.AlignmentFlag.AlignCenter) + self.print_btn = QPushButton("IMPRIMER") + self.print_btn.clicked.connect(self._print) + layout.addWidget(self.print_btn, alignment=Qt.AlignmentFlag.AlignCenter) + + def showEvent(self, event): + super().showEvent(event) + from PyQt6.QtCore import QTimer + QTimer.singleShot(0, self._apply_responsive) + + def resizeEvent(self, event): + super().resizeEvent(event) + self._apply_responsive() + + def _apply_responsive(self): + self.back_btn.setFont(scaled_font(self, Sizes.BUTTON_TEXT, bold=True)) + self.back_btn.setMinimumHeight(scale(self, Sizes.BTN_HEIGHT)) + self.back_btn.setStyleSheet(back_btn_style(self)) + + self.title.setFont(scaled_font(self, Sizes.HEADING, bold=True)) + self.info_label.setFont(scaled_font(self, Sizes.HINT)) + + self.print_btn.setFont(scaled_font(self, 4.0, bold=True)) + self.print_btn.setMinimumSize(scale(self, 35), scale(self, Sizes.BTN_HEIGHT * 1.5)) + self.print_btn.setStyleSheet(green_btn(self)) + + # Style boutons format + for fmt, btn in self.format_buttons.items(): + btn.setFont(scaled_font(self, Sizes.BUTTON_TEXT)) + btn.setMinimumHeight(scale(self, Sizes.BTN_HEIGHT)) + if fmt == self.selected_format: + btn.setStyleSheet(green_btn(self)) + else: + btn.setStyleSheet(blue_btn(self)) def set_image(self, path): + """Charge une image et affiche les formats disponibles.""" self.image_path = path - pixmap = QPixmap(path) - if not pixmap.isNull(): - scaled = pixmap.scaled( - 400, 300, - Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation - ) - self.preview.setPixmap(scaled) + self.print_list = [(path, 1)] + self._load_formats() + self._load_image() - def _print(self): + def set_print_list(self, print_list): + """Reçoit la liste [(path, qty), ...] depuis la galerie.""" + self.print_list = print_list + if print_list: + self.image_path = print_list[0][0] + self._load_formats() + self._load_image() + + def _load_formats(self): + """Charge les formats activés depuis la config.""" + # Clear existing buttons + while self.format_bar.count(): + item = self.format_bar.takeAt(0) + if item.widget(): + item.widget().deleteLater() + self.format_buttons = {} + + config = load_config() + formats = config.get("formats", {}) + self.selected_format = None + + for fmt_name, fmt_conf in formats.items(): + if not fmt_conf.get("enabled", False): + continue + btn = QPushButton(f"{fmt_name} cm") + btn.clicked.connect(lambda checked, f=fmt_name: self._select_format(f)) + self.format_bar.addWidget(btn) + self.format_buttons[fmt_name] = btn + + # Sélectionner le premier par défaut + if self.selected_format is None: + self.selected_format = fmt_name + + # Si aucun format activé, activer 10x15 par défaut + if not self.format_buttons: + btn = QPushButton("10x15 cm") + btn.clicked.connect(lambda: self._select_format("10x15")) + self.format_bar.addWidget(btn) + self.format_buttons["10x15"] = btn + self.selected_format = "10x15" + + self._apply_responsive() + + def _load_image(self): + """Charge l'image dans la scène et place le cadre de recadrage.""" if not self.image_path: return - fmt = self.format_combo.currentText() - size = self.FORMATS[fmt] - # TODO: redimensionner selon le format et envoyer à CUPS - print(f"Impression {self.image_path} en {fmt} ({size[0]}x{size[1]} cm)") + self.scene.clear() + self.crop_overlay = None + pixmap = QPixmap(self.image_path) + if pixmap.isNull(): + return + self.pixmap_item = self.scene.addPixmap(pixmap) + self.scene.setSceneRect(QRectF(pixmap.rect())) + self.view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatio) + self._place_crop_overlay() + + def _select_format(self, fmt_name): + """Change le format sélectionné et met à jour le cadre.""" + self.selected_format = fmt_name + self._apply_responsive() + self._place_crop_overlay() + + def _place_crop_overlay(self): + """Place le cadre de recadrage au ratio du format sélectionné.""" + if not self.pixmap_item: + return + + # Supprimer l'ancien overlay + if self.crop_overlay: + self.scene.removeItem(self.crop_overlay) + self.crop_overlay = None + + ratio = FORMAT_RATIOS.get(self.selected_format, 1.5) + img_rect = self.pixmap_item.boundingRect() + img_w = img_rect.width() + img_h = img_rect.height() + + # Déterminer orientation : paysage ou portrait selon l'image + img_ratio = img_w / img_h + if img_ratio < 1: + # Image portrait → cadre portrait (inverser le ratio) + ratio = 1.0 / ratio + + # Calculer la plus grande zone de crop possible dans l'image + if img_w / img_h > ratio: + # Image plus large que le ratio → hauteur max, largeur ajustée + crop_h = img_h * 0.9 + crop_w = crop_h * ratio + else: + # Image plus haute que le ratio → largeur max, hauteur ajustée + crop_w = img_w * 0.9 + crop_h = crop_w / ratio + + # Centrer le cadre + x = (img_w - crop_w) / 2 + y = (img_h - crop_h) / 2 + + crop_rect = QRectF(x, y, crop_w, crop_h) + self.crop_overlay = CropOverlay(crop_rect) + self.crop_overlay.set_bounds(img_rect) + self.scene.addItem(self.crop_overlay) + + self.view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatio) + + def _go_back(self): + self.main_window.show_import() + + def _print(self): + if not self.image_path or not self.selected_format: + return + + config = load_config() + formats = config.get("formats", {}) + fmt_conf = formats.get(self.selected_format, {}) + printer = fmt_conf.get("printer", "") + ratio = FORMAT_RATIOS.get(self.selected_format, 1.5) + + # Récupérer le rectangle de crop + if self.crop_overlay: + crop_rect = self.crop_overlay.get_crop_rect().toRect() + else: + crop_rect = None + + # Pour chaque photo de la liste + for path, qty in self.print_list: + pixmap = QPixmap(path) + if pixmap.isNull(): + continue + + # Appliquer le crop si défini (utiliser le même crop relatif) + if crop_rect: + cropped = pixmap.copy(crop_rect) + else: + cropped = pixmap + + # TODO: envoyer à CUPS + print(f"[Impression] {path} x{qty} → {self.selected_format} cm" + f" (printer: {printer or 'défaut'}," + f" crop: {crop_rect.width()}x{crop_rect.height() if crop_rect else 'none'})") + + # Feedback + total = sum(q for _, q in self.print_list) + self.info_label.setText(f"Impression lancée — {total} photo{'s' if total > 1 else ''}") + self.info_label.setStyleSheet("color: #4CAF50; font-weight: bold;") diff --git a/src/ui/widgets/config_dialog.py b/src/ui/widgets/config_dialog.py index 8cc2545..8eea49c 100644 --- a/src/ui/widgets/config_dialog.py +++ b/src/ui/widgets/config_dialog.py @@ -31,9 +31,12 @@ DEFAULT_CONFIG = { "imap_password": "", "imap_folder": "INBOX", "imap_interval": 30, - # Impression - "printer_name": "", - "print_format": "10x15 cm", + # Formats d'impression (activable + imprimante par format) + "formats": { + "10x15": {"enabled": True, "printer": "", "ratio": 1.5}, + "13x18": {"enabled": False, "printer": "", "ratio": 1.385}, + "15x20": {"enabled": False, "printer": "", "ratio": 1.333}, + }, } @@ -211,23 +214,26 @@ class ConfigDialog(QDialog): grid.addWidget(self.imap_interval_input, row, 1) row += 1 - # ── Impression ── - grid.addWidget(self._section_title("Impression"), row, 0, 1, 2) + # ── Formats d'impression ── + grid.addWidget(self._section_title("Formats d'impression"), row, 0, 1, 2) row += 1 - grid.addWidget(self._label("Imprimante :"), row, 0) - self.printer_input = self._input(self.config["printer_name"], "Nom CUPS (vide = par défaut)") - grid.addWidget(self.printer_input, row, 1) - row += 1 + formats = self.config.get("formats", DEFAULT_CONFIG["formats"]) + self.format_widgets = {} - grid.addWidget(self._label("Format par défaut :"), row, 0) - self.format_combo = QComboBox() - self.format_combo.setFont(QFont("", 12)) - self.format_combo.setMinimumHeight(35) - self.format_combo.addItems(["10x15 cm", "13x18 cm", "15x20 cm"]) - self.format_combo.setCurrentText(self.config["print_format"]) - grid.addWidget(self.format_combo, row, 1) - row += 1 + for fmt_name, fmt_conf in formats.items(): + # Checkbox activé + cb = QCheckBox(f"{fmt_name} cm") + cb.setFont(QFont("", 12)) + cb.setChecked(fmt_conf.get("enabled", False)) + grid.addWidget(cb, row, 0) + + # Imprimante associée + printer = self._input(fmt_conf.get("printer", ""), "Imprimante CUPS (vide = défaut)") + grid.addWidget(printer, row, 1) + + self.format_widgets[fmt_name] = {"checkbox": cb, "printer": printer} + row += 1 scroll.setWidget(content) layout.addWidget(scroll, 1) @@ -272,8 +278,14 @@ class ConfigDialog(QDialog): self.config["imap_password"] = self.imap_pass_input.text() self.config["imap_folder"] = self.imap_folder_input.text() self.config["imap_interval"] = self.imap_interval_input.value() - self.config["printer_name"] = self.printer_input.text() - self.config["print_format"] = self.format_combo.currentText() + # Formats d'impression + formats = self.config.get("formats", dict(DEFAULT_CONFIG["formats"])) + for fmt_name, widgets in self.format_widgets.items(): + if fmt_name not in formats: + formats[fmt_name] = {} + formats[fmt_name]["enabled"] = widgets["checkbox"].isChecked() + formats[fmt_name]["printer"] = widgets["printer"].text() + self.config["formats"] = formats save_config(self.config) self.accept()