V2.0.0 — Refonte interface complete

- Nouveau layout : menus en haut, vignettes a gauche, viewer au centre
- Panneau vignettes redimensionnable (splitter)
  - Miniatures de chaque page
  - Drag & drop pour reordonner
  - Clic droit : supprimer page, inserer page blanche
  - Clic = naviguer vers la page
- Viewer PDF ameliore :
  - Ajustement auto a l'ouverture
  - Roulette = defiler les pages
  - Ctrl+roulette = zoom
- Drag & drop de fichier PDF sur la fenetre
- Barre de progression pendant les traitements
- Fusion de PDF (Fichier > Fusionner)
- Enlever les fonds perdus (Annotations > Enlever)
- Menus par categorie : Imposition, Pages, Annotations
- Toolbar : Ouvrir, Enregistrer, Zoom, info page
- Style sombre coherent CopyDev
- Suppression du numero de telephone dans A propos

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jules
2026-03-21 07:49:44 +01:00
parent de3e83b57e
commit 9cc256ff51
5 changed files with 488 additions and 225 deletions

View File

@@ -1,15 +1,18 @@
"""Fenetre principale de Copydev Imposing Tool."""
"""Fenetre principale de Copydev Imposing Tool — V2."""
import os
import sys
import tempfile
import shutil
from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QFileDialog, QMessageBox,
QGroupBox, QStatusBar, QMenuBar, QSplitter,
QProgressBar)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QAction
QLabel, QFileDialog, QMessageBox, QStatusBar,
QSplitter, QProgressBar, QApplication, QToolBar)
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QAction, QIcon, QDragEnterEvent, QDropEvent
from app.pdf_viewer import PdfViewer
from app.page_panel import PagePanel
from app.dialogs.booklet_dialog import BookletDialog
from app.dialogs.nup_dialog import NupDialog
from app.dialogs.step_repeat_dialog import StepRepeatDialog
@@ -36,172 +39,259 @@ class MainWindow(QMainWindow):
super().__init__()
from app.updater import VERSION
self.setWindowTitle(f"CopyDev Imposing Tool v{VERSION} — www.copydev.fr")
self.setMinimumSize(1000, 700)
self.setMinimumSize(1100, 750)
# Icone de la fenetre
import sys as _sys
from PyQt6.QtGui import QIcon
_base = _sys._MEIPASS if getattr(_sys, 'frozen', False) else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Icone
_base = sys._MEIPASS if getattr(sys, 'frozen', False) else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
icon_path = os.path.join(_base, "resources", "logo.png")
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
self._current_file = None
self._result_file = None
# Drag & drop
self.setAcceptDrops(True)
self._setup_menu()
self._setup_toolbar()
self._setup_ui()
self._setup_statusbar()
# Style global
self.setStyleSheet("""
QMainWindow { background: #1a1a2e; }
QMenuBar { background: #16213e; color: #e0e0e0; font-size: 13px; padding: 2px; }
QMenuBar::item:selected { background: #0f3460; }
QMenu { background: #16213e; color: #e0e0e0; border: 1px solid #333; }
QMenu::item:selected { background: #0f3460; }
QMenu::separator { background: #333; height: 1px; margin: 4px 10px; }
QToolBar { background: #16213e; border: none; spacing: 4px; padding: 2px; }
QToolBar QToolButton { color: #e0e0e0; padding: 4px 8px; border-radius: 3px; }
QToolBar QToolButton:hover { background: #0f3460; }
QStatusBar { background: #0f0f23; color: #888; font-size: 11px; }
QSplitter::handle { background: #333; width: 3px; }
""")
# ── Drag & Drop ──
def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
if url.toLocalFile().lower().endswith('.pdf'):
event.acceptProposedAction()
return
def dropEvent(self, event: QDropEvent):
for url in event.mimeData().urls():
path = url.toLocalFile()
if path.lower().endswith('.pdf'):
self._open_pdf(path)
return
# ── Menu ──
def _setup_menu(self):
menubar = self.menuBar()
# Menu Fichier
# Fichier
file_menu = menubar.addMenu("Fichier")
open_act = QAction("Ouvrir PDF...", self)
open_act.setShortcut("Ctrl+O")
open_act.triggered.connect(self._open_file)
file_menu.addAction(open_act)
save_act = QAction("Enregistrer le resultat...", self)
save_act.setShortcut("Ctrl+S")
save_act.triggered.connect(self._save_result)
file_menu.addAction(save_act)
self._add_action(file_menu, "Ouvrir PDF...", "Ctrl+O", self._open_file)
self._add_action(file_menu, "Enregistrer le resultat...", "Ctrl+S", self._save_result)
file_menu.addSeparator()
self._add_action(file_menu, "Fusionner des PDF...", "Ctrl+M", self._do_merge)
file_menu.addSeparator()
self._add_action(file_menu, "Quitter", "Ctrl+Q", self.close)
quit_act = QAction("Quitter", self)
quit_act.setShortcut("Ctrl+Q")
quit_act.triggered.connect(self.close)
file_menu.addAction(quit_act)
# Imposition
impose_menu = menubar.addMenu("Imposition")
self._add_action(impose_menu, "Creer un livret", None, self._do_booklet)
self._add_action(impose_menu, "N pages par feuille", None, self._do_nup)
self._add_action(impose_menu, "Repetition (Step && Repeat)", None, self._do_step_repeat)
self._add_action(impose_menu, "Imposition manuelle", None, self._do_manual_impose)
self._add_action(impose_menu, "Joindre deux pages", None, self._do_join_pages)
# Menu Aide
# Pages
pages_menu = menubar.addMenu("Pages")
self._add_action(pages_menu, "Inserer des pages blanches", None, self._do_insert_blank)
# Annotations
annot_menu = menubar.addMenu("Annotations")
self._add_action(annot_menu, "Numerotation de pages", None, self._do_page_numbers)
self._add_action(annot_menu, "Masquage (ruban blanc)", None, self._do_masking)
annot_menu.addSeparator()
self._add_action(annot_menu, "Definir les fonds perdus", None, self._do_bleeds)
self._add_action(annot_menu, "Enlever les fonds perdus", None, self._do_remove_bleeds)
# Aide
help_menu = menubar.addMenu("Aide")
about_act = QAction("A propos...", self)
about_act.triggered.connect(self._show_about)
help_menu.addAction(about_act)
self._add_action(help_menu, "A propos de CopyDev Imposing Tool", None, self._show_about)
def _add_action(self, menu, text, shortcut, callback):
act = QAction(text, self)
if shortcut:
act.setShortcut(shortcut)
act.triggered.connect(callback)
menu.addAction(act)
# ── Toolbar ──
def _setup_toolbar(self):
tb = QToolBar()
tb.setMovable(False)
self.addToolBar(tb)
tb.addAction("Ouvrir", self._open_file)
tb.addAction("Enregistrer", self._save_result)
tb.addSeparator()
tb.addAction("Zoom +", lambda: self._viewer.zoom_in())
tb.addAction("Zoom -", lambda: self._viewer.zoom_out())
tb.addAction("Ajuster", lambda: self._viewer.zoom_fit())
tb.addSeparator()
self._page_label = QLabel(" Aucun fichier ")
self._page_label.setStyleSheet("color: #aaa; padding: 0 10px;")
tb.addWidget(self._page_label)
# ── UI principale ──
def _setup_ui(self):
central = QWidget()
self.setCentralWidget(central)
main_layout = QHBoxLayout(central)
layout = QVBoxLayout(central)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# === Panneau gauche : boutons ===
left_panel = QWidget()
left_panel.setFixedWidth(200)
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(5, 5, 5, 5)
# Splitter : vignettes | viewer
splitter = QSplitter(Qt.Orientation.Horizontal)
# Ouvrir
btn_open = QPushButton("Ouvrir PDF...")
btn_open.clicked.connect(self._open_file)
left_layout.addWidget(btn_open)
# Panneau vignettes
self._page_panel = PagePanel()
self._page_panel.page_selected.connect(self._on_thumbnail_click)
self._page_panel.page_delete_requested.connect(self._on_page_delete)
self._page_panel.page_insert_requested.connect(self._on_page_insert)
self._page_panel.pages_reordered.connect(self._on_pages_reordered)
splitter.addWidget(self._page_panel)
btn_save = QPushButton("Enregistrer resultat...")
btn_save.clicked.connect(self._save_result)
left_layout.addWidget(btn_save)
left_layout.addSpacing(10)
# Imposition
grp_impose = QGroupBox("Imposition")
il = QVBoxLayout(grp_impose)
btn_booklet = QPushButton("Creer un livret")
btn_booklet.clicked.connect(self._do_booklet)
il.addWidget(btn_booklet)
btn_nup = QPushButton("N pages par feuille")
btn_nup.clicked.connect(self._do_nup)
il.addWidget(btn_nup)
btn_repeat = QPushButton("Repetition")
btn_repeat.clicked.connect(self._do_step_repeat)
il.addWidget(btn_repeat)
btn_manual = QPushButton("Imposition manuelle")
btn_manual.clicked.connect(self._do_manual_impose)
il.addWidget(btn_manual)
btn_join = QPushButton("Joindre deux pages")
btn_join.clicked.connect(self._do_join_pages)
il.addWidget(btn_join)
left_layout.addWidget(grp_impose)
# Pages
grp_pages = QGroupBox("Pages")
pl = QVBoxLayout(grp_pages)
btn_blank = QPushButton("Pages blanches")
btn_blank.clicked.connect(self._do_insert_blank)
pl.addWidget(btn_blank)
left_layout.addWidget(grp_pages)
# Annotations
grp_annot = QGroupBox("Annotations")
al = QVBoxLayout(grp_annot)
btn_numbers = QPushButton("Numerotation")
btn_numbers.clicked.connect(self._do_page_numbers)
al.addWidget(btn_numbers)
btn_mask = QPushButton("Masquage")
btn_mask.clicked.connect(self._do_masking)
al.addWidget(btn_mask)
btn_bleeds = QPushButton("Fonds perdus")
btn_bleeds.clicked.connect(self._do_bleeds)
al.addWidget(btn_bleeds)
left_layout.addWidget(grp_annot)
left_layout.addStretch()
main_layout.addWidget(left_panel)
# === Zone centrale : viewer PDF ===
# Viewer PDF
self._viewer = PdfViewer()
self._viewer.page_changed.connect(self._on_page_changed)
main_layout.addWidget(self._viewer, 1)
splitter.addWidget(self._viewer)
splitter.setSizes([160, 900])
splitter.setStretchFactor(0, 0)
splitter.setStretchFactor(1, 1)
layout.addWidget(splitter)
# Barre de progression
self._progress = QProgressBar()
self._progress.setFixedHeight(3)
self._progress.setTextVisible(False)
self._progress.setStyleSheet("""
QProgressBar { background: #0f0f23; border: none; }
QProgressBar::chunk { background: #0066cc; }
""")
self._progress.hide()
layout.addWidget(self._progress)
# ── Statusbar ──
def _setup_statusbar(self):
self._statusbar = QStatusBar()
self.setStatusBar(self._statusbar)
self._status_label = QLabel("Pret")
self._status_label = QLabel("Pret — Glissez un PDF ou utilisez Fichier > Ouvrir")
self._statusbar.addWidget(self._status_label, 1)
self._file_label = QLabel("")
self._statusbar.addPermanentWidget(self._file_label)
brand = QLabel(" CopyDev — contact@copydev.fr ")
brand.setStyleSheet("color: #64748b; font-size: 10px;")
brand.setStyleSheet("color: #555; font-size: 10px;")
self._statusbar.addPermanentWidget(brand)
# ── Callbacks ──
def _on_page_changed(self, page, total):
pass
self._page_label.setText(f" Page {page + 1} / {total} | Zoom {int(self._viewer.zoom_level * 100)}% ")
self._page_panel.select_page(page)
def _on_thumbnail_click(self, index):
self._viewer.go_to_page(index)
def _on_page_delete(self, index):
if not self._current_file:
return
from pypdf import PdfReader, PdfWriter
reader = PdfReader(self._current_file)
if len(reader.pages) <= 1:
QMessageBox.warning(self, "Erreur", "Impossible de supprimer la derniere page.")
return
writer = PdfWriter()
for i, page in enumerate(reader.pages):
if i != index:
writer.add_page(page)
output = self._make_temp()
with open(output, "wb") as f:
writer.write(f)
self._show_result(output)
def _on_page_insert(self, after_index):
if not self._current_file:
return
from pypdf import PdfReader, PdfWriter
from app.engine.pdf_utils import get_page_size
reader = PdfReader(self._current_file)
writer = PdfWriter()
ref_page = reader.pages[min(after_index, len(reader.pages) - 1)]
w, h = get_page_size(ref_page)
for i, page in enumerate(reader.pages):
writer.add_page(page)
if i == after_index:
writer.add_blank_page(w, h)
output = self._make_temp()
with open(output, "wb") as f:
writer.write(f)
self._show_result(output)
def _on_pages_reordered(self, indices):
if not self._current_file:
return
from pypdf import PdfReader, PdfWriter
reader = PdfReader(self._current_file)
writer = PdfWriter()
for idx in indices:
if 0 <= idx < len(reader.pages):
writer.add_page(reader.pages[idx])
output = self._make_temp()
with open(output, "wb") as f:
writer.write(f)
self._current_file = output
self._result_file = output
# ── Fichiers ──
def _open_file(self):
path, _ = QFileDialog.getOpenFileName(
self, "Ouvrir un fichier PDF", "", "PDF (*.pdf)"
)
path, _ = QFileDialog.getOpenFileName(self, "Ouvrir un fichier PDF", "", "PDF (*.pdf)")
if path:
self._current_file = path
self._result_file = None
self._viewer.load_pdf(path)
self._file_label.setText(os.path.basename(path))
self._status_label.setText(f"Fichier charge : {os.path.basename(path)}")
self._open_pdf(path)
def _open_pdf(self, path):
self._current_file = path
self._result_file = None
self._viewer.load_pdf(path)
self._page_panel.load_thumbnails(self._viewer)
self._file_label.setText(os.path.basename(path))
self._status_label.setText(f"Fichier charge : {os.path.basename(path)}")
# Ajuster apres affichage
QTimer.singleShot(100, self._viewer.zoom_fit)
def _save_result(self):
source = self._result_file or self._current_file
if not source:
QMessageBox.warning(self, "Erreur", "Aucun fichier a enregistrer.")
return
path, _ = QFileDialog.getSaveFileName(
self, "Enregistrer le resultat", "", "PDF (*.pdf)"
)
path, _ = QFileDialog.getSaveFileName(self, "Enregistrer le resultat", "", "PDF (*.pdf)")
if path:
if not path.lower().endswith(".pdf"):
path += ".pdf"
import shutil
shutil.copy2(source, path)
self._status_label.setText(f"Enregistre : {os.path.basename(path)}")
@@ -216,22 +306,40 @@ class MainWindow(QMainWindow):
os.close(fd)
return path
def _show_progress(self):
self._progress.setRange(0, 0)
self._progress.show()
self._status_label.setText("Traitement en cours...")
QApplication.processEvents()
def _hide_progress(self):
self._progress.hide()
self._progress.setRange(0, 1)
def _show_result(self, output_path):
self._result_file = output_path
self._current_file = output_path
self._viewer.load_pdf(output_path)
self._page_panel.load_thumbnails(self._viewer)
self._hide_progress()
self._status_label.setText("Traitement termine")
QTimer.singleShot(100, self._viewer.zoom_fit)
# ── Actions imposition ──
def _do_booklet(self):
if not self._require_file():
return
dlg = BookletDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
create_booklet(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_nup(self):
@@ -239,12 +347,14 @@ class MainWindow(QMainWindow):
return
dlg = NupDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
nup_pages(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_step_repeat(self):
@@ -252,12 +362,14 @@ class MainWindow(QMainWindow):
return
dlg = StepRepeatDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
step_and_repeat(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_manual_impose(self):
@@ -267,12 +379,14 @@ class MainWindow(QMainWindow):
reader = PdfReader(self._current_file)
dlg = ManualImposeDialog(total_pages=len(reader.pages), parent=self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
manual_impose(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_join_pages(self):
@@ -280,12 +394,14 @@ class MainWindow(QMainWindow):
return
dlg = JoinPagesDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
join_pages(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_insert_blank(self):
@@ -295,12 +411,14 @@ class MainWindow(QMainWindow):
reader = PdfReader(self._current_file)
dlg = InsertBlankDialog(total_pages=len(reader.pages), parent=self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
insert_blank_pages(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_page_numbers(self):
@@ -308,12 +426,14 @@ class MainWindow(QMainWindow):
return
dlg = PageNumbersDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
add_page_numbers(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_masking(self):
@@ -321,12 +441,14 @@ class MainWindow(QMainWindow):
return
dlg = MaskingDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
apply_masking(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_bleeds(self):
@@ -334,30 +456,75 @@ class MainWindow(QMainWindow):
return
dlg = BleedsDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
define_bleeds(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_remove_bleeds(self):
if not self._require_file():
return
from pypdf import PdfReader, PdfWriter
self._show_progress()
try:
reader = PdfReader(self._current_file)
writer = PdfWriter()
for page in reader.pages:
if hasattr(page, 'trimbox'):
page.trimbox = page.mediabox
if hasattr(page, 'bleedbox'):
page.bleedbox = page.mediabox
writer.add_page(page)
output = self._make_temp()
with open(output, "wb") as f:
writer.write(f)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_merge(self):
paths, _ = QFileDialog.getOpenFileNames(self, "Selectionner les PDF a fusionner", "", "PDF (*.pdf)")
if not paths or len(paths) < 2:
if paths and len(paths) == 1:
QMessageBox.information(self, "Fusion", "Selectionnez au moins 2 fichiers PDF.")
return
from pypdf import PdfWriter
self._show_progress()
try:
writer = PdfWriter()
for path in paths:
writer.append(path)
output = self._make_temp()
with open(output, "wb") as f:
writer.write(f)
self._show_result(output)
self._status_label.setText(f"Fusion de {len(paths)} fichiers terminee")
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
# ── A propos ──
def _show_about(self):
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QDialogButtonBox
from PyQt6.QtGui import QPixmap, QFont
from PyQt6.QtCore import Qt
dlg = QDialog(self)
dlg.setWindowTitle("A propos — CopyDev Imposing Tool")
dlg.setFixedSize(520, 520)
dlg.setFixedSize(520, 480)
dlg.setStyleSheet("QDialog { background-color: #0f172a; }")
layout = QVBoxLayout(dlg)
layout.setContentsMargins(40, 30, 40, 25)
# Logo
logo_label = QLabel()
import sys as _sys
_base = _sys._MEIPASS if getattr(_sys, 'frozen', False) else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_base = sys._MEIPASS if getattr(sys, 'frozen', False) else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
logo_path = os.path.join(_base, "resources", "logo.png")
if os.path.exists(logo_path):
pixmap = QPixmap(logo_path).scaled(150, 150, Qt.AspectRatioMode.KeepAspectRatio,
@@ -368,14 +535,12 @@ class MainWindow(QMainWindow):
layout.addSpacing(10)
# Titre
title = QLabel("COPYDEV IMPOSING TOOL")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setFont(QFont("Segoe UI", 20, QFont.Weight.Bold))
title.setStyleSheet("color: white;")
layout.addWidget(title)
# Version
from app.updater import VERSION
version = QLabel(f"Version {VERSION}")
version.setAlignment(Qt.AlignmentFlag.AlignCenter)
@@ -385,7 +550,6 @@ class MainWindow(QMainWindow):
layout.addSpacing(15)
# Description
desc = QLabel(
"Outil professionnel d'imposition PDF\n"
"Livrets, n-up, repetition, imposition manuelle,\n"
@@ -399,7 +563,6 @@ class MainWindow(QMainWindow):
layout.addSpacing(20)
# Separateur
sep = QLabel()
sep.setFixedHeight(1)
sep.setStyleSheet("background-color: #1e293b;")
@@ -407,7 +570,6 @@ class MainWindow(QMainWindow):
layout.addSpacing(15)
# Coordonnees
contact = QLabel(
'<p style="text-align:center; line-height: 1.6;">'
'<span style="font-size:14px; font-weight:bold; color:white;">CopyDev</span><br>'
@@ -415,9 +577,7 @@ class MainWindow(QMainWindow):
'<span style="color:#94a3b8;">Email : </span>'
'<a href="mailto:contact@copydev.fr" style="color:#38bdf8;">contact@copydev.fr</a><br>'
'<span style="color:#94a3b8;">Web : </span>'
'<a href="https://copydev.fr" style="color:#38bdf8;">www.copydev.fr</a><br>'
'<span style="color:#94a3b8;">Tel : </span>'
'<span style="color:#e2e8f0;">06 52 07 92 35</span>'
'<a href="https://copydev.fr" style="color:#38bdf8;">www.copydev.fr</a>'
'</p>'
)
contact.setAlignment(Qt.AlignmentFlag.AlignCenter)
@@ -426,14 +586,12 @@ class MainWindow(QMainWindow):
layout.addStretch()
# Copyright
copy_label = QLabel("2026 CopyDev — Tous droits reserves\nLogiciel sous licence — Reproduction interdite")
copy_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
copy_label.setFont(QFont("Segoe UI", 8))
copy_label.setStyleSheet("color: #334155;")
layout.addWidget(copy_label)
# Bouton Fermer
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
buttons.setStyleSheet("QPushButton { background: #1e293b; color: white; border: 1px solid #334155; padding: 6px 20px; border-radius: 4px; } QPushButton:hover { background: #334155; }")
buttons.rejected.connect(dlg.close)

109
app/page_panel.py Normal file
View File

@@ -0,0 +1,109 @@
"""Panneau lateral de vignettes des pages — V2."""
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
QAbstractItemView, QMenu, QSizePolicy)
from PyQt6.QtGui import QIcon, QPixmap
from PyQt6.QtCore import Qt, pyqtSignal, QSize
class PagePanel(QWidget):
"""Panneau de vignettes avec drag & drop, suppression, insertion."""
page_selected = pyqtSignal(int) # page index cliquee
pages_reordered = pyqtSignal(list) # nouvelle liste d'indices
page_delete_requested = pyqtSignal(int) # demande suppression
page_insert_requested = pyqtSignal(int) # demande insertion page blanche apres
def __init__(self, parent=None):
super().__init__(parent)
self.setMinimumWidth(100)
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
self._list = QListWidget()
self._list.setIconSize(QSize(120, 170))
self._list.setSpacing(4)
self._list.setStyleSheet("""
QListWidget {
background: #252525;
border: none;
padding: 4px;
}
QListWidget::item {
background: #333;
border: 1px solid #444;
border-radius: 4px;
padding: 4px;
color: #ccc;
font-size: 10px;
}
QListWidget::item:selected {
background: #0066cc;
border: 1px solid #0088ff;
color: white;
}
""")
self._list.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
self._list.setDefaultDropAction(Qt.DropAction.MoveAction)
self._list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self._list.customContextMenuRequested.connect(self._context_menu)
self._list.currentRowChanged.connect(self._on_row_changed)
self._list.model().rowsMoved.connect(self._on_rows_moved)
layout.addWidget(self._list)
def load_thumbnails(self, viewer):
"""Charger les miniatures depuis le viewer."""
self._list.clear()
for i in range(viewer.total_pages):
thumb = viewer.get_page_thumbnail(i, 120)
item = QListWidgetItem()
item.setText(f" Page {i + 1}")
item.setData(Qt.ItemDataRole.UserRole, i)
if thumb:
item.setIcon(QIcon(thumb))
self._list.addItem(item)
def select_page(self, index):
"""Selectionner une page sans emettre le signal."""
self._list.blockSignals(True)
self._list.setCurrentRow(index)
self._list.blockSignals(False)
def _on_row_changed(self, row):
if row >= 0:
self.page_selected.emit(row)
def _on_rows_moved(self):
indices = []
for i in range(self._list.count()):
item = self._list.item(i)
original_index = item.data(Qt.ItemDataRole.UserRole)
indices.append(original_index)
self.pages_reordered.emit(indices)
def _context_menu(self, pos):
item = self._list.itemAt(pos)
if not item:
return
row = self._list.row(item)
menu = QMenu(self)
menu.setStyleSheet("""
QMenu { background: #333; color: #eee; border: 1px solid #555; }
QMenu::item:selected { background: #0066cc; }
""")
act_insert = menu.addAction("Inserer une page blanche apres")
act_delete = menu.addAction("Supprimer cette page")
action = menu.exec(self._list.mapToGlobal(pos))
if action == act_delete:
self.page_delete_requested.emit(row)
elif action == act_insert:
self.page_insert_requested.emit(row)

View File

@@ -1,20 +1,21 @@
"""Widget de visualisation PDF avec pypdfium2."""
"""Widget de visualisation PDF avec pypdfium2 — V2."""
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QSpinBox, QScrollArea, QSizePolicy)
from PyQt6.QtGui import QImage, QPixmap
from PyQt6.QtCore import Qt, pyqtSignal
QScrollArea, QSizePolicy, QApplication)
from PyQt6.QtGui import QImage, QPixmap, QWheelEvent
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
import pypdfium2 as pdfium
class PdfViewer(QWidget):
"""Widget d'apercu PDF avec navigation et zoom."""
"""Widget d'apercu PDF avec zoom molette et navigation fluide."""
page_changed = pyqtSignal(int, int) # page_num, total_pages
page_changed = pyqtSignal(int, int) # page_num (0-based), total_pages
def __init__(self, parent=None):
super().__init__(parent)
self._pdf = None
self._pdf_path = None
self._current_page = 0
self._total_pages = 0
self._zoom = 1.0
@@ -23,73 +24,48 @@ class PdfViewer(QWidget):
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Zone d'affichage avec scroll
self._scroll = QScrollArea()
self._scroll.setWidgetResizable(True)
self._scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._scroll.setStyleSheet("QScrollArea { background: #1e1e1e; border: none; }")
self._image_label = QLabel()
self._image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._image_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
self._image_label.setStyleSheet("background: #1e1e1e;")
self._scroll.setWidget(self._image_label)
# Intercepter la molette sur le scroll area
self._scroll.wheelEvent = self._on_wheel
layout.addWidget(self._scroll, 1)
# Barre de navigation
nav = QHBoxLayout()
self._btn_prev = QPushButton("<")
self._btn_prev.setFixedWidth(40)
self._btn_prev.clicked.connect(self._prev_page)
nav.addWidget(self._btn_prev)
self._page_spin = QSpinBox()
self._page_spin.setMinimum(1)
self._page_spin.setMaximum(1)
self._page_spin.valueChanged.connect(self._go_to_page)
nav.addWidget(self._page_spin)
self._label_total = QLabel("/ 0")
nav.addWidget(self._label_total)
self._btn_next = QPushButton(">")
self._btn_next.setFixedWidth(40)
self._btn_next.clicked.connect(self._next_page)
nav.addWidget(self._btn_next)
nav.addSpacing(20)
self._btn_zoom_out = QPushButton("-")
self._btn_zoom_out.setFixedWidth(30)
self._btn_zoom_out.clicked.connect(self._zoom_out)
nav.addWidget(self._btn_zoom_out)
self._label_zoom = QLabel("100%")
self._label_zoom.setFixedWidth(45)
self._label_zoom.setAlignment(Qt.AlignmentFlag.AlignCenter)
nav.addWidget(self._label_zoom)
self._btn_zoom_in = QPushButton("+")
self._btn_zoom_in.setFixedWidth(30)
self._btn_zoom_in.clicked.connect(self._zoom_in)
nav.addWidget(self._btn_zoom_in)
self._btn_fit = QPushButton("Ajuster")
self._btn_fit.clicked.connect(self._zoom_fit)
nav.addWidget(self._btn_fit)
nav.addStretch()
layout.addLayout(nav)
def _on_wheel(self, event: QWheelEvent):
"""Molette = pages, Ctrl+Molette = zoom."""
if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
delta = event.angleDelta().y()
if delta > 0:
self._zoom = min(self._zoom * 1.15, 5.0)
else:
self._zoom = max(self._zoom / 1.15, 0.2)
self._render_page()
else:
delta = event.angleDelta().y()
if delta < 0:
self.next_page()
elif delta > 0:
self.prev_page()
def load_pdf(self, path):
"""Charger un fichier PDF."""
try:
self._pdf_path = path
self._pdf = pdfium.PdfDocument(path)
self._total_pages = len(self._pdf)
self._current_page = 0
self._page_spin.setMaximum(self._total_pages)
self._page_spin.setValue(1)
self._label_total.setText(f"/ {self._total_pages}")
self._zoom_fit()
self._render_page()
except Exception as e:
self._image_label.setText(f"Erreur: {e}")
@@ -100,9 +76,7 @@ class PdfViewer(QWidget):
self._pdf = pdfium.PdfDocument(data)
self._total_pages = len(self._pdf)
self._current_page = 0
self._page_spin.setMaximum(self._total_pages)
self._page_spin.setValue(1)
self._label_total.setText(f"/ {self._total_pages}")
self._zoom_fit()
self._render_page()
except Exception as e:
self._image_label.setText(f"Erreur: {e}")
@@ -113,44 +87,17 @@ class PdfViewer(QWidget):
return
page = self._pdf[self._current_page]
# Rendu a 150 DPI * zoom
scale = 150 / 72 * self._zoom
bitmap = page.render(scale=scale)
pil_image = bitmap.to_pil()
# Convertir PIL -> QImage -> QPixmap
data = pil_image.tobytes("raw", "RGBA")
qimage = QImage(data, pil_image.width, pil_image.height, QImage.Format.Format_RGBA8888)
pixmap = QPixmap.fromImage(qimage)
self._image_label.setPixmap(pixmap)
self._image_label.adjustSize()
self._label_zoom.setText(f"{int(self._zoom * 100)}%")
self.page_changed.emit(self._current_page + 1, self._total_pages)
def _prev_page(self):
if self._current_page > 0:
self._current_page -= 1
self._page_spin.setValue(self._current_page + 1)
def _next_page(self):
if self._current_page < self._total_pages - 1:
self._current_page += 1
self._page_spin.setValue(self._current_page + 1)
def _go_to_page(self, value):
page = value - 1
if 0 <= page < self._total_pages and page != self._current_page:
self._current_page = page
self._render_page()
def _zoom_in(self):
self._zoom = min(self._zoom * 1.25, 5.0)
self._render_page()
def _zoom_out(self):
self._zoom = max(self._zoom / 1.25, 0.25)
self._render_page()
self.page_changed.emit(self._current_page, self._total_pages)
def _zoom_fit(self):
"""Ajuster le zoom pour que la page tienne dans la zone visible."""
@@ -160,15 +107,56 @@ class PdfViewer(QWidget):
pw, ph = page.get_size()
vw = self._scroll.viewport().width() - 20
vh = self._scroll.viewport().height() - 20
if vw <= 0 or vh <= 0:
self._zoom = 1.0
return
scale_x = vw / pw
scale_y = vh / ph
self._zoom = min(scale_x, scale_y) * (72 / 150)
def prev_page(self):
if self._current_page > 0:
self._current_page -= 1
self._render_page()
def next_page(self):
if self._current_page < self._total_pages - 1:
self._current_page += 1
self._render_page()
def go_to_page(self, page_index):
if 0 <= page_index < self._total_pages and page_index != self._current_page:
self._current_page = page_index
self._render_page()
def zoom_in(self):
self._zoom = min(self._zoom * 1.25, 5.0)
self._render_page()
def zoom_out(self):
self._zoom = max(self._zoom / 1.25, 0.2)
self._render_page()
def zoom_fit(self):
self._zoom_fit()
self._render_page()
def refresh(self):
"""Rafraichir l'affichage."""
self._render_page()
def get_page_thumbnail(self, page_index, width=120):
"""Generer une miniature pour le panneau de vignettes."""
if not self._pdf or page_index >= self._total_pages:
return None
page = self._pdf[page_index]
pw, ph = page.get_size()
scale = width / pw
bitmap = page.render(scale=scale)
pil_image = bitmap.to_pil()
data = pil_image.tobytes("raw", "RGBA")
qimage = QImage(data, pil_image.width, pil_image.height, QImage.Format.Format_RGBA8888)
return QPixmap.fromImage(qimage)
@property
def current_page(self):
return self._current_page
@@ -176,3 +164,11 @@ class PdfViewer(QWidget):
@property
def total_pages(self):
return self._total_pages
@property
def zoom_level(self):
return self._zoom
@property
def pdf_path(self):
return self._pdf_path

View File

@@ -14,7 +14,7 @@ REPO = "jules/copydev-imposing-tool"
BRANCH = "master"
# Version actuelle (incrementee a chaque release)
VERSION = "1.0.0"
VERSION = "2.0.0"
def get_remote_version():