split out overlays
Python CI / test (push) Failing after 6m15s

This commit is contained in:
2025-11-09 00:06:32 +01:00
parent 18be4306bf
commit 2aae1a88ea
6 changed files with 927 additions and 216 deletions
+61 -216
View File
@@ -54,6 +54,7 @@ from .state import OverlayState
from .overlay import OverlayManager
from .managers import DocumentManager, SettingsManager, HighlightCoordinator
from .handlers import GestureRouter
from .overlays import NavigationOverlay, SettingsOverlay, TOCOverlay
class EbookReader:
@@ -129,9 +130,17 @@ class EbookReader:
self.base_font_scale = 1.0
self.font_scale_step = 0.1
# Overlay management
# Overlay management (legacy - kept for backward compatibility)
self.overlay_manager = OverlayManager(page_size=page_size)
self.current_overlay_state = OverlayState.NONE
# Overlay sub-applications (NEW architecture)
self._overlay_subapps = {
OverlayState.NAVIGATION: NavigationOverlay(self),
OverlayState.SETTINGS: SettingsOverlay(self),
OverlayState.TOC: TOCOverlay(self),
}
self._active_overlay = None # Current active overlay sub-application
def load_epub(self, epub_path: str) -> bool:
"""
@@ -250,13 +259,13 @@ class EbookReader:
return None
# If an overlay is open, return the cached composited overlay image
if self.is_overlay_open() and self.overlay_manager._cached_base_page:
# Return the last composited overlay image
# The overlay manager keeps this updated when settings change
return self.overlay_manager.composite_overlay(
self.overlay_manager._cached_base_page,
self.overlay_manager._cached_overlay_image
)
if self.is_overlay_open() and self._active_overlay:
# Return the composited overlay from the sub-application
if self._active_overlay._cached_base_page and self._active_overlay._cached_overlay_image:
return self._active_overlay.composite_overlay(
self._active_overlay._cached_base_page,
self._active_overlay._cached_overlay_image
)
try:
page = self.manager.get_current_page()
@@ -702,211 +711,26 @@ class EbookReader:
def _handle_overlay_tap(self, x: int, y: int) -> GestureResponse:
"""Handle tap when overlay is open - select chapter, adjust settings, or close overlay"""
# For TOC overlay, use pyWebLayout link query to detect chapter clicks
if self.current_overlay_state == OverlayState.TOC:
# Query the overlay to see what was tapped
query_result = self.overlay_manager.query_overlay_pixel(x, y)
"""
Handle tap when overlay is open.
# If query failed (tap outside overlay), close it
if not query_result:
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
# Check if tapped on a link (chapter)
if query_result.get("is_interactive") and query_result.get("link_target"):
link_target = query_result["link_target"]
# Parse "chapter:N" format
if link_target.startswith("chapter:"):
try:
chapter_idx = int(link_target.split(":")[1])
# Get chapter title for response
chapters = self.get_chapters()
chapter_title = None
for title, idx in chapters:
if idx == chapter_idx:
chapter_title = title
break
# Jump to selected chapter
self.jump_to_chapter(chapter_idx)
# Close overlay
self.close_overlay()
return GestureResponse(ActionType.CHAPTER_SELECTED, {
"chapter_index": chapter_idx,
"chapter_title": chapter_title or f"Chapter {chapter_idx}"
})
except (ValueError, IndexError):
pass
# Not a chapter link, close overlay
Delegates to the active overlay sub-application for handling.
If the response indicates the overlay should be closed, closes it.
"""
if not self._active_overlay:
# No active overlay, close legacy overlay if any
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
# For settings overlay, handle setting adjustments
elif self.current_overlay_state == OverlayState.SETTINGS:
# Query the overlay to see what was tapped
query_result = self.overlay_manager.query_overlay_pixel(x, y)
# Delegate to the active overlay sub-application
response = self._active_overlay.handle_tap(x, y)
# If query failed (tap outside overlay), close it
if not query_result:
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
# Check if tapped on a settings control link
if query_result.get("is_interactive") and query_result.get("link_target"):
link_target = query_result["link_target"]
# Parse "setting:action" format
if link_target.startswith("setting:"):
action = link_target.split(":", 1)[1]
# Apply the setting change
if action == "font_increase":
self.increase_font_size()
elif action == "font_decrease":
self.decrease_font_size()
elif action == "line_spacing_increase":
new_spacing = self.page_style.line_spacing + 2
self.set_line_spacing(new_spacing)
elif action == "line_spacing_decrease":
new_spacing = max(0, self.page_style.line_spacing - 2)
self.set_line_spacing(new_spacing)
elif action == "block_spacing_increase":
new_spacing = self.page_style.inter_block_spacing + 3
self.set_inter_block_spacing(new_spacing)
elif action == "block_spacing_decrease":
new_spacing = max(0, self.page_style.inter_block_spacing - 3)
self.set_inter_block_spacing(new_spacing)
elif action == "word_spacing_increase":
new_spacing = self.page_style.word_spacing + 2
self.set_word_spacing(new_spacing)
elif action == "word_spacing_decrease":
new_spacing = max(0, self.page_style.word_spacing - 2)
self.set_word_spacing(new_spacing)
# Re-render the base page with new settings applied
# Must get directly from manager, not get_current_page() which returns overlay
page = self.manager.get_current_page()
updated_page = page.render()
# Refresh the settings overlay with updated values and page
self.overlay_manager.refresh_settings_overlay(
updated_base_page=updated_page,
font_scale=self.base_font_scale,
line_spacing=self.page_style.line_spacing,
inter_block_spacing=self.page_style.inter_block_spacing,
word_spacing=self.page_style.word_spacing
)
return GestureResponse(ActionType.SETTING_CHANGED, {
"action": action,
"font_scale": self.base_font_scale,
"line_spacing": self.page_style.line_spacing,
"inter_block_spacing": self.page_style.inter_block_spacing,
"word_spacing": self.page_style.word_spacing
})
# Parse "action:command" format for other actions
elif link_target.startswith("action:"):
action = link_target.split(":", 1)[1]
if action == "back_to_library":
# Close the overlay first
self.close_overlay()
# Return a special action for the application to handle
return GestureResponse(ActionType.BACK_TO_LIBRARY, {})
# Not a setting control, close overlay
# If the response indicates overlay should be closed, close it
if response.action in (ActionType.OVERLAY_CLOSED, ActionType.CHAPTER_SELECTED,
ActionType.BOOKMARK_SELECTED):
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
# For navigation overlay, handle tab switching, chapter/bookmark selection, and close
elif self.current_overlay_state == OverlayState.NAVIGATION:
# Query the overlay to see what was tapped
query_result = self.overlay_manager.query_overlay_pixel(x, y)
# If query failed (tap outside overlay), close it
if not query_result:
self.close_overlay()
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"]
# Parse "tab:tabname" format for tab switching
if link_target.startswith("tab:"):
tab_name = link_target.split(":", 1)[1]
# Switch to the selected tab
self.switch_navigation_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
chapters = self.get_chapters()
chapter_title = None
for title, idx in chapters:
if idx == chapter_idx:
chapter_title = title
break
# Jump to selected chapter
self.jump_to_chapter(chapter_idx)
# Close overlay
self.close_overlay()
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.load_position(bookmark_name)
if page:
# Close overlay
self.close_overlay()
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":
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
# Not an interactive element, close overlay
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
# For other overlays, just close on any tap for now
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
return response
# ===================================================================
@@ -1139,8 +963,12 @@ class EbookReader:
# Get chapters
chapters = self.get_chapters()
# Open overlay and get composited image
result = self.overlay_manager.open_toc_overlay(chapters, base_page)
# Use the TOC sub-application
overlay_subapp = self._overlay_subapps[OverlayState.TOC]
result = overlay_subapp.open(base_page, chapters=chapters)
# Update state
self._active_overlay = overlay_subapp
self.current_overlay_state = OverlayState.TOC
return result
@@ -1166,14 +994,18 @@ class EbookReader:
inter_block_spacing = self.page_style.inter_block_spacing
word_spacing = self.page_style.word_spacing
# Open overlay and get composited image
result = self.overlay_manager.open_settings_overlay(
# Use the Settings sub-application
overlay_subapp = self._overlay_subapps[OverlayState.SETTINGS]
result = overlay_subapp.open(
base_page,
font_scale=font_scale,
line_spacing=line_spacing,
inter_block_spacing=inter_block_spacing,
word_spacing=word_spacing
)
# Update state
self._active_overlay = overlay_subapp
self.current_overlay_state = OverlayState.SETTINGS
return result
@@ -1237,13 +1069,17 @@ class EbookReader:
for name in bookmark_names
]
# Open overlay and get composited image
result = self.overlay_manager.open_navigation_overlay(
# Use the Navigation sub-application
overlay_subapp = self._overlay_subapps[OverlayState.NAVIGATION]
result = overlay_subapp.open(
base_page,
chapters=chapters,
bookmarks=bookmarks,
base_page=base_page,
active_tab=active_tab
)
# Update state
self._active_overlay = overlay_subapp
self.current_overlay_state = OverlayState.NAVIGATION
return result
@@ -1261,8 +1097,12 @@ class EbookReader:
if self.current_overlay_state != OverlayState.NAVIGATION:
return None
result = self.overlay_manager.switch_navigation_tab(new_tab)
return result if result else self.get_current_page()
# Delegate to the Navigation sub-application
if isinstance(self._active_overlay, NavigationOverlay):
result = self._active_overlay.switch_tab(new_tab)
return result if result else self.get_current_page()
return None
def close_overlay(self) -> Optional[Image.Image]:
"""
@@ -1274,7 +1114,12 @@ class EbookReader:
if self.current_overlay_state == OverlayState.NONE:
return None
result = self.overlay_manager.close_overlay()
# Close the active overlay sub-application
if self._active_overlay:
self._active_overlay.close()
self._active_overlay = None
# Update state
self.current_overlay_state = OverlayState.NONE
# Return fresh current page