Metadata-Version: 2.4
Name: sideview
Version: 0.4.0
Summary: A lightweight native webview widget for PySide6.
Keywords: pyside6,qt,webview,webview2,webkit,webkitgtk
Author: AlexsanderMe
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: X11 Applications :: Qt
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries
Project-URL: Homepage, https://github.com/AlexsanderMe/SideView
Project-URL: Documentation, https://github.com/AlexsanderMe/SideView#readme
Project-URL: Repository, https://github.com/AlexsanderMe/SideView
Project-URL: Issues, https://github.com/AlexsanderMe/SideView/issues
Project-URL: Changelog, https://github.com/AlexsanderMe/SideView/blob/master/CHANGELOG.md
Requires-Python: >=3.10
Requires-Dist: PySide6>=6.5
Provides-Extra: dev
Requires-Dist: build<2,>=1.3; extra == "dev"
Requires-Dist: pytest<10,>=8; extra == "dev"
Requires-Dist: ruff<1,>=0.12; extra == "dev"
Requires-Dist: twine<7,>=6; extra == "dev"
Description-Content-Type: text/markdown

# SideView

A lightweight native webview widget for PySide6 applications that need the operating
system browser stack without bundling Qt WebEngine.

The library exposes a normal `QWidget` subclass in Python and delegates rendering to:

- Windows: Microsoft Edge WebView2, hosted inside the widget's native `HWND`.
- macOS: `WKWebView`, hosted inside the widget's native `NSView`.
- Linux: WebKitGTK 4.1, embedded into the Qt widget through an X11 foreign window.

This is intentionally not a full browser. It is a small embedded view for in-app navigation, OAuth/help pages, web dashboards, and video playback using codecs available through the operating system browser stack.

It also gives the host application control over browser-adjacent behavior that should not be left to the embedded engine by default:

- Downloads are intercepted and blocked unless your Python whitelist callback allows them.
- Links that request a new window are intercepted and surfaced as a signal, so the app can open an internal tab instead of letting the engine spawn a separate native window.
- JavaScript can be injected at document creation, and page scripts can send messages back to Python.
- Native context menus and devtools can be disabled so the host app can provide its own controlled menu.
- The visible webview can be captured as PNG/JPEG bytes, either as a full frame, viewport-relative region, or a continuous native frame stream where supported.
- Browser zoom can be read, changed, and observed through a platform-neutral API.

## Why not fork pywebview?

`pywebview` is a window abstraction. Its backends are designed to own top-level windows and integrate with their own GUI loops. This library uses the opposite contract: PySide6 owns the application, and the native webview is a child of a Qt widget. That keeps focus, resizing, lifecycle, and application shutdown under Qt's control.

## Current status

SideView is currently an alpha API. The repository provides:

- Focused Python `NativeWebView` API.
- Native library loader with clear platform errors.
- Windows C++ backend using WebView2.
- macOS Objective-C++ backend using WKWebView.
- Linux C++ backend using WebKitGTK and GTK 3.
- CMake build files for native libraries.
- A tabbed PySide6 browser example with back, forward, reload, new tab, close tab, URL/search bar, download policy hooks, and new-window routing.
- Script bridge hooks for custom overlays, page-to-Python messages, and controlled context menus.
- Asynchronous native capture hooks for tab-live previews, region projection, and high-FPS frame streaming on WebView2.

## Installation

Windows x64 and macOS universal2 wheels contain the native backend:

```bash
python -m pip install sideview
```

Linux is distributed as source because WebKitGTK is a system library. Install the
WebKitGTK 4.1 and GTK 3 development packages for your distribution before running
the same `pip install` command. For Ubuntu 22.04 and newer:

```bash
sudo apt-get install libgtk-3-dev libwebkit2gtk-4.1-dev pkg-config
python -m pip install sideview
```

Linux embedding requires Qt's XCB platform. Native X11 works directly; Wayland
sessions can run through XWayland by setting `QT_QPA_PLATFORM=xcb` before the
`QApplication` is created.

## Python usage

```python
from PySide6.QtWidgets import QApplication, QMainWindow
from sideview import NativeWebView

app = QApplication([])

window = QMainWindow()
view = NativeWebView()
window.setCentralWidget(view)
window.resize(1200, 800)
window.show()

view.navigate("https://www.youtube.com")

app.exec()
```

## Downloads and new windows

Downloads are denied by default. Register a callback with `set_download_policy()` to allow only the URLs your app trusts. The callback should return `True` to let the native webview continue the download, or `False` to cancel it so your app can handle the URL itself.

```python
from urllib.parse import urlparse

ALLOWED_DOWNLOAD_HOSTS = {"example.com", "cdn.example.com"}


def allow_download(url: str) -> bool:
    return (urlparse(url).hostname or "").lower() in ALLOWED_DOWNLOAD_HOSTS


view.set_download_policy(allow_download)
view.downloadRequested.connect(lambda url: print("Download requested:", url))
```

Links that try to open a new tab or popup are not opened as separate native windows. Instead, the widget emits `newWindowRequested(url)`.

```python
view.newWindowRequested.connect(lambda url: open_internal_tab(url))
```

## JavaScript bridge and context menus

Use `add_document_script()` for persistent JavaScript that should run at document creation on every future navigation. Use `eval_js()` for one-shot JavaScript on the currently loaded page.

```python
view.add_document_script("""
window.addEventListener("DOMContentLoaded", () => {
  document.documentElement.dataset.hostApp = "solin";
});
""")
```

`install_script_bridge()` exposes `window.nativeWebView.postMessage(message)` to page scripts. Messages arrive in Python through `scriptMessageReceived`.

```python
view.install_script_bridge()
view.scriptMessageReceived.connect(lambda message: print("JS:", message))

view.eval_js("""
window.nativeWebView.postMessage({
  type: "overlay-ready",
  href: location.href
});
""")
```

For a controlled right-click menu, call `install_context_menu_bridge()`. It disables the native browser context menu, prevents default page context menus, and emits `contextMenuRequested(dict)` with coordinates and basic target metadata.

```python
def show_context_menu(payload: dict) -> None:
    print(payload["x"], payload["y"], payload.get("href"), payload.get("src"))


view.set_devtools_enabled(False)
view.install_context_menu_bridge()
view.contextMenuRequested.connect(show_context_menu)
```

## Native capture

`capture_frame()` and `capture_region()` return immediately with a request id. The PNG bytes arrive later through `captureCompleted(request_id, data)`. `capture_frame_jpeg()` follows the same request/response model, but returns JPEG bytes for lighter live-preview snapshots. Failures arrive through `captureFailed(request_id, error)`.

```python
def save_capture(request_id: int, data: bytes) -> None:
    Path(f"capture-{request_id}.png").write_bytes(data)


view.captureCompleted.connect(save_capture)
view.captureFailed.connect(lambda request_id, error: print("Capture failed:", error))

full_request_id = view.capture_frame()
region_request_id = view.capture_region(120, 80, 640, 360)
jpeg_request_id = view.capture_frame_jpeg()
```

The capture API is intentionally native and asynchronous:

- Windows uses WebView2 `CapturePreview` and crops regions with Windows Imaging Component.
- macOS uses WKWebView snapshots with `WKSnapshotConfiguration`.
- Linux uses WebKitGTK visible-region snapshots and GdkPixbuf encoding.
- Coordinates use widget pixels with a top-left origin.

For live projection on Windows, prefer `start_frame_stream()`. It uses WebView2 DevTools screencast frames and emits JPEG bytes through `frameStreamFrame(data)`. Unsupported platforms return `False`, so production apps should fall back to `capture_frame_jpeg()`.

```python
def show_live_frame(data: bytes) -> None:
    pixmap = QPixmap()
    if pixmap.loadFromData(data):
        update_projection(pixmap)


view.frameStreamFrame.connect(show_live_frame)
view.frameStreamFailed.connect(lambda error: print("Frame stream failed:", error))

if not view.start_frame_stream(quality=75, max_width=1280, max_height=720):
    start_timer_based_jpeg_capture()

# Later, when projection stops:
view.stop_frame_stream()
```

## Sessions and cookies

Use `session_id` to isolate browser state per application profile. Views created with the same `session_id` share cookies/cache; views created with different IDs get separate sessions.

```python
profile_id = "default"  # for example, your active app profile id

view = NativeWebView(
    session_id=f"solin_session_{profile_id}",
    session_data_root="path/to/user/data/webview",
)
```

On Windows, the session maps to a WebView2 `userDataFolder`. On macOS, the session maps to a persistent `WKWebsiteDataStore` identifier when the platform supports it. On Linux, it maps to a WebKitGTK website-data directory with persistent cookie storage. You can still pass `user_data_folder` directly when you need full control over the storage path.

Cookies can be set before or after the native view is ready. If the view is not ready yet, the cookie operation is queued and applied before the first pending navigation.

```python
view.set_cookie(
    name="session",
    value="abc123",
    domain=".example.com",
    path="/",
    secure=True,
    http_only=True,
    same_site="lax",
)
```

To clear the current session cookies:

```python
view.clear_cookies()
view.reload()
```

## Zoom

`set_zoom_factor()` applies native page zoom. User changes, including
`Ctrl + scroll` in WebView2, emit `zoomFactorChanged`, allowing a host with
multiple views to keep one shared zoom preference.

```python
view.set_zoom_factor(1.25)
view.zoomFactorChanged.connect(lambda factor: print("Zoom:", factor))
print(view.zoom_factor())
```

## Building from source

### GitHub Actions

Every pull request runs tests and builds the release distributions through
`.github/workflows/build-distributions.yml`. Release builds produce:

- a Windows x64 `py3-none-win_amd64` wheel;
- a macOS `py3-none-macosx_*_universal2` wheel;
- one source distribution used to build the Linux backend on the target machine.

The workflow can also be dispatched manually for a branch, tag, or commit.

### Windows

Install the WebView2 Runtime and the WebView2 SDK headers/libraries. The CMake project expects `WEBVIEW2_SDK_DIR` to point at the SDK root that contains `build/native/include/WebView2.h` and the matching loader library.

```powershell
.\scripts\build_windows.ps1 -WebView2SdkDir C:\path\to\Microsoft.Web.WebView2
```

The script uses `vswhere` to locate Visual Studio Build Tools, configures MSVC
through `vcvars64.bat`, builds the DLL, and copies it next to the Python package.
If you build manually, copy `sideview_native.dll` next to `src/sideview/` or set:

```powershell
$env:SIDEVIEW_NATIVE_LIB="C:\path\to\sideview_native.dll"
```

### macOS

```bash
cmake -S native -B build/native -DCMAKE_BUILD_TYPE=Release
cmake --build build/native
```

Copy `libsideview_native.dylib` next to `src/sideview/` or set:

```bash
export SIDEVIEW_NATIVE_LIB=/path/to/libsideview_native.dylib
```

### Linux

The Linux backend uses the GTK 3 build of WebKitGTK 4.1. On Ubuntu 22.04 or
newer Debian-based distributions, install the build dependencies and build the
library with:

```bash
sudo apt-get install libgtk-3-dev libwebkit2gtk-4.1-dev pkg-config
bash scripts/build_linux.sh
```

The build script copies `libsideview_native.so` next to the Python package. A
custom location can be selected with:

```bash
export SIDEVIEW_NATIVE_LIB=/path/to/libsideview_native.so
```

Embedding GTK inside Qt requires the X11/XCB window model. It works on native X11 desktops and through XWayland. On a Wayland session, launch the application with the Qt XCB backend before creating `QApplication`:

```bash
QT_QPA_PLATFORM=xcb python examples/simple_browser.py
```

WebKitGTK and GTK remain dynamically linked system dependencies.

## Design constraints

- The native webview is a real native child view. It draws outside Qt's paint engine, so it should not be overlapped by translucent Qt widgets.
- Because rendering happens in a native child view, Qt `QWidget.grab()` is not a reliable production-grade way to capture tab-live thumbnails or crop projected page regions. Use the native capture API instead.
- Keep one webview per visible widget unless the product truly needs more; native browser views are heavier than normal widgets.
- Navigation policy, custom context menus, downloads, permission prompts, and devtools should be added deliberately as product requirements, not by default.
- Linux embedding requires Qt to use the XCB platform because GTK 3 foreign-window embedding is X11-specific. Native Wayland cross-toolkit embedding is not available.

## Example

Run:

```bash
python examples/simple_browser.py
```

The example opens Google in a new tab by default. The address bar goes directly to valid URLs and searches Google when the input looks like plain text. Its download whitelist is intentionally empty:

```python
DOWNLOAD_HOST_WHITELIST: set[str] = set()
```

Replace `is_download_allowed(url)` in `examples/simple_browser.py` with your application policy when you decide how downloads should be handled.
