592 lines
21 KiB
Python
592 lines
21 KiB
Python
"""
|
||
File operations mixin for pyPhotoAlbum
|
||
"""
|
||
|
||
from PyQt6.QtWidgets import QFileDialog, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QDoubleSpinBox, QSpinBox, QPushButton, QGroupBox, QRadioButton, QButtonGroup
|
||
from pyPhotoAlbum.decorators import ribbon_action, numerical_input
|
||
from pyPhotoAlbum.project import Project
|
||
|
||
|
||
class FileOperationsMixin:
|
||
"""Mixin providing file-related operations"""
|
||
|
||
@ribbon_action(
|
||
label="New",
|
||
tooltip="Create a new project",
|
||
tab="Home",
|
||
group="File",
|
||
shortcut="Ctrl+N"
|
||
)
|
||
def new_project(self):
|
||
"""Create a new project with initial setup dialog"""
|
||
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QDoubleSpinBox, QSpinBox, QPushButton, QGroupBox, QLineEdit
|
||
|
||
# Create new project setup dialog
|
||
dialog = QDialog(self)
|
||
dialog.setWindowTitle("New Project Setup")
|
||
dialog.setMinimumWidth(450)
|
||
|
||
layout = QVBoxLayout()
|
||
|
||
# Project name group
|
||
name_group = QGroupBox("Project Name")
|
||
name_layout = QVBoxLayout()
|
||
name_input = QLineEdit()
|
||
name_input.setText("New Project")
|
||
name_input.selectAll()
|
||
name_layout.addWidget(name_input)
|
||
name_group.setLayout(name_layout)
|
||
layout.addWidget(name_group)
|
||
|
||
# Default page size group
|
||
size_group = QGroupBox("Default Page Size")
|
||
size_layout = QVBoxLayout()
|
||
|
||
info_label = QLabel("This will be the default size for all new pages in this project.")
|
||
info_label.setWordWrap(True)
|
||
info_label.setStyleSheet("font-size: 9pt; color: gray;")
|
||
size_layout.addWidget(info_label)
|
||
|
||
# Width
|
||
width_layout = QHBoxLayout()
|
||
width_layout.addWidget(QLabel("Width:"))
|
||
width_spinbox = QDoubleSpinBox()
|
||
width_spinbox.setRange(10, 1000)
|
||
width_spinbox.setValue(140) # Default 14cm
|
||
width_spinbox.setSuffix(" mm")
|
||
width_layout.addWidget(width_spinbox)
|
||
size_layout.addLayout(width_layout)
|
||
|
||
# Height
|
||
height_layout = QHBoxLayout()
|
||
height_layout.addWidget(QLabel("Height:"))
|
||
height_spinbox = QDoubleSpinBox()
|
||
height_spinbox.setRange(10, 1000)
|
||
height_spinbox.setValue(140) # Default 14cm
|
||
height_spinbox.setSuffix(" mm")
|
||
height_layout.addWidget(height_spinbox)
|
||
size_layout.addLayout(height_layout)
|
||
|
||
# Add common size presets
|
||
presets_layout = QHBoxLayout()
|
||
presets_layout.addWidget(QLabel("Presets:"))
|
||
|
||
def set_preset(w, h):
|
||
width_spinbox.setValue(w)
|
||
height_spinbox.setValue(h)
|
||
|
||
preset_a4 = QPushButton("A4 (210×297)")
|
||
preset_a4.clicked.connect(lambda: set_preset(210, 297))
|
||
presets_layout.addWidget(preset_a4)
|
||
|
||
preset_a5 = QPushButton("A5 (148×210)")
|
||
preset_a5.clicked.connect(lambda: set_preset(148, 210))
|
||
presets_layout.addWidget(preset_a5)
|
||
|
||
preset_square = QPushButton("Square (200×200)")
|
||
preset_square.clicked.connect(lambda: set_preset(200, 200))
|
||
presets_layout.addWidget(preset_square)
|
||
|
||
presets_layout.addStretch()
|
||
size_layout.addLayout(presets_layout)
|
||
|
||
size_group.setLayout(size_layout)
|
||
layout.addWidget(size_group)
|
||
|
||
# DPI settings group
|
||
dpi_group = QGroupBox("DPI Settings")
|
||
dpi_layout = QVBoxLayout()
|
||
|
||
# Working DPI
|
||
working_dpi_layout = QHBoxLayout()
|
||
working_dpi_layout.addWidget(QLabel("Working DPI:"))
|
||
working_dpi_spinbox = QSpinBox()
|
||
working_dpi_spinbox.setRange(72, 1200)
|
||
working_dpi_spinbox.setValue(300)
|
||
working_dpi_layout.addWidget(working_dpi_spinbox)
|
||
dpi_layout.addLayout(working_dpi_layout)
|
||
|
||
# Export DPI
|
||
export_dpi_layout = QHBoxLayout()
|
||
export_dpi_layout.addWidget(QLabel("Export DPI:"))
|
||
export_dpi_spinbox = QSpinBox()
|
||
export_dpi_spinbox.setRange(72, 1200)
|
||
export_dpi_spinbox.setValue(300)
|
||
export_dpi_layout.addWidget(export_dpi_spinbox)
|
||
dpi_layout.addLayout(export_dpi_layout)
|
||
|
||
dpi_group.setLayout(dpi_layout)
|
||
layout.addWidget(dpi_group)
|
||
|
||
# Buttons
|
||
button_layout = QHBoxLayout()
|
||
cancel_btn = QPushButton("Cancel")
|
||
cancel_btn.clicked.connect(dialog.reject)
|
||
create_btn = QPushButton("Create Project")
|
||
create_btn.clicked.connect(dialog.accept)
|
||
create_btn.setDefault(True)
|
||
|
||
button_layout.addStretch()
|
||
button_layout.addWidget(cancel_btn)
|
||
button_layout.addWidget(create_btn)
|
||
layout.addLayout(button_layout)
|
||
|
||
dialog.setLayout(layout)
|
||
|
||
# Show dialog
|
||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||
# Get values
|
||
project_name = name_input.text().strip() or "New Project"
|
||
width_mm = width_spinbox.value()
|
||
height_mm = height_spinbox.value()
|
||
working_dpi = working_dpi_spinbox.value()
|
||
export_dpi = export_dpi_spinbox.value()
|
||
|
||
# Create project with custom settings
|
||
self.project = Project(project_name)
|
||
self.project.page_size_mm = (width_mm, height_mm)
|
||
self.project.working_dpi = working_dpi
|
||
self.project.export_dpi = export_dpi
|
||
|
||
# Set asset resolution context
|
||
from pyPhotoAlbum.models import set_asset_resolution_context
|
||
set_asset_resolution_context(self.project.folder_path)
|
||
|
||
# Update view
|
||
self.update_view()
|
||
|
||
self.show_status(f"New project created: {project_name} ({width_mm}×{height_mm} mm)")
|
||
print(f"New project created: {project_name}, default page size: {width_mm}×{height_mm} mm")
|
||
else:
|
||
# User cancelled - keep current project
|
||
print("New project creation cancelled")
|
||
|
||
@ribbon_action(
|
||
label="Open",
|
||
tooltip="Open an existing project",
|
||
tab="Home",
|
||
group="File",
|
||
shortcut="Ctrl+O"
|
||
)
|
||
def open_project(self):
|
||
"""Open an existing project"""
|
||
from pyPhotoAlbum.project_serializer import load_from_zip
|
||
|
||
file_path, _ = QFileDialog.getOpenFileName(
|
||
self,
|
||
"Open Project",
|
||
"",
|
||
"pyPhotoAlbum Projects (*.ppz);;All Files (*)"
|
||
)
|
||
|
||
if file_path:
|
||
print(f"Opening project: {file_path}")
|
||
|
||
# Load project from ZIP
|
||
project, error = load_from_zip(file_path)
|
||
|
||
if project:
|
||
self.project = project
|
||
self.current_page_index = 0 # Reset to first page
|
||
self.update_view()
|
||
self.show_status(f"Project opened: {file_path}")
|
||
print(f"Successfully loaded project: {project.name}")
|
||
else:
|
||
error_msg = f"Failed to open project: {error}"
|
||
self.show_status(error_msg)
|
||
print(error_msg)
|
||
|
||
@ribbon_action(
|
||
label="Save",
|
||
tooltip="Save the current project",
|
||
tab="Home",
|
||
group="File",
|
||
shortcut="Ctrl+S"
|
||
)
|
||
def save_project(self):
|
||
"""Save the current project"""
|
||
from pyPhotoAlbum.project_serializer import save_to_zip
|
||
|
||
file_path, _ = QFileDialog.getSaveFileName(
|
||
self,
|
||
"Save Project",
|
||
"",
|
||
"pyPhotoAlbum Projects (*.ppz);;All Files (*)"
|
||
)
|
||
|
||
if file_path:
|
||
print(f"Saving project to: {file_path}")
|
||
|
||
# Save project to ZIP
|
||
success, error = save_to_zip(self.project, file_path)
|
||
|
||
if success:
|
||
self.show_status(f"Project saved: {file_path}")
|
||
print(f"Successfully saved project to: {file_path}")
|
||
else:
|
||
error_msg = f"Failed to save project: {error}"
|
||
self.show_status(error_msg)
|
||
print(error_msg)
|
||
|
||
@ribbon_action(
|
||
label="Heal Assets",
|
||
tooltip="Reconnect missing image assets",
|
||
tab="Home",
|
||
group="File"
|
||
)
|
||
def heal_assets(self):
|
||
"""Open the asset healing dialog to reconnect missing images"""
|
||
from pyPhotoAlbum.asset_heal_dialog import AssetHealDialog
|
||
|
||
dialog = AssetHealDialog(self.project, self)
|
||
dialog.exec()
|
||
|
||
# Update the view to reflect any changes
|
||
self.update_view()
|
||
|
||
@ribbon_action(
|
||
label="Project Settings",
|
||
tooltip="Configure project-wide page size and defaults",
|
||
tab="Home",
|
||
group="File"
|
||
)
|
||
@numerical_input(
|
||
fields=[
|
||
('width', 'Width', 'mm', 10, 1000),
|
||
('height', 'Height', 'mm', 10, 1000)
|
||
]
|
||
)
|
||
def project_settings(self):
|
||
"""Configure project-wide settings including default page size"""
|
||
# Create dialog
|
||
dialog = QDialog(self)
|
||
dialog.setWindowTitle("Project Settings")
|
||
dialog.setMinimumWidth(500)
|
||
|
||
layout = QVBoxLayout()
|
||
|
||
# Page size group
|
||
size_group = QGroupBox("Default Page Size")
|
||
size_layout = QVBoxLayout()
|
||
|
||
# Width
|
||
width_layout = QHBoxLayout()
|
||
width_layout.addWidget(QLabel("Width:"))
|
||
width_spinbox = QDoubleSpinBox()
|
||
width_spinbox.setRange(10, 1000)
|
||
width_spinbox.setValue(self.project.page_size_mm[0])
|
||
width_spinbox.setSuffix(" mm")
|
||
width_layout.addWidget(width_spinbox)
|
||
size_layout.addLayout(width_layout)
|
||
|
||
# Height
|
||
height_layout = QHBoxLayout()
|
||
height_layout.addWidget(QLabel("Height:"))
|
||
height_spinbox = QDoubleSpinBox()
|
||
height_spinbox.setRange(10, 1000)
|
||
height_spinbox.setValue(self.project.page_size_mm[1])
|
||
height_spinbox.setSuffix(" mm")
|
||
height_layout.addWidget(height_spinbox)
|
||
size_layout.addLayout(height_layout)
|
||
|
||
size_group.setLayout(size_layout)
|
||
layout.addWidget(size_group)
|
||
|
||
# DPI settings group
|
||
dpi_group = QGroupBox("DPI Settings")
|
||
dpi_layout = QVBoxLayout()
|
||
|
||
# Working DPI
|
||
working_dpi_layout = QHBoxLayout()
|
||
working_dpi_layout.addWidget(QLabel("Working DPI:"))
|
||
working_dpi_spinbox = QSpinBox()
|
||
working_dpi_spinbox.setRange(72, 1200)
|
||
working_dpi_spinbox.setValue(self.project.working_dpi)
|
||
working_dpi_layout.addWidget(working_dpi_spinbox)
|
||
dpi_layout.addLayout(working_dpi_layout)
|
||
|
||
# Export DPI
|
||
export_dpi_layout = QHBoxLayout()
|
||
export_dpi_layout.addWidget(QLabel("Export DPI:"))
|
||
export_dpi_spinbox = QSpinBox()
|
||
export_dpi_spinbox.setRange(72, 1200)
|
||
export_dpi_spinbox.setValue(self.project.export_dpi)
|
||
export_dpi_layout.addWidget(export_dpi_spinbox)
|
||
dpi_layout.addLayout(export_dpi_layout)
|
||
|
||
dpi_group.setLayout(dpi_layout)
|
||
layout.addWidget(dpi_group)
|
||
|
||
# Content scaling options (only if pages exist and size is changing)
|
||
scaling_group = None
|
||
scaling_buttons = None
|
||
|
||
if self.project.pages:
|
||
scaling_group = QGroupBox("Apply to Existing Pages")
|
||
scaling_layout = QVBoxLayout()
|
||
|
||
info_label = QLabel("How should existing content be adjusted?\n(Pages with manual sizing will not be affected)")
|
||
info_label.setWordWrap(True)
|
||
scaling_layout.addWidget(info_label)
|
||
|
||
scaling_buttons = QButtonGroup()
|
||
|
||
proportional_radio = QRadioButton("Resize proportionally (fit to smallest axis)")
|
||
proportional_radio.setToolTip("Scale content uniformly to fit the new page size")
|
||
scaling_buttons.addButton(proportional_radio, 0)
|
||
scaling_layout.addWidget(proportional_radio)
|
||
|
||
stretch_radio = QRadioButton("Resize on both axes (stretch)")
|
||
stretch_radio.setToolTip("Scale width and height independently")
|
||
scaling_buttons.addButton(stretch_radio, 1)
|
||
scaling_layout.addWidget(stretch_radio)
|
||
|
||
reposition_radio = QRadioButton("Keep content size, reposition to center")
|
||
reposition_radio.setToolTip("Maintain element sizes but center them on new page")
|
||
scaling_buttons.addButton(reposition_radio, 2)
|
||
scaling_layout.addWidget(reposition_radio)
|
||
|
||
none_radio = QRadioButton("Don't adjust content (page size only)")
|
||
none_radio.setToolTip("Only change page size, leave content as-is")
|
||
none_radio.setChecked(True) # Default
|
||
scaling_buttons.addButton(none_radio, 3)
|
||
scaling_layout.addWidget(none_radio)
|
||
|
||
scaling_group.setLayout(scaling_layout)
|
||
layout.addWidget(scaling_group)
|
||
|
||
# Buttons
|
||
button_layout = QHBoxLayout()
|
||
cancel_btn = QPushButton("Cancel")
|
||
cancel_btn.clicked.connect(dialog.reject)
|
||
ok_btn = QPushButton("OK")
|
||
ok_btn.clicked.connect(dialog.accept)
|
||
ok_btn.setDefault(True)
|
||
|
||
button_layout.addStretch()
|
||
button_layout.addWidget(cancel_btn)
|
||
button_layout.addWidget(ok_btn)
|
||
layout.addLayout(button_layout)
|
||
|
||
dialog.setLayout(layout)
|
||
|
||
# Show dialog
|
||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||
# Get new values
|
||
new_width = width_spinbox.value()
|
||
new_height = height_spinbox.value()
|
||
new_working_dpi = working_dpi_spinbox.value()
|
||
new_export_dpi = export_dpi_spinbox.value()
|
||
|
||
# Determine scaling mode
|
||
scaling_mode = 'none'
|
||
if scaling_buttons:
|
||
selected_id = scaling_buttons.checkedId()
|
||
modes = {0: 'proportional', 1: 'stretch', 2: 'reposition', 3: 'none'}
|
||
scaling_mode = modes.get(selected_id, 'none')
|
||
|
||
# Apply settings
|
||
old_size = self.project.page_size_mm
|
||
self.project.page_size_mm = (new_width, new_height)
|
||
self.project.working_dpi = new_working_dpi
|
||
self.project.export_dpi = new_export_dpi
|
||
|
||
# Update existing pages (exclude manually sized ones)
|
||
if self.project.pages and old_size != (new_width, new_height):
|
||
self._apply_page_size_to_project(old_size, (new_width, new_height), scaling_mode)
|
||
|
||
self.update_view()
|
||
self.show_status(f"Project settings updated: {new_width}×{new_height} mm", 2000)
|
||
print(f"Project settings updated: {new_width}×{new_height} mm, scaling mode: {scaling_mode}")
|
||
|
||
def _apply_page_size_to_project(self, old_size, new_size, scaling_mode):
|
||
"""
|
||
Apply new page size to all non-manually-sized pages
|
||
|
||
Args:
|
||
old_size: Old page size (width, height) in mm
|
||
new_size: New page size (width, height) in mm
|
||
scaling_mode: 'proportional', 'stretch', 'reposition', or 'none'
|
||
"""
|
||
old_width, old_height = old_size
|
||
new_width, new_height = new_size
|
||
|
||
width_ratio = new_width / old_width if old_width > 0 else 1.0
|
||
height_ratio = new_height / old_height if old_height > 0 else 1.0
|
||
|
||
for page in self.project.pages:
|
||
# Skip manually sized pages
|
||
if page.manually_sized:
|
||
continue
|
||
|
||
# Update page size
|
||
old_page_width, old_page_height = page.layout.size
|
||
|
||
# For double spreads, maintain the 2x multiplier
|
||
if page.is_double_spread:
|
||
page.layout.size = (new_width * 2, new_height)
|
||
else:
|
||
page.layout.size = (new_width, new_height)
|
||
|
||
# Apply content scaling based on mode
|
||
if scaling_mode == 'proportional':
|
||
# Use smallest ratio to fit content
|
||
scale = min(width_ratio, height_ratio)
|
||
self._scale_page_elements(page, scale, scale)
|
||
elif scaling_mode == 'stretch':
|
||
# Scale independently on each axis
|
||
self._scale_page_elements(page, width_ratio, height_ratio)
|
||
elif scaling_mode == 'reposition':
|
||
# Keep size, center content
|
||
self._reposition_page_elements(page, old_size, new_size)
|
||
# 'none' - do nothing to elements
|
||
|
||
def _scale_page_elements(self, page, x_scale, y_scale):
|
||
"""
|
||
Scale all elements on a page
|
||
|
||
Args:
|
||
page: Page object
|
||
x_scale: Horizontal scale factor
|
||
y_scale: Vertical scale factor
|
||
"""
|
||
for element in page.layout.elements:
|
||
# Scale position
|
||
x, y = element.position
|
||
element.position = (x * x_scale, y * y_scale)
|
||
|
||
# Scale size
|
||
width, height = element.size
|
||
element.size = (width * x_scale, height * y_scale)
|
||
|
||
def _reposition_page_elements(self, page, old_size, new_size):
|
||
"""
|
||
Reposition elements to center them on the new page size
|
||
|
||
Args:
|
||
page: Page object
|
||
old_size: Old page size (width, height) in mm
|
||
new_size: New page size (width, height) in mm
|
||
"""
|
||
old_width, old_height = old_size
|
||
new_width, new_height = new_size
|
||
|
||
x_offset = (new_width - old_width) / 2.0
|
||
y_offset = (new_height - old_height) / 2.0
|
||
|
||
for element in page.layout.elements:
|
||
x, y = element.position
|
||
element.position = (x + x_offset, y + y_offset)
|
||
|
||
@ribbon_action(
|
||
label="Export PDF",
|
||
tooltip="Export project to PDF",
|
||
tab="Export",
|
||
group="Export"
|
||
)
|
||
def export_pdf(self):
|
||
"""Export project to PDF"""
|
||
from PyQt6.QtWidgets import QProgressDialog
|
||
from PyQt6.QtCore import Qt
|
||
from pyPhotoAlbum.pdf_exporter import PDFExporter
|
||
|
||
# Check if we have pages to export
|
||
if not self.project or not self.project.pages:
|
||
self.show_status("No pages to export")
|
||
return
|
||
|
||
# Show file save dialog
|
||
file_path, _ = QFileDialog.getSaveFileName(
|
||
self,
|
||
"Export to PDF",
|
||
"",
|
||
"PDF Files (*.pdf);;All Files (*)"
|
||
)
|
||
|
||
if not file_path:
|
||
return
|
||
|
||
# Ensure .pdf extension
|
||
if not file_path.lower().endswith('.pdf'):
|
||
file_path += '.pdf'
|
||
|
||
# Calculate total pages for progress
|
||
total_pages = sum(2 if page.is_double_spread else 1 for page in self.project.pages)
|
||
|
||
# Create progress dialog
|
||
progress = QProgressDialog("Exporting to PDF...", "Cancel", 0, total_pages, self)
|
||
progress.setWindowModality(Qt.WindowModality.WindowModal)
|
||
progress.setMinimumDuration(0)
|
||
progress.setValue(0)
|
||
|
||
# Progress callback
|
||
def update_progress(current, total, message):
|
||
progress.setLabelText(message)
|
||
progress.setValue(current)
|
||
if progress.wasCanceled():
|
||
return False
|
||
return True
|
||
|
||
# Export to PDF
|
||
exporter = PDFExporter(self.project)
|
||
success, warnings = exporter.export(file_path, update_progress)
|
||
|
||
progress.close()
|
||
|
||
if success:
|
||
message = f"PDF exported successfully to {file_path}"
|
||
if warnings:
|
||
message += f"\n\nWarnings:\n" + "\n".join(warnings)
|
||
self.show_status(message)
|
||
print(message)
|
||
else:
|
||
error_message = f"PDF export failed"
|
||
if warnings:
|
||
error_message += f":\n" + "\n".join(warnings)
|
||
self.show_status(error_message)
|
||
print(error_message)
|
||
|
||
@ribbon_action(
|
||
label="About",
|
||
tooltip="About pyPhotoAlbum and data format version",
|
||
tab="Home",
|
||
group="File"
|
||
)
|
||
def show_about(self):
|
||
"""Show about dialog with version information"""
|
||
from PyQt6.QtWidgets import QTextEdit
|
||
from pyPhotoAlbum.version_manager import format_version_info, CURRENT_DATA_VERSION
|
||
|
||
dialog = QDialog(self)
|
||
dialog.setWindowTitle("About pyPhotoAlbum")
|
||
dialog.setMinimumWidth(600)
|
||
dialog.setMinimumHeight(400)
|
||
|
||
layout = QVBoxLayout()
|
||
|
||
# Application info
|
||
app_info = QLabel("<h2>pyPhotoAlbum</h2>")
|
||
app_info.setWordWrap(True)
|
||
layout.addWidget(app_info)
|
||
|
||
description = QLabel(
|
||
"A photo album layout and design application with advanced "
|
||
"page composition features and PDF export capabilities."
|
||
)
|
||
description.setWordWrap(True)
|
||
layout.addWidget(description)
|
||
|
||
# Version information
|
||
version_text = QTextEdit()
|
||
version_text.setReadOnly(True)
|
||
version_text.setPlainText(format_version_info())
|
||
layout.addWidget(version_text)
|
||
|
||
# Close button
|
||
close_button = QPushButton("Close")
|
||
close_button.clicked.connect(dialog.accept)
|
||
layout.addWidget(close_button)
|
||
|
||
dialog.setLayout(layout)
|
||
dialog.exec()
|