245 lines
9.1 KiB
Python
245 lines
9.1 KiB
Python
"""
|
|
Page navigation mixin for GLWidget - handles page detection and ghost pages
|
|
"""
|
|
|
|
from typing import Optional, Tuple, List
|
|
|
|
|
|
class PageNavigationMixin:
|
|
"""
|
|
Mixin providing page navigation and ghost page functionality.
|
|
|
|
This mixin handles page detection from screen coordinates, calculating
|
|
page positions with ghost pages, and managing ghost page interactions.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Current page tracking for operations that need to know which page to work on
|
|
self.current_page_index: int = 0
|
|
|
|
# Store page renderers for later use (mouse interaction, text overlays, etc.)
|
|
self._page_renderers: List = []
|
|
|
|
def _get_page_at(self, x: float, y: float):
|
|
"""
|
|
Get the page at the given screen coordinates.
|
|
|
|
Args:
|
|
x: Screen X coordinate
|
|
y: Screen Y coordinate
|
|
|
|
Returns:
|
|
Tuple of (page, page_index, renderer) or (None, -1, None) if no page at coordinates
|
|
"""
|
|
if not hasattr(self, '_page_renderers') or not self._page_renderers:
|
|
return None, -1, None
|
|
|
|
main_window = self.window()
|
|
if not hasattr(main_window, 'project') or not main_window.project or not main_window.project.pages:
|
|
return None, -1, None
|
|
|
|
# Check each page to find which one contains the coordinates
|
|
for renderer, page in self._page_renderers:
|
|
if renderer.is_point_in_page(x, y):
|
|
# Find the page index in the project's pages list
|
|
page_index = main_window.project.pages.index(page)
|
|
return page, page_index, renderer
|
|
|
|
return None, -1, None
|
|
|
|
def _get_page_positions(self):
|
|
"""
|
|
Calculate page positions including ghost pages.
|
|
|
|
Returns:
|
|
List of tuples (page_type, page_or_ghost_data, y_offset)
|
|
"""
|
|
main_window = self.window()
|
|
if not hasattr(main_window, 'project'):
|
|
return []
|
|
|
|
dpi = main_window.project.working_dpi
|
|
|
|
# Use project's page_spacing_mm setting (default is 10mm = 1cm)
|
|
# Convert to pixels at working DPI
|
|
spacing_mm = main_window.project.page_spacing_mm
|
|
spacing_px = spacing_mm * dpi / 25.4
|
|
|
|
# Start with a small top margin (5mm)
|
|
top_margin_mm = 5.0
|
|
top_margin_px = top_margin_mm * dpi / 25.4
|
|
|
|
result = []
|
|
current_y = top_margin_px # Initial top offset in pixels (not screen pixels)
|
|
|
|
# First, render cover if it exists
|
|
for page in main_window.project.pages:
|
|
if page.is_cover:
|
|
result.append(('page', page, current_y))
|
|
|
|
# Calculate cover height in pixels
|
|
page_height_mm = page.layout.size[1]
|
|
page_height_px = page_height_mm * dpi / 25.4
|
|
|
|
# Move to next position (add height + spacing)
|
|
current_y += page_height_px + spacing_px
|
|
break # Only one cover allowed
|
|
|
|
# Get page layout with ghosts from project (this excludes cover)
|
|
layout_with_ghosts = main_window.project.calculate_page_layout_with_ghosts()
|
|
|
|
for page_type, page_obj, logical_pos in layout_with_ghosts:
|
|
if page_type == 'page':
|
|
# Regular page (single or double spread)
|
|
result.append((page_type, page_obj, current_y))
|
|
|
|
# Calculate page height in pixels
|
|
# For double spreads, layout.size already contains the doubled width
|
|
page_height_mm = page_obj.layout.size[1]
|
|
page_height_px = page_height_mm * dpi / 25.4
|
|
|
|
# Move to next position (add height + spacing)
|
|
current_y += page_height_px + spacing_px
|
|
|
|
elif page_type == 'ghost':
|
|
# Ghost page - use default page size
|
|
page_size_mm = main_window.project.page_size_mm
|
|
from pyPhotoAlbum.models import GhostPageData
|
|
|
|
# Create ghost page data with correct size
|
|
ghost = GhostPageData(page_size=page_size_mm)
|
|
result.append((page_type, ghost, current_y))
|
|
|
|
# Calculate ghost page height
|
|
page_height_px = page_size_mm[1] * dpi / 25.4
|
|
|
|
# Move to next position (add height + spacing)
|
|
current_y += page_height_px + spacing_px
|
|
|
|
return result
|
|
|
|
def _check_ghost_page_click(self, x: float, y: float) -> bool:
|
|
"""
|
|
Check if click is on a ghost page (entire page is clickable) and handle it.
|
|
|
|
Args:
|
|
x: Screen X coordinate
|
|
y: Screen Y coordinate
|
|
|
|
Returns:
|
|
bool: True if a ghost page was clicked and a new page was created
|
|
"""
|
|
if not hasattr(self, '_page_renderers'):
|
|
return False
|
|
|
|
main_window = self.window()
|
|
if not hasattr(main_window, 'project'):
|
|
return False
|
|
|
|
# Get page positions which includes ghosts
|
|
page_positions = self._get_page_positions()
|
|
|
|
# Check each position for ghost pages
|
|
for idx, (page_type, page_or_ghost, y_offset) in enumerate(page_positions):
|
|
# Skip non-ghost pages
|
|
if page_type != 'ghost':
|
|
continue
|
|
|
|
ghost = page_or_ghost
|
|
dpi = main_window.project.working_dpi
|
|
|
|
# Calculate ghost page renderer
|
|
ghost_width_mm, ghost_height_mm = ghost.page_size
|
|
screen_x = 50 + self.pan_offset[0]
|
|
screen_y = (y_offset * self.zoom_level) + self.pan_offset[1]
|
|
|
|
from pyPhotoAlbum.page_renderer import PageRenderer
|
|
renderer = PageRenderer(
|
|
page_width_mm=ghost_width_mm,
|
|
page_height_mm=ghost_height_mm,
|
|
screen_x=screen_x,
|
|
screen_y=screen_y,
|
|
dpi=dpi,
|
|
zoom=self.zoom_level
|
|
)
|
|
|
|
# Check if click is anywhere on the ghost page (entire page is clickable)
|
|
if renderer.is_point_in_page(x, y):
|
|
# User clicked the ghost page!
|
|
# Calculate the insertion index (count real pages before this ghost in page_positions)
|
|
insert_index = sum(1 for i, (pt, _, _) in enumerate(page_positions) if i < idx and pt == 'page')
|
|
|
|
print(f"Ghost page clicked at index {insert_index} - inserting new page in place")
|
|
|
|
# Create a new page and insert it directly into the pages list
|
|
from pyPhotoAlbum.project import Page
|
|
from pyPhotoAlbum.page_layout import PageLayout
|
|
|
|
# Create new page with next page number
|
|
new_page_number = insert_index + 1
|
|
new_page = Page(
|
|
layout=PageLayout(
|
|
width=main_window.project.page_size_mm[0],
|
|
height=main_window.project.page_size_mm[1]
|
|
),
|
|
page_number=new_page_number
|
|
)
|
|
|
|
# Insert the page at the correct position
|
|
main_window.project.pages.insert(insert_index, new_page)
|
|
|
|
# Renumber all pages after this one
|
|
for i, page in enumerate(main_window.project.pages):
|
|
page.page_number = i + 1
|
|
|
|
print(f"Inserted page at index {insert_index}, renumbered pages")
|
|
self.update()
|
|
return True
|
|
|
|
return False
|
|
|
|
def _update_page_status(self, x: float, y: float):
|
|
"""
|
|
Update status bar with current page and total page count.
|
|
|
|
Args:
|
|
x: Screen X coordinate
|
|
y: Screen Y coordinate
|
|
"""
|
|
main_window = self.window()
|
|
if not hasattr(main_window, 'project') or not main_window.project or not main_window.project.pages:
|
|
return
|
|
|
|
if not hasattr(self, '_page_renderers') or not self._page_renderers:
|
|
return
|
|
|
|
# Get total page count (accounting for double spreads = 2 pages each)
|
|
total_pages = sum(page.get_page_count() for page in main_window.project.pages)
|
|
|
|
# Find which page mouse is over
|
|
current_page_info = None
|
|
|
|
for renderer, page in self._page_renderers:
|
|
# Check if mouse is within this page bounds
|
|
if renderer.is_point_in_page(x, y):
|
|
# For facing page spreads, determine left or right
|
|
if page.is_double_spread:
|
|
side = renderer.get_sub_page_at(x, is_facing_page=True)
|
|
page_nums = page.get_page_numbers()
|
|
if side == 'left':
|
|
current_page_info = f"Page {page_nums[0]}"
|
|
else:
|
|
current_page_info = f"Page {page_nums[1]}"
|
|
else:
|
|
current_page_info = f"Page {page.page_number}"
|
|
break
|
|
|
|
# Update status bar
|
|
if hasattr(main_window, 'status_bar'):
|
|
if current_page_info:
|
|
main_window.status_bar.showMessage(f"{current_page_info} of {total_pages} | Zoom: {int(self.zoom_level * 100)}%")
|
|
else:
|
|
main_window.status_bar.showMessage(f"Total pages: {total_pages} | Zoom: {int(self.zoom_level * 100)}%")
|