bowser/src/browser/browser.py
Benedikt Willi 3838aa17af Add DOM Visualization Feature with Integrated Browser Support
- Introduced a new feature to visualize the Document Object Model (DOM) tree of the loaded web page.
- Implemented keyboard shortcut (Ctrl+Shift+D) for generating and displaying the DOM graph.
- Added core implementation files:
  - src/debug/dom_graph.py: Handles generation and rendering of graphs in DOT and SVG formats.
- Created templates and documentation for the new feature, including:
  - docs/DOM_VISUALIZATION.md: Overview and usage instructions.
  - docs/DOM_GRAPH_UX.md: User experience design documentation.
- Allowed the visualization to open in a new browser tab instead of an external viewer.
- Enhanced visual representation through color coding of different HTML elements.
- Implemented comprehensive tests for graph generation and page rendering.
- Updated README.md to include usage instructions for DOM visualization.
- Included test_holder.html as an example test page.
- Modified various components in the browser to integrate tab management and enhance the graphical rendering experience.
2026-01-10 00:19:21 +01:00

127 lines
4.2 KiB
Python

"""Browser entry and orchestration."""
import gi
import logging
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk
from ..network.url import URL
from .chrome import Chrome
from .tab import Tab
class Browser:
def __init__(self):
self.logger = logging.getLogger("bowser.browser")
self.app = Gtk.Application(application_id="ch.bowser.bowser")
self.app.connect("activate", self.on_activate)
self.tabs = []
self.active_tab: Tab | None = None
self.chrome = Chrome(self)
def _log(self, msg: str, level: int = logging.INFO):
self.logger.log(level, msg)
def on_activate(self, app):
"""Called when the application is activated."""
self._log("Application activated", logging.DEBUG)
self.chrome.create_window()
def new_tab(self, url: str):
tab = Tab(self)
# Normalize URL to ensure https:// protocol
url = self._normalize_url(url)
tab.load(URL(url))
self.tabs.append(tab)
self.active_tab = tab
# Add to UI
self.chrome.add_tab(tab)
self.chrome.update_tab(tab) # Update title after load
self.chrome.update_address_bar()
self._log(f"New tab opened: {url}", logging.INFO)
return tab
def set_active_tab(self, tab: Tab, *_args):
if tab in self.tabs:
self.active_tab = tab
self._log(f"Active tab set: {tab.title}", logging.DEBUG)
# Update UI
self.chrome.set_active_tab(tab)
# Trigger repaint of content area
self.chrome.paint()
self.chrome.update_address_bar()
def close_tab(self, tab: Tab, *_args):
if tab not in self.tabs:
return
idx = self.tabs.index(tab)
self.tabs.remove(tab)
# Choose new active tab if needed
if self.active_tab is tab:
if self.tabs:
self.active_tab = self.tabs[max(0, idx - 1)]
self.chrome.set_active_tab(self.active_tab)
else:
self.active_tab = None
# Open a new tab when all tabs are closed
self.new_tab("about:startpage")
return # Return early - don't try to remove a tab that's being closed
# Update UI (tab_pages already cleaned in _on_close_page)
self.chrome.paint()
self.chrome.update_address_bar()
self._log(f"Tab closed: {tab.title}", logging.INFO)
# Navigation and history wrappers
def navigate_to(self, url_str: str):
if not url_str:
return
# Add https:// if no protocol provided
url_str = self._normalize_url(url_str)
if not self.active_tab:
self.new_tab(url_str)
return
self.active_tab.load(URL(url_str))
self.chrome.paint()
self.chrome.update_tab(self.active_tab) # Update title after load
self.chrome.update_address_bar()
self._log(f"Navigate to: {url_str}", logging.INFO)
def _normalize_url(self, url_str: str) -> str:
"""Add https:// protocol if not present."""
url_str = url_str.strip()
# If URL already has a protocol, return as-is
if "://" in url_str:
return url_str
# Special about: URLs
if url_str.startswith("about:"):
return url_str
# Otherwise, assume https://
return f"https://{url_str}"
def go_back(self):
if self.active_tab and self.active_tab.go_back():
self.chrome.paint()
self.chrome.update_tab(self.active_tab)
self.chrome.update_address_bar()
self._log("Go back", logging.DEBUG)
def go_forward(self):
if self.active_tab and self.active_tab.go_forward():
self.chrome.paint()
self.chrome.update_tab(self.active_tab)
self.chrome.update_address_bar()
self._log("Go forward", logging.DEBUG)
def reload(self):
if self.active_tab:
self.active_tab.reload()
self.chrome.paint()
self.chrome.update_tab(self.active_tab)
self.chrome.update_address_bar()
self._log("Reload", logging.DEBUG)
def run(self):
"""Start the GTK application main loop."""
return self.app.run()