diff --git a/app/main_window.py b/app/main_window.py index e18c294..466c226 100644 --- a/app/main_window.py +++ b/app/main_window.py @@ -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( '

' 'CopyDev
' @@ -415,9 +577,7 @@ class MainWindow(QMainWindow): 'Email : ' 'contact@copydev.fr
' 'Web : ' - 'www.copydev.fr
' - 'Tel : ' - '06 52 07 92 35' + 'www.copydev.fr' '

' ) 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) diff --git a/app/page_panel.py b/app/page_panel.py new file mode 100644 index 0000000..e575848 --- /dev/null +++ b/app/page_panel.py @@ -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) diff --git a/app/pdf_viewer.py b/app/pdf_viewer.py index 6fb1f6d..f32cfb4 100644 --- a/app/pdf_viewer.py +++ b/app/pdf_viewer.py @@ -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 diff --git a/app/updater.py b/app/updater.py index d26b025..258bf92 100644 --- a/app/updater.py +++ b/app/updater.py @@ -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(): diff --git a/version.txt b/version.txt index 3eefcb9..227cea2 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.0.0 +2.0.0