"""Dialogue Recadrer les pages — avec selection visuelle.""" from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, QGroupBox, QDoubleSpinBox, QComboBox, QRadioButton, QDialogButtonBox, QLineEdit, QPushButton) from PyQt6.QtCore import Qt from app.engine.pdf_utils import mm_to_pt, pt_to_mm class CropDialog(QDialog): def __init__(self, page_width_pt=595, page_height_pt=842, total_pages=1, current_page=0, viewer=None, parent=None): super().__init__(parent) self.setWindowTitle("Recadrer") self.setMinimumWidth(450) self._page_w = page_width_pt self._page_h = page_height_pt self._total_pages = total_pages self._current_page = current_page self._viewer = viewer self._setup_ui() self._update_preview() def _setup_ui(self): layout = QVBoxLayout(self) # Info page pw_mm = pt_to_mm(self._page_w) ph_mm = pt_to_mm(self._page_h) info = QLabel(f"Page actuelle : {pw_mm:.1f} x {ph_mm:.1f} mm") info.setStyleSheet("color: #888; font-size: 11px;") layout.addWidget(info) # Bouton selection visuelle if self._viewer: sel_layout = QHBoxLayout() self.btn_select = QPushButton("Selectionner sur l'apercu") self.btn_select.setCheckable(True) self.btn_select.setStyleSheet(""" QPushButton { background: #0066cc; color: white; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QPushButton:checked { background: #cc3300; } QPushButton:hover { background: #0055aa; } QPushButton:checked:hover { background: #aa2200; } """) self.btn_select.toggled.connect(self._toggle_select_mode) sel_layout.addWidget(self.btn_select) sel_layout.addStretch() layout.addLayout(sel_layout) # Connecter les signaux du viewer self._viewer.selection_changed.connect(self._on_selection_changed) # Mode grp_mode = QGroupBox("Mode de recadrage") ml = QVBoxLayout(grp_mode) self.rb_margins = QRadioButton("Par marges (retirer de chaque cote)") self.rb_margins.setChecked(True) self.rb_size = QRadioButton("Par taille finale") ml.addWidget(self.rb_margins) ml.addWidget(self.rb_size) layout.addWidget(grp_mode) self.rb_margins.toggled.connect(self._on_mode_changed) self.rb_size.toggled.connect(self._on_mode_changed) # Marges self.grp_margins_input = QGroupBox("Marges a retirer (mm)") mgl = QVBoxLayout(self.grp_margins_input) self.margin_spins = {} row1 = QHBoxLayout() row1.addStretch() row1.addWidget(QLabel("Haut :")) self.margin_spins["haut"] = QDoubleSpinBox() self.margin_spins["haut"].setRange(0, 500) self.margin_spins["haut"].setSuffix(" mm") self.margin_spins["haut"].valueChanged.connect(self._update_preview) self.margin_spins["haut"].valueChanged.connect(self._show_selection_from_margins) row1.addWidget(self.margin_spins["haut"]) row1.addStretch() mgl.addLayout(row1) row2 = QHBoxLayout() row2.addWidget(QLabel("Gauche :")) self.margin_spins["gauche"] = QDoubleSpinBox() self.margin_spins["gauche"].setRange(0, 500) self.margin_spins["gauche"].setSuffix(" mm") self.margin_spins["gauche"].valueChanged.connect(self._update_preview) self.margin_spins["gauche"].valueChanged.connect(self._show_selection_from_margins) row2.addWidget(self.margin_spins["gauche"]) row2.addStretch() row2.addWidget(QLabel("Droite :")) self.margin_spins["droite"] = QDoubleSpinBox() self.margin_spins["droite"].setRange(0, 500) self.margin_spins["droite"].setSuffix(" mm") self.margin_spins["droite"].valueChanged.connect(self._update_preview) self.margin_spins["droite"].valueChanged.connect(self._show_selection_from_margins) row2.addWidget(self.margin_spins["droite"]) mgl.addLayout(row2) row3 = QHBoxLayout() row3.addStretch() row3.addWidget(QLabel("Bas :")) self.margin_spins["bas"] = QDoubleSpinBox() self.margin_spins["bas"].setRange(0, 500) self.margin_spins["bas"].setSuffix(" mm") self.margin_spins["bas"].valueChanged.connect(self._update_preview) self.margin_spins["bas"].valueChanged.connect(self._show_selection_from_margins) row3.addWidget(self.margin_spins["bas"]) row3.addStretch() mgl.addLayout(row3) layout.addWidget(self.grp_margins_input) # Taille finale self.grp_size_input = QGroupBox("Taille finale (mm)") sgl = QHBoxLayout(self.grp_size_input) sgl.addWidget(QLabel("Largeur :")) self.spin_final_w = QDoubleSpinBox() self.spin_final_w.setRange(10, 5000) self.spin_final_w.setValue(pt_to_mm(self._page_w)) self.spin_final_w.setSuffix(" mm") self.spin_final_w.valueChanged.connect(self._update_preview) sgl.addWidget(self.spin_final_w) sgl.addWidget(QLabel("Hauteur :")) self.spin_final_h = QDoubleSpinBox() self.spin_final_h.setRange(10, 5000) self.spin_final_h.setValue(pt_to_mm(self._page_h)) self.spin_final_h.setSuffix(" mm") self.spin_final_h.valueChanged.connect(self._update_preview) sgl.addWidget(self.spin_final_h) layout.addWidget(self.grp_size_input) self.grp_size_input.setVisible(False) # Apercu self.lbl_preview = QLabel() self.lbl_preview.setStyleSheet("font-weight: bold; font-size: 13px; color: #0066cc; padding: 8px;") layout.addWidget(self.lbl_preview) # Appliquer a grp_apply = QGroupBox("Appliquer a") al = QVBoxLayout(grp_apply) self.rb_current = QRadioButton(f"Page courante ({self._current_page + 1})") self.rb_current.setChecked(True) self.rb_all = QRadioButton("Toutes les pages") self.rb_even = QRadioButton("Pages paires") self.rb_odd = QRadioButton("Pages impaires") self.rb_range = QRadioButton("Plage de pages :") al.addWidget(self.rb_current) al.addWidget(self.rb_all) al.addWidget(self.rb_even) al.addWidget(self.rb_odd) range_layout = QHBoxLayout() range_layout.addWidget(self.rb_range) self.edit_range = QLineEdit() self.edit_range.setPlaceholderText("ex: 1-3, 5, 8-10") self.edit_range.setEnabled(False) range_layout.addWidget(self.edit_range) al.addLayout(range_layout) layout.addWidget(grp_apply) self.rb_range.toggled.connect(self.edit_range.setEnabled) # Boutons buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) def _toggle_select_mode(self, checked): if self._viewer: self._viewer.set_select_mode(checked) if checked: self.btn_select.setText("Cliquez et tracez sur l'apercu...") else: self.btn_select.setText("Selectionner sur l'apercu") def _on_selection_changed(self, left, top, right, bottom): """Recoit les ratios de selection du viewer et met a jour les marges.""" if not self.rb_margins.isChecked(): return pw_mm = pt_to_mm(self._page_w) ph_mm = pt_to_mm(self._page_h) # Les ratios representent la zone SELECTIONNEE (a garder) # Les marges sont ce qu'on ENLEVE margin_left = left * pw_mm margin_right = (1 - right) * pw_mm margin_top = top * ph_mm margin_bottom = (1 - bottom) * ph_mm # Mettre a jour sans declencher les signaux en boucle for spin in self.margin_spins.values(): spin.blockSignals(True) self.margin_spins["gauche"].setValue(margin_left) self.margin_spins["droite"].setValue(margin_right) self.margin_spins["haut"].setValue(margin_top) self.margin_spins["bas"].setValue(margin_bottom) for spin in self.margin_spins.values(): spin.blockSignals(False) self._update_preview() def _show_selection_from_margins(self): """Afficher le rectangle de selection sur le viewer a partir des marges.""" if not self._viewer or not self.rb_margins.isChecked(): return pw_mm = pt_to_mm(self._page_w) ph_mm = pt_to_mm(self._page_h) left = self.margin_spins["gauche"].value() / pw_mm if pw_mm > 0 else 0 top = self.margin_spins["haut"].value() / ph_mm if ph_mm > 0 else 0 right = 1 - (self.margin_spins["droite"].value() / pw_mm) if pw_mm > 0 else 1 bottom = 1 - (self.margin_spins["bas"].value() / ph_mm) if ph_mm > 0 else 1 self._viewer.show_selection(left, top, right, bottom) def _on_mode_changed(self): self.grp_margins_input.setVisible(self.rb_margins.isChecked()) self.grp_size_input.setVisible(self.rb_size.isChecked()) self._update_preview() def _update_preview(self): if self.rb_margins.isChecked(): top = self.margin_spins["haut"].value() bottom = self.margin_spins["bas"].value() left = self.margin_spins["gauche"].value() right = self.margin_spins["droite"].value() w_mm = pt_to_mm(self._page_w) - left - right h_mm = pt_to_mm(self._page_h) - top - bottom else: w_mm = self.spin_final_w.value() h_mm = self.spin_final_h.value() if w_mm <= 0 or h_mm <= 0: self.lbl_preview.setText("Dimensions invalides !") self.lbl_preview.setStyleSheet("font-weight: bold; font-size: 13px; color: red; padding: 8px;") else: self.lbl_preview.setText(f"Resultat : {w_mm:.1f} x {h_mm:.1f} mm") self.lbl_preview.setStyleSheet("font-weight: bold; font-size: 13px; color: #0066cc; padding: 8px;") def _parse_range(self): text = self.edit_range.text().strip() if not text: return [] indices = [] for part in text.split(","): part = part.strip() if "-" in part: try: start, end = part.split("-", 1) for p in range(int(start), int(end) + 1): if 1 <= p <= self._total_pages: indices.append(p - 1) except ValueError: pass else: try: p = int(part) if 1 <= p <= self._total_pages: indices.append(p - 1) except ValueError: pass return sorted(set(indices)) def closeEvent(self, event): """Nettoyer le mode selection a la fermeture.""" if self._viewer: self._viewer.set_select_mode(False) self._viewer.clear_selection() super().closeEvent(event) def reject(self): if self._viewer: self._viewer.set_select_mode(False) self._viewer.clear_selection() super().reject() def get_settings(self): if self._viewer: self._viewer.set_select_mode(False) self._viewer.clear_selection() if self.rb_margins.isChecked(): crop_top = mm_to_pt(self.margin_spins["haut"].value()) crop_bottom = mm_to_pt(self.margin_spins["bas"].value()) crop_left = mm_to_pt(self.margin_spins["gauche"].value()) crop_right = mm_to_pt(self.margin_spins["droite"].value()) else: final_w = mm_to_pt(self.spin_final_w.value()) final_h = mm_to_pt(self.spin_final_h.value()) crop_left = (self._page_w - final_w) / 2 crop_right = crop_left crop_top = (self._page_h - final_h) / 2 crop_bottom = crop_top if self.rb_current.isChecked(): pages = [self._current_page] elif self.rb_all.isChecked(): pages = list(range(self._total_pages)) elif self.rb_even.isChecked(): pages = [i for i in range(self._total_pages) if (i + 1) % 2 == 0] elif self.rb_odd.isChecked(): pages = [i for i in range(self._total_pages) if (i + 1) % 2 != 0] else: pages = self._parse_range() return { "crop_top": crop_top, "crop_bottom": crop_bottom, "crop_left": crop_left, "crop_right": crop_right, "pages": pages, }