pyPhotoAlbum/pyPhotoAlbum/mixins/page_navigation.py
Duncan Tourolle 7f32858baf
All checks were successful
Python CI / test (push) Successful in 1m7s
Lint / lint (push) Successful in 1m11s
Tests / test (3.10) (push) Successful in 50s
Tests / test (3.11) (push) Successful in 51s
Tests / test (3.9) (push) Successful in 47s
big refactor to use mixin architecture
2025-11-11 10:35:24 +01:00

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)}%")