@@ -0,0 +1,217 @@
|
||||
"""
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
# 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,
|
||||
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
|
||||
"""
|
||||
# Query the overlay to see what was tapped
|
||||
query_result = self.query_overlay_pixel(x, y)
|
||||
|
||||
# If query failed (tap outside overlay), close it
|
||||
if not query_result:
|
||||
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]
|
||||
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":
|
||||
return GestureResponse(ActionType.BACK_TO_LIBRARY, {})
|
||||
|
||||
# Not a setting control, close overlay
|
||||
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
|
||||
|
||||
def refresh(self, updated_base_page: Image.Image,
|
||||
font_scale: float,
|
||||
line_spacing: int,
|
||||
inter_block_spacing: int,
|
||||
word_spacing: int = 0) -> 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
|
||||
|
||||
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,
|
||||
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")
|
||||
|
||||
Returns:
|
||||
GestureResponse with SETTING_CHANGED action
|
||||
"""
|
||||
# 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 == "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()
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
return GestureResponse(ActionType.SETTING_CHANGED, {
|
||||
"action": action,
|
||||
"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
|
||||
})
|
||||
Reference in New Issue
Block a user