772 lines
30 KiB
Python
772 lines
30 KiB
Python
#!/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()
|