283 lines
11 KiB
Python
283 lines
11 KiB
Python
"""
|
|
Gesture routing and handling.
|
|
|
|
This module handles all touch event routing and gesture logic.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
from typing import TYPE_CHECKING, Optional, Tuple
|
|
|
|
from ..gesture import TouchEvent, GestureType, GestureResponse, ActionType
|
|
|
|
if TYPE_CHECKING:
|
|
from ..application import EbookReader
|
|
from pyWebLayout.core.query import SelectionRange
|
|
|
|
|
|
class GestureRouter:
|
|
"""
|
|
Routes and handles all gestures.
|
|
|
|
This class centralizes all gesture handling logic, making it easier
|
|
to test and maintain gesture interactions.
|
|
"""
|
|
|
|
def __init__(self, reader: 'EbookReader'):
|
|
"""
|
|
Initialize the gesture router.
|
|
|
|
Args:
|
|
reader: EbookReader instance to route gestures for
|
|
"""
|
|
self.reader = reader
|
|
|
|
# Selection state (for text selection gestures)
|
|
self._selection_start: Optional[Tuple[int, int]] = None
|
|
self._selection_end: Optional[Tuple[int, int]] = None
|
|
self._selected_range: Optional['SelectionRange'] = None
|
|
|
|
def handle_touch(self, event: TouchEvent) -> GestureResponse:
|
|
"""
|
|
Handle a touch event from HAL.
|
|
|
|
This is the main entry point for all touch interactions.
|
|
|
|
Args:
|
|
event: TouchEvent from HAL with gesture type and coordinates
|
|
|
|
Returns:
|
|
GestureResponse with action and data for UI to process
|
|
"""
|
|
if not self.reader.is_loaded():
|
|
return GestureResponse(ActionType.ERROR, {"message": "No book loaded"})
|
|
|
|
# Handle overlay-specific gestures first
|
|
if self.reader.is_overlay_open():
|
|
if event.gesture == GestureType.TAP:
|
|
return self._handle_overlay_tap(event.x, event.y)
|
|
elif event.gesture == GestureType.SWIPE_DOWN:
|
|
return self._handle_overlay_close()
|
|
|
|
# Dispatch based on gesture type for normal reading mode
|
|
if event.gesture == GestureType.TAP:
|
|
return self._handle_tap(event.x, event.y)
|
|
elif event.gesture == GestureType.LONG_PRESS:
|
|
return self._handle_long_press(event.x, event.y)
|
|
elif event.gesture == GestureType.SWIPE_LEFT:
|
|
return self._handle_page_forward()
|
|
elif event.gesture == GestureType.SWIPE_RIGHT:
|
|
return self._handle_page_back()
|
|
elif event.gesture == GestureType.SWIPE_UP:
|
|
return self._handle_swipe_up(event.y)
|
|
elif event.gesture == GestureType.SWIPE_DOWN:
|
|
return self._handle_swipe_down(event.y)
|
|
elif event.gesture == GestureType.PINCH_IN:
|
|
return self._handle_zoom_out()
|
|
elif event.gesture == GestureType.PINCH_OUT:
|
|
return self._handle_zoom_in()
|
|
elif event.gesture == GestureType.DRAG_START:
|
|
return self._handle_selection_start(event.x, event.y)
|
|
elif event.gesture == GestureType.DRAG_MOVE:
|
|
return self._handle_selection_move(event.x, event.y)
|
|
elif event.gesture == GestureType.DRAG_END:
|
|
return self._handle_selection_end(event.x, event.y)
|
|
|
|
return GestureResponse(ActionType.NONE, {})
|
|
|
|
# ===================================================================
|
|
# Reading Mode Gesture Handlers
|
|
# ===================================================================
|
|
|
|
def _handle_tap(self, x: int, y: int) -> GestureResponse:
|
|
"""Handle tap gesture - activates links or selects words"""
|
|
page = self.reader.manager.get_current_page()
|
|
result = page.query_point((x, y))
|
|
|
|
if not result or result.object_type == "empty":
|
|
return GestureResponse(ActionType.NONE, {})
|
|
|
|
# If it's a link, navigate
|
|
if result.is_interactive and result.link_target:
|
|
# Handle different link types
|
|
if result.link_target.endswith('.epub'):
|
|
# Open new book
|
|
success = self.reader.load_epub(result.link_target)
|
|
if success:
|
|
return GestureResponse(ActionType.BOOK_LOADED, {
|
|
"title": self.reader.book_title,
|
|
"author": self.reader.book_author,
|
|
"path": result.link_target
|
|
})
|
|
else:
|
|
return GestureResponse(ActionType.ERROR, {
|
|
"message": f"Failed to load {result.link_target}"
|
|
})
|
|
else:
|
|
# Internal navigation (chapter)
|
|
self.reader.jump_to_chapter(result.link_target)
|
|
return GestureResponse(ActionType.NAVIGATE, {
|
|
"target": result.link_target,
|
|
"chapter": self.reader.get_current_chapter_info()
|
|
})
|
|
|
|
# Just a tap on text - select word
|
|
if result.text:
|
|
return GestureResponse(ActionType.WORD_SELECTED, {
|
|
"word": result.text,
|
|
"bounds": result.bounds
|
|
})
|
|
|
|
return GestureResponse(ActionType.NONE, {})
|
|
|
|
def _handle_long_press(self, x: int, y: int) -> GestureResponse:
|
|
"""Handle long-press - show definition or menu"""
|
|
page = self.reader.manager.get_current_page()
|
|
result = page.query_point((x, y))
|
|
|
|
if result and result.text:
|
|
return GestureResponse(ActionType.DEFINE, {
|
|
"word": result.text,
|
|
"bounds": result.bounds
|
|
})
|
|
|
|
# Long-press on empty - show menu
|
|
return GestureResponse(ActionType.SHOW_MENU, {
|
|
"options": ["bookmark", "settings", "toc", "search"]
|
|
})
|
|
|
|
def _handle_page_forward(self) -> GestureResponse:
|
|
"""Handle swipe left - next page"""
|
|
img = self.reader.next_page()
|
|
if img:
|
|
return GestureResponse(ActionType.PAGE_TURN, {
|
|
"direction": "forward",
|
|
"progress": self.reader.get_reading_progress(),
|
|
"chapter": self.reader.get_current_chapter_info()
|
|
})
|
|
return GestureResponse(ActionType.AT_END, {})
|
|
|
|
def _handle_page_back(self) -> GestureResponse:
|
|
"""Handle swipe right - previous page"""
|
|
img = self.reader.previous_page()
|
|
if img:
|
|
return GestureResponse(ActionType.PAGE_TURN, {
|
|
"direction": "back",
|
|
"progress": self.reader.get_reading_progress(),
|
|
"chapter": self.reader.get_current_chapter_info()
|
|
})
|
|
return GestureResponse(ActionType.AT_START, {})
|
|
|
|
def _handle_zoom_in(self) -> GestureResponse:
|
|
"""Handle pinch out - increase font"""
|
|
self.reader.increase_font_size()
|
|
return GestureResponse(ActionType.ZOOM, {
|
|
"direction": "in",
|
|
"font_scale": self.reader.base_font_scale
|
|
})
|
|
|
|
def _handle_zoom_out(self) -> GestureResponse:
|
|
"""Handle pinch in - decrease font"""
|
|
self.reader.decrease_font_size()
|
|
return GestureResponse(ActionType.ZOOM, {
|
|
"direction": "out",
|
|
"font_scale": self.reader.base_font_scale
|
|
})
|
|
|
|
def _handle_selection_start(self, x: int, y: int) -> GestureResponse:
|
|
"""Start text selection"""
|
|
self._selection_start = (x, y)
|
|
self._selection_end = None
|
|
self._selected_range = None
|
|
|
|
return GestureResponse(ActionType.SELECTION_START, {
|
|
"start": (x, y)
|
|
})
|
|
|
|
def _handle_selection_move(self, x: int, y: int) -> GestureResponse:
|
|
"""Update text selection"""
|
|
if not self._selection_start:
|
|
return GestureResponse(ActionType.NONE, {})
|
|
|
|
self._selection_end = (x, y)
|
|
|
|
# Query range
|
|
page = self.reader.manager.get_current_page()
|
|
self._selected_range = page.query_range(
|
|
self._selection_start,
|
|
self._selection_end
|
|
)
|
|
|
|
return GestureResponse(ActionType.SELECTION_UPDATE, {
|
|
"start": self._selection_start,
|
|
"end": self._selection_end,
|
|
"text_count": len(self._selected_range.results),
|
|
"bounds": self._selected_range.bounds_list
|
|
})
|
|
|
|
def _handle_selection_end(self, x: int, y: int) -> GestureResponse:
|
|
"""End text selection and return selected text"""
|
|
if not self._selection_start:
|
|
return GestureResponse(ActionType.NONE, {})
|
|
|
|
self._selection_end = (x, y)
|
|
|
|
page = self.reader.manager.get_current_page()
|
|
self._selected_range = page.query_range(
|
|
self._selection_start,
|
|
self._selection_end
|
|
)
|
|
|
|
return GestureResponse(ActionType.SELECTION_COMPLETE, {
|
|
"text": self._selected_range.text,
|
|
"word_count": len(self._selected_range.results),
|
|
"bounds": self._selected_range.bounds_list
|
|
})
|
|
|
|
def _handle_swipe_up(self, y: int) -> GestureResponse:
|
|
"""Handle swipe up gesture - opens TOC overlay if from bottom of screen"""
|
|
# Check if swipe started from bottom 20% of screen
|
|
bottom_threshold = self.reader.page_size[1] * 0.8
|
|
|
|
if y >= bottom_threshold:
|
|
# Open TOC overlay
|
|
overlay_image = self.reader.open_toc_overlay()
|
|
if overlay_image:
|
|
return GestureResponse(ActionType.OVERLAY_OPENED, {
|
|
"overlay_type": "toc",
|
|
"chapters": self.reader.get_chapters()
|
|
})
|
|
|
|
return GestureResponse(ActionType.NONE, {})
|
|
|
|
def _handle_swipe_down(self, y: int) -> GestureResponse:
|
|
"""Handle swipe down gesture - opens settings overlay if from top of screen"""
|
|
# Check if swipe started from top 20% of screen
|
|
top_threshold = self.reader.page_size[1] * 0.2
|
|
|
|
if y <= top_threshold:
|
|
# Open settings overlay
|
|
overlay_image = self.reader.open_settings_overlay()
|
|
if overlay_image:
|
|
return GestureResponse(ActionType.OVERLAY_OPENED, {
|
|
"overlay_type": "settings",
|
|
"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
|
|
})
|
|
|
|
return GestureResponse(ActionType.NONE, {})
|
|
|
|
# ===================================================================
|
|
# Overlay Mode Gesture Handlers
|
|
# ===================================================================
|
|
|
|
def _handle_overlay_tap(self, x: int, y: int) -> GestureResponse:
|
|
"""Handle tap when overlay is open - delegates to EbookReader overlay handlers"""
|
|
# This remains in EbookReader because it's tightly coupled with overlay state
|
|
return self.reader._handle_overlay_tap(x, y)
|
|
|
|
def _handle_overlay_close(self) -> GestureResponse:
|
|
"""Handle overlay close gesture (swipe down)"""
|
|
self.reader.close_overlay()
|
|
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
|