- Rectangle de selection redimensionnable par les 8 poignees (angles + cotes) - Curseurs adaptes selon la poignee survolee - Deplacement du rectangle entier en cliquant a l'interieur - Zone sombre en dehors, cadre cyan, poignees blanches - Bouton Effacer dans le dialogue recadrage - Fix remontee des valeurs de selection dans les spinbox Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
336 lines
14 KiB
Python
336 lines
14 KiB
Python
"""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.setFixedWidth(360)
|
|
self.setModal(False)
|
|
self.setWindowFlags(self.windowFlags() | Qt.WindowType.Tool)
|
|
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: #00b4d8; color: white; padding: 6px 14px;
|
|
border-radius: 6px; font-weight: bold; font-size: 11px; }
|
|
QPushButton:checked { background: #e74c3c; }
|
|
QPushButton:hover { background: #0096b7; }
|
|
QPushButton:checked:hover { background: #c0392b; }
|
|
""")
|
|
self.btn_select.toggled.connect(self._toggle_select_mode)
|
|
sel_layout.addWidget(self.btn_select)
|
|
|
|
self.btn_clear = QPushButton("Effacer")
|
|
self.btn_clear.setStyleSheet("""
|
|
QPushButton { background: #e8e8e8; color: #555; padding: 6px 14px;
|
|
border-radius: 6px; font-size: 11px; border: 1px solid #ccc; }
|
|
QPushButton:hover { background: #ddd; }
|
|
""")
|
|
self.btn_clear.clicked.connect(self._clear_selection)
|
|
sel_layout.addWidget(self.btn_clear)
|
|
|
|
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("Mode selection actif")
|
|
else:
|
|
self.btn_select.setText("Selectionner sur l'apercu")
|
|
|
|
def _clear_selection(self):
|
|
if self._viewer:
|
|
self._viewer.clear_selection()
|
|
for spin in self.margin_spins.values():
|
|
spin.blockSignals(True)
|
|
spin.setValue(0)
|
|
spin.blockSignals(False)
|
|
self._update_preview()
|
|
|
|
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,
|
|
}
|