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'
'