- Introduced `allowed_urls` in configuration to specify whitelisted domains/patterns. - Implemented `RestrictedWebEngineView` to enforce URL restrictions in the web view. - Updated `MainWindow` to utilize the new restricted web view and added navigation toolbar. - Enhanced unit tests for configuration and restricted web view to cover new functionality.
210 lines
6.6 KiB
Python
210 lines
6.6 KiB
Python
"""Main application window with web engine integration."""
|
|
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from PySide6.QtCore import QSize, Qt, QUrl
|
|
from PySide6.QtGui import QIcon
|
|
from PySide6.QtWidgets import QMainWindow, QToolBar, QVBoxLayout, QWidget
|
|
|
|
from webdrop_bridge.config import Config
|
|
from webdrop_bridge.core.drag_interceptor import DragInterceptor
|
|
from webdrop_bridge.core.validator import PathValidator
|
|
from webdrop_bridge.ui.restricted_web_view import RestrictedWebEngineView
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
"""Main application window for WebDrop Bridge.
|
|
|
|
Displays web content in a QWebEngineView and provides drag-and-drop
|
|
integration with the native filesystem.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
config: Config,
|
|
parent: Optional[QWidget] = None,
|
|
):
|
|
"""Initialize the main window.
|
|
|
|
Args:
|
|
config: Application configuration
|
|
parent: Parent widget
|
|
"""
|
|
super().__init__(parent)
|
|
self.config = config
|
|
|
|
# Set window properties
|
|
self.setWindowTitle(f"{config.app_name} v{config.app_version}")
|
|
self.setGeometry(
|
|
100,
|
|
100,
|
|
config.window_width,
|
|
config.window_height,
|
|
)
|
|
|
|
# Create web engine view
|
|
self.web_view = RestrictedWebEngineView(config.allowed_urls)
|
|
|
|
# Create navigation toolbar (Kiosk-mode navigation)
|
|
self._create_navigation_toolbar()
|
|
|
|
# Create drag interceptor
|
|
self.drag_interceptor = DragInterceptor()
|
|
|
|
# Set up path validator
|
|
validator = PathValidator(config.allowed_roots)
|
|
self.drag_interceptor.set_validator(validator)
|
|
|
|
# Connect drag interceptor signals
|
|
self.drag_interceptor.drag_started.connect(self._on_drag_started)
|
|
self.drag_interceptor.drag_failed.connect(self._on_drag_failed)
|
|
|
|
# Set up central widget with layout
|
|
central_widget = QWidget()
|
|
layout = QVBoxLayout()
|
|
layout.addWidget(self.web_view)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
central_widget.setLayout(layout)
|
|
self.setCentralWidget(central_widget)
|
|
|
|
# Load web application
|
|
self._load_webapp()
|
|
|
|
# Apply styling if available
|
|
self._apply_stylesheet()
|
|
|
|
def _load_webapp(self) -> None:
|
|
"""Load the web application.
|
|
|
|
Loads HTML from the configured webapp URL or from local file.
|
|
"""
|
|
webapp_url = self.config.webapp_url
|
|
|
|
if webapp_url.startswith("http://") or webapp_url.startswith("https://"):
|
|
# Remote URL
|
|
self.web_view.load(QUrl(webapp_url))
|
|
else:
|
|
# Local file path
|
|
try:
|
|
file_path = Path(webapp_url).resolve()
|
|
if not file_path.exists():
|
|
self.web_view.setHtml(
|
|
f"<html><body><h1>Error</h1>"
|
|
f"<p>Web application file not found: {file_path}</p>"
|
|
f"</body></html>"
|
|
)
|
|
return
|
|
|
|
# Load local file as file:// URL
|
|
file_url = file_path.as_uri()
|
|
self.web_view.load(QUrl(file_url))
|
|
|
|
except (OSError, ValueError) as e:
|
|
self.web_view.setHtml(
|
|
f"<html><body><h1>Error</h1>"
|
|
f"<p>Failed to load web application: {e}</p>"
|
|
f"</body></html>"
|
|
)
|
|
|
|
def _apply_stylesheet(self) -> None:
|
|
"""Apply application stylesheet if available."""
|
|
stylesheet_path = Path(__file__).parent.parent.parent.parent / \
|
|
"resources" / "stylesheets" / "default.qss"
|
|
|
|
if stylesheet_path.exists():
|
|
try:
|
|
with open(stylesheet_path, "r") as f:
|
|
stylesheet = f.read()
|
|
self.setStyleSheet(stylesheet)
|
|
except (OSError, IOError):
|
|
# Silently fail if stylesheet can't be read
|
|
pass
|
|
|
|
def _on_drag_started(self, paths: list) -> None:
|
|
"""Handle successful drag initiation.
|
|
|
|
Args:
|
|
paths: List of paths that were dragged
|
|
"""
|
|
# Can be extended with logging or status bar updates
|
|
pass
|
|
|
|
def _on_drag_failed(self, error: str) -> None:
|
|
"""Handle drag operation failure.
|
|
|
|
Args:
|
|
error: Error message
|
|
"""
|
|
# Can be extended with logging or user notification
|
|
pass
|
|
|
|
def _create_navigation_toolbar(self) -> None:
|
|
"""Create navigation toolbar with Home, Back, Forward, Refresh buttons.
|
|
|
|
In Kiosk-mode, users can navigate history but cannot freely browse.
|
|
"""
|
|
toolbar = QToolBar("Navigation")
|
|
toolbar.setMovable(False)
|
|
toolbar.setIconSize(QSize(24, 24))
|
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, toolbar)
|
|
|
|
# Back button
|
|
back_action = self.web_view.pageAction(
|
|
self.web_view.WebAction.Back
|
|
)
|
|
toolbar.addAction(back_action)
|
|
|
|
# Forward button
|
|
forward_action = self.web_view.pageAction(
|
|
self.web_view.WebAction.Forward
|
|
)
|
|
toolbar.addAction(forward_action)
|
|
|
|
# Separator
|
|
toolbar.addSeparator()
|
|
|
|
# Home button
|
|
home_action = toolbar.addAction("Home")
|
|
home_action.triggered.connect(self._navigate_home)
|
|
|
|
# Refresh button
|
|
refresh_action = self.web_view.pageAction(
|
|
self.web_view.WebAction.Reload
|
|
)
|
|
toolbar.addAction(refresh_action)
|
|
|
|
def _navigate_home(self) -> None:
|
|
"""Navigate to the home (start) URL."""
|
|
home_url = self.config.webapp_url
|
|
if home_url.startswith("http://") or home_url.startswith("https://"):
|
|
self.web_view.load(QUrl(home_url))
|
|
else:
|
|
try:
|
|
file_path = Path(home_url).resolve()
|
|
file_url = file_path.as_uri()
|
|
self.web_view.load(QUrl(file_url))
|
|
except (OSError, ValueError):
|
|
pass
|
|
|
|
def closeEvent(self, event) -> None:
|
|
"""Handle window close event.
|
|
|
|
Args:
|
|
event: Close event
|
|
"""
|
|
# Can be extended with save operations or cleanup
|
|
event.accept()
|
|
|
|
def initiate_drag(self, file_paths: list) -> bool:
|
|
"""Initiate a drag operation for the given files.
|
|
|
|
Called from web content via JavaScript bridge.
|
|
|
|
Args:
|
|
file_paths: List of file paths to drag
|
|
|
|
Returns:
|
|
True if drag was initiated successfully
|
|
"""
|
|
return self.drag_interceptor.initiate_drag(file_paths)
|