From 5fe4db4cbe374adf921156000f7ba6a414ccb285 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Fri, 7 Nov 2025 20:34:28 +0100 Subject: [PATCH] more refactoring --- .../examples/demo_alignment_refactor.py | 111 -- pyWebLayout/examples/epub_reader_tk.py | 406 ------- pyWebLayout/examples/epub_viewer.py | 100 -- pyWebLayout/examples/html_browser.py | 1047 ----------------- pyWebLayout/examples/test_page.html | 59 - 5 files changed, 1723 deletions(-) delete mode 100644 pyWebLayout/examples/demo_alignment_refactor.py delete mode 100644 pyWebLayout/examples/epub_reader_tk.py delete mode 100644 pyWebLayout/examples/epub_viewer.py delete mode 100644 pyWebLayout/examples/html_browser.py delete mode 100644 pyWebLayout/examples/test_page.html diff --git a/pyWebLayout/examples/demo_alignment_refactor.py b/pyWebLayout/examples/demo_alignment_refactor.py deleted file mode 100644 index 5843e39..0000000 --- a/pyWebLayout/examples/demo_alignment_refactor.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/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 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() diff --git a/pyWebLayout/examples/epub_reader_tk.py b/pyWebLayout/examples/epub_reader_tk.py deleted file mode 100644 index 2cf6540..0000000 --- a/pyWebLayout/examples/epub_reader_tk.py +++ /dev/null @@ -1,406 +0,0 @@ -#!/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('<>', 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('', lambda e: self.previous_page()) - self.root.bind('', lambda e: self.next_page()) - self.root.bind('', 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('', 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() diff --git a/pyWebLayout/examples/epub_viewer.py b/pyWebLayout/examples/epub_viewer.py deleted file mode 100644 index 22ebd28..0000000 --- a/pyWebLayout/examples/epub_viewer.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -""" -Example EPUB viewer using pyWebLayout. - -This example demonstrates how to use pyWebLayout to load an EPUB file, -paginate it, and render pages as images. -""" - -import os -import sys -import argparse -from pathlib import Path -from PIL import Image - -# Add the parent directory to the path to import pyWebLayout -sys.path.append(str(Path(__file__).parent.parent.parent)) - -from pyWebLayout import ( - Document, Book, read_epub, - DocumentPaginator, Page -) - - -def main(): - # Parse command line arguments - parser = argparse.ArgumentParser(description='EPUB viewer example') - parser.add_argument('epub_file', help='Path to EPUB file') - parser.add_argument('--output-dir', '-o', default='output', help='Output directory for rendered pages') - parser.add_argument('--width', '-w', type=int, default=800, help='Page width') - parser.add_argument('--height', '-y', type=int, default=1000, help='Page height') - parser.add_argument('--margin', '-m', type=int, default=50, help='Page margin') - parser.add_argument('--max-pages', '-p', type=int, default=10, help='Maximum number of pages to render') - args = parser.parse_args() - - # Create output directory - os.makedirs(args.output_dir, exist_ok=True) - - # Read EPUB file - print(f"Reading EPUB file: {args.epub_file}") - book = read_epub(args.epub_file) - - # Display book metadata - print(f"Title: {book.get_title()}") - print(f"Author: {book.get_metadata('AUTHOR')}") - print(f"Chapters: {len(book.chapters)}") - - # Create a paginator - page_size = (args.width, args.height) - margins = (args.margin, args.margin, args.margin, args.margin) - paginator = DocumentPaginator( - document=book, - page_size=page_size, - margins=margins - ) - - # Paginate and render pages - print("Paginating and rendering pages...") - - # Option 1: Render all pages at once - pages = paginator.paginate(max_pages=args.max_pages) - for i, page in enumerate(pages): - # Render the page - image = page.render() - - # Save the image - output_path = os.path.join(args.output_dir, f"page_{i+1:03d}.png") - image.save(output_path) - print(f"Saved page {i+1} to {output_path}") - - # Option 2: Render pages one by one with state saving - """ - # Clear paginator state - paginator.state = DocumentPaginationState() - - for i in range(args.max_pages): - # Get next page - page = paginator.paginate_next() - if page is None: - print(f"No more pages after page {i}") - break - - # Render the page - image = page.render() - - # Save the image - output_path = os.path.join(args.output_dir, f"page_{i+1:03d}.png") - image.save(output_path) - print(f"Saved page {i+1} to {output_path}") - - # Save pagination state (could be saved to a file for later resumption) - state_dict = paginator.get_state() - - # Progress information - progress = paginator.get_progress() * 100 - print(f"Progress: {progress:.1f}%") - """ - - -if __name__ == "__main__": - main() diff --git a/pyWebLayout/examples/html_browser.py b/pyWebLayout/examples/html_browser.py deleted file mode 100644 index 43d85b8..0000000 --- a/pyWebLayout/examples/html_browser.py +++ /dev/null @@ -1,1047 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple HTML Browser using pyWebLayout - -This browser can render basic HTML content using the pyWebLayout concrete objects. -It supports text, images, links, forms, and basic styling. -""" - -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 -from pyWebLayout.concrete import ( - Page, Container, Box, Text, RenderableImage, - RenderableLink, RenderableButton, RenderableForm, RenderableFormField -) -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 import Alignment -from pyWebLayout.layout.paragraph_layout import ParagraphLayout, ParagraphLayoutResult - - -class HTMLParser: - """Simple HTML parser that converts HTML to pyWebLayout objects""" - - def __init__(self): - self.font_stack = [Font(font_size=14)] # Default font - self.current_container = None - - def parse_html_string(self, html_content: str, base_url: str = "") -> Page: - """Parse HTML string and return a Page object""" - # Create the main page - page = Page(size=(800, 10000), background_color=(255, 255, 255)) - self.current_container = page - self.base_url = base_url - - # Simple HTML parsing using regex (not production-ready, but works for demo) - # Remove comments and scripts - html_content = re.sub(r'', '', html_content, flags=re.DOTALL) - html_content = re.sub(r'', '', html_content, flags=re.DOTALL) - html_content = re.sub(r'', '', html_content, flags=re.DOTALL) - - # Extract title - title_match = re.search(r'(.*?)', html_content, re.IGNORECASE) - if title_match: - page.title = title_match.group(1) - - # Extract body content - body_match = re.search(r']*>(.*?)', html_content, re.DOTALL | re.IGNORECASE) - if body_match: - body_content = body_match.group(1) - else: - # If no body tag, use the entire content - body_content = html_content - - # Parse the body content - self._parse_content(body_content, page) - - return page - - def parse_html_file(self, file_path: str) -> Page: - """Parse HTML file and return a Page 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) - except Exception as e: - # Create error page - page = Page(size=(800, 10000), background_color=(255, 255, 255)) - error_text = Text(f"Error loading file: {str(e)}", Font(font_size=16, colour=(255, 0, 0))) - page.add_child(error_text) - return page - - def _parse_content(self, content: str, container: Container): - """Parse HTML content and add elements to container""" - # Simple token-based parsing - tokens = self._tokenize_html(content) - - # Group tokens into paragraphs and other elements - self._process_tokens_into_elements(tokens, container) - - def _process_tokens_into_elements(self, tokens: List[Dict], container: Container): - """Process tokens and create appropriate elements (paragraphs, images, etc.)""" - i = 0 - current_paragraph_content = [] - - while i < len(tokens): - token = tokens[i] - - if token['type'] == 'text': - if token['content'].strip(): # Only add non-empty text - current_paragraph_content.append((token['content'].strip(), self.font_stack[-1])) - - elif token['type'] == 'tag': - tag_name = token['name'] - is_closing = token['closing'] - - # Handle block-level elements that should end the current paragraph - if tag_name in ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'br', 'img'] and not is_closing: - # Finalize any pending paragraph content - if current_paragraph_content: - self._create_and_add_paragraph(current_paragraph_content, container) - current_paragraph_content = [] - - # Handle the block element - if tag_name == 'p': - # Start a new paragraph - i = self._handle_paragraph_tag(token, tokens, i, container) - continue - elif tag_name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']: - # Handle header - i = self._handle_header_tag(token, tokens, i, container) - continue - elif tag_name == 'br': - # Add line break - spacer = Box((0, 0), (1, 10)) - container.add_child(spacer) - elif tag_name == 'img': - # Handle image - self._handle_tag(token, container) - elif tag_name == 'div': - # Continue processing div content - pass - - # Handle inline elements or continue processing - elif tag_name in ['b', 'strong', 'i', 'em', 'u', 'a']: - i = self._handle_inline_tag_with_content(token, tokens, i, current_paragraph_content) - continue - else: - # Handle other tags normally - self._handle_tag(token, container) - - i += 1 - - # Finalize any remaining paragraph content - if current_paragraph_content: - self._create_and_add_paragraph(current_paragraph_content, container) - - def _create_and_add_paragraph(self, content_list: List[Tuple[str, Font]], container: Container): - """Create a paragraph from content and add it to the container using proper layout""" - if not content_list: - return - - # Create a paragraph object - paragraph = Paragraph(style=content_list[0][1]) # Use first font as paragraph style - - # Add words to the paragraph - for text_content, font in content_list: - words = text_content.split() - for word_text in words: - if word_text.strip(): - word = Word(word_text.strip(), font) - paragraph.add_word(word) - - # Use paragraph layout to break into lines - layout = ParagraphLayout( - line_width=750, # Page width minus margins - line_height=20, - word_spacing=(3, 8), - line_spacing=3, - halign=Alignment.LEFT - ) - - # Layout the paragraph into lines - lines = layout.layout_paragraph(paragraph) - - # Add each line to the container - for line in lines: - container.add_child(line) - - # Add some space after the paragraph - spacer = Box((0, 0), (1, 5)) - container.add_child(spacer) - - def _handle_paragraph_tag(self, token, tokens, current_index, container): - """Handle paragraph tags with proper text flow""" - content_start = current_index + 1 - content_end = self._find_matching_closing_tag(tokens, current_index, 'p') - - # Collect content within the paragraph - paragraph_content = [] - - i = content_start - while i < content_end: - content_token = tokens[i] - if content_token['type'] == 'text': - if content_token['content'].strip(): - paragraph_content.append((content_token['content'].strip(), self.font_stack[-1])) - elif content_token['type'] == 'tag' and not content_token['closing']: - # Handle inline formatting within paragraph - if content_token['name'] in ['b', 'strong', 'i', 'em', 'u', 'a']: - i = self._handle_inline_tag_with_content(content_token, tokens, i, paragraph_content) - continue - i += 1 - - # Create and add the paragraph - if paragraph_content: - self._create_and_add_paragraph(paragraph_content, container) - - return content_end + 1 if content_end < len(tokens) else len(tokens) - - def _handle_header_tag(self, token, tokens, current_index, container): - """Handle header tags with proper styling""" - tag_name = token['name'] - - # Push header font onto stack - size_map = {'h1': 24, 'h2': 20, 'h3': 18, 'h4': 16, 'h5': 14, 'h6': 12} - font = self.font_stack[-1].with_size(size_map[tag_name]).with_weight(FontWeight.BOLD) - self.font_stack.append(font) - - content_start = current_index + 1 - content_end = self._find_matching_closing_tag(tokens, current_index, tag_name) - - # Collect header content - header_content = [] - - i = content_start - while i < content_end: - content_token = tokens[i] - if content_token['type'] == 'text': - if content_token['content'].strip(): - header_content.append((content_token['content'].strip(), self.font_stack[-1])) - elif content_token['type'] == 'tag' and not content_token['closing']: - # Handle inline formatting within header - if content_token['name'] in ['b', 'strong', 'i', 'em', 'u']: - i = self._handle_inline_tag_with_content(content_token, tokens, i, header_content) - continue - i += 1 - - # Pop the header font - if len(self.font_stack) > 1: - self.font_stack.pop() - - # Create and add the header paragraph with extra spacing - if header_content: - self._create_and_add_paragraph(header_content, container) - # Add extra space after headers - spacer = Box((0, 0), (1, 10)) - container.add_child(spacer) - - return content_end + 1 if content_end < len(tokens) else len(tokens) - - def _handle_inline_tag_with_content(self, token, tokens, current_index, paragraph_content): - """Handle inline formatting tags and collect their content""" - tag_name = token['name'] - - # Push formatted font onto stack - if tag_name in ['b', 'strong']: - font = self.font_stack[-1].with_weight(FontWeight.BOLD) - self.font_stack.append(font) - elif tag_name in ['i', 'em']: - font = self.font_stack[-1].with_style(FontStyle.ITALIC) - self.font_stack.append(font) - elif tag_name == 'u': - font = self.font_stack[-1].with_decoration(TextDecoration.UNDERLINE) - self.font_stack.append(font) - elif tag_name == 'a': - font = self.font_stack[-1].with_colour((0, 0, 255)).with_decoration(TextDecoration.UNDERLINE) - self.font_stack.append(font) - - content_start = current_index + 1 - content_end = self._find_matching_closing_tag(tokens, current_index, tag_name) - - # Collect content with the formatting applied - i = content_start - while i < content_end: - content_token = tokens[i] - if content_token['type'] == 'text': - if content_token['content'].strip(): - paragraph_content.append((content_token['content'].strip(), self.font_stack[-1])) - elif content_token['type'] == 'tag' and not content_token['closing']: - # Handle nested inline formatting - if content_token['name'] in ['b', 'strong', 'i', 'em', 'u']: - i = self._handle_inline_tag_with_content(content_token, tokens, i, paragraph_content) - continue - i += 1 - - # Pop the formatting font - if len(self.font_stack) > 1: - self.font_stack.pop() - - return content_end + 1 if content_end < len(tokens) else len(tokens) - - def _handle_tag_with_content(self, token, tokens, current_index, container): - """Handle tags and their content, returning the new index position""" - tag_name = token['name'] - is_closing = token['closing'] - - if is_closing: - # Handle closing tags - if tag_name in ['b', 'strong', 'i', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']: - if len(self.font_stack) > 1: # Don't pop the last font - self.font_stack.pop() - return current_index + 1 - - # For opening tags that affect text styling, parse their content with the new style - if tag_name in ['b', 'strong', 'i', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']: - # Push new font onto stack - self._handle_tag(token, container) - - # Find the matching closing tag and parse content in between - content_start = current_index + 1 - content_end = self._find_matching_closing_tag(tokens, current_index, tag_name) - - if content_end > content_start: - # Parse content between opening and closing tags with current font style - for j in range(content_start, content_end): - content_token = tokens[j] - if content_token['type'] == 'text': - if content_token['content'].strip(): - text_obj = Text(content_token['content'].strip(), self.font_stack[-1]) - container.add_child(text_obj) - elif content_token['type'] == 'tag' and not content_token['closing']: - # Handle nested tags - self._handle_tag(content_token, container) - - # Pop the font from stack - if len(self.font_stack) > 1: - self.font_stack.pop() - - return content_end + 1 if content_end < len(tokens) else len(tokens) - - else: - # Handle other tags normally - self._handle_tag(token, container) - return current_index + 1 - - def _find_matching_closing_tag(self, tokens, start_index, tag_name): - """Find the index of the matching closing tag""" - open_count = 1 - i = start_index + 1 - - while i < len(tokens) and open_count > 0: - token = tokens[i] - if token['type'] == 'tag' and token['name'] == tag_name: - if token['closing']: - open_count -= 1 - else: - open_count += 1 - i += 1 - - return i - 1 if open_count == 0 else len(tokens) - - def _tokenize_html(self, content: str) -> List[Dict]: - """Simple HTML tokenizer""" - tokens = [] - tag_pattern = r'<(/?)([^>]+)>' - - last_end = 0 - for match in re.finditer(tag_pattern, content): - # Add text before tag - text_content = content[last_end:match.start()] - if text_content: - tokens.append({'type': 'text', 'content': text_content}) - - # Add tag - is_closing = bool(match.group(1)) - tag_content = match.group(2) - tag_parts = tag_content.split() - tag_name = tag_parts[0].lower() - - # Parse attributes - attributes = {} - if len(tag_parts) > 1: - attr_text = ' '.join(tag_parts[1:]) - attr_pattern = r'(\w+)=(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+))' - for attr_match in re.finditer(attr_pattern, attr_text): - attr_name = attr_match.group(1).lower() - attr_value = attr_match.group(2) or attr_match.group(3) or attr_match.group(4) - attributes[attr_name] = attr_value - - tokens.append({ - 'type': 'tag', - 'name': tag_name, - 'closing': is_closing, - 'attributes': attributes, - 'content': tag_content - }) - - last_end = match.end() - - # Add remaining text - if last_end < len(content): - text_content = content[last_end:] - if text_content: - tokens.append({'type': 'text', 'content': text_content}) - - return tokens - - def _handle_tag(self, token: Dict, container: Container): - """Handle HTML tags""" - tag_name = token['name'] - is_closing = token['closing'] - attributes = token['attributes'] - - if is_closing: - # Handle closing tags - if tag_name in ['b', 'strong']: - self.font_stack.pop() - elif tag_name in ['i', 'em']: - self.font_stack.pop() - elif tag_name == 'u': - self.font_stack.pop() - return - - # Handle opening tags - if tag_name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']: - # Headers - size_map = {'h1': 24, 'h2': 20, 'h3': 18, 'h4': 16, 'h5': 14, 'h6': 12} - font = self.font_stack[-1].with_size(size_map[tag_name]).with_weight(FontWeight.BOLD) - self.font_stack.append(font) - - elif tag_name in ['b', 'strong']: - # Bold text - font = self.font_stack[-1].with_weight(FontWeight.BOLD) - self.font_stack.append(font) - - elif tag_name in ['i', 'em']: - # Italic text - font = self.font_stack[-1].with_style(FontStyle.ITALIC) - self.font_stack.append(font) - - elif tag_name == 'u': - # Underlined text - font = self.font_stack[-1].with_decoration(TextDecoration.UNDERLINE) - self.font_stack.append(font) - - elif tag_name == 'a': - # Links - href = attributes.get('href', '#') - title = attributes.get('title', href) - - # Determine link type - if href.startswith('http'): - link_type = LinkType.EXTERNAL - elif href.startswith('#'): - link_type = LinkType.INTERNAL - else: - link_type = LinkType.INTERNAL - - # Create link callback - def link_callback(location, **kwargs): - return f"Navigate to: {location}" - - link = Link(href, link_type, link_callback, title=title) - link_font = self.font_stack[-1].with_colour((0, 0, 255)).with_decoration(TextDecoration.UNDERLINE) - - # For now, just add the link text with link styling - link_text = attributes.get('title', href) - renderable_link = RenderableLink(link, link_text, link_font) - container.add_child(renderable_link) - - elif tag_name == 'img': - # Images - src = attributes.get('src', '') - alt = attributes.get('alt', 'Image') - width = attributes.get('width') - height = attributes.get('height') - - if src: - # Resolve relative URLs - if self.base_url and not src.startswith(('http://', 'https://')): - if os.path.isdir(self.base_url): - src = os.path.join(self.base_url, src) - else: - src = urljoin(self.base_url, src) - - try: - # Create abstract image - from pyWebLayout.abstract.block import Image as AbstractImage - abstract_img = AbstractImage(src, alt) - - # Parse dimensions if provided - max_width = int(width) if width and width.isdigit() else None - max_height = int(height) if height and height.isdigit() else None - - renderable_img = RenderableImage(abstract_img, max_width, max_height) - container.add_child(renderable_img) - - except Exception as e: - # Add error text if image fails to load - error_text = Text(f"[Image Error: {alt}]", Font(colour=(255, 0, 0))) - container.add_child(error_text) - - elif tag_name == 'br': - # Line breaks - add some vertical space - spacer = Box((0, 0), (1, 10)) - container.add_child(spacer) - - elif tag_name == 'p': - # Paragraphs - add some vertical space - spacer = Box((0, 0), (1, 5)) - container.add_child(spacer) - - elif tag_name in ['div', 'span']: - # Generic containers - just continue parsing - pass - - -class BrowserWindow: - """Main browser window using Tkinter""" - - def __init__(self): - self.root = tk.Tk() - self.root.title("pyWebLayout HTML Browser") - self.root.geometry("900x700") - - self.current_page = None - self.history = [] - self.history_index = -1 - - # Text selection variables - self.selection_start = None - self.selection_end = None - self.is_selecting = False - self.selected_text = "" - self.text_elements = [] # Store text elements with positions - self.selection_overlay = None # Canvas overlay for selection highlighting - - 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('', 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 - content_frame = ttk.Frame(main_frame) - content_frame.pack(fill=tk.BOTH, expand=True) - - # Create canvas with scrollbars - self.canvas = tk.Canvas(content_frame, bg='white') - - v_scrollbar = ttk.Scrollbar(content_frame, orient=tk.VERTICAL, command=self.canvas.yview) - h_scrollbar = ttk.Scrollbar(content_frame, orient=tk.HORIZONTAL, command=self.canvas.xview) - - self.canvas.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) - - v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) - self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - - # 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 mouse events - self.canvas.bind('', self.on_click) - self.canvas.bind('', self.on_drag) - self.canvas.bind('', self.on_release) - self.canvas.bind('', self.on_mouse_move) - - # Keyboard shortcuts - self.root.bind('', self.copy_selection) - self.root.bind('', self.select_all) - - # 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) - - # Bind right-click to show context menu - self.canvas.bind('', 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_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: - self.selected_text = "" - return - - # Get selection bounds - 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) - - # Extract text elements in selection area - selected_elements = [] - self._collect_text_in_area(self.current_page, (0, 0), left, top, right, bottom, selected_elements) - - # Sort by position (top to bottom, left to right) - selected_elements.sort(key=lambda x: (x[2], x[1])) # Sort by y, then x - - # Combine text - self.selected_text = " ".join([element[0] for element in selected_elements]) - - def _collect_text_in_area(self, container, offset, left, top, right, bottom, collected): - """Recursively collect text elements within the selection area""" - if not hasattr(container, '_children'): - return - - for child in container._children: - if hasattr(child, '_origin') and hasattr(child, '_size'): - # Calculate absolute position - child_origin = tuple(child._origin) if hasattr(child._origin, '__iter__') else child._origin - child_size = tuple(child._size) if hasattr(child._size, '__iter__') else child._size - - abs_x = offset[0] + child_origin[0] - abs_y = offset[1] + child_origin[1] - abs_w = child_size[0] - abs_h = child_size[1] - - # Check if element intersects with selection area - if (abs_x < right and abs_x + abs_w > left and - abs_y < bottom and abs_y + abs_h > top): - - # If it's a text element, add its text - if isinstance(child, Text): - text_content = getattr(child, '_text', '') - if text_content.strip(): - collected.append((text_content.strip(), abs_x, abs_y)) - - # If it's a line with words, extract word text - elif hasattr(child, '_words'): - for word in child._words: - if hasattr(word, 'text'): - word_text = word.text - if word_text.strip(): - collected.append((word_text.strip(), abs_x, abs_y)) - - # Recursively check children - if hasattr(child, '_children'): - self._collect_text_in_area(child, (abs_x, abs_y), left, top, right, bottom, collected) - - 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_page: - return - - # Set selection to entire canvas area - canvas_width = self.canvas.winfo_width() - canvas_height = self.canvas.winfo_height() - - self.selection_start = (0, 0) - self.selection_end = (canvas_width, canvas_height) - self.is_selecting = True - - # Extract all text - self.extract_selected_text() - - # Update visual - self.update_selection_visual() - - if self.selected_text: - self.status_var.set(f"Selected all 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 = """ - - pyWebLayout Browser - Welcome - -

Welcome to pyWebLayout Browser

-

This is a simple HTML browser built using pyWebLayout components.

- -

Features:

-
    -
  • Basic HTML rendering
  • -
  • Text formatting (bold, italic, underline)
  • -
  • Headers (H1-H6)
  • -
  • Links (clickable)
  • -
  • Images
  • -
  • Forms (basic support)
  • -
- -

Try these features:

-

Bold text, italic text, and underlined text

- -

Sample link: Visit Example.com

- -

File Operations

-

Use the "Open File" button to load local HTML files.

- -

Or enter a URL in the address bar above.

- - - """ - - parser = HTMLParser() - self.current_page = parser.parse_html_string(html_content) - self.render_page() - self.status_var.set("Welcome page loaded") - - 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() - - try: - if url.startswith(('http://', 'https://')): - # Web URL - response = requests.get(url, timeout=10) - response.raise_for_status() - html_content = response.text - - parser = HTMLParser() - self.current_page = parser.parse_html_string(html_content, url) - - elif os.path.isfile(url): - # Local file - parser = HTMLParser() - self.current_page = parser.parse_html_file(url) - - 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): - parser = HTMLParser() - self.current_page = parser.parse_html_file(file_path) - else: - raise FileNotFoundError(f"File not found: {file_path}") - - # Add to history - self.add_to_history(url) - self.render_page() - 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 render_page(self): - """Render the current page to the canvas""" - if not self.current_page: - return - - # Clear canvas - self.canvas.delete("all") - - # Render the page to PIL Image - page_image = self.current_page.render() - - # Convert to PhotoImage - self.photo = ImageTk.PhotoImage(page_image) - - # Display on canvas - self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo) - - # Update scroll region - self.canvas.configure(scrollregion=self.canvas.bbox("all")) - - # Store page elements for interaction - self.page_elements = self._get_clickable_elements(self.current_page) - - def _get_clickable_elements(self, container, offset=(0, 0)) -> List[Tuple]: - """Get list of clickable elements with their positions""" - elements = [] - - if hasattr(container, '_children'): - for child in container._children: - if hasattr(child, '_origin'): - # Convert numpy arrays to tuples for consistent coordinate handling - child_origin = tuple(child._origin) if hasattr(child._origin, '__iter__') else child._origin - child_size = tuple(child._size) if hasattr(child._size, '__iter__') else child._size - - child_offset = (offset[0] + child_origin[0], offset[1] + child_origin[1]) - - # Check if element is clickable - if isinstance(child, (RenderableLink, RenderableButton)): - elements.append((child, child_offset, child_size)) - - # Recursively check children - if hasattr(child, '_children'): - elements.extend(self._get_clickable_elements(child, child_offset)) - - return elements - - def on_click(self, event): - """Handle mouse clicks on the canvas""" - # Convert canvas coordinates to image coordinates - canvas_x = self.canvas.canvasx(event.x) - canvas_y = self.canvas.canvasy(event.y) - - # Check if click is on any clickable element - for element, offset, size in self.page_elements: - element_x, element_y = offset - element_w, element_h = size - - if (element_x <= canvas_x <= element_x + element_w and - element_y <= canvas_y <= element_y + element_h): - - # Handle the click - if isinstance(element, RenderableLink): - result = element._callback() - if result: - self.status_var.set(result) - # For external links, open in system browser - if element._link.link_type == LinkType.EXTERNAL: - webbrowser.open(element._link.location) - - elif isinstance(element, RenderableButton): - result = element._callback() - if result: - self.status_var.set(f"Button clicked: {result}") - - break - - def on_mouse_move(self, event): - """Handle mouse movement for hover effects""" - # Convert canvas coordinates to image coordinates - canvas_x = self.canvas.canvasx(event.x) - canvas_y = self.canvas.canvasy(event.y) - - # Check if mouse is over any clickable element - cursor = "arrow" - for element, offset, size in self.page_elements: - element_x, element_y = offset - element_w, element_h = size - - if (element_x <= canvas_x <= element_x + element_w and - element_y <= canvas_y <= element_y + element_h): - cursor = "hand2" - break - - self.canvas.configure(cursor=cursor) - - 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""" - if self.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 browser""" - print("Starting pyWebLayout HTML Browser...") - - try: - browser = BrowserWindow() - browser.run() - except Exception as e: - print(f"Error starting browser: {e}") - import traceback - traceback.print_exc() - - -if __name__ == "__main__": - main() diff --git a/pyWebLayout/examples/test_page.html b/pyWebLayout/examples/test_page.html deleted file mode 100644 index 1508869..0000000 --- a/pyWebLayout/examples/test_page.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - Test Page for pyWebLayout Browser - - -

pyWebLayout Browser Test Page

-

Images

-

Here's a sample image:

- Sample Image -

Text Formatting

-

This is a paragraph with bold text, italic text, and underlined text.

- -

Links

-

Here are some test links:

- - -

Headers

-

H1 Header

-

H2 Header

-

H3 Header

-

H4 Header

-
H5 Header
-
H6 Header
- -

Line Breaks and Paragraphs

-

This is the first paragraph.

-
-

This is the second paragraph after a line break.

- -

-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.” -

-

-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. -

-

-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 Tuolomee, and informed Cody that a wind might catch him and break him up in half an hour. -

-

-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. -

- -

Section 1

-

This is the content of section 1. You can link to this section using the internal link above.

- -

Images

-

Here's a sample image:

- Sample Image - -

Mixed Content

-

This paragraph contains bold and italic text, as well as an external link.

- -

Strong text and emphasized text should also work.

- -