232 lines
8.1 KiB
Python

"""
Pagination system for pyWebLayout.
This module provides functionality for paginating content across multiple pages,
with the ability to stop, save state, and resume pagination.
"""
from typing import List, Tuple, Dict, Any, Optional, Iterator, Generator
import copy
from pyWebLayout.core import Layoutable
from pyWebLayout.style import Alignment
from pyWebLayout.typesetting.flow import FlowLayout
class PaginationState:
"""
Class to hold the state of a pagination process.
This allows pagination to be paused, saved, and resumed later.
"""
def __init__(self):
"""Initialize a new pagination state."""
self.current_page = 0
self.current_element_index = 0
self.position_in_element = 0 # For elements that might be split across pages
self.consumed_elements = []
self.metadata = {} # For any additional state information
def save(self) -> Dict[str, Any]:
"""
Save the current pagination state to a dictionary.
Returns:
A dictionary representing the pagination state
"""
return {
'current_page': self.current_page,
'current_element_index': self.current_element_index,
'position_in_element': self.position_in_element,
'consumed_elements': self.consumed_elements,
'metadata': self.metadata
}
@classmethod
def load(cls, state_dict: Dict[str, Any]) -> 'PaginationState':
"""
Load pagination state from a dictionary.
Args:
state_dict: Dictionary containing pagination state
Returns:
A PaginationState object
"""
state = cls()
state.current_page = state_dict.get('current_page', 0)
state.current_element_index = state_dict.get('current_element_index', 0)
state.position_in_element = state_dict.get('position_in_element', 0)
state.consumed_elements = state_dict.get('consumed_elements', [])
state.metadata = state_dict.get('metadata', {})
return state
class Paginator:
"""
Class for paginating content across multiple pages.
Supports flow layout within each page and maintains state between pages.
"""
def __init__(
self,
elements: List[Layoutable],
page_size: Tuple[int, int],
margins: Tuple[int, int, int, int] = (20, 20, 20, 20), # top, right, bottom, left
spacing: int = 5,
halign: Alignment = Alignment.LEFT,
):
"""
Initialize a paginator.
Args:
elements: List of elements to paginate
page_size: Size of each page (width, height)
margins: Margins for each page (top, right, bottom, left)
spacing: Spacing between elements
halign: Horizontal alignment of elements
"""
self.elements = elements
self.page_size = page_size
self.margins = margins
self.spacing = spacing
self.halign = halign
self.state = PaginationState()
def paginate(self, max_pages: Optional[int] = None) -> List[List[Tuple[Layoutable, Tuple[int, int]]]]:
"""
Paginate all content into pages.
Args:
max_pages: Maximum number of pages to generate (None for all)
Returns:
List of pages, where each page is a list of (element, position) tuples
"""
pages = []
# Reset state
self.state = PaginationState()
# Create a generator for pagination
page_generator = self._paginate_generator()
# Generate pages up to max_pages or until all content is paginated
page_count = 0
for page in page_generator:
pages.append(page)
page_count += 1
if max_pages is not None and page_count >= max_pages:
break
return pages
def paginate_next(self) -> Optional[List[Tuple[Layoutable, Tuple[int, int]]]]:
"""
Paginate and return the next page only.
Returns:
A list of (element, position) tuples for the next page, or None if no more content
"""
try:
return next(self._paginate_generator())
except StopIteration:
return None
def _paginate_generator(self) -> Generator[List[Tuple[Layoutable, Tuple[int, int]]], None, None]:
"""
Generator that yields one page at a time.
Yields:
A list of (element, position) tuples for each page
"""
# Calculate available space on a page
avail_width = self.page_size[0] - self.margins[1] - self.margins[3]
avail_height = self.page_size[1] - self.margins[0] - self.margins[2]
# Current position on the page
current_index = self.state.current_element_index
remaining_elements = self.elements[current_index:]
# Process elements until we run out
while current_index < len(self.elements):
# Start a new page
page_elements = []
current_y = self.margins[0]
# Fill the page with elements
while current_index < len(self.elements):
element = self.elements[current_index]
# Ensure element is laid out properly
if hasattr(element, 'layout'):
element.layout()
# Get element size
element_width = element.size[0] if hasattr(element, 'size') else 0
element_height = element.size[1] if hasattr(element, 'size') else 0
# Check if element fits on current page
if current_y + element_height > self.margins[0] + avail_height:
# Element doesn't fit, move to next page
break
# Position the element on the page based on alignment
if self.halign == Alignment.LEFT:
element_x = self.margins[3]
elif self.halign == Alignment.CENTER:
element_x = self.margins[3] + (avail_width - element_width) // 2
elif self.halign == Alignment.RIGHT:
element_x = self.margins[3] + (avail_width - element_width)
else:
element_x = self.margins[3] # Default to left alignment
# Add element to page
page_elements.append((element, (element_x, current_y)))
# Move to next element and update position
current_index += 1
current_y += element_height + self.spacing
# Update state
self.state.current_page += 1
self.state.current_element_index = current_index
# If we couldn't fit any elements on this page, we're done
if not page_elements and current_index < len(self.elements):
# This could happen if an element is too large for a page
# Skip the element to avoid an infinite loop
current_index += 1
self.state.current_element_index = current_index
# Add a warning element to the page
warning_message = f"Element at index {current_index-1} is too large to fit on a page"
print(f"Warning: {warning_message}")
# Yield the page if it has elements
if page_elements:
yield page_elements
else:
# No more elements to paginate
break
def get_state(self) -> Dict[str, Any]:
"""
Get the current pagination state.
Returns:
Dictionary representing pagination state
"""
return self.state.save()
def set_state(self, state: Dict[str, Any]) -> None:
"""
Set the pagination state.
Args:
state: Dictionary representing pagination state
"""
self.state = PaginationState.load(state)