2025-11-12 18:52:08 +00:00

252 lines
9.7 KiB
Python

"""
Settings overlay sub-application.
Provides interactive controls for adjusting reading settings with live preview.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from PIL import Image
from .base import OverlaySubApplication
from ..gesture import GestureResponse, ActionType
from ..state import OverlayState
from ..html_generator import generate_settings_overlay
if TYPE_CHECKING:
from ..application import EbookReader
class SettingsOverlay(OverlaySubApplication):
"""
Settings overlay with live preview.
Features:
- Font size adjustment (increase/decrease)
- Line spacing adjustment
- Inter-block spacing adjustment
- Word spacing adjustment
- Live preview of changes on base page
- Back to library button
"""
def get_overlay_type(self) -> OverlayState:
"""Return SETTINGS overlay type."""
return OverlayState.SETTINGS
def open(self, base_page: Image.Image, **kwargs) -> Image.Image:
"""
Open the settings overlay.
Args:
base_page: Current reading page to show underneath
font_scale: Current font scale
line_spacing: Current line spacing in pixels
inter_block_spacing: Current inter-block spacing in pixels
word_spacing: Current word spacing in pixels
font_family: Current font family name (e.g., "SERIF", "SANS", "MONOSPACE", or None)
Returns:
Composited image with settings overlay
"""
font_scale = kwargs.get('font_scale', 1.0)
line_spacing = kwargs.get('line_spacing', 5)
inter_block_spacing = kwargs.get('inter_block_spacing', 15)
word_spacing = kwargs.get('word_spacing', 0)
font_family = kwargs.get('font_family', 'Default')
# Calculate panel size (60% width, 70% height)
panel_size = self._calculate_panel_size(0.6, 0.7)
# Generate settings HTML with current values
html = generate_settings_overlay(
font_scale=font_scale,
line_spacing=line_spacing,
inter_block_spacing=inter_block_spacing,
word_spacing=word_spacing,
font_family=font_family,
page_size=panel_size
)
# 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 settings overlay.
Detects:
- Setting adjustment controls (setting:action)
- Back to library button (action:back_to_library)
- 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"[SETTINGS_OVERLAY] Handling tap at ({x}, {y})")
logger.info(f"[SETTINGS_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"[SETTINGS_OVERLAY] Query result: {query_result}")
# If query failed (tap outside overlay panel), close it
if query_result is None:
logger.info(f"[SETTINGS_OVERLAY] Tap outside overlay panel, closing")
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"]
logger.info(f"[SETTINGS_OVERLAY] Found interactive link: {link_target}")
# Parse "setting:action" format
if link_target.startswith("setting:"):
action = link_target.split(":", 1)[1]
logger.info(f"[SETTINGS_OVERLAY] Applying setting change: {action}")
return self._apply_setting_change(action)
# Parse "action:command" format for other actions
elif link_target.startswith("action:"):
action = link_target.split(":", 1)[1]
if action == "back_to_library":
logger.info(f"[SETTINGS_OVERLAY] Back to library clicked")
return GestureResponse(ActionType.BACK_TO_LIBRARY, {})
# Tap inside overlay but not on interactive element - keep overlay open
logger.info(f"[SETTINGS_OVERLAY] Tap on non-interactive area inside overlay, ignoring")
return GestureResponse(ActionType.NONE, {})
def refresh(self, updated_base_page: Image.Image,
font_scale: float,
line_spacing: int,
inter_block_spacing: int,
word_spacing: int = 0,
font_family: str = "Default") -> Image.Image:
"""
Refresh the settings overlay with updated values and background page.
This is used for live preview when settings change - it updates both
the background page (with new settings applied) and the overlay panel
(with new values displayed).
Args:
updated_base_page: Updated reading page with new settings applied
font_scale: Updated font scale
line_spacing: Updated line spacing
inter_block_spacing: Updated inter-block spacing
word_spacing: Updated word spacing
font_family: Updated font family
Returns:
Composited image with updated settings overlay
"""
# Calculate panel size (60% width, 70% height)
panel_size = self._calculate_panel_size(0.6, 0.7)
# Generate updated settings HTML
html = generate_settings_overlay(
font_scale=font_scale,
line_spacing=line_spacing,
inter_block_spacing=inter_block_spacing,
word_spacing=word_spacing,
font_family=font_family,
page_size=panel_size
)
# Render HTML to image
overlay_panel = self.render_html_to_image(html, panel_size)
# Update caches
self._cached_base_page = updated_base_page.copy()
self._cached_overlay_image = overlay_panel
# Composite and return
return self.composite_overlay(updated_base_page, overlay_panel)
def _apply_setting_change(self, action: str) -> GestureResponse:
"""
Apply a setting change and refresh the overlay.
Args:
action: Setting action (e.g., "font_increase", "line_spacing_decrease", "font_family_serif")
Returns:
GestureResponse with SETTING_CHANGED action
"""
from pyWebLayout.style.fonts import BundledFont
# Apply the setting change via reader
if action == "font_increase":
self.reader.increase_font_size()
elif action == "font_decrease":
self.reader.decrease_font_size()
elif action == "font_family_default":
self.reader.set_font_family(None)
elif action == "font_family_serif":
self.reader.set_font_family(BundledFont.SERIF)
elif action == "font_family_sans":
self.reader.set_font_family(BundledFont.SANS)
elif action == "font_family_monospace":
self.reader.set_font_family(BundledFont.MONOSPACE)
elif action == "line_spacing_increase":
new_spacing = self.reader.page_style.line_spacing + 2
self.reader.set_line_spacing(new_spacing)
elif action == "line_spacing_decrease":
new_spacing = max(0, self.reader.page_style.line_spacing - 2)
self.reader.set_line_spacing(new_spacing)
elif action == "block_spacing_increase":
new_spacing = self.reader.page_style.inter_block_spacing + 3
self.reader.set_inter_block_spacing(new_spacing)
elif action == "block_spacing_decrease":
new_spacing = max(0, self.reader.page_style.inter_block_spacing - 3)
self.reader.set_inter_block_spacing(new_spacing)
elif action == "word_spacing_increase":
new_spacing = self.reader.page_style.word_spacing + 2
self.reader.set_word_spacing(new_spacing)
elif action == "word_spacing_decrease":
new_spacing = max(0, self.reader.page_style.word_spacing - 2)
self.reader.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.reader.manager.get_current_page()
updated_page = page.render()
# Get font family for display
font_family = self.reader.get_font_family()
font_family_name = font_family.name if font_family else "Default"
# Refresh the settings overlay with updated values and page
self.refresh(
updated_base_page=updated_page,
font_scale=self.reader.base_font_scale,
line_spacing=self.reader.page_style.line_spacing,
inter_block_spacing=self.reader.page_style.inter_block_spacing,
word_spacing=self.reader.page_style.word_spacing,
font_family=font_family_name
)
return GestureResponse(ActionType.SETTING_CHANGED, {
"action": action,
"font_scale": self.reader.base_font_scale,
"font_family": font_family_name,
"line_spacing": self.reader.page_style.line_spacing,
"inter_block_spacing": self.reader.page_style.inter_block_spacing,
"word_spacing": self.reader.page_style.word_spacing
})