- Added logging functionality to app.py and rns.py for better error tracking. - Improved exception handling in RNSManager methods to log specific failures. - Refactored code in various modules to ensure consistent logging practices. - Updated UI components to handle exceptions with user feedback. - Cleaned up formatting in several files for better readability.
375 lines
13 KiB
Python
375 lines
13 KiB
Python
"""Tab management system for Ren Browser.
|
|
|
|
Provides tab creation, switching, and content management functionality
|
|
for the browser interface.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import flet as ft
|
|
|
|
from ren_browser.pages.page_request import PageFetcher, PageRequest
|
|
from ren_browser.renderer.micron import render_micron
|
|
from ren_browser.renderer.plaintext import render_plaintext
|
|
from ren_browser.storage.storage import get_storage_manager
|
|
|
|
|
|
class TabsManager:
|
|
"""Manages browser tabs and their content.
|
|
|
|
Handles tab creation, switching, closing, and content rendering.
|
|
"""
|
|
|
|
def __init__(self, page: ft.Page) -> None:
|
|
"""Initialize the tab manager.
|
|
|
|
Args:
|
|
page: Flet page instance for UI updates.
|
|
|
|
"""
|
|
import ren_browser.app as app_module
|
|
|
|
self.page = page
|
|
self.page.on_resize = self._on_resize
|
|
self.manager = SimpleNamespace(tabs=[], index=0)
|
|
|
|
storage = get_storage_manager(page)
|
|
self.settings = storage.load_app_settings()
|
|
|
|
self.tab_bar = ft.Container(
|
|
content=ft.Row(
|
|
spacing=6,
|
|
scroll=ft.ScrollMode.AUTO,
|
|
),
|
|
padding=ft.padding.symmetric(horizontal=8, vertical=8),
|
|
)
|
|
self.overflow_menu = None
|
|
self.content_container = ft.Container(
|
|
expand=True,
|
|
bgcolor=self.settings.get("page_bgcolor", ft.Colors.BLACK),
|
|
padding=ft.padding.all(16),
|
|
)
|
|
|
|
def handle_link_click_home(link_url):
|
|
if len(self.manager.tabs) > 0:
|
|
tab = self.manager.tabs[0]
|
|
full_url = link_url
|
|
if ":" not in link_url:
|
|
full_url = f"{link_url}:/page/index.mu"
|
|
tab["url_field"].value = full_url
|
|
self._on_tab_go(None, 0)
|
|
|
|
default_content = (
|
|
render_micron(
|
|
"Welcome to Ren Browser",
|
|
on_link_click=handle_link_click_home,
|
|
)
|
|
if app_module.RENDERER == "micron"
|
|
else render_plaintext("Welcome to Ren Browser")
|
|
)
|
|
self._add_tab_internal("Home", default_content)
|
|
self.add_btn = ft.IconButton(
|
|
ft.Icons.ADD,
|
|
tooltip="New Tab",
|
|
on_click=self._on_add_click,
|
|
icon_color=ft.Colors.WHITE,
|
|
)
|
|
self.close_btn = ft.IconButton(
|
|
ft.Icons.CLOSE,
|
|
tooltip="Close Tab",
|
|
on_click=self._on_close_click,
|
|
icon_color=ft.Colors.WHITE,
|
|
)
|
|
self.tab_bar.content.controls.append(self.add_btn)
|
|
self.tab_bar.content.controls.append(self.close_btn)
|
|
self.select_tab(0)
|
|
self._update_tab_visibility()
|
|
|
|
def _on_resize(self, e) -> None: # type: ignore
|
|
"""Handle page resize event and update tab visibility."""
|
|
self._update_tab_visibility()
|
|
|
|
def apply_settings(self, settings: dict) -> None:
|
|
"""Apply appearance settings to the tab manager.
|
|
|
|
Args:
|
|
settings: Dictionary containing appearance settings.
|
|
|
|
"""
|
|
self.settings = settings
|
|
bgcolor = settings.get("page_bgcolor", "#000000")
|
|
self.content_container.bgcolor = bgcolor
|
|
|
|
horizontal_scroll = settings.get("horizontal_scroll", False)
|
|
scroll_mode = ft.ScrollMode.ALWAYS if horizontal_scroll else ft.ScrollMode.AUTO
|
|
|
|
for tab in self.manager.tabs:
|
|
if "content" in tab and hasattr(tab["content"], "scroll"):
|
|
tab["content"].scroll = scroll_mode
|
|
if "content_control" in tab and hasattr(tab["content_control"], "scroll"):
|
|
tab["content_control"].scroll = scroll_mode
|
|
|
|
if self.content_container.content:
|
|
self.content_container.content.update()
|
|
self.page.update()
|
|
|
|
def _update_tab_visibility(self) -> None:
|
|
"""Dynamically adjust tab visibility based on page width.
|
|
|
|
Hides tabs that do not fit and moves them to an overflow menu.
|
|
"""
|
|
if not self.page.width or self.page.width == 0:
|
|
return
|
|
|
|
if self.overflow_menu and self.overflow_menu in self.tab_bar.content.controls:
|
|
self.tab_bar.content.controls.remove(self.overflow_menu)
|
|
self.overflow_menu = None
|
|
|
|
available_width = self.page.width - 100
|
|
|
|
cumulative_width = 0
|
|
visible_tabs_count = 0
|
|
|
|
tab_containers = [
|
|
c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)
|
|
]
|
|
|
|
for i, tab in enumerate(self.manager.tabs):
|
|
estimated_width = len(tab["title"]) * 10 + 32 + self.tab_bar.content.spacing
|
|
|
|
if cumulative_width + estimated_width <= available_width or i == 0:
|
|
cumulative_width += estimated_width
|
|
if i < len(tab_containers):
|
|
tab_containers[i].visible = True
|
|
visible_tabs_count += 1
|
|
elif i < len(tab_containers):
|
|
tab_containers[i].visible = False
|
|
|
|
if len(self.manager.tabs) > visible_tabs_count:
|
|
overflow_items = []
|
|
for i in range(visible_tabs_count, len(self.manager.tabs)):
|
|
tab_data = self.manager.tabs[i]
|
|
overflow_items.append(
|
|
ft.PopupMenuItem(
|
|
text=tab_data["title"],
|
|
on_click=lambda e, idx=i: self.select_tab(idx), # type: ignore
|
|
),
|
|
)
|
|
|
|
self.overflow_menu = ft.PopupMenuButton(
|
|
icon=ft.Icons.MORE_HORIZ,
|
|
tooltip=f"{len(self.manager.tabs) - visible_tabs_count} more tabs",
|
|
items=overflow_items,
|
|
)
|
|
|
|
self.tab_bar.content.controls.insert(visible_tabs_count, self.overflow_menu)
|
|
|
|
def _add_tab_internal(self, title: str, content: ft.Control) -> None:
|
|
"""Add a new tab to the manager with the given title and content."""
|
|
idx = len(self.manager.tabs)
|
|
url_field = ft.TextField(
|
|
value=title,
|
|
expand=True,
|
|
text_style=ft.TextStyle(size=14),
|
|
content_padding=ft.padding.symmetric(horizontal=16, vertical=12),
|
|
border_radius=24,
|
|
border_color=ft.Colors.GREY_700,
|
|
focused_border_color=ft.Colors.BLUE_400,
|
|
bgcolor=ft.Colors.GREY_800,
|
|
prefix_icon=ft.Icons.SEARCH,
|
|
)
|
|
go_btn = ft.IconButton(
|
|
ft.Icons.ARROW_FORWARD,
|
|
tooltip="Go",
|
|
on_click=lambda e, i=idx: self._on_tab_go(e, i),
|
|
icon_color=ft.Colors.BLUE_400,
|
|
bgcolor=ft.Colors.BLUE_900,
|
|
)
|
|
content_control = content
|
|
horizontal_scroll = self.settings.get("horizontal_scroll", False)
|
|
scroll_mode = ft.ScrollMode.ALWAYS if horizontal_scroll else ft.ScrollMode.AUTO
|
|
|
|
tab_content = ft.Column(
|
|
expand=True,
|
|
scroll=scroll_mode,
|
|
controls=[
|
|
content_control,
|
|
],
|
|
)
|
|
self.manager.tabs.append(
|
|
{
|
|
"title": title,
|
|
"url_field": url_field,
|
|
"go_btn": go_btn,
|
|
"content_control": content_control,
|
|
"content": tab_content,
|
|
},
|
|
)
|
|
tab_container = ft.Container(
|
|
content=ft.Row(
|
|
controls=[
|
|
ft.Text(
|
|
title,
|
|
size=13,
|
|
weight=ft.FontWeight.W_500,
|
|
overflow=ft.TextOverflow.ELLIPSIS,
|
|
),
|
|
],
|
|
spacing=8,
|
|
),
|
|
on_click=lambda e, i=idx: self.select_tab(i), # type: ignore
|
|
padding=ft.padding.symmetric(horizontal=16, vertical=10),
|
|
border_radius=8,
|
|
bgcolor=ft.Colors.GREY_800,
|
|
ink=True,
|
|
width=150,
|
|
)
|
|
insert_pos = max(0, len(self.tab_bar.content.controls) - 2)
|
|
self.tab_bar.content.controls.insert(insert_pos, tab_container)
|
|
self._update_tab_visibility()
|
|
|
|
def _on_add_click(self, e) -> None: # type: ignore
|
|
"""Handle the add tab button click event."""
|
|
title = f"Tab {len(self.manager.tabs) + 1}"
|
|
content_text = f"Content for {title}"
|
|
import ren_browser.app as app_module
|
|
|
|
new_idx = len(self.manager.tabs)
|
|
|
|
def handle_link_click_new(link_url):
|
|
tab = self.manager.tabs[new_idx]
|
|
full_url = link_url
|
|
if ":" not in link_url:
|
|
full_url = f"{link_url}:/page/index.mu"
|
|
tab["url_field"].value = full_url
|
|
self._on_tab_go(None, new_idx)
|
|
|
|
content = (
|
|
render_micron(content_text, on_link_click=handle_link_click_new)
|
|
if app_module.RENDERER == "micron"
|
|
else render_plaintext(content_text)
|
|
)
|
|
self._add_tab_internal(title, content)
|
|
self.select_tab(len(self.manager.tabs) - 1)
|
|
self.page.update()
|
|
|
|
def _on_close_click(self, e) -> None: # type: ignore
|
|
"""Handle the close tab button click event."""
|
|
if len(self.manager.tabs) <= 1:
|
|
return
|
|
idx = self.manager.index
|
|
|
|
tab_containers = [
|
|
c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)
|
|
]
|
|
control_to_remove = tab_containers[idx]
|
|
|
|
self.manager.tabs.pop(idx)
|
|
self.tab_bar.content.controls.remove(control_to_remove)
|
|
|
|
updated_tab_containers = [
|
|
c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)
|
|
]
|
|
for i, control in enumerate(updated_tab_containers):
|
|
control.on_click = lambda e, i=i: self.select_tab(i) # type: ignore
|
|
|
|
new_idx = min(idx, len(self.manager.tabs) - 1)
|
|
self.select_tab(new_idx)
|
|
self._update_tab_visibility()
|
|
self.page.update()
|
|
|
|
def select_tab(self, idx: int) -> None:
|
|
"""Select and display the tab at the given index.
|
|
|
|
Args:
|
|
idx: Index of the tab to select.
|
|
|
|
"""
|
|
self.manager.index = idx
|
|
|
|
tab_containers = [
|
|
c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)
|
|
]
|
|
for i, control in enumerate(tab_containers):
|
|
if i == idx:
|
|
control.bgcolor = ft.Colors.BLUE_900
|
|
control.border = ft.border.all(2, ft.Colors.BLUE_400)
|
|
else:
|
|
control.bgcolor = ft.Colors.GREY_800
|
|
control.border = None
|
|
|
|
self.content_container.content = self.manager.tabs[idx]["content"]
|
|
self.page.update()
|
|
|
|
def _on_tab_go(self, e, idx: int) -> None: # type: ignore
|
|
"""Handle the go button click event for a tab, loading new content."""
|
|
tab = self.manager.tabs[idx]
|
|
url = tab["url_field"].value.strip()
|
|
if not url:
|
|
return
|
|
|
|
placeholder_text = f"Loading content for {url}..."
|
|
import ren_browser.app as app_module
|
|
|
|
current_node_hash = None
|
|
if ":" in url:
|
|
current_node_hash = url.split(":")[0]
|
|
|
|
def handle_link_click(link_url):
|
|
full_url = link_url
|
|
if ":" not in link_url:
|
|
full_url = f"{link_url}:/page/index.mu"
|
|
elif link_url.startswith(":/"):
|
|
if current_node_hash:
|
|
full_url = f"{current_node_hash}{link_url}"
|
|
else:
|
|
full_url = link_url
|
|
tab["url_field"].value = full_url
|
|
self._on_tab_go(None, idx)
|
|
|
|
placeholder_control = (
|
|
render_micron(placeholder_text, on_link_click=handle_link_click)
|
|
if app_module.RENDERER == "micron"
|
|
else render_plaintext(placeholder_text)
|
|
)
|
|
tab["content_control"] = placeholder_control
|
|
tab["content"].controls[0] = placeholder_control
|
|
if self.manager.index == idx:
|
|
self.content_container.content = tab["content"]
|
|
self.page.update()
|
|
|
|
def fetch_and_update():
|
|
parts = url.split(":", 1)
|
|
if len(parts) != 2:
|
|
result = "Error: Invalid URL format. Expected format: hash:/page/path"
|
|
page_path = ""
|
|
else:
|
|
dest_hash = parts[0]
|
|
page_path = parts[1] if parts[1].startswith("/") else f"/{parts[1]}"
|
|
|
|
req = PageRequest(destination_hash=dest_hash, page_path=page_path)
|
|
page_fetcher = PageFetcher()
|
|
try:
|
|
result = page_fetcher.fetch_page(req)
|
|
except Exception as ex:
|
|
app_module.log_error(str(ex))
|
|
result = f"Error: {ex}"
|
|
|
|
try:
|
|
tab = self.manager.tabs[idx]
|
|
except IndexError:
|
|
return
|
|
|
|
if page_path and page_path.endswith(".mu"):
|
|
new_control = render_micron(result, on_link_click=handle_link_click)
|
|
else:
|
|
new_control = render_plaintext(result)
|
|
|
|
tab["content_control"] = new_control
|
|
tab["content"].controls[0] = new_control
|
|
if self.manager.index == idx:
|
|
self.content_container.content = tab["content"]
|
|
self.page.update()
|
|
|
|
self.page.run_thread(fetch_and_update)
|