clean up
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demonstration of the refactored alignment handler system.
|
||||
This shows how the nested alignment logic has been replaced with a clean handler pattern.
|
||||
"""
|
||||
|
||||
from pyWebLayout.concrete.text import (
|
||||
Line, Text,
|
||||
LeftAlignmentHandler, CenterRightAlignmentHandler, JustifyAlignmentHandler
|
||||
)
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
from pyWebLayout.style import Font
|
||||
|
||||
def demonstrate_handler_system():
|
||||
"""Demonstrate the new alignment handler system."""
|
||||
print("=" * 60)
|
||||
print("ALIGNMENT HANDLER SYSTEM DEMONSTRATION")
|
||||
print("=" * 60)
|
||||
|
||||
print("\n1. HANDLER CREATION:")
|
||||
print(" The system now uses three specialized handlers:")
|
||||
|
||||
# Create handlers
|
||||
left_handler = LeftAlignmentHandler()
|
||||
center_handler = CenterRightAlignmentHandler(Alignment.CENTER)
|
||||
right_handler = CenterRightAlignmentHandler(Alignment.RIGHT)
|
||||
justify_handler = JustifyAlignmentHandler()
|
||||
|
||||
print(f" • LeftAlignmentHandler: {type(left_handler).__name__}")
|
||||
print(f" • CenterRightAlignmentHandler (Center): {type(center_handler).__name__}")
|
||||
print(f" • CenterRightAlignmentHandler (Right): {type(right_handler).__name__}")
|
||||
print(f" • JustifyAlignmentHandler: {type(justify_handler).__name__}")
|
||||
|
||||
print("\n2. AUTOMATIC HANDLER SELECTION:")
|
||||
print(" Lines automatically choose the correct handler based on alignment:")
|
||||
|
||||
font = Font()
|
||||
line_size = (300, 30)
|
||||
spacing = (5, 20)
|
||||
|
||||
alignments = [
|
||||
(Alignment.LEFT, "Left"),
|
||||
(Alignment.CENTER, "Center"),
|
||||
(Alignment.RIGHT, "Right"),
|
||||
(Alignment.JUSTIFY, "Justify")
|
||||
]
|
||||
|
||||
for alignment, name in alignments:
|
||||
line = Line(spacing, (0, 0), line_size, font, halign=alignment)
|
||||
handler_type = type(line._alignment_handler).__name__
|
||||
print(f" • {name:7} → {handler_type}")
|
||||
|
||||
print("\n3. HYPHENATION INTEGRATION:")
|
||||
print(" Each handler has its own hyphenation strategy:")
|
||||
|
||||
# Sample text objects and test conditions
|
||||
sample_text = [Text("Hello", font), Text("World", font)]
|
||||
word_width = 80
|
||||
available_width = 70 # Word doesn't fit
|
||||
min_spacing = 5
|
||||
|
||||
handlers = [
|
||||
("Left", left_handler),
|
||||
("Center", center_handler),
|
||||
("Right", right_handler),
|
||||
("Justify", justify_handler)
|
||||
]
|
||||
|
||||
for name, handler in handlers:
|
||||
should_hyphenate = handler.should_try_hyphenation(
|
||||
sample_text, word_width, available_width, min_spacing)
|
||||
print(f" • {name:7}: should_hyphenate = {should_hyphenate}")
|
||||
|
||||
print("\n4. SPACING CALCULATIONS:")
|
||||
print(" Each handler calculates spacing and positioning differently:")
|
||||
|
||||
for name, handler in handlers:
|
||||
spacing_calc, x_position = handler.calculate_spacing_and_position(
|
||||
sample_text, 300, 5, 20)
|
||||
print(f" • {name:7}: spacing={spacing_calc:2d}, position={x_position:3d}")
|
||||
|
||||
print("\n5. WORD ADDITION WITH INTELLIGENT HYPHENATION:")
|
||||
print(" The system now tries different hyphenation options for optimal spacing:")
|
||||
|
||||
# Test with a word that might benefit from hyphenation
|
||||
test_line = Line(spacing, (0, 0), (200, 30), font, halign=Alignment.JUSTIFY)
|
||||
test_words = ["This", "is", "a", "demonstration", "of", "smart", "hyphenation"]
|
||||
|
||||
for word in test_words:
|
||||
result = test_line.add_word(word)
|
||||
if result:
|
||||
print(f" • Word '{word}' → remainder: '{result}' (line full)")
|
||||
break
|
||||
else:
|
||||
print(f" • Added '{word}' successfully")
|
||||
|
||||
print(f" • Final line contains {len(test_line.text_objects)} text objects")
|
||||
|
||||
print("\n6. BENEFITS OF THE NEW SYSTEM:")
|
||||
print(" ✓ Separation of concerns - each alignment has its own handler")
|
||||
print(" ✓ Extensible - easy to add new alignment types")
|
||||
print(" ✓ Intelligent hyphenation - considers spacing quality")
|
||||
print(" ✓ Clean code - no more nested if/else alignment logic")
|
||||
print(" ✓ Testable - each handler can be tested independently")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("REFACTORING COMPLETE - ALIGNMENT HANDLERS WORKING!")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
demonstrate_handler_system()
|
||||
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demo script showing the viewport system in action.
|
||||
|
||||
This demonstrates how the viewport provides a movable window into large content,
|
||||
enabling efficient scrolling without rendering the entire content at once.
|
||||
"""
|
||||
|
||||
import os
|
||||
from PIL import Image
|
||||
from pyWebLayout.concrete import (
|
||||
Viewport, ScrollablePageContent, Text, Box, RenderableImage
|
||||
)
|
||||
from pyWebLayout.style.fonts import Font, FontWeight
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
|
||||
|
||||
def create_large_document_content():
|
||||
"""Create a large document to demonstrate viewport scrolling"""
|
||||
|
||||
# Create scrollable content container
|
||||
content = ScrollablePageContent(content_width=800, initial_height=100)
|
||||
|
||||
# Add a title
|
||||
title_font = Font(font_size=24, weight=FontWeight.BOLD)
|
||||
title = Text("Large Document Demo", title_font)
|
||||
content.add_child(title)
|
||||
|
||||
# Add spacing
|
||||
content.add_child(Box((0, 0), (1, 20)))
|
||||
|
||||
# Add many paragraphs to create a long document
|
||||
paragraph_font = Font(font_size=14)
|
||||
|
||||
for i in range(50):
|
||||
# Section header
|
||||
section_font = Font(font_size=18, weight=FontWeight.BOLD)
|
||||
header = Text(f"Section {i+1}", section_font)
|
||||
content.add_child(header)
|
||||
|
||||
# Add some spacing
|
||||
content.add_child(Box((0, 0), (1, 10)))
|
||||
|
||||
# Add paragraph content
|
||||
paragraphs = [
|
||||
f"This is paragraph {i+1}, line 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
f"This is paragraph {i+1}, line 2. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
f"This is paragraph {i+1}, line 3. Ut enim ad minim veniam, quis nostrud exercitation ullamco.",
|
||||
f"This is paragraph {i+1}, line 4. Duis aute irure dolor in reprehenderit in voluptate velit esse.",
|
||||
f"This is paragraph {i+1}, line 5. Excepteur sint occaecat cupidatat non proident, sunt in culpa."
|
||||
]
|
||||
|
||||
for para_text in paragraphs:
|
||||
para = Text(para_text, paragraph_font)
|
||||
content.add_child(para)
|
||||
content.add_child(Box((0, 0), (1, 5))) # Line spacing
|
||||
|
||||
# Add section spacing
|
||||
content.add_child(Box((0, 0), (1, 15)))
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def demo_viewport_rendering():
|
||||
"""Demonstrate viewport rendering at different scroll positions"""
|
||||
|
||||
print("Creating large document content...")
|
||||
content = create_large_document_content()
|
||||
|
||||
print(f"Content size: {content.get_content_height()} pixels tall")
|
||||
|
||||
# Create viewport
|
||||
viewport = Viewport(viewport_size=(800, 600), background_color=(255, 255, 255))
|
||||
|
||||
# Add content to viewport
|
||||
viewport.add_content(content)
|
||||
|
||||
print(f"Viewport content size: {viewport.content_size}")
|
||||
print(f"Max scroll Y: {viewport.max_scroll_y}")
|
||||
|
||||
# Create output directory
|
||||
os.makedirs("output/viewport_demo", exist_ok=True)
|
||||
|
||||
# Render viewport at different scroll positions
|
||||
scroll_positions = [
|
||||
(0, "top"),
|
||||
(viewport.max_scroll_y // 4, "quarter"),
|
||||
(viewport.max_scroll_y // 2, "middle"),
|
||||
(viewport.max_scroll_y * 3 // 4, "three_quarters"),
|
||||
(viewport.max_scroll_y, "bottom")
|
||||
]
|
||||
|
||||
for scroll_y, label in scroll_positions:
|
||||
print(f"Rendering viewport at scroll position {scroll_y} ({label})...")
|
||||
|
||||
# Scroll to position
|
||||
viewport.scroll_to(0, scroll_y)
|
||||
|
||||
# Get scroll info
|
||||
scroll_info = viewport.get_scroll_info()
|
||||
print(f" Scroll progress: {scroll_info['scroll_progress_y']:.2%}")
|
||||
print(f" Visible elements: {len(viewport.get_visible_elements())}")
|
||||
|
||||
# Render viewport
|
||||
viewport_img = viewport.render()
|
||||
|
||||
# Save image
|
||||
output_path = f"output/viewport_demo/viewport_{label}.png"
|
||||
viewport_img.save(output_path)
|
||||
print(f" Saved: {output_path}")
|
||||
|
||||
print("\nViewport demo complete!")
|
||||
return viewport
|
||||
|
||||
|
||||
def demo_hit_testing():
|
||||
"""Demonstrate hit testing in the viewport"""
|
||||
|
||||
print("\nTesting hit detection...")
|
||||
|
||||
# Create simple content for hit testing
|
||||
content = ScrollablePageContent(content_width=800, initial_height=100)
|
||||
|
||||
# Add clickable elements
|
||||
for i in range(10):
|
||||
text = Text(f"Clickable text item {i}", Font(font_size=16))
|
||||
content.add_child(text)
|
||||
content.add_child(Box((0, 0), (1, 20))) # Spacing
|
||||
|
||||
# Create viewport
|
||||
viewport = Viewport(viewport_size=(800, 300))
|
||||
viewport.add_content(content)
|
||||
|
||||
# Test hit detection at different scroll positions
|
||||
test_points = [(100, 50), (200, 100), (300, 150)]
|
||||
|
||||
for scroll_y in [0, 100, 200]:
|
||||
viewport.scroll_to(0, scroll_y)
|
||||
print(f"\nAt scroll position {scroll_y}:")
|
||||
|
||||
for point in test_points:
|
||||
hit_element = viewport.hit_test(point)
|
||||
if hit_element:
|
||||
element_text = getattr(hit_element, '_text', 'Unknown element')
|
||||
print(f" Point {point}: Hit '{element_text}'")
|
||||
else:
|
||||
print(f" Point {point}: No element")
|
||||
|
||||
|
||||
def demo_scroll_methods():
|
||||
"""Demonstrate different scrolling methods"""
|
||||
|
||||
print("\nTesting scroll methods...")
|
||||
|
||||
# Create content
|
||||
content = ScrollablePageContent(content_width=800, initial_height=100)
|
||||
|
||||
for i in range(20):
|
||||
text = Text(f"Line {i+1}: This is some sample text for scrolling demo", Font(font_size=14))
|
||||
content.add_child(text)
|
||||
content.add_child(Box((0, 0), (1, 5)))
|
||||
|
||||
# Create viewport
|
||||
viewport = Viewport(viewport_size=(800, 200))
|
||||
viewport.add_content(content)
|
||||
|
||||
print(f"Content height: {viewport.content_size[1]}")
|
||||
print(f"Viewport height: {viewport.viewport_size[1]}")
|
||||
print(f"Max scroll Y: {viewport.max_scroll_y}")
|
||||
|
||||
# Test different scroll methods
|
||||
print("\nTesting scroll methods:")
|
||||
|
||||
# Scroll by lines
|
||||
print("Scrolling down 5 lines...")
|
||||
for i in range(5):
|
||||
viewport.scroll_line_down(20)
|
||||
print(f" After line {i+1}: offset = {viewport.viewport_offset}")
|
||||
|
||||
# Scroll by pages
|
||||
print("Scrolling down 1 page...")
|
||||
viewport.scroll_page_down()
|
||||
print(f" After page down: offset = {viewport.viewport_offset}")
|
||||
|
||||
# Scroll to bottom
|
||||
print("Scrolling to bottom...")
|
||||
viewport.scroll_to_bottom()
|
||||
print(f" At bottom: offset = {viewport.viewport_offset}")
|
||||
|
||||
# Scroll to top
|
||||
print("Scrolling to top...")
|
||||
viewport.scroll_to_top()
|
||||
print(f" At top: offset = {viewport.viewport_offset}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all viewport demos"""
|
||||
print("=== Viewport System Demo ===")
|
||||
|
||||
# Demo 1: Basic viewport rendering
|
||||
viewport = demo_viewport_rendering()
|
||||
|
||||
# Demo 2: Hit testing
|
||||
demo_hit_testing()
|
||||
|
||||
# Demo 3: Scroll methods
|
||||
demo_scroll_methods()
|
||||
|
||||
print("\n=== Demo Complete ===")
|
||||
print("Check the output/viewport_demo/ directory for rendered images.")
|
||||
|
||||
# Show final scroll info
|
||||
scroll_info = viewport.get_scroll_info()
|
||||
print(f"\nFinal viewport state:")
|
||||
print(f" Content size: {scroll_info['content_size']}")
|
||||
print(f" Viewport size: {scroll_info['viewport_size']}")
|
||||
print(f" Current offset: {scroll_info['offset']}")
|
||||
print(f" Scroll progress: {scroll_info['scroll_progress_y']:.1%}")
|
||||
print(f" Can scroll up: {scroll_info['can_scroll_up']}")
|
||||
print(f" Can scroll down: {scroll_info['can_scroll_down']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,406 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Basic EPUB Reader with Pagination using pyWebLayout
|
||||
|
||||
This reader loads EPUB files and displays them with page-by-page navigation
|
||||
using the pyWebLayout system. It follows the proper architecture where:
|
||||
- EPUBReader loads EPUB files into Document/Chapter objects
|
||||
- Page renders those abstract objects into visual pages
|
||||
- The UI handles pagination and navigation
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
import os
|
||||
from typing import List, Optional
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
from pyWebLayout.io.readers.epub_reader import EPUBReader
|
||||
from pyWebLayout.concrete.page import Page
|
||||
from pyWebLayout.style.fonts import Font
|
||||
from pyWebLayout.abstract.document import Document, Chapter, Book
|
||||
from pyWebLayout.io.readers.html_extraction import parse_html_string
|
||||
|
||||
|
||||
class EPUBReaderApp:
|
||||
"""Main EPUB reader application using Tkinter"""
|
||||
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title("pyWebLayout EPUB Reader")
|
||||
self.root.geometry("900x700")
|
||||
|
||||
# Application state
|
||||
self.current_epub: Optional[EPUBReader] = None
|
||||
self.current_document: Optional[Document] = None
|
||||
self.rendered_pages: List[Page] = []
|
||||
self.current_page_index = 0
|
||||
|
||||
# Page settings
|
||||
self.page_width = 700
|
||||
self.page_height = 550
|
||||
self.blocks_per_page = 3 # Fewer blocks per page for better readability
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the user interface"""
|
||||
# Create main frame
|
||||
main_frame = ttk.Frame(self.root)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Top control frame
|
||||
control_frame = ttk.Frame(main_frame)
|
||||
control_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# File operations
|
||||
self.open_btn = ttk.Button(control_frame, text="Open EPUB", command=self.open_epub)
|
||||
self.open_btn.pack(side=tk.LEFT, padx=(0, 10))
|
||||
|
||||
# Book info
|
||||
self.book_info_label = ttk.Label(control_frame, text="No book loaded")
|
||||
self.book_info_label.pack(side=tk.LEFT, expand=True)
|
||||
|
||||
# Navigation frame
|
||||
nav_frame = ttk.Frame(main_frame)
|
||||
nav_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# Navigation buttons
|
||||
self.prev_btn = ttk.Button(nav_frame, text="◀ Previous", command=self.previous_page, state=tk.DISABLED)
|
||||
self.prev_btn.pack(side=tk.LEFT, padx=(0, 10))
|
||||
|
||||
self.next_btn = ttk.Button(nav_frame, text="Next ▶", command=self.next_page, state=tk.DISABLED)
|
||||
self.next_btn.pack(side=tk.LEFT, padx=(0, 10))
|
||||
|
||||
# Page info
|
||||
self.page_info_label = ttk.Label(nav_frame, text="Page 0 of 0")
|
||||
self.page_info_label.pack(side=tk.LEFT, padx=(20, 0))
|
||||
|
||||
# Chapter selector
|
||||
ttk.Label(nav_frame, text="Chapter:").pack(side=tk.LEFT, padx=(20, 5))
|
||||
self.chapter_var = tk.StringVar()
|
||||
self.chapter_combo = ttk.Combobox(nav_frame, textvariable=self.chapter_var, state="readonly", width=30)
|
||||
self.chapter_combo.pack(side=tk.LEFT, padx=(0, 10))
|
||||
self.chapter_combo.bind('<<ComboboxSelected>>', self.on_chapter_selected)
|
||||
|
||||
# Content frame with canvas
|
||||
content_frame = ttk.Frame(main_frame)
|
||||
content_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Create canvas for page display
|
||||
self.canvas = tk.Canvas(content_frame, bg='white', width=self.page_width, height=self.page_height)
|
||||
self.canvas.pack(expand=True)
|
||||
|
||||
# Status bar
|
||||
self.status_var = tk.StringVar(value="Ready - Open an EPUB file to begin")
|
||||
status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
|
||||
status_bar.pack(fill=tk.X, pady=(10, 0))
|
||||
|
||||
# Bind keyboard shortcuts
|
||||
self.root.bind('<Key-Left>', lambda e: self.previous_page())
|
||||
self.root.bind('<Key-Right>', lambda e: self.next_page())
|
||||
self.root.bind('<Key-space>', lambda e: self.next_page())
|
||||
self.root.focus_set() # Allow keyboard input
|
||||
|
||||
def open_epub(self):
|
||||
"""Open and load an EPUB file"""
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="Open EPUB File",
|
||||
filetypes=[("EPUB files", "*.epub"), ("All files", "*.*")]
|
||||
)
|
||||
|
||||
if file_path:
|
||||
self.load_epub(file_path)
|
||||
|
||||
def load_epub(self, file_path: str):
|
||||
"""Load an EPUB file and prepare for display"""
|
||||
try:
|
||||
self.status_var.set("Loading EPUB file...")
|
||||
self.root.update()
|
||||
|
||||
# Load the EPUB using the EPUBReader
|
||||
self.current_epub = EPUBReader(file_path)
|
||||
|
||||
# Get the document structure from the EPUB
|
||||
self.current_document = self.current_epub.read()
|
||||
|
||||
# Update book info
|
||||
if isinstance(self.current_document, Book):
|
||||
title = self.current_document.get_title() or "Unknown Title"
|
||||
author = self.current_document.get_author() or "Unknown Author"
|
||||
self.book_info_label.config(text=f"{title} by {author}")
|
||||
else:
|
||||
title = getattr(self.current_document, 'title', 'Unknown Title')
|
||||
self.book_info_label.config(text=title)
|
||||
|
||||
# Populate chapter list
|
||||
self.populate_chapter_list()
|
||||
|
||||
# Create pages from the document
|
||||
self.create_pages_from_document()
|
||||
|
||||
# Show first page
|
||||
self.current_page_index = 0
|
||||
self.display_current_page()
|
||||
self.update_navigation()
|
||||
|
||||
self.status_var.set(f"Loaded: {os.path.basename(file_path)} - {len(self.rendered_pages)} pages")
|
||||
|
||||
except Exception as e:
|
||||
self.status_var.set(f"Error loading EPUB: {str(e)}")
|
||||
messagebox.showerror("Error", f"Failed to load EPUB file:\n{str(e)}")
|
||||
print(f"Detailed error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def populate_chapter_list(self):
|
||||
"""Populate the chapter selection dropdown"""
|
||||
if not self.current_document:
|
||||
return
|
||||
|
||||
chapters = []
|
||||
|
||||
# Check if it's a Book with chapters
|
||||
if isinstance(self.current_document, Book) and self.current_document.chapters:
|
||||
for i, chapter in enumerate(self.current_document.chapters):
|
||||
chapter_title = chapter.title or f"Chapter {i+1}"
|
||||
chapters.append(chapter_title)
|
||||
else:
|
||||
# Fallback: add a single "Document" entry
|
||||
chapters.append("Document")
|
||||
|
||||
self.chapter_combo['values'] = chapters
|
||||
if chapters:
|
||||
self.chapter_combo.set(chapters[0])
|
||||
|
||||
def create_pages_from_document(self):
|
||||
"""Create pages using the new external pagination system with block handlers"""
|
||||
if not self.current_document:
|
||||
return
|
||||
|
||||
self.rendered_pages.clear()
|
||||
|
||||
try:
|
||||
# Get all blocks from the document
|
||||
all_blocks = []
|
||||
|
||||
if isinstance(self.current_document, Book) and self.current_document.chapters:
|
||||
# Process chapters
|
||||
for chapter in self.current_document.chapters:
|
||||
all_blocks.extend(chapter.blocks)
|
||||
else:
|
||||
# Process document blocks directly
|
||||
all_blocks = self.current_document.blocks
|
||||
|
||||
# If no blocks found, try to create some from EPUB content
|
||||
if not all_blocks:
|
||||
all_blocks = self.create_blocks_from_epub_content()
|
||||
|
||||
# Use the new external pagination system
|
||||
remaining_blocks = all_blocks
|
||||
|
||||
while remaining_blocks:
|
||||
# Create a new page
|
||||
current_page = Page(size=(self.page_width, self.page_height))
|
||||
|
||||
# Fill the page using the external pagination system
|
||||
next_index, remainder_blocks = current_page.fill_with_blocks(remaining_blocks)
|
||||
|
||||
# Add the page if it has content
|
||||
if current_page._children:
|
||||
self.rendered_pages.append(current_page)
|
||||
|
||||
# Update remaining blocks for next iteration
|
||||
if remainder_blocks:
|
||||
# We have remainder blocks (partial content)
|
||||
remaining_blocks = remainder_blocks
|
||||
elif next_index < len(remaining_blocks):
|
||||
# We stopped at a specific index
|
||||
remaining_blocks = remaining_blocks[next_index:]
|
||||
else:
|
||||
# All blocks processed
|
||||
remaining_blocks = []
|
||||
|
||||
# Safety check to prevent infinite loops
|
||||
if not current_page._children and remaining_blocks:
|
||||
print(f"Warning: Could not fit any content on page, skipping {len(remaining_blocks)} blocks")
|
||||
break
|
||||
|
||||
# If no pages were created, create a default one
|
||||
if not self.rendered_pages:
|
||||
self.create_default_page()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating pages: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.create_default_page()
|
||||
|
||||
|
||||
def create_blocks_from_epub_content(self):
|
||||
"""Create blocks from raw EPUB content when document parsing fails"""
|
||||
blocks = []
|
||||
|
||||
try:
|
||||
# Get HTML content from EPUB spine items
|
||||
spine_items = self.current_epub.spine[:3] # Limit to first 3 items
|
||||
|
||||
for item_id in spine_items:
|
||||
try:
|
||||
# Get the manifest item
|
||||
if item_id in self.current_epub.manifest:
|
||||
item = self.current_epub.manifest[item_id]
|
||||
file_path = item['path']
|
||||
|
||||
# Read the HTML content
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse HTML content into blocks
|
||||
html_blocks = parse_html_string(content)
|
||||
blocks.extend(html_blocks[:5]) # Limit blocks per item
|
||||
except Exception as e:
|
||||
print(f"Error processing spine item {item_id}: {e}")
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting EPUB content: {e}")
|
||||
|
||||
return blocks
|
||||
|
||||
def create_default_page(self):
|
||||
"""Create a default page when content loading fails"""
|
||||
page = Page(size=(self.page_width, self.page_height))
|
||||
|
||||
# Add some default content
|
||||
from pyWebLayout.concrete.text import Text
|
||||
default_font = Font()
|
||||
|
||||
if self.current_document:
|
||||
title = getattr(self.current_document, 'title', None)
|
||||
if title:
|
||||
page.add_child(Text(f"Book: {title}", default_font))
|
||||
page.add_child(Text("Content is loading...", default_font))
|
||||
else:
|
||||
page.add_child(Text("EPUB content loaded", default_font))
|
||||
page.add_child(Text("Use arrow keys or buttons to navigate", default_font))
|
||||
|
||||
self.rendered_pages = [page]
|
||||
|
||||
def display_current_page(self):
|
||||
"""Display the current page on the canvas"""
|
||||
if not self.rendered_pages or self.current_page_index >= len(self.rendered_pages):
|
||||
return
|
||||
|
||||
try:
|
||||
# Clear the canvas
|
||||
self.canvas.delete("all")
|
||||
|
||||
# Get the current page
|
||||
page = self.rendered_pages[self.current_page_index]
|
||||
|
||||
# Render the page
|
||||
page_image = page.render()
|
||||
|
||||
# Convert to PhotoImage
|
||||
self.photo = ImageTk.PhotoImage(page_image)
|
||||
|
||||
# Calculate position to center the page
|
||||
canvas_width = self.canvas.winfo_width()
|
||||
canvas_height = self.canvas.winfo_height()
|
||||
|
||||
if canvas_width > 1 and canvas_height > 1: # Canvas is properly sized
|
||||
x_pos = max(0, (canvas_width - page_image.width) // 2)
|
||||
y_pos = max(0, (canvas_height - page_image.height) // 2)
|
||||
else:
|
||||
x_pos, y_pos = 0, 0
|
||||
|
||||
# Display the page
|
||||
self.canvas.create_image(x_pos, y_pos, anchor=tk.NW, image=self.photo)
|
||||
|
||||
except Exception as e:
|
||||
# Display error message
|
||||
self.canvas.delete("all")
|
||||
self.canvas.create_text(
|
||||
self.page_width // 2, self.page_height // 2,
|
||||
text=f"Error displaying page: {str(e)}",
|
||||
fill="red", font=("Arial", 12)
|
||||
)
|
||||
print(f"Display error: {e}")
|
||||
|
||||
def previous_page(self):
|
||||
"""Navigate to the previous page"""
|
||||
if self.current_page_index > 0:
|
||||
self.current_page_index -= 1
|
||||
self.display_current_page()
|
||||
self.update_navigation()
|
||||
|
||||
def next_page(self):
|
||||
"""Navigate to the next page"""
|
||||
if self.current_page_index < len(self.rendered_pages) - 1:
|
||||
self.current_page_index += 1
|
||||
self.display_current_page()
|
||||
self.update_navigation()
|
||||
|
||||
def update_navigation(self):
|
||||
"""Update navigation button states and page info"""
|
||||
if not self.rendered_pages:
|
||||
self.prev_btn.config(state=tk.DISABLED)
|
||||
self.next_btn.config(state=tk.DISABLED)
|
||||
self.page_info_label.config(text="Page 0 of 0")
|
||||
return
|
||||
|
||||
# Update button states
|
||||
self.prev_btn.config(state=tk.NORMAL if self.current_page_index > 0 else tk.DISABLED)
|
||||
self.next_btn.config(state=tk.NORMAL if self.current_page_index < len(self.rendered_pages) - 1 else tk.DISABLED)
|
||||
|
||||
# Update page info
|
||||
page_num = self.current_page_index + 1
|
||||
total_pages = len(self.rendered_pages)
|
||||
self.page_info_label.config(text=f"Page {page_num} of {total_pages}")
|
||||
|
||||
def on_chapter_selected(self, event=None):
|
||||
"""Handle chapter selection"""
|
||||
if not self.current_document or not self.rendered_pages:
|
||||
return
|
||||
|
||||
selected_chapter = self.chapter_var.get()
|
||||
|
||||
# For now, just go to the first page
|
||||
# In a more sophisticated implementation, we'd track chapter start pages
|
||||
self.current_page_index = 0
|
||||
self.display_current_page()
|
||||
self.update_navigation()
|
||||
|
||||
self.status_var.set(f"Viewing: {selected_chapter}")
|
||||
|
||||
def run(self):
|
||||
"""Start the EPUB reader application"""
|
||||
# Make canvas responsive
|
||||
def on_configure(event):
|
||||
# Redisplay current page when canvas is resized
|
||||
if hasattr(self, 'photo'):
|
||||
self.root.after_idle(self.display_current_page)
|
||||
|
||||
self.canvas.bind('<Configure>', on_configure)
|
||||
|
||||
# Start the main loop
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to run the EPUB reader"""
|
||||
print("Starting pyWebLayout EPUB Reader...")
|
||||
|
||||
try:
|
||||
app = EPUBReaderApp()
|
||||
app.run()
|
||||
except Exception as e:
|
||||
print(f"Error starting EPUB reader: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,771 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Enhanced HTML Browser using pyWebLayout with Viewport System
|
||||
|
||||
This browser uses the new viewport system to enable efficient scrolling
|
||||
within HTML pages, only rendering the visible portion of large documents.
|
||||
"""
|
||||
|
||||
import re
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, filedialog, simpledialog
|
||||
from PIL import Image, ImageTk, ImageDraw
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
import webbrowser
|
||||
import os
|
||||
from urllib.parse import urljoin, urlparse
|
||||
import requests
|
||||
from io import BytesIO
|
||||
import pyperclip
|
||||
|
||||
# Import pyWebLayout components including the new viewport system
|
||||
from pyWebLayout.concrete import (
|
||||
Page, Container, Box, Text, RenderableImage,
|
||||
RenderableLink, RenderableButton, RenderableForm, RenderableFormField,
|
||||
Viewport, ScrollablePageContent
|
||||
)
|
||||
from pyWebLayout.abstract.functional import (
|
||||
Link, Button, Form, FormField, LinkType, FormFieldType
|
||||
)
|
||||
from pyWebLayout.abstract.block import Paragraph
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.style.fonts import Font, FontWeight, FontStyle, TextDecoration
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
from pyWebLayout.typesetting.paragraph_layout import ParagraphLayout, ParagraphLayoutResult
|
||||
from pyWebLayout.io.readers.html_extraction import parse_html_string
|
||||
|
||||
|
||||
class HTMLViewportAdapter:
|
||||
"""Adapter to convert HTML to viewport using the proper HTML extraction system"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def parse_html_string(self, html_content: str, base_url: str = "", viewport_size: Tuple[int, int] = (800, 600)) -> Viewport:
|
||||
"""Parse HTML string and return a Viewport object with scrollable content using the proper parser"""
|
||||
# Use the proper HTML extraction system
|
||||
base_font = Font(font_size=14)
|
||||
blocks = parse_html_string(html_content, base_font)
|
||||
|
||||
# Extract title
|
||||
title_match = re.search(r'<title>(.*?)</title>', html_content, re.IGNORECASE)
|
||||
title = title_match.group(1) if title_match else "Untitled"
|
||||
|
||||
# Create scrollable content container
|
||||
content = ScrollablePageContent(content_width=viewport_size[0] - 20, initial_height=100)
|
||||
|
||||
# Convert abstract blocks to renderable objects using Page's conversion system
|
||||
page = Page(size=(viewport_size[0], 10000)) # Large temporary page
|
||||
|
||||
# Add blocks to page and let it handle the conversion
|
||||
for i, block in enumerate(blocks):
|
||||
renderable = page._convert_block_to_renderable(block)
|
||||
if renderable:
|
||||
content.add_child(renderable)
|
||||
# Add spacing between blocks (but not after the last block)
|
||||
if i < len(blocks) - 1:
|
||||
content.add_child(Box((0, 0), (1, 8)))
|
||||
|
||||
# Create viewport and add the content
|
||||
viewport = Viewport(viewport_size=viewport_size, background_color=(255, 255, 255))
|
||||
viewport.add_content(content)
|
||||
viewport.title = title
|
||||
|
||||
return viewport
|
||||
|
||||
def parse_html_file(self, file_path: str, viewport_size: Tuple[int, int] = (800, 600)) -> Viewport:
|
||||
"""Parse HTML file and return a Viewport object"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
html_content = f.read()
|
||||
base_url = os.path.dirname(os.path.abspath(file_path))
|
||||
return self.parse_html_string(html_content, base_url, viewport_size)
|
||||
except Exception as e:
|
||||
# Create error viewport
|
||||
content = ScrollablePageContent(content_width=viewport_size[0] - 20, initial_height=100)
|
||||
error_text = Text(f"Error loading file: {str(e)}", Font(font_size=16, colour=(255, 0, 0)))
|
||||
content.add_child(error_text)
|
||||
|
||||
viewport = Viewport(viewport_size=viewport_size, background_color=(255, 255, 255))
|
||||
viewport.add_content(content)
|
||||
viewport.title = "Error"
|
||||
return viewport
|
||||
|
||||
|
||||
class ViewportBrowserWindow:
|
||||
"""Enhanced browser window using Tkinter with Viewport support"""
|
||||
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title("pyWebLayout HTML Browser with Viewport")
|
||||
self.root.geometry("1000x800")
|
||||
|
||||
self.current_viewport = None
|
||||
self.history = []
|
||||
self.history_index = -1
|
||||
|
||||
# Scrolling parameters
|
||||
self.scroll_speed = 20 # pixels per scroll
|
||||
self.page_scroll_ratio = 0.9 # fraction of viewport height for page scroll
|
||||
|
||||
# Text selection variables
|
||||
self.selection_start = None
|
||||
self.selection_end = None
|
||||
self.is_selecting = False
|
||||
self.selected_text = ""
|
||||
self.text_elements = []
|
||||
self.selection_overlay = None
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the user interface"""
|
||||
# Create main frame
|
||||
main_frame = ttk.Frame(self.root)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
# Navigation frame
|
||||
nav_frame = ttk.Frame(main_frame)
|
||||
nav_frame.pack(fill=tk.X, pady=(0, 5))
|
||||
|
||||
# Navigation buttons
|
||||
self.back_btn = ttk.Button(nav_frame, text="←", command=self.go_back, state=tk.DISABLED)
|
||||
self.back_btn.pack(side=tk.LEFT, padx=(0, 5))
|
||||
|
||||
self.forward_btn = ttk.Button(nav_frame, text="→", command=self.go_forward, state=tk.DISABLED)
|
||||
self.forward_btn.pack(side=tk.LEFT, padx=(0, 5))
|
||||
|
||||
self.refresh_btn = ttk.Button(nav_frame, text="⟳", command=self.refresh)
|
||||
self.refresh_btn.pack(side=tk.LEFT, padx=(0, 10))
|
||||
|
||||
# Address bar
|
||||
ttk.Label(nav_frame, text="URL:").pack(side=tk.LEFT)
|
||||
self.url_var = tk.StringVar()
|
||||
self.url_entry = ttk.Entry(nav_frame, textvariable=self.url_var, width=50)
|
||||
self.url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 5))
|
||||
self.url_entry.bind('<Return>', self.navigate_to_url)
|
||||
|
||||
self.go_btn = ttk.Button(nav_frame, text="Go", command=self.navigate_to_url)
|
||||
self.go_btn.pack(side=tk.LEFT, padx=(0, 10))
|
||||
|
||||
# File operations
|
||||
self.open_btn = ttk.Button(nav_frame, text="Open File", command=self.open_file)
|
||||
self.open_btn.pack(side=tk.LEFT)
|
||||
|
||||
# Content frame with scrollbars and viewport controls
|
||||
content_frame = ttk.Frame(main_frame)
|
||||
content_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Create scrollbar frame
|
||||
scroll_frame = ttk.Frame(content_frame)
|
||||
scroll_frame.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# Vertical scrollbar
|
||||
self.v_scrollbar = ttk.Scrollbar(scroll_frame, orient=tk.VERTICAL, command=self.on_scrollbar)
|
||||
self.v_scrollbar.pack(fill=tk.Y)
|
||||
|
||||
# Canvas for displaying viewport content
|
||||
self.canvas = tk.Canvas(content_frame, bg='white')
|
||||
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
# Scroll info frame
|
||||
info_frame = ttk.Frame(main_frame)
|
||||
info_frame.pack(fill=tk.X, pady=(5, 0))
|
||||
|
||||
self.scroll_info_var = tk.StringVar(value="")
|
||||
ttk.Label(info_frame, textvariable=self.scroll_info_var).pack(side=tk.LEFT)
|
||||
|
||||
# Status bar
|
||||
self.status_var = tk.StringVar(value="Ready")
|
||||
status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
|
||||
status_bar.pack(fill=tk.X, pady=(5, 0))
|
||||
|
||||
# Bind events
|
||||
self.canvas.bind('<Button-1>', self.on_click)
|
||||
self.canvas.bind('<B1-Motion>', self.on_drag)
|
||||
self.canvas.bind('<ButtonRelease-1>', self.on_release)
|
||||
self.canvas.bind('<Motion>', self.on_mouse_move)
|
||||
self.canvas.bind('<MouseWheel>', self.on_mouse_wheel) # Windows/Mac
|
||||
self.canvas.bind('<Button-4>', self.on_mouse_wheel) # Linux scroll up
|
||||
self.canvas.bind('<Button-5>', self.on_mouse_wheel) # Linux scroll down
|
||||
|
||||
# Keyboard shortcuts
|
||||
self.root.bind('<Control-c>', self.copy_selection)
|
||||
self.root.bind('<Control-a>', self.select_all)
|
||||
self.root.bind('<Prior>', self.page_up) # Page Up
|
||||
self.root.bind('<Next>', self.page_down) # Page Down
|
||||
self.root.bind('<Home>', self.scroll_to_top) # Home
|
||||
self.root.bind('<End>', self.scroll_to_bottom) # End
|
||||
self.root.bind('<Up>', lambda e: self.scroll_by_lines(-1))
|
||||
self.root.bind('<Down>', lambda e: self.scroll_by_lines(1))
|
||||
|
||||
# Context menu
|
||||
self.setup_context_menu()
|
||||
|
||||
# Make canvas focusable
|
||||
self.canvas.config(highlightthickness=1)
|
||||
self.canvas.focus_set()
|
||||
|
||||
# Load default page
|
||||
self.load_default_page()
|
||||
|
||||
def setup_context_menu(self):
|
||||
"""Setup the right-click context menu"""
|
||||
self.context_menu = tk.Menu(self.root, tearoff=0)
|
||||
self.context_menu.add_command(label="Copy", command=self.copy_selection)
|
||||
self.context_menu.add_command(label="Select All", command=self.select_all)
|
||||
self.context_menu.add_separator()
|
||||
self.context_menu.add_command(label="Scroll to Top", command=self.scroll_to_top)
|
||||
self.context_menu.add_command(label="Scroll to Bottom", command=self.scroll_to_bottom)
|
||||
|
||||
# Bind right-click to show context menu
|
||||
self.canvas.bind('<Button-3>', self.show_context_menu)
|
||||
|
||||
def show_context_menu(self, event):
|
||||
"""Show context menu at mouse position"""
|
||||
try:
|
||||
self.context_menu.tk_popup(event.x_root, event.y_root)
|
||||
finally:
|
||||
self.context_menu.grab_release()
|
||||
|
||||
def on_scrollbar(self, *args):
|
||||
"""Handle scrollbar movement"""
|
||||
if not self.current_viewport:
|
||||
return
|
||||
|
||||
action = args[0]
|
||||
|
||||
if action == "moveto":
|
||||
# Absolute position (0.0 to 1.0)
|
||||
fraction = float(args[1])
|
||||
max_scroll = self.current_viewport.max_scroll_y
|
||||
new_y = int(fraction * max_scroll)
|
||||
self.current_viewport.scroll_to(0, new_y)
|
||||
self.update_viewport_display()
|
||||
|
||||
elif action in ["scroll", "step"]:
|
||||
# Relative movement
|
||||
direction = int(args[1])
|
||||
# Handle the units parameter properly - it might be a string "units"
|
||||
if len(args) > 2:
|
||||
try:
|
||||
units = int(args[2])
|
||||
except ValueError:
|
||||
# If args[2] is "units" string, default to 1
|
||||
units = 1
|
||||
else:
|
||||
units = 1
|
||||
|
||||
if action == "scroll":
|
||||
# Line-based scrolling
|
||||
self.scroll_by_lines(direction * units)
|
||||
elif action == "step":
|
||||
# Page-based scrolling
|
||||
self.scroll_by_pages(direction * units)
|
||||
|
||||
def update_scrollbar(self):
|
||||
"""Update scrollbar position and size based on viewport state"""
|
||||
if not self.current_viewport:
|
||||
self.v_scrollbar.set(0, 1)
|
||||
return
|
||||
|
||||
scroll_info = self.current_viewport.get_scroll_info()
|
||||
content_height = scroll_info['content_size'][1]
|
||||
viewport_height = scroll_info['viewport_size'][1]
|
||||
current_offset = scroll_info['offset'][1]
|
||||
|
||||
if content_height <= viewport_height:
|
||||
# No scrolling needed
|
||||
self.v_scrollbar.set(0, 1)
|
||||
else:
|
||||
# Calculate scrollbar position and size
|
||||
top_fraction = current_offset / content_height
|
||||
bottom_fraction = (current_offset + viewport_height) / content_height
|
||||
self.v_scrollbar.set(top_fraction, bottom_fraction)
|
||||
|
||||
# Update scroll info display
|
||||
progress = scroll_info['scroll_progress_y']
|
||||
self.scroll_info_var.set(f"Scroll: {progress:.1%} ({current_offset}/{content_height - viewport_height})")
|
||||
|
||||
def on_mouse_wheel(self, event):
|
||||
"""Handle mouse wheel scrolling"""
|
||||
if not self.current_viewport:
|
||||
return
|
||||
|
||||
# Determine scroll direction and amount
|
||||
if event.num == 4 or event.delta > 0:
|
||||
# Scroll up
|
||||
self.scroll_by_lines(-3)
|
||||
elif event.num == 5 or event.delta < 0:
|
||||
# Scroll down
|
||||
self.scroll_by_lines(3)
|
||||
|
||||
def scroll_by_lines(self, lines: int):
|
||||
"""Scroll by a number of lines"""
|
||||
if not self.current_viewport:
|
||||
return
|
||||
|
||||
delta_y = lines * self.scroll_speed
|
||||
self.current_viewport.scroll_by(0, delta_y)
|
||||
self.update_viewport_display()
|
||||
|
||||
def scroll_by_pages(self, pages: int):
|
||||
"""Scroll by a number of pages"""
|
||||
if not self.current_viewport:
|
||||
return
|
||||
|
||||
viewport_height = self.current_viewport.viewport_size[1]
|
||||
delta_y = int(pages * viewport_height * self.page_scroll_ratio)
|
||||
self.current_viewport.scroll_by(0, delta_y)
|
||||
self.update_viewport_display()
|
||||
|
||||
def page_up(self, event=None):
|
||||
"""Scroll up by one page"""
|
||||
self.scroll_by_pages(-1)
|
||||
|
||||
def page_down(self, event=None):
|
||||
"""Scroll down by one page"""
|
||||
self.scroll_by_pages(1)
|
||||
|
||||
def scroll_to_top(self, event=None):
|
||||
"""Scroll to the top of the document"""
|
||||
if not self.current_viewport:
|
||||
return
|
||||
|
||||
self.current_viewport.scroll_to_top()
|
||||
self.update_viewport_display()
|
||||
|
||||
def scroll_to_bottom(self, event=None):
|
||||
"""Scroll to the bottom of the document"""
|
||||
if not self.current_viewport:
|
||||
return
|
||||
|
||||
self.current_viewport.scroll_to_bottom()
|
||||
self.update_viewport_display()
|
||||
|
||||
def update_viewport_display(self):
|
||||
"""Update the canvas display with current viewport content"""
|
||||
if not self.current_viewport:
|
||||
return
|
||||
|
||||
try:
|
||||
# Render the current viewport
|
||||
viewport_img = self.current_viewport.render()
|
||||
|
||||
# Convert to PhotoImage for Tkinter
|
||||
self.photo = ImageTk.PhotoImage(viewport_img)
|
||||
|
||||
# Clear canvas and display image
|
||||
self.canvas.delete("all")
|
||||
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo)
|
||||
|
||||
# Update scrollbar
|
||||
self.update_scrollbar()
|
||||
|
||||
except Exception as e:
|
||||
self.status_var.set(f"Error rendering viewport: {str(e)}")
|
||||
|
||||
def on_drag(self, event):
|
||||
"""Handle mouse dragging for text selection"""
|
||||
canvas_x = self.canvas.canvasx(event.x)
|
||||
canvas_y = self.canvas.canvasy(event.y)
|
||||
|
||||
if not self.is_selecting:
|
||||
# Start selection
|
||||
self.is_selecting = True
|
||||
self.selection_start = (canvas_x, canvas_y)
|
||||
self.selection_end = (canvas_x, canvas_y)
|
||||
else:
|
||||
# Update selection end
|
||||
self.selection_end = (canvas_x, canvas_y)
|
||||
|
||||
# Update visual selection
|
||||
self.update_selection_visual()
|
||||
|
||||
# Update status
|
||||
self.status_var.set("Selecting text...")
|
||||
|
||||
def on_release(self, event):
|
||||
"""Handle mouse release to complete text selection"""
|
||||
if self.is_selecting:
|
||||
canvas_x = self.canvas.canvasx(event.x)
|
||||
canvas_y = self.canvas.canvasy(event.y)
|
||||
self.selection_end = (canvas_x, canvas_y)
|
||||
|
||||
# Extract selected text
|
||||
self.extract_selected_text()
|
||||
|
||||
# Update status
|
||||
if self.selected_text:
|
||||
self.status_var.set(f"Selected: {len(self.selected_text)} characters")
|
||||
else:
|
||||
self.status_var.set("No text selected")
|
||||
self.clear_selection()
|
||||
|
||||
def update_selection_visual(self):
|
||||
"""Update the visual representation of text selection"""
|
||||
# Remove existing selection overlay
|
||||
if self.selection_overlay:
|
||||
self.canvas.delete(self.selection_overlay)
|
||||
|
||||
if self.selection_start and self.selection_end:
|
||||
# Create selection rectangle
|
||||
x1, y1 = self.selection_start
|
||||
x2, y2 = self.selection_end
|
||||
|
||||
# Ensure proper coordinates (top-left to bottom-right)
|
||||
left = min(x1, x2)
|
||||
top = min(y1, y2)
|
||||
right = max(x1, x2)
|
||||
bottom = max(y1, y2)
|
||||
|
||||
# Draw selection rectangle with transparency effect
|
||||
self.selection_overlay = self.canvas.create_rectangle(
|
||||
left, top, right, bottom,
|
||||
fill='blue', stipple='gray50', outline='blue', width=1
|
||||
)
|
||||
|
||||
def extract_selected_text(self):
|
||||
"""Extract text that falls within the selection area"""
|
||||
if not self.selection_start or not self.selection_end or not self.current_viewport:
|
||||
self.selected_text = ""
|
||||
return
|
||||
|
||||
# Get selection bounds in viewport coordinates
|
||||
x1, y1 = self.selection_start
|
||||
x2, y2 = self.selection_end
|
||||
left = min(x1, x2)
|
||||
top = min(y1, y2)
|
||||
right = max(x1, x2)
|
||||
bottom = max(y1, y2)
|
||||
|
||||
# Convert to content coordinates
|
||||
viewport_offset = self.current_viewport.viewport_offset
|
||||
content_left = left + viewport_offset[0]
|
||||
content_top = top + viewport_offset[1]
|
||||
content_right = right + viewport_offset[0]
|
||||
content_bottom = bottom + viewport_offset[1]
|
||||
|
||||
# Extract text elements in selection area
|
||||
selected_elements = []
|
||||
visible_elements = self.current_viewport.get_visible_elements()
|
||||
|
||||
for element, visible_origin, visible_size, clip_info in visible_elements:
|
||||
# Check if element intersects with selection
|
||||
elem_left = visible_origin[0]
|
||||
elem_top = visible_origin[1]
|
||||
elem_right = elem_left + visible_size[0]
|
||||
elem_bottom = elem_top + visible_size[1]
|
||||
|
||||
if (elem_left < right and elem_right > left and
|
||||
elem_top < bottom and elem_bottom > top):
|
||||
|
||||
# Extract text from element
|
||||
if hasattr(element, '_text'):
|
||||
text_content = element._text
|
||||
if text_content.strip():
|
||||
selected_elements.append((text_content.strip(), elem_left, elem_top))
|
||||
|
||||
# Sort by position (top to bottom, left to right)
|
||||
selected_elements.sort(key=lambda x: (x[2], x[1]))
|
||||
|
||||
# Combine text
|
||||
self.selected_text = " ".join([element[0] for element in selected_elements])
|
||||
|
||||
def copy_selection(self, event=None):
|
||||
"""Copy selected text to clipboard"""
|
||||
if self.selected_text:
|
||||
try:
|
||||
pyperclip.copy(self.selected_text)
|
||||
self.status_var.set(f"Copied {len(self.selected_text)} characters to clipboard")
|
||||
except Exception as e:
|
||||
self.status_var.set(f"Error copying to clipboard: {str(e)}")
|
||||
else:
|
||||
self.status_var.set("No text selected to copy")
|
||||
|
||||
def select_all(self, event=None):
|
||||
"""Select all text on the page"""
|
||||
if not self.current_viewport:
|
||||
return
|
||||
|
||||
# Set selection to entire viewport area
|
||||
viewport_size = self.current_viewport.viewport_size
|
||||
|
||||
self.selection_start = (0, 0)
|
||||
self.selection_end = viewport_size
|
||||
self.is_selecting = True
|
||||
|
||||
# Extract all visible text
|
||||
self.extract_selected_text()
|
||||
|
||||
# Update visual
|
||||
self.update_selection_visual()
|
||||
|
||||
if self.selected_text:
|
||||
self.status_var.set(f"Selected all visible text: {len(self.selected_text)} characters")
|
||||
else:
|
||||
self.status_var.set("No text found to select")
|
||||
|
||||
def clear_selection(self):
|
||||
"""Clear the current text selection"""
|
||||
self.selection_start = None
|
||||
self.selection_end = None
|
||||
self.is_selecting = False
|
||||
self.selected_text = ""
|
||||
|
||||
# Remove visual selection
|
||||
if self.selection_overlay:
|
||||
self.canvas.delete(self.selection_overlay)
|
||||
self.selection_overlay = None
|
||||
|
||||
self.status_var.set("Selection cleared")
|
||||
|
||||
def load_default_page(self):
|
||||
"""Load a default welcome page"""
|
||||
html_content = """
|
||||
<html>
|
||||
<head><title>pyWebLayout Browser with Viewport - Welcome</title></head>
|
||||
<body>
|
||||
<h1>Welcome to pyWebLayout Browser with Viewport System</h1>
|
||||
<p>This enhanced browser uses the new viewport system for efficient scrolling through large documents.</p>
|
||||
|
||||
<h2>New Viewport Features:</h2>
|
||||
<ul>
|
||||
<li><b>Efficient Rendering:</b> Only visible content is rendered</li>
|
||||
<li><b>Smooth Scrolling:</b> Mouse wheel, keyboard, and scrollbar support</li>
|
||||
<li><b>Large Document Support:</b> Handle documents of any size</li>
|
||||
<li><b>Memory Efficient:</b> Low memory usage even for huge pages</li>
|
||||
</ul>
|
||||
|
||||
<h2>Scrolling Controls:</h2>
|
||||
<p><b>Mouse Wheel:</b> Scroll up and down</p>
|
||||
<p><b>Page Up/Down:</b> Scroll by viewport height</p>
|
||||
<p><b>Home/End:</b> Jump to top/bottom</p>
|
||||
<p><b>Arrow Keys:</b> Scroll line by line</p>
|
||||
<p><b>Scrollbar:</b> Click and drag for precise positioning</p>
|
||||
|
||||
<h2>Text Selection:</h2>
|
||||
<p>Click and drag to select text, then use <b>Ctrl+C</b> to copy</p>
|
||||
<p>Use <b>Ctrl+A</b> to select all visible text</p>
|
||||
|
||||
<h3>Try scrolling with different methods!</h3>
|
||||
<p>This page demonstrates the viewport system. All the content above and below is efficiently managed.</p>
|
||||
|
||||
<h2>Sample Content for Scrolling</h2>
|
||||
<p>Here's some additional content to demonstrate scrolling capabilities:</p>
|
||||
|
||||
<h3>Lorem Ipsum</h3>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
||||
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
|
||||
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
|
||||
|
||||
<h3>More Sample Text</h3>
|
||||
<p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
<p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.</p>
|
||||
<p>Totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.</p>
|
||||
|
||||
<h3>Even More Content</h3>
|
||||
<p>Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos.</p>
|
||||
<p>Qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet.</p>
|
||||
<p>Consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.</p>
|
||||
|
||||
<h2>Technical Details</h2>
|
||||
<p>The viewport system works by:</p>
|
||||
<ul>
|
||||
<li>Creating a large content container that can hold any amount of content</li>
|
||||
<li>Providing a viewport window that shows only a portion of the content</li>
|
||||
<li>Efficiently calculating which elements are visible</li>
|
||||
<li>Rendering only the visible elements</li>
|
||||
<li>Supporting smooth scrolling through the content</li>
|
||||
</ul>
|
||||
|
||||
<p>This allows handling of very large documents without performance issues.</p>
|
||||
|
||||
<h2>Load Your Own Content</h2>
|
||||
<p>Use the "Open File" button to load local HTML files, or enter a URL in the address bar.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# Get current canvas size for viewport
|
||||
self.root.update_idletasks()
|
||||
canvas_width = max(800, self.canvas.winfo_width())
|
||||
canvas_height = max(600, self.canvas.winfo_height())
|
||||
|
||||
parser = HTMLViewportAdapter()
|
||||
self.current_viewport = parser.parse_html_string(html_content, viewport_size=(canvas_width, canvas_height))
|
||||
|
||||
# Update window title
|
||||
if hasattr(self.current_viewport, 'title'):
|
||||
self.root.title(f"pyWebLayout Browser - {self.current_viewport.title}")
|
||||
|
||||
self.update_viewport_display()
|
||||
self.status_var.set("Welcome page loaded with viewport system")
|
||||
|
||||
def navigate_to_url(self, event=None):
|
||||
"""Navigate to the URL in the address bar"""
|
||||
url = self.url_var.get().strip()
|
||||
if not url:
|
||||
return
|
||||
|
||||
self.status_var.set(f"Loading {url}...")
|
||||
self.root.update()
|
||||
|
||||
# Get current canvas size for viewport
|
||||
self.root.update_idletasks()
|
||||
canvas_width = max(800, self.canvas.winfo_width())
|
||||
canvas_height = max(600, self.canvas.winfo_height())
|
||||
|
||||
try:
|
||||
parser = HTMLViewportAdapter()
|
||||
|
||||
if url.startswith(('http://', 'https://')):
|
||||
# Web URL
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
html_content = response.text
|
||||
|
||||
self.current_viewport = parser.parse_html_string(html_content, url, (canvas_width, canvas_height))
|
||||
|
||||
elif os.path.isfile(url):
|
||||
# Local file
|
||||
self.current_viewport = parser.parse_html_file(url, (canvas_width, canvas_height))
|
||||
|
||||
else:
|
||||
# Try to treat as a local file path
|
||||
if not url.startswith('file://'):
|
||||
url = 'file://' + os.path.abspath(url)
|
||||
|
||||
file_path = url.replace('file://', '')
|
||||
if os.path.isfile(file_path):
|
||||
self.current_viewport = parser.parse_html_file(file_path, (canvas_width, canvas_height))
|
||||
else:
|
||||
raise FileNotFoundError(f"File not found: {file_path}")
|
||||
|
||||
# Update window title
|
||||
if hasattr(self.current_viewport, 'title'):
|
||||
self.root.title(f"pyWebLayout Browser - {self.current_viewport.title}")
|
||||
|
||||
# Add to history
|
||||
self.add_to_history(url)
|
||||
self.update_viewport_display()
|
||||
self.status_var.set(f"Loaded {url}")
|
||||
|
||||
except Exception as e:
|
||||
self.status_var.set(f"Error loading {url}: {str(e)}")
|
||||
messagebox.showerror("Error", f"Failed to load {url}:\n{str(e)}")
|
||||
|
||||
def open_file(self):
|
||||
"""Open a local HTML file"""
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="Open HTML File",
|
||||
filetypes=[("HTML files", "*.html *.htm"), ("All files", "*.*")]
|
||||
)
|
||||
|
||||
if file_path:
|
||||
self.url_var.set(file_path)
|
||||
self.navigate_to_url()
|
||||
|
||||
def on_click(self, event):
|
||||
"""Handle mouse clicks on the canvas"""
|
||||
if not self.current_viewport:
|
||||
return
|
||||
|
||||
# Convert canvas coordinates to viewport coordinates
|
||||
canvas_x = self.canvas.canvasx(event.x)
|
||||
canvas_y = self.canvas.canvasy(event.y)
|
||||
|
||||
# Use viewport hit testing
|
||||
hit_element = self.current_viewport.hit_test((canvas_x, canvas_y))
|
||||
|
||||
if hit_element and hasattr(hit_element, '_callback'):
|
||||
# Handle clickable elements
|
||||
try:
|
||||
result = hit_element._callback()
|
||||
if result:
|
||||
self.status_var.set(result)
|
||||
|
||||
# For external links, open in system browser
|
||||
if hasattr(hit_element, '_link') and hit_element._link.link_type == LinkType.EXTERNAL:
|
||||
webbrowser.open(hit_element._link.location)
|
||||
except Exception as e:
|
||||
self.status_var.set(f"Click error: {str(e)}")
|
||||
|
||||
def on_mouse_move(self, event):
|
||||
"""Handle mouse movement for hover effects"""
|
||||
if not self.current_viewport:
|
||||
return
|
||||
|
||||
# Convert canvas coordinates to viewport coordinates
|
||||
canvas_x = self.canvas.canvasx(event.x)
|
||||
canvas_y = self.canvas.canvasy(event.y)
|
||||
|
||||
# Check if mouse is over any clickable element
|
||||
hit_element = self.current_viewport.hit_test((canvas_x, canvas_y))
|
||||
|
||||
if hit_element and hasattr(hit_element, '_callback'):
|
||||
self.canvas.configure(cursor="hand2")
|
||||
else:
|
||||
self.canvas.configure(cursor="arrow")
|
||||
|
||||
def add_to_history(self, url):
|
||||
"""Add URL to navigation history"""
|
||||
# Remove any forward history
|
||||
self.history = self.history[:self.history_index + 1]
|
||||
|
||||
# Add new URL
|
||||
self.history.append(url)
|
||||
self.history_index = len(self.history) - 1
|
||||
|
||||
# Update navigation buttons
|
||||
self.update_nav_buttons()
|
||||
|
||||
def update_nav_buttons(self):
|
||||
"""Update the state of navigation buttons"""
|
||||
self.back_btn.configure(state=tk.NORMAL if self.history_index > 0 else tk.DISABLED)
|
||||
self.forward_btn.configure(state=tk.NORMAL if self.history_index < len(self.history) - 1 else tk.DISABLED)
|
||||
|
||||
def go_back(self):
|
||||
"""Navigate back in history"""
|
||||
if self.history_index > 0:
|
||||
self.history_index -= 1
|
||||
url = self.history[self.history_index]
|
||||
self.url_var.set(url)
|
||||
self.navigate_to_url()
|
||||
|
||||
def go_forward(self):
|
||||
"""Navigate forward in history"""
|
||||
if self.history_index < len(self.history) - 1:
|
||||
self.history_index += 1
|
||||
url = self.history[self.history_index]
|
||||
self.url_var.set(url)
|
||||
self.navigate_to_url()
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the current page"""
|
||||
current_url = self.url_var.get()
|
||||
if current_url:
|
||||
self.navigate_to_url()
|
||||
else:
|
||||
self.load_default_page()
|
||||
|
||||
def run(self):
|
||||
"""Start the browser"""
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to run the enhanced browser"""
|
||||
print("Starting pyWebLayout HTML Browser with Viewport System...")
|
||||
|
||||
try:
|
||||
browser = ViewportBrowserWindow()
|
||||
browser.run()
|
||||
except Exception as e:
|
||||
print(f"Error starting browser: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page for pyWebLayout Browser</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>pyWebLayout Browser Test Page</h1>
|
||||
<h3>Images</h3>
|
||||
<p>Here's a sample image:</p>
|
||||
<img src="tests/data/sample_image.jpg" alt="Sample Image" width="200" height="150">
|
||||
<h2>Text Formatting</h2>
|
||||
<p>This is a paragraph with <b>bold text</b>, <i>italic text</i>, and <u>underlined text</u>.</p>
|
||||
|
||||
<h3>Links</h3>
|
||||
<p>Here are some test links:</p>
|
||||
<ul>
|
||||
<li><a href="https://www.google.com" title="Google">External link to Google</a></li>
|
||||
<li><a href="#section1" title="Section 1">Internal link to Section 1</a></li>
|
||||
</ul>
|
||||
|
||||
<h3>Headers</h3>
|
||||
<h1>H1 Header</h1>
|
||||
<h2>H2 Header</h2>
|
||||
<h3>H3 Header</h3>
|
||||
<h4>H4 Header</h4>
|
||||
<h5>H5 Header</h5>
|
||||
<h6>H6 Header</h6>
|
||||
|
||||
<h3>Line Breaks and Paragraphs</h3>
|
||||
<p>This is the first paragraph.</p>
|
||||
<br>
|
||||
<p>This is the second paragraph after a line break.</p>
|
||||
|
||||
<p>
|
||||
It transpired after a confused five minutes that the man had heard Gatsby’s name around his office in a connection which he either wouldn’t reveal or didn’t fully understand. This was his day off and with laudable initiative he had hurried out “to see.”
|
||||
</p>
|
||||
<p>
|
||||
It was a random shot, and yet the reporter’s instinct was right. Gatsby’s notoriety, spread about by the hundreds who had accepted his hospitality and so become authorities upon his past, had increased all summer until he fell just short of being news. Contemporary legends such as the “underground pipeline to Canada” attached themselves to him, and there was one persistent story that he didn’t live in a house at all, but in a boat that looked like a house and was moved secretly up and down the Long Island shore. Just why these inventions were a source of satisfaction to James Gatz of North Dakota, isn’t easy to say.
|
||||
</p>
|
||||
<p>
|
||||
James Gatz—that was really, or at least legally, his name. He had changed it at the age of seventeen and at the specific moment that witnessed the beginning of his career—when he saw Dan Cody’s yacht drop anchor over the most insidious flat on Lake Superior. It was James Gatz who had been loafing along the beach that afternoon in a torn green jersey and a pair of canvas pants, but it was already Jay Gatsby who borrowed a rowboat, pulled out to the <i>Tuolomee</i>, and informed Cody that a wind might catch him and break him up in half an hour.
|
||||
</p>
|
||||
<p>
|
||||
I suppose he’d had the name ready for a long time, even then. His parents were shiftless and unsuccessful farm people—his imagination had never really accepted them as his parents at all. The truth was that Jay Gatsby of West Egg, Long Island, sprang from his Platonic conception of himself. He was a son of God—a phrase which, if it means anything, means just that—and he must be about His Father’s business, the service of a vast, vulgar, and meretricious beauty. So he invented just the sort of Jay Gatsby that a seventeen-year-old boy would be likely to invent, and to this conception he was faithful to the end.
|
||||
</p>
|
||||
|
||||
<h3 id="section1">Section 1</h3>
|
||||
<p>This is the content of section 1. You can link to this section using the internal link above.</p>
|
||||
|
||||
<h3>Images</h3>
|
||||
<p>Here's a sample image:</p>
|
||||
<img src="tests/data/sample_image.jpg" alt="Sample Image" width="200" height="150">
|
||||
|
||||
<h3>Mixed Content</h3>
|
||||
<p>This paragraph contains <b>bold</b> and <i>italic</i> text, as well as an <a href="https://www.example.com">external link</a>.</p>
|
||||
|
||||
<p><strong>Strong text</strong> and <em>emphasized text</em> should also work.</p>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user