Added ACC flipping of page
This commit is contained in:
parent
70c0b4a1f2
commit
a1775baa76
410
ACCELEROMETER_PAGE_FLIP.md
Normal file
410
ACCELEROMETER_PAGE_FLIP.md
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
# Accelerometer-Based Page Flipping
|
||||||
|
|
||||||
|
This document describes the accelerometer-based page flipping feature that allows users to navigate pages by tilting the device.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The accelerometer page flipping feature uses the BMA400 3-axis accelerometer to detect device tilt and automatically turn pages. This provides a hands-free way to read, which is useful when:
|
||||||
|
|
||||||
|
- Eating or drinking while reading
|
||||||
|
- Holding the device with one hand
|
||||||
|
- Device is mounted (e.g., on a stand)
|
||||||
|
- Accessibility needs
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
1. **Gesture Types** ([dreader/gesture.py](dreader/gesture.py:29-30))
|
||||||
|
- `TILT_FORWARD` - Tilt device forward to go to next page
|
||||||
|
- `TILT_BACKWARD` - Tilt device backward to go to previous page
|
||||||
|
|
||||||
|
2. **HAL Integration** ([dreader/hal_hardware.py](dreader/hal_hardware.py:414-563))
|
||||||
|
- `load_accelerometer_calibration()` - Loads calibration from JSON file
|
||||||
|
- `get_tilt_gesture()` - Polls accelerometer and detects tilt gestures
|
||||||
|
- Gravity direction calculation based on calibrated "up" vector
|
||||||
|
- Debouncing to prevent multiple page flips from single tilt
|
||||||
|
|
||||||
|
3. **Gesture Handlers** ([dreader/handlers/gestures.py](dreader/handlers/gestures.py:84-87))
|
||||||
|
- `TILT_FORWARD` → calls `_handle_page_forward()`
|
||||||
|
- `TILT_BACKWARD` → calls `_handle_page_back()`
|
||||||
|
- Uses same page navigation logic as swipe gestures
|
||||||
|
|
||||||
|
4. **Calibration Tool** ([examples/calibrate_accelerometer.py](examples/calibrate_accelerometer.py))
|
||||||
|
- Interactive calibration using display
|
||||||
|
- Shows live arrow pointing in gravity direction
|
||||||
|
- User rotates device until arrow points "up"
|
||||||
|
- Saves calibration to JSON file
|
||||||
|
|
||||||
|
5. **Demo Application** ([examples/demo_accelerometer_page_flip.py](examples/demo_accelerometer_page_flip.py))
|
||||||
|
- Complete integration example
|
||||||
|
- Combines touch and accelerometer gestures
|
||||||
|
- Shows how to poll both input sources
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Calibration
|
||||||
|
|
||||||
|
The calibration process establishes which direction is "up" for the device:
|
||||||
|
|
||||||
|
1. Run `python examples/calibrate_accelerometer.py`
|
||||||
|
2. Device displays an arrow showing gravity direction
|
||||||
|
3. Rotate device until arrow points up
|
||||||
|
4. Tap screen to save calibration
|
||||||
|
5. Calibration stored in `accelerometer_config.json`
|
||||||
|
|
||||||
|
**Calibration Data:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"up_vector": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 9.8,
|
||||||
|
"z": 0.0
|
||||||
|
},
|
||||||
|
"tilt_threshold": 0.3,
|
||||||
|
"debounce_time": 0.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tilt Detection Algorithm
|
||||||
|
|
||||||
|
The algorithm detects when the device is tilted beyond a threshold angle from the calibrated "up" position:
|
||||||
|
|
||||||
|
1. **Read Accelerometer**: Get (x, y, z) acceleration in m/s²
|
||||||
|
2. **Normalize Vectors**: Normalize both current gravity and calibrated up vector
|
||||||
|
3. **Calculate Tilt Angle**:
|
||||||
|
- Project gravity onto plane perpendicular to up vector
|
||||||
|
- Calculate angle using `atan2(perpendicular_magnitude, vertical_component)`
|
||||||
|
4. **Compare to Threshold**: Default 0.3 radians (~17 degrees)
|
||||||
|
5. **Determine Direction**:
|
||||||
|
- Positive perpendicular y-component → Forward tilt → Next page
|
||||||
|
- Negative perpendicular y-component → Backward tilt → Previous page
|
||||||
|
6. **Debounce**: Prevent repeated triggers within debounce time (default 0.5s)
|
||||||
|
|
||||||
|
**Math Details:**
|
||||||
|
|
||||||
|
Given:
|
||||||
|
- Up vector (calibrated): `U = (ux, uy, uz)`
|
||||||
|
- Current gravity: `G = (gx, gy, gz)`
|
||||||
|
|
||||||
|
Calculate:
|
||||||
|
```python
|
||||||
|
# Dot product: component of G along U
|
||||||
|
dot = gx*ux + gy*uy + gz*uz
|
||||||
|
|
||||||
|
# Perpendicular component
|
||||||
|
perp = G - dot*U
|
||||||
|
perp_magnitude = |perp|
|
||||||
|
|
||||||
|
# Tilt angle
|
||||||
|
angle = atan2(perp_magnitude, |dot|)
|
||||||
|
|
||||||
|
# Direction (simplified)
|
||||||
|
if perp_y > 0:
|
||||||
|
gesture = TILT_FORWARD
|
||||||
|
else:
|
||||||
|
gesture = TILT_BACKWARD
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Loop Integration
|
||||||
|
|
||||||
|
The main application event loop polls both touch and accelerometer:
|
||||||
|
|
||||||
|
```python
|
||||||
|
while running:
|
||||||
|
# Check touch events
|
||||||
|
touch_event = await hal.get_touch_event()
|
||||||
|
if touch_event:
|
||||||
|
handle_gesture(touch_event)
|
||||||
|
|
||||||
|
# Check accelerometer tilt (if calibrated)
|
||||||
|
if calibrated:
|
||||||
|
tilt_event = await hal.get_tilt_gesture()
|
||||||
|
if tilt_event:
|
||||||
|
handle_gesture(tilt_event)
|
||||||
|
|
||||||
|
await asyncio.sleep(0.05) # ~20Hz polling
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### 1. Calibration (One-time)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python examples/calibrate_accelerometer.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates `accelerometer_config.json` in the current directory.
|
||||||
|
|
||||||
|
### 2. Load Calibration in Your Application
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dreader.hal_hardware import HardwareDisplayHAL
|
||||||
|
|
||||||
|
# Create HAL with accelerometer enabled
|
||||||
|
hal = HardwareDisplayHAL(
|
||||||
|
width=1872,
|
||||||
|
height=1404,
|
||||||
|
enable_orientation=True # Important!
|
||||||
|
)
|
||||||
|
|
||||||
|
await hal.initialize()
|
||||||
|
|
||||||
|
# Load calibration
|
||||||
|
if hal.load_accelerometer_calibration("accelerometer_config.json"):
|
||||||
|
print("Accelerometer calibrated!")
|
||||||
|
else:
|
||||||
|
print("No calibration found - tilt gestures disabled")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Poll for Gestures
|
||||||
|
|
||||||
|
**Option A: Unified Event API (Recommended)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Main event loop - simplest approach
|
||||||
|
while True:
|
||||||
|
# Get event from any source (touch or accelerometer)
|
||||||
|
event = await hal.get_event()
|
||||||
|
|
||||||
|
if event:
|
||||||
|
response = gesture_router.handle_touch(event)
|
||||||
|
# ... process response
|
||||||
|
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Separate Polling (More Control)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Main event loop - explicit control
|
||||||
|
while True:
|
||||||
|
# Get touch events
|
||||||
|
touch_event = await hal.get_touch_event()
|
||||||
|
|
||||||
|
# Get tilt events (returns None if not calibrated)
|
||||||
|
tilt_event = await hal.get_tilt_gesture()
|
||||||
|
|
||||||
|
# Handle events
|
||||||
|
if touch_event:
|
||||||
|
response = gesture_router.handle_touch(touch_event)
|
||||||
|
# ... process response
|
||||||
|
|
||||||
|
if tilt_event:
|
||||||
|
response = gesture_router.handle_touch(tilt_event)
|
||||||
|
# ... process response
|
||||||
|
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Run Demo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simple demo using unified API
|
||||||
|
python examples/demo_accelerometer_simple.py ~/Books/mybook.epub
|
||||||
|
|
||||||
|
# Full-featured demo with separate polling
|
||||||
|
python examples/demo_accelerometer_page_flip.py ~/Books/mybook.epub
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Tilt Threshold
|
||||||
|
|
||||||
|
Adjust sensitivity by changing `tilt_threshold` in the config file:
|
||||||
|
|
||||||
|
- **0.1 rad (~6°)**: Very sensitive, small tilts trigger pages
|
||||||
|
- **0.3 rad (~17°)**: Default, moderate sensitivity
|
||||||
|
- **0.5 rad (~29°)**: Less sensitive, requires larger tilt
|
||||||
|
|
||||||
|
### Debounce Time
|
||||||
|
|
||||||
|
Adjust `debounce_time` to control how quickly you can trigger repeated page flips:
|
||||||
|
|
||||||
|
- **0.2s**: Fast, can quickly flip multiple pages
|
||||||
|
- **0.5s**: Default, prevents accidental double-flips
|
||||||
|
- **1.0s**: Slow, requires deliberate pauses between flips
|
||||||
|
|
||||||
|
### Example Custom Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"up_vector": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 9.8,
|
||||||
|
"z": 0.0
|
||||||
|
},
|
||||||
|
"tilt_threshold": 0.2,
|
||||||
|
"debounce_time": 0.3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run the test suite:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest tests/test_accelerometer_gestures.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tests include:**
|
||||||
|
- Calibration loading
|
||||||
|
- Tilt angle calculation (forward, backward, upright)
|
||||||
|
- Threshold detection
|
||||||
|
- Gesture type definitions
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Accelerometer calibration file not found"
|
||||||
|
|
||||||
|
Run the calibration script first:
|
||||||
|
```bash
|
||||||
|
python examples/calibrate_accelerometer.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tilt gestures not working
|
||||||
|
|
||||||
|
1. Check accelerometer is enabled in HAL:
|
||||||
|
```python
|
||||||
|
hal = HardwareDisplayHAL(enable_orientation=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Verify calibration loaded:
|
||||||
|
```python
|
||||||
|
result = hal.load_accelerometer_calibration()
|
||||||
|
print(f"Calibrated: {result}")
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Check you're polling tilt events:
|
||||||
|
```python
|
||||||
|
tilt_event = await hal.get_tilt_gesture()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tilt too sensitive / not sensitive enough
|
||||||
|
|
||||||
|
Edit `accelerometer_config.json` and adjust `tilt_threshold`:
|
||||||
|
- Lower value = more sensitive
|
||||||
|
- Higher value = less sensitive
|
||||||
|
|
||||||
|
### Pages flip too fast / too slow
|
||||||
|
|
||||||
|
Edit `accelerometer_config.json` and adjust `debounce_time`:
|
||||||
|
- Lower value = faster repeat flips
|
||||||
|
- Higher value = slower repeat flips
|
||||||
|
|
||||||
|
### Wrong direction (forward goes backward)
|
||||||
|
|
||||||
|
The tilt direction detection is device-specific. You may need to adjust the direction logic in [dreader/hal_hardware.py](dreader/hal_hardware.py:547-550):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Current logic (line 547)
|
||||||
|
if perp_y > 0:
|
||||||
|
gesture = AppGestureType.TILT_FORWARD
|
||||||
|
else:
|
||||||
|
gesture = AppGestureType.TILT_BACKWARD
|
||||||
|
|
||||||
|
# Try inverting:
|
||||||
|
if perp_y < 0: # Changed > to <
|
||||||
|
gesture = AppGestureType.TILT_FORWARD
|
||||||
|
else:
|
||||||
|
gesture = AppGestureType.TILT_BACKWARD
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
1. **Orientation Lock**: Tilt detection assumes fixed device orientation. Auto-rotation may interfere.
|
||||||
|
|
||||||
|
2. **Walking/Movement**: May trigger false positives when walking. Use higher threshold or disable while moving.
|
||||||
|
|
||||||
|
3. **Calibration Drift**: Accelerometer may drift over time. Re-calibrate periodically.
|
||||||
|
|
||||||
|
4. **Direction Heuristic**: Current direction detection is simplified. Complex orientations may not work correctly.
|
||||||
|
|
||||||
|
5. **Single Axis**: Only detects tilt in one plane. Doesn't distinguish left/right tilts.
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
- [ ] Shake gesture to open TOC/settings
|
||||||
|
- [ ] Multi-axis tilt for 4-direction navigation
|
||||||
|
- [ ] Auto-calibration on startup
|
||||||
|
- [ ] Gyroscope integration for rotation gestures
|
||||||
|
- [ ] Adaptive threshold based on reading posture
|
||||||
|
- [ ] Tilt gesture visualization for debugging
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### HardwareDisplayHAL
|
||||||
|
|
||||||
|
#### `load_accelerometer_calibration(config_path: str = "accelerometer_config.json") -> bool`
|
||||||
|
|
||||||
|
Load accelerometer calibration from JSON file.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `config_path`: Path to calibration JSON file
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `True` if calibration loaded successfully, `False` otherwise
|
||||||
|
|
||||||
|
#### `async get_event() -> Optional[TouchEvent]`
|
||||||
|
|
||||||
|
**[Recommended]** Get the next event from any input source (touch or accelerometer).
|
||||||
|
|
||||||
|
This is a convenience method that polls both touch and accelerometer in a single call.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `TouchEvent` from either touch sensor or accelerometer
|
||||||
|
- `None` if no event available
|
||||||
|
- Touch events are prioritized over tilt events
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
while running:
|
||||||
|
event = await hal.get_event()
|
||||||
|
if event:
|
||||||
|
handle_gesture(event)
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `async get_tilt_gesture() -> Optional[TouchEvent]`
|
||||||
|
|
||||||
|
Poll accelerometer and check for tilt gestures.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `TouchEvent` with `TILT_FORWARD` or `TILT_BACKWARD` gesture if tilt detected
|
||||||
|
- `None` if no tilt, not calibrated, or within debounce period
|
||||||
|
|
||||||
|
**Note:** Must call `load_accelerometer_calibration()` first. Consider using `get_event()` instead for simpler code.
|
||||||
|
|
||||||
|
### GestureType
|
||||||
|
|
||||||
|
#### `TILT_FORWARD = "tilt_forward"`
|
||||||
|
Gesture type for forward tilt (next page)
|
||||||
|
|
||||||
|
#### `TILT_BACKWARD = "tilt_backward"`
|
||||||
|
Gesture type for backward tilt (previous page)
|
||||||
|
|
||||||
|
### Calibration File Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"up_vector": {
|
||||||
|
"x": float, // X-component of gravity when upright (m/s²)
|
||||||
|
"y": float, // Y-component of gravity when upright (m/s²)
|
||||||
|
"z": float // Z-component of gravity when upright (m/s²)
|
||||||
|
},
|
||||||
|
"tilt_threshold": float, // Tilt angle threshold in radians
|
||||||
|
"debounce_time": float // Minimum time between gestures in seconds
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See the `examples/` directory for complete working examples:
|
||||||
|
|
||||||
|
- **[calibrate_accelerometer.py](examples/calibrate_accelerometer.py)** - Interactive calibration tool
|
||||||
|
- **[demo_accelerometer_simple.py](examples/demo_accelerometer_simple.py)** - Simple demo using unified `get_event()` API
|
||||||
|
- **[demo_accelerometer_page_flip.py](examples/demo_accelerometer_page_flip.py)** - Full-featured demo with separate event polling
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Same as the main DReader project.
|
||||||
@ -507,7 +507,28 @@ class EbookReader:
|
|||||||
Current font scale factor
|
Current font scale factor
|
||||||
"""
|
"""
|
||||||
return self.settings_manager.get_font_size()
|
return self.settings_manager.get_font_size()
|
||||||
|
|
||||||
|
def set_font_family(self, font_family) -> Optional[Image.Image]:
|
||||||
|
"""
|
||||||
|
Set the font family and re-render current page.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
font_family: BundledFont enum value (SERIF, SANS, MONOSPACE) or None for document default
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PIL Image of the re-rendered page
|
||||||
|
"""
|
||||||
|
return self.settings_manager.set_font_family(font_family)
|
||||||
|
|
||||||
|
def get_font_family(self):
|
||||||
|
"""
|
||||||
|
Get the current font family.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current BundledFont or None if using document default
|
||||||
|
"""
|
||||||
|
return self.settings_manager.get_font_family()
|
||||||
|
|
||||||
def set_line_spacing(self, spacing: int) -> Optional[Image.Image]:
|
def set_line_spacing(self, spacing: int) -> Optional[Image.Image]:
|
||||||
"""
|
"""
|
||||||
Set line spacing using pyWebLayout's native support.
|
Set line spacing using pyWebLayout's native support.
|
||||||
@ -989,6 +1010,8 @@ class EbookReader:
|
|||||||
line_spacing = self.page_style.line_spacing
|
line_spacing = self.page_style.line_spacing
|
||||||
inter_block_spacing = self.page_style.inter_block_spacing
|
inter_block_spacing = self.page_style.inter_block_spacing
|
||||||
word_spacing = self.page_style.word_spacing
|
word_spacing = self.page_style.word_spacing
|
||||||
|
font_family = self.get_font_family()
|
||||||
|
font_family_name = font_family.name if font_family else "Default"
|
||||||
|
|
||||||
# Use the Settings sub-application
|
# Use the Settings sub-application
|
||||||
overlay_subapp = self._overlay_subapps[OverlayState.SETTINGS]
|
overlay_subapp = self._overlay_subapps[OverlayState.SETTINGS]
|
||||||
@ -997,7 +1020,8 @@ class EbookReader:
|
|||||||
font_scale=font_scale,
|
font_scale=font_scale,
|
||||||
line_spacing=line_spacing,
|
line_spacing=line_spacing,
|
||||||
inter_block_spacing=inter_block_spacing,
|
inter_block_spacing=inter_block_spacing,
|
||||||
word_spacing=word_spacing
|
word_spacing=word_spacing,
|
||||||
|
font_family=font_family_name
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update state
|
# Update state
|
||||||
|
|||||||
@ -197,6 +197,7 @@ def generate_settings_overlay(
|
|||||||
line_spacing: int = 5,
|
line_spacing: int = 5,
|
||||||
inter_block_spacing: int = 15,
|
inter_block_spacing: int = 15,
|
||||||
word_spacing: int = 0,
|
word_spacing: int = 0,
|
||||||
|
font_family: str = "Default",
|
||||||
page_size: tuple = (800, 1200)
|
page_size: tuple = (800, 1200)
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
@ -210,6 +211,7 @@ def generate_settings_overlay(
|
|||||||
line_spacing: Current line spacing in pixels
|
line_spacing: Current line spacing in pixels
|
||||||
inter_block_spacing: Current inter-block spacing in pixels
|
inter_block_spacing: Current inter-block spacing in pixels
|
||||||
word_spacing: Current word spacing in pixels
|
word_spacing: Current word spacing in pixels
|
||||||
|
font_family: Current font family ("Default", "SERIF", "SANS", "MONOSPACE")
|
||||||
page_size: Page dimensions (width, height) for sizing the overlay
|
page_size: Page dimensions (width, height) for sizing the overlay
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -218,6 +220,15 @@ def generate_settings_overlay(
|
|||||||
# Format current values for display
|
# Format current values for display
|
||||||
font_percent = int(font_scale * 100)
|
font_percent = int(font_scale * 100)
|
||||||
|
|
||||||
|
# Map font family names to display names
|
||||||
|
font_display_names = {
|
||||||
|
"Default": "Document Default",
|
||||||
|
"SERIF": "Serif",
|
||||||
|
"SANS": "Sans-Serif",
|
||||||
|
"MONOSPACE": "Monospace"
|
||||||
|
}
|
||||||
|
font_family_display = font_display_names.get(font_family, font_family)
|
||||||
|
|
||||||
html = f'''
|
html = f'''
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
@ -237,6 +248,22 @@ def generate_settings_overlay(
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="margin: 15px 0;">
|
<div style="margin: 15px 0;">
|
||||||
|
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #6f42c1;">
|
||||||
|
<b>Font Family: {font_family_display}</b>
|
||||||
|
</p>
|
||||||
|
<p style="margin: 5px 0; background-color: #f0f0f0;">
|
||||||
|
<a href="setting:font_family_default" style="text-decoration: none; color: #000; display: block; padding: 12px;">Document Default</a>
|
||||||
|
</p>
|
||||||
|
<p style="margin: 5px 0; background-color: #f0f0f0;">
|
||||||
|
<a href="setting:font_family_serif" style="text-decoration: none; color: #000; display: block; padding: 12px;">Serif</a>
|
||||||
|
</p>
|
||||||
|
<p style="margin: 5px 0; background-color: #f0f0f0;">
|
||||||
|
<a href="setting:font_family_sans" style="text-decoration: none; color: #000; display: block; padding: 12px;">Sans-Serif</a>
|
||||||
|
</p>
|
||||||
|
<p style="margin: 5px 0; background-color: #f0f0f0;">
|
||||||
|
<a href="setting:font_family_monospace" style="text-decoration: none; color: #000; display: block; padding: 12px;">Monospace</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #007bff;">
|
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #007bff;">
|
||||||
<b>Font Size: {font_percent}%</b>
|
<b>Font Size: {font_percent}%</b>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -9,14 +9,16 @@ from typing import Dict, Any, Optional
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from pyWebLayout.layout.ereader_manager import EreaderLayoutManager
|
from pyWebLayout.layout.ereader_manager import EreaderLayoutManager
|
||||||
|
from pyWebLayout.style.fonts import BundledFont
|
||||||
|
|
||||||
|
|
||||||
class SettingsManager:
|
class SettingsManager:
|
||||||
"""
|
"""
|
||||||
Manages font size, spacing, and rendering settings.
|
Manages font size, spacing, font family, and rendering settings.
|
||||||
|
|
||||||
Responsibilities:
|
Responsibilities:
|
||||||
- Font scale adjustment
|
- Font scale adjustment
|
||||||
|
- Font family selection (serif, sans-serif, monospace)
|
||||||
- Line spacing control
|
- Line spacing control
|
||||||
- Inter-block spacing control
|
- Inter-block spacing control
|
||||||
- Word spacing control
|
- Word spacing control
|
||||||
@ -27,6 +29,7 @@ class SettingsManager:
|
|||||||
"""Initialize the settings manager."""
|
"""Initialize the settings manager."""
|
||||||
self.font_scale = 1.0
|
self.font_scale = 1.0
|
||||||
self.font_scale_step = 0.1 # 10% change per step
|
self.font_scale_step = 0.1 # 10% change per step
|
||||||
|
self.font_family: Optional[BundledFont] = None # None = use document default
|
||||||
self.manager: Optional[EreaderLayoutManager] = None
|
self.manager: Optional[EreaderLayoutManager] = None
|
||||||
|
|
||||||
def set_manager(self, manager: EreaderLayoutManager):
|
def set_manager(self, manager: EreaderLayoutManager):
|
||||||
@ -38,6 +41,7 @@ class SettingsManager:
|
|||||||
"""
|
"""
|
||||||
self.manager = manager
|
self.manager = manager
|
||||||
self.font_scale = manager.font_scale
|
self.font_scale = manager.font_scale
|
||||||
|
self.font_family = manager.get_font_family()
|
||||||
|
|
||||||
def set_font_size(self, scale: float) -> Optional[Image.Image]:
|
def set_font_size(self, scale: float) -> Optional[Image.Image]:
|
||||||
"""
|
"""
|
||||||
@ -89,6 +93,36 @@ class SettingsManager:
|
|||||||
"""
|
"""
|
||||||
return self.font_scale
|
return self.font_scale
|
||||||
|
|
||||||
|
def set_font_family(self, font_family: Optional[BundledFont]) -> Optional[Image.Image]:
|
||||||
|
"""
|
||||||
|
Set the font family and re-render current page.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
font_family: BundledFont enum value (SERIF, SANS, MONOSPACE) or None for document default
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Rendered page with new font family, or None if no manager
|
||||||
|
"""
|
||||||
|
if not self.manager:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.font_family = font_family
|
||||||
|
page = self.manager.set_font_family(font_family)
|
||||||
|
return page.render() if page else None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error setting font family: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_font_family(self) -> Optional[BundledFont]:
|
||||||
|
"""
|
||||||
|
Get the current font family.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current BundledFont or None if using document default
|
||||||
|
"""
|
||||||
|
return self.font_family
|
||||||
|
|
||||||
def set_line_spacing(self, spacing: int) -> Optional[Image.Image]:
|
def set_line_spacing(self, spacing: int) -> Optional[Image.Image]:
|
||||||
"""
|
"""
|
||||||
Set line spacing using pyWebLayout's native support.
|
Set line spacing using pyWebLayout's native support.
|
||||||
@ -195,6 +229,7 @@ class SettingsManager:
|
|||||||
if not self.manager:
|
if not self.manager:
|
||||||
return {
|
return {
|
||||||
'font_scale': self.font_scale,
|
'font_scale': self.font_scale,
|
||||||
|
'font_family': self.font_family.name if self.font_family else None,
|
||||||
'line_spacing': 5,
|
'line_spacing': 5,
|
||||||
'inter_block_spacing': 15,
|
'inter_block_spacing': 15,
|
||||||
'word_spacing': 0
|
'word_spacing': 0
|
||||||
@ -202,6 +237,7 @@ class SettingsManager:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'font_scale': self.font_scale,
|
'font_scale': self.font_scale,
|
||||||
|
'font_family': self.font_family.name if self.font_family else None,
|
||||||
'line_spacing': self.manager.page_style.line_spacing,
|
'line_spacing': self.manager.page_style.line_spacing,
|
||||||
'inter_block_spacing': self.manager.page_style.inter_block_spacing,
|
'inter_block_spacing': self.manager.page_style.inter_block_spacing,
|
||||||
'word_spacing': self.manager.page_style.word_spacing
|
'word_spacing': self.manager.page_style.word_spacing
|
||||||
@ -214,7 +250,7 @@ class SettingsManager:
|
|||||||
This should be called after loading a book to restore user preferences.
|
This should be called after loading a book to restore user preferences.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
settings: Dictionary with settings (font_scale, line_spacing, etc.)
|
settings: Dictionary with settings (font_scale, font_family, line_spacing, etc.)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if settings applied successfully, False otherwise
|
True if settings applied successfully, False otherwise
|
||||||
@ -223,6 +259,19 @@ class SettingsManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Apply font family
|
||||||
|
font_family_name = settings.get('font_family', None)
|
||||||
|
if font_family_name:
|
||||||
|
try:
|
||||||
|
font_family = BundledFont[font_family_name]
|
||||||
|
if font_family != self.font_family:
|
||||||
|
self.set_font_family(font_family)
|
||||||
|
except KeyError:
|
||||||
|
print(f"Warning: Unknown font family '{font_family_name}', using default")
|
||||||
|
elif font_family_name is None and self.font_family is not None:
|
||||||
|
# Restore to document default
|
||||||
|
self.set_font_family(None)
|
||||||
|
|
||||||
# Apply font scale
|
# Apply font scale
|
||||||
font_scale = settings.get('font_scale', 1.0)
|
font_scale = settings.get('font_scale', 1.0)
|
||||||
if font_scale != self.font_scale:
|
if font_scale != self.font_scale:
|
||||||
|
|||||||
@ -44,6 +44,7 @@ class SettingsOverlay(OverlaySubApplication):
|
|||||||
line_spacing: Current line spacing in pixels
|
line_spacing: Current line spacing in pixels
|
||||||
inter_block_spacing: Current inter-block spacing in pixels
|
inter_block_spacing: Current inter-block spacing in pixels
|
||||||
word_spacing: Current word spacing in pixels
|
word_spacing: Current word spacing in pixels
|
||||||
|
font_family: Current font family name (e.g., "SERIF", "SANS", "MONOSPACE", or None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Composited image with settings overlay
|
Composited image with settings overlay
|
||||||
@ -52,6 +53,7 @@ class SettingsOverlay(OverlaySubApplication):
|
|||||||
line_spacing = kwargs.get('line_spacing', 5)
|
line_spacing = kwargs.get('line_spacing', 5)
|
||||||
inter_block_spacing = kwargs.get('inter_block_spacing', 15)
|
inter_block_spacing = kwargs.get('inter_block_spacing', 15)
|
||||||
word_spacing = kwargs.get('word_spacing', 0)
|
word_spacing = kwargs.get('word_spacing', 0)
|
||||||
|
font_family = kwargs.get('font_family', 'Default')
|
||||||
|
|
||||||
# Calculate panel size (60% width, 70% height)
|
# Calculate panel size (60% width, 70% height)
|
||||||
panel_size = self._calculate_panel_size(0.6, 0.7)
|
panel_size = self._calculate_panel_size(0.6, 0.7)
|
||||||
@ -62,6 +64,7 @@ class SettingsOverlay(OverlaySubApplication):
|
|||||||
line_spacing=line_spacing,
|
line_spacing=line_spacing,
|
||||||
inter_block_spacing=inter_block_spacing,
|
inter_block_spacing=inter_block_spacing,
|
||||||
word_spacing=word_spacing,
|
word_spacing=word_spacing,
|
||||||
|
font_family=font_family,
|
||||||
page_size=panel_size
|
page_size=panel_size
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -132,7 +135,8 @@ class SettingsOverlay(OverlaySubApplication):
|
|||||||
font_scale: float,
|
font_scale: float,
|
||||||
line_spacing: int,
|
line_spacing: int,
|
||||||
inter_block_spacing: int,
|
inter_block_spacing: int,
|
||||||
word_spacing: int = 0) -> Image.Image:
|
word_spacing: int = 0,
|
||||||
|
font_family: str = "Default") -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Refresh the settings overlay with updated values and background page.
|
Refresh the settings overlay with updated values and background page.
|
||||||
|
|
||||||
@ -146,6 +150,7 @@ class SettingsOverlay(OverlaySubApplication):
|
|||||||
line_spacing: Updated line spacing
|
line_spacing: Updated line spacing
|
||||||
inter_block_spacing: Updated inter-block spacing
|
inter_block_spacing: Updated inter-block spacing
|
||||||
word_spacing: Updated word spacing
|
word_spacing: Updated word spacing
|
||||||
|
font_family: Updated font family
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Composited image with updated settings overlay
|
Composited image with updated settings overlay
|
||||||
@ -159,6 +164,7 @@ class SettingsOverlay(OverlaySubApplication):
|
|||||||
line_spacing=line_spacing,
|
line_spacing=line_spacing,
|
||||||
inter_block_spacing=inter_block_spacing,
|
inter_block_spacing=inter_block_spacing,
|
||||||
word_spacing=word_spacing,
|
word_spacing=word_spacing,
|
||||||
|
font_family=font_family,
|
||||||
page_size=panel_size
|
page_size=panel_size
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -177,16 +183,26 @@ class SettingsOverlay(OverlaySubApplication):
|
|||||||
Apply a setting change and refresh the overlay.
|
Apply a setting change and refresh the overlay.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
action: Setting action (e.g., "font_increase", "line_spacing_decrease")
|
action: Setting action (e.g., "font_increase", "line_spacing_decrease", "font_family_serif")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
GestureResponse with SETTING_CHANGED action
|
GestureResponse with SETTING_CHANGED action
|
||||||
"""
|
"""
|
||||||
|
from pyWebLayout.style.fonts import BundledFont
|
||||||
|
|
||||||
# Apply the setting change via reader
|
# Apply the setting change via reader
|
||||||
if action == "font_increase":
|
if action == "font_increase":
|
||||||
self.reader.increase_font_size()
|
self.reader.increase_font_size()
|
||||||
elif action == "font_decrease":
|
elif action == "font_decrease":
|
||||||
self.reader.decrease_font_size()
|
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":
|
elif action == "line_spacing_increase":
|
||||||
new_spacing = self.reader.page_style.line_spacing + 2
|
new_spacing = self.reader.page_style.line_spacing + 2
|
||||||
self.reader.set_line_spacing(new_spacing)
|
self.reader.set_line_spacing(new_spacing)
|
||||||
@ -211,18 +227,24 @@ class SettingsOverlay(OverlaySubApplication):
|
|||||||
page = self.reader.manager.get_current_page()
|
page = self.reader.manager.get_current_page()
|
||||||
updated_page = page.render()
|
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
|
# Refresh the settings overlay with updated values and page
|
||||||
self.refresh(
|
self.refresh(
|
||||||
updated_base_page=updated_page,
|
updated_base_page=updated_page,
|
||||||
font_scale=self.reader.base_font_scale,
|
font_scale=self.reader.base_font_scale,
|
||||||
line_spacing=self.reader.page_style.line_spacing,
|
line_spacing=self.reader.page_style.line_spacing,
|
||||||
inter_block_spacing=self.reader.page_style.inter_block_spacing,
|
inter_block_spacing=self.reader.page_style.inter_block_spacing,
|
||||||
word_spacing=self.reader.page_style.word_spacing
|
word_spacing=self.reader.page_style.word_spacing,
|
||||||
|
font_family=font_family_name
|
||||||
)
|
)
|
||||||
|
|
||||||
return GestureResponse(ActionType.SETTING_CHANGED, {
|
return GestureResponse(ActionType.SETTING_CHANGED, {
|
||||||
"action": action,
|
"action": action,
|
||||||
"font_scale": self.reader.base_font_scale,
|
"font_scale": self.reader.base_font_scale,
|
||||||
|
"font_family": font_family_name,
|
||||||
"line_spacing": self.reader.page_style.line_spacing,
|
"line_spacing": self.reader.page_style.line_spacing,
|
||||||
"inter_block_spacing": self.reader.page_style.inter_block_spacing,
|
"inter_block_spacing": self.reader.page_style.inter_block_spacing,
|
||||||
"word_spacing": self.reader.page_style.word_spacing
|
"word_spacing": self.reader.page_style.word_spacing
|
||||||
|
|||||||
96
examples/test_font_family.py
Executable file
96
examples/test_font_family.py
Executable file
@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to demonstrate font family setting functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pyWebLayout.style.fonts import BundledFont
|
||||||
|
from dreader.application import EbookReader
|
||||||
|
import os
|
||||||
|
|
||||||
|
def test_font_family():
|
||||||
|
"""Test the font family setting feature."""
|
||||||
|
# Initialize reader
|
||||||
|
reader = EbookReader(page_size=(600, 800), margin=20)
|
||||||
|
|
||||||
|
# Load a sample book
|
||||||
|
book_path = os.path.join(os.path.dirname(__file__), '..', 'examples', 'beowulf.epub')
|
||||||
|
|
||||||
|
if not os.path.exists(book_path):
|
||||||
|
print(f"Book not found at {book_path}")
|
||||||
|
print("Skipping book loading - testing with HTML instead...")
|
||||||
|
# Load a simple HTML document instead
|
||||||
|
sample_html = """
|
||||||
|
<html>
|
||||||
|
<head><title>Font Family Test</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Font Family Test Document</h1>
|
||||||
|
<p>This is a test document to demonstrate the font family setting feature.</p>
|
||||||
|
<p>The quick brown fox jumps over the lazy dog. 0123456789</p>
|
||||||
|
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
reader.load_html(sample_html, title="Font Family Test")
|
||||||
|
else:
|
||||||
|
print(f"Loading book from: {book_path}")
|
||||||
|
reader.load_epub(book_path)
|
||||||
|
|
||||||
|
# Get initial page
|
||||||
|
print("\n1. Rendering with default font family...")
|
||||||
|
page1 = reader.get_current_page()
|
||||||
|
print(f" Current font family: {reader.get_font_family()}")
|
||||||
|
|
||||||
|
# Switch to serif
|
||||||
|
print("\n2. Switching to SERIF font family...")
|
||||||
|
reader.set_font_family(BundledFont.SERIF)
|
||||||
|
page2 = reader.get_current_page()
|
||||||
|
print(f" Current font family: {reader.get_font_family()}")
|
||||||
|
|
||||||
|
# Switch to sans-serif
|
||||||
|
print("\n3. Switching to SANS font family...")
|
||||||
|
reader.set_font_family(BundledFont.SANS)
|
||||||
|
page3 = reader.get_current_page()
|
||||||
|
print(f" Current font family: {reader.get_font_family()}")
|
||||||
|
|
||||||
|
# Switch to monospace
|
||||||
|
print("\n4. Switching to MONOSPACE font family...")
|
||||||
|
reader.set_font_family(BundledFont.MONOSPACE)
|
||||||
|
page4 = reader.get_current_page()
|
||||||
|
print(f" Current font family: {reader.get_font_family()}")
|
||||||
|
|
||||||
|
# Restore original fonts
|
||||||
|
print("\n5. Restoring document default font family...")
|
||||||
|
reader.set_font_family(None)
|
||||||
|
page5 = reader.get_current_page()
|
||||||
|
print(f" Current font family: {reader.get_font_family()}")
|
||||||
|
|
||||||
|
# Test settings persistence
|
||||||
|
print("\n6. Testing settings persistence...")
|
||||||
|
reader.set_font_family(BundledFont.SERIF)
|
||||||
|
settings = reader.get_current_settings()
|
||||||
|
print(f" Settings: {settings}")
|
||||||
|
print(f" Font family in settings: {settings.get('font_family')}")
|
||||||
|
|
||||||
|
# Apply settings
|
||||||
|
print("\n7. Applying settings with MONOSPACE...")
|
||||||
|
new_settings = settings.copy()
|
||||||
|
new_settings['font_family'] = 'MONOSPACE'
|
||||||
|
reader.apply_settings(new_settings)
|
||||||
|
print(f" Current font family: {reader.get_font_family()}")
|
||||||
|
|
||||||
|
# Test with settings overlay
|
||||||
|
print("\n8. Opening settings overlay...")
|
||||||
|
overlay_image = reader.open_settings_overlay()
|
||||||
|
print(f" Settings overlay opened successfully: {overlay_image is not None}")
|
||||||
|
print(f" Settings overlay dimensions: {overlay_image.size if overlay_image else 'N/A'}")
|
||||||
|
|
||||||
|
print("\n✓ All font family tests passed!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
test_font_family()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n✗ Test failed with error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
Loading…
x
Reference in New Issue
Block a user