V2.3.1 — Recadrage avec selection visuelle

- Bouton "Selectionner sur l'apercu" dans le dialogue Recadrer
- Tracer un rectangle a la souris directement sur le PDF
- Zone sombre en dehors de la selection, cadre bleu en pointilles
- Les marges se mettent a jour automatiquement depuis la selection
- Les marges saisies manuellement affichent le cadre sur l'apercu
- Synchronisation bidirectionnelle selection <-> valeurs
- Nettoyage auto du mode selection a la fermeture du dialogue

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jules
2026-03-21 14:19:44 +01:00
parent e7a27018e5
commit 1f8f6bb89d
5 changed files with 263 additions and 35 deletions

View File

@@ -1,14 +1,15 @@
"""Dialogue Recadrer les pages."""
"""Dialogue Recadrer les pages — avec selection visuelle."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QDoubleSpinBox, QComboBox, QRadioButton,
QDialogButtonBox, QLineEdit)
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, parent=None):
current_page=0, viewer=None, parent=None):
super().__init__(parent)
self.setWindowTitle("Recadrer")
self.setMinimumWidth(450)
@@ -16,19 +17,40 @@ class CropDialog(QDialog):
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 actuelle
# 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)
@@ -38,14 +60,14 @@ class CropDialog(QDialog):
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 de recadrage
# 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 :"))
@@ -53,6 +75,7 @@ class CropDialog(QDialog):
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)
@@ -63,6 +86,7 @@ class CropDialog(QDialog):
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 :"))
@@ -70,6 +94,7 @@ class CropDialog(QDialog):
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)
@@ -80,6 +105,7 @@ class CropDialog(QDialog):
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)
@@ -105,7 +131,7 @@ class CropDialog(QDialog):
layout.addWidget(self.grp_size_input)
self.grp_size_input.setVisible(False)
# Apercu dimension finale
# Apercu
self.lbl_preview = QLabel()
self.lbl_preview.setStyleSheet("font-weight: bold; font-size: 13px; color: #0066cc; padding: 8px;")
layout.addWidget(self.lbl_preview)
@@ -123,7 +149,6 @@ class CropDialog(QDialog):
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()
@@ -132,7 +157,6 @@ class CropDialog(QDialog):
range_layout.addWidget(self.edit_range)
al.addLayout(range_layout)
layout.addWidget(grp_apply)
self.rb_range.toggled.connect(self.edit_range.setEnabled)
# Boutons
@@ -143,6 +167,54 @@ class CropDialog(QDialog):
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())
@@ -168,7 +240,6 @@ class CropDialog(QDialog):
self.lbl_preview.setStyleSheet("font-weight: bold; font-size: 13px; color: #0066cc; padding: 8px;")
def _parse_range(self):
"""Parser la plage de pages. Retourne une liste d'indices (0-based)."""
text = self.edit_range.text().strip()
if not text:
return []
@@ -192,16 +263,30 @@ class CropDialog(QDialog):
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):
# Mode
if self._viewer:
self._viewer.set_select_mode(False)
self._viewer.clear_selection()
if self.rb_margins.isChecked():
mode = "margins"
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:
mode = "size"
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
@@ -209,7 +294,6 @@ class CropDialog(QDialog):
crop_top = (self._page_h - final_h) / 2
crop_bottom = crop_top
# Pages cibles
if self.rb_current.isChecked():
pages = [self._current_page]
elif self.rb_all.isChecked():