""" Navigation overlay sub-application. Provides tabbed interface for Contents (TOC) and Bookmarks. """ from __future__ import annotations from typing import TYPE_CHECKING, List, Tuple, Dict, Any, Optional from PIL import Image from .base import OverlaySubApplication from ..gesture import GestureResponse, ActionType from ..state import OverlayState from ..html_generator import generate_navigation_overlay if TYPE_CHECKING: from ..application import EbookReader class NavigationOverlay(OverlaySubApplication): """ Unified navigation overlay with Contents and Bookmarks tabs. Features: - Tab switching between Contents and Bookmarks - Chapter navigation via clickable links - Bookmark navigation - Close button """ def __init__(self, reader: 'EbookReader'): """Initialize navigation overlay.""" super().__init__(reader) # Tab state self._active_tab: str = "contents" self._cached_chapters: List[Tuple[str, int]] = [] self._cached_bookmarks: List[Dict[str, Any]] = [] # Pagination state self._toc_page: int = 0 # Current page in TOC self._toc_items_per_page: int = 10 # Items per page self._bookmarks_page: int = 0 # Current page in bookmarks def get_overlay_type(self) -> OverlayState: """Return NAVIGATION overlay type.""" return OverlayState.NAVIGATION def open(self, base_page: Image.Image, **kwargs) -> Image.Image: """ Open the navigation overlay. Args: base_page: Current reading page to show underneath chapters: List of (chapter_title, chapter_index) tuples bookmarks: List of bookmark dicts with 'name' and optional 'position' active_tab: Which tab to show initially ("contents" or "bookmarks") Returns: Composited image with navigation overlay """ chapters = kwargs.get('chapters', []) bookmarks = kwargs.get('bookmarks', []) active_tab = kwargs.get('active_tab', 'contents') # Store for later use (tab switching) self._cached_chapters = chapters self._cached_bookmarks = bookmarks self._active_tab = active_tab # Reset pagination when opening self._toc_page = 0 self._bookmarks_page = 0 # Calculate panel size (60% width, 70% height) panel_size = self._calculate_panel_size(0.6, 0.7) # Convert chapters to format expected by HTML generator chapter_data = [ {"index": idx, "title": title} for title, idx in chapters ] # Generate navigation HTML with tabs html = generate_navigation_overlay( chapters=chapter_data, bookmarks=bookmarks, active_tab=active_tab, page_size=panel_size, toc_page=self._toc_page, toc_items_per_page=self._toc_items_per_page, bookmarks_page=self._bookmarks_page ) # Render HTML to image overlay_panel = self.render_html_to_image(html, panel_size) # Cache for later use self._cached_base_page = base_page.copy() self._cached_overlay_image = overlay_panel # Composite and return return self.composite_overlay(base_page, overlay_panel) def handle_tap(self, x: int, y: int) -> GestureResponse: """ Handle tap within navigation overlay. Detects: - Tab switching (tab:contents, tab:bookmarks) - Chapter selection (chapter:N) - Bookmark selection (bookmark:name) - Close button (action:close) - Tap outside overlay (closes) Args: x, y: Screen coordinates of tap Returns: GestureResponse with appropriate action """ import logging logger = logging.getLogger(__name__) logger.info(f"[NAV_OVERLAY] Handling tap at ({x}, {y})") logger.info(f"[NAV_OVERLAY] Panel offset: {self._overlay_panel_offset}, Panel size: {self._panel_size}") # Query the overlay to see what was tapped query_result = self.query_overlay_pixel(x, y) logger.info(f"[NAV_OVERLAY] Query result: {query_result}") # If query failed (tap outside overlay panel), close it if query_result is None: logger.info(f"[NAV_OVERLAY] Tap outside overlay panel, closing") return GestureResponse(ActionType.OVERLAY_CLOSED, {}) # Check if tapped on a link if query_result.get("is_interactive") and query_result.get("link_target"): link_target = query_result["link_target"] logger.info(f"[NAV_OVERLAY] Found interactive link: {link_target}") # Parse "tab:tabname" format for tab switching if link_target.startswith("tab:"): tab_name = link_target.split(":", 1)[1] self._switch_tab(tab_name) return GestureResponse(ActionType.TAB_SWITCHED, { "tab": tab_name }) # Parse "chapter:N" format for chapter navigation elif link_target.startswith("chapter:"): try: chapter_idx = int(link_target.split(":")[1]) # Get chapter title for response chapter_title = None for title, idx in self._cached_chapters: if idx == chapter_idx: chapter_title = title break # Jump to selected chapter self.reader.jump_to_chapter(chapter_idx) return GestureResponse(ActionType.CHAPTER_SELECTED, { "chapter_index": chapter_idx, "chapter_title": chapter_title or f"Chapter {chapter_idx}" }) except (ValueError, IndexError): pass # Parse "bookmark:name" format for bookmark navigation elif link_target.startswith("bookmark:"): bookmark_name = link_target.split(":", 1)[1] # Load the bookmark position page = self.reader.load_position(bookmark_name) if page: return GestureResponse(ActionType.BOOKMARK_SELECTED, { "bookmark_name": bookmark_name }) else: # Failed to load bookmark return GestureResponse(ActionType.ERROR, { "message": f"Failed to load bookmark: {bookmark_name}" }) # Parse "action:close" format for close button elif link_target.startswith("action:"): action = link_target.split(":", 1)[1] if action == "close": logger.info(f"[NAV_OVERLAY] Close button clicked") return GestureResponse(ActionType.OVERLAY_CLOSED, {}) # Parse "page:direction" format for pagination elif link_target.startswith("page:"): direction = link_target.split(":", 1)[1] logger.info(f"[NAV_OVERLAY] Pagination button clicked: {direction}") self._handle_pagination(direction) return GestureResponse(ActionType.PAGE_CHANGED, { "direction": direction, "tab": self._active_tab }) # Tap inside overlay but not on interactive element - keep overlay open logger.info(f"[NAV_OVERLAY] Tap on non-interactive area inside overlay, ignoring") return GestureResponse(ActionType.NONE, {}) def switch_tab(self, new_tab: str) -> Optional[Image.Image]: """ Switch between tabs in the navigation overlay. Args: new_tab: Tab to switch to ("contents" or "bookmarks") Returns: Updated image with new tab active """ return self._switch_tab(new_tab) def _switch_tab(self, new_tab: str) -> Optional[Image.Image]: """ Internal tab switching implementation. Args: new_tab: Tab to switch to Returns: Updated composited image with new tab active """ if not self._cached_base_page: return None self._active_tab = new_tab # Regenerate overlay with new active tab panel_size = self._calculate_panel_size(0.6, 0.7) # Convert chapters to format expected by HTML generator chapter_data = [ {"index": idx, "title": title} for title, idx in self._cached_chapters ] # Generate navigation HTML with new active tab html = generate_navigation_overlay( chapters=chapter_data, bookmarks=self._cached_bookmarks, active_tab=new_tab, page_size=panel_size, toc_page=self._toc_page, toc_items_per_page=self._toc_items_per_page, bookmarks_page=self._bookmarks_page ) # Render HTML to image overlay_panel = self.render_html_to_image(html, panel_size) # Update cache self._cached_overlay_image = overlay_panel # Composite and return return self.composite_overlay(self._cached_base_page, overlay_panel) def _handle_pagination(self, direction: str) -> Optional[Image.Image]: """ Handle pagination within the active tab. Args: direction: Either "next" or "prev" Returns: Updated composited image with new page, or None if invalid """ import logging logger = logging.getLogger(__name__) if self._active_tab == "contents": # Calculate total pages total_items = len(self._cached_chapters) total_pages = (total_items + self._toc_items_per_page - 1) // self._toc_items_per_page # Update page number if direction == "next" and self._toc_page < total_pages - 1: self._toc_page += 1 logger.info(f"[NAV_OVERLAY] TOC page -> {self._toc_page + 1}/{total_pages}") elif direction == "prev" and self._toc_page > 0: self._toc_page -= 1 logger.info(f"[NAV_OVERLAY] TOC page -> {self._toc_page + 1}/{total_pages}") else: logger.info(f"[NAV_OVERLAY] Can't paginate {direction} from page {self._toc_page + 1}/{total_pages}") return None elif self._active_tab == "bookmarks": # Calculate total pages total_items = len(self._cached_bookmarks) total_pages = (total_items + self._toc_items_per_page - 1) // self._toc_items_per_page # Update page number if direction == "next" and self._bookmarks_page < total_pages - 1: self._bookmarks_page += 1 logger.info(f"[NAV_OVERLAY] Bookmarks page -> {self._bookmarks_page + 1}/{total_pages}") elif direction == "prev" and self._bookmarks_page > 0: self._bookmarks_page -= 1 logger.info(f"[NAV_OVERLAY] Bookmarks page -> {self._bookmarks_page + 1}/{total_pages}") else: logger.info(f"[NAV_OVERLAY] Can't paginate {direction} from page {self._bookmarks_page + 1}/{total_pages}") return None # Regenerate the overlay with new page return self._switch_tab(self._active_tab)