Duncan Tourolle 01e79dfa4b
All checks were successful
Python CI / test (3.12) (push) Successful in 22m19s
Python CI / test (3.13) (push) Successful in 8m23s
Test appplication for offdevice testing
2025-11-09 17:47:34 +01:00

276 lines
10 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 Navigation overlay (TOC + Bookmarks)"""
# Open navigation overlay from anywhere on screen
overlay_image = self.reader.open_navigation_overlay(active_tab="contents")
if overlay_image:
return GestureResponse(ActionType.OVERLAY_OPENED, {
"overlay_type": "navigation",
"active_tab": "contents",
"chapters": self.reader.get_chapters()
})
return GestureResponse(ActionType.NONE, {})
def _handle_swipe_down(self, y: int) -> GestureResponse:
"""Handle swipe down gesture - opens Settings overlay"""
# Open settings overlay from anywhere on screen
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, {})