232 lines
8.1 KiB
Python
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)
|