feat(*): implement v0.1.0, add docs
This commit is contained in:
26
.github/workflows/publish.yml
vendored
Normal file
26
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: "Publish"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: pypi
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Install Python 3.13
|
||||||
|
run: uv python install 3.13
|
||||||
|
- name: Build
|
||||||
|
run: uv build
|
||||||
|
- name: Publish
|
||||||
|
run: uv publish
|
||||||
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# Development
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Docs
|
||||||
|
docs/_build/
|
||||||
|
docs/_static/
|
||||||
|
docs/_templates/
|
||||||
17
.pre-commit-config.yaml
Normal file
17
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.14.8
|
||||||
|
hooks:
|
||||||
|
- id: ruff-check
|
||||||
|
types_or: [ python, pyi ]
|
||||||
|
args: [ --fix ]
|
||||||
|
- id: ruff-format
|
||||||
|
types_or: [ python, pyi ]
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: ty
|
||||||
|
name: ty check
|
||||||
|
entry: uvx ty check
|
||||||
|
language: python
|
||||||
|
types_or: [ python, pyi ]
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
20
LICENSE.md
Normal file
20
LICENSE.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
Copyright (c) 2026 h
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
84
README.md
Normal file
84
README.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# pyqt-liquidglass
|
||||||
|
|
||||||
|
[](https://www.python.org/downloads/)
|
||||||
|
[](LICENSE.md)
|
||||||
|
|
||||||
|
macOS Liquid Glass effects for PySide6 and PyQt6.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
pyqt-liquidglass provides a Python API to apply Apple's native glass visual effects to Qt windows and widgets. On macOS 26+, it uses `NSGlassEffectView` for Liquid Glass. On older versions, it falls back to `NSVisualEffectView`.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Apply glass effects to entire windows or specific widgets
|
||||||
|
- Configure corner radius, padding, and materials
|
||||||
|
- Reposition, hide, or show window traffic lights
|
||||||
|
- Automatic Qt binding detection (PySide6, PyQt6)
|
||||||
|
- Safe no-ops on non-macOS platforms
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pyqt-liquidglass
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with uv:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv add pyqt-liquidglass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```python
|
||||||
|
from PySide6.QtWidgets import QApplication, QMainWindow
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
window = QMainWindow()
|
||||||
|
window.resize(800, 600)
|
||||||
|
|
||||||
|
# Prepare before showing
|
||||||
|
glass.prepare_window_for_glass(window)
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# Apply glass after showing
|
||||||
|
glass.apply_glass_to_window(window)
|
||||||
|
|
||||||
|
app.exec()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sidebar Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Apply glass to a sidebar widget
|
||||||
|
glass.apply_glass_to_widget(sidebar, options=glass.GlassOptions.sidebar())
|
||||||
|
|
||||||
|
# Position traffic lights
|
||||||
|
glass.setup_traffic_lights_inset(window, x_offset=18, y_offset=12)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Options
|
||||||
|
|
||||||
|
```python
|
||||||
|
options = glass.GlassOptions(
|
||||||
|
corner_radius=16.0,
|
||||||
|
padding=(10, 10, 10, 10),
|
||||||
|
)
|
||||||
|
glass.apply_glass_to_window(window, options=options)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.12+
|
||||||
|
- macOS
|
||||||
|
- PySide6 or PyQt6
|
||||||
|
|
||||||
|
Tested with PySide6. PyQt6 should work but is not explicitly tested.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
BIN
assets/screenshot.png
Normal file
BIN
assets/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line, and also
|
||||||
|
# from the environment for the first two.
|
||||||
|
SPHINXOPTS ?=
|
||||||
|
SPHINXBUILD ?= sphinx-build
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
40
docs/api.rst
Normal file
40
docs/api.rst
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
API Reference
|
||||||
|
=============
|
||||||
|
|
||||||
|
This page documents the public API of pyqt-liquidglass.
|
||||||
|
|
||||||
|
pyqt_liquidglass
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. automodule:: pyqt_liquidglass
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
:exclude-members: __all__
|
||||||
|
|
||||||
|
Platform Constants
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. py:data:: pyqt_liquidglass.IS_MACOS
|
||||||
|
:type: bool
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
``True`` if running on macOS, ``False`` otherwise.
|
||||||
|
|
||||||
|
.. py:data:: pyqt_liquidglass.MACOS_VERSION
|
||||||
|
:type: tuple[int, int, int] | None
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
macOS version as a tuple (major, minor, patch), or ``None`` on other platforms.
|
||||||
|
|
||||||
|
.. py:data:: pyqt_liquidglass.HAS_GLASS_EFFECT
|
||||||
|
:type: bool
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
``True`` if ``NSGlassEffectView`` is available (macOS 26+).
|
||||||
|
|
||||||
|
.. py:data:: pyqt_liquidglass.HAS_VISUAL_EFFECT
|
||||||
|
:type: bool
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
``True`` if ``NSVisualEffectView`` is available.
|
||||||
55
docs/conf.py
Normal file
55
docs/conf.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
#
|
||||||
|
# For the full list of built-in configuration values, see the documentation:
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath("."))
|
||||||
|
sys.path.insert(0, os.path.abspath("../."))
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
|
project = "pyqt-liquidglass"
|
||||||
|
copyright = "2025, h"
|
||||||
|
author = "h"
|
||||||
|
release = tomllib.load(open("../pyproject.toml", "rb"))["project"]["version"]
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
||||||
|
extensions = [
|
||||||
|
"sphinx.ext.autodoc",
|
||||||
|
"sphinx.ext.viewcode",
|
||||||
|
"sphinx.ext.napoleon",
|
||||||
|
"sphinx_autodoc_typehints",
|
||||||
|
]
|
||||||
|
|
||||||
|
templates_path = ["_templates"]
|
||||||
|
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
|
|
||||||
|
autodoc_default_options = {
|
||||||
|
"members": True,
|
||||||
|
"member-order": "bysource",
|
||||||
|
"special-members": "__init__",
|
||||||
|
"undoc-members": True,
|
||||||
|
"exclude-members": "__weakref__",
|
||||||
|
"inherited-members": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
autodoc_typehints = "description"
|
||||||
|
typehints_use_signature = True
|
||||||
|
typehints_use_signature_return = True
|
||||||
|
|
||||||
|
autodoc_member_order = "bysource"
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
|
|
||||||
|
html_theme = "furo"
|
||||||
|
html_static_path = ["_static"]
|
||||||
|
html_title = "pyqt-liquidglass"
|
||||||
|
html_theme_options = {"sidebar_hide_name": False, "navigation_with_keys": True}
|
||||||
200
docs/core_concepts.rst
Normal file
200
docs/core_concepts.rst
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
Core Concepts
|
||||||
|
=============
|
||||||
|
|
||||||
|
This page explains how pyqt-liquidglass works under the hood.
|
||||||
|
|
||||||
|
How Glass Effects Work
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Native Views
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
macOS provides two classes for blur/glass effects:
|
||||||
|
|
||||||
|
- **NSGlassEffectView**: New in macOS 26 (Tahoe), provides the Liquid Glass effect
|
||||||
|
- **NSVisualEffectView**: Available since macOS 10.10, provides vibrancy effects
|
||||||
|
|
||||||
|
pyqt-liquidglass automatically uses ``NSGlassEffectView`` when available and falls back to ``NSVisualEffectView`` on older systems.
|
||||||
|
|
||||||
|
View Hierarchy
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Qt widgets map to native ``NSView`` objects. When you apply a glass effect, the library:
|
||||||
|
|
||||||
|
1. Gets the native ``NSView`` for your Qt widget
|
||||||
|
2. Creates a glass effect view (``NSGlassEffectView`` or ``NSVisualEffectView``)
|
||||||
|
3. Inserts it behind your widget's view in the z-order
|
||||||
|
4. Configures autoresizing so it tracks widget size changes
|
||||||
|
|
||||||
|
The glass view renders the blur effect, and your Qt content draws on top.
|
||||||
|
|
||||||
|
Window vs Widget Glass
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Window Glass
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
``apply_glass_to_window()`` fills the entire window content area with glass:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
glass.apply_glass_to_window(window)
|
||||||
|
|
||||||
|
This is ideal for:
|
||||||
|
|
||||||
|
- Full-window blur backgrounds
|
||||||
|
- Floating panels
|
||||||
|
- Modal dialogs
|
||||||
|
|
||||||
|
The function configures the window for transparent titlebar and full-size content view automatically.
|
||||||
|
|
||||||
|
Widget Glass
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
``apply_glass_to_widget()`` applies glass to a specific widget region:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
glass.apply_glass_to_widget(sidebar, options=glass.GlassOptions.sidebar())
|
||||||
|
|
||||||
|
This is ideal for:
|
||||||
|
|
||||||
|
- Sidebars (like macOS System Settings)
|
||||||
|
- Navigation panels
|
||||||
|
- Toolbars
|
||||||
|
|
||||||
|
The glass view tracks the widget's position and size, updating automatically on resize or move.
|
||||||
|
|
||||||
|
GlassOptions Configuration
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
The ``GlassOptions`` dataclass controls glass effect appearance:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from pyqt_liquidglass import GlassOptions
|
||||||
|
|
||||||
|
options = GlassOptions(
|
||||||
|
corner_radius=16.0, # Rounded corners (NSGlassEffectView only)
|
||||||
|
material=GlassMaterial.SIDEBAR, # Material type
|
||||||
|
blending_mode=BlendingMode.BEHIND_WINDOW, # Blending mode
|
||||||
|
padding=(10, 10, 10, 10), # Left, top, right, bottom
|
||||||
|
)
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
**corner_radius** (float)
|
||||||
|
Corner radius in points. Only applies to ``NSGlassEffectView`` on macOS 26+. Default: 0.0
|
||||||
|
|
||||||
|
**material** (GlassMaterial)
|
||||||
|
The visual effect material. Only applies to ``NSVisualEffectView`` fallback. Options include:
|
||||||
|
|
||||||
|
- ``TITLEBAR``, ``SIDEBAR``, ``MENU``, ``POPOVER``
|
||||||
|
- ``SHEET``, ``WINDOW_BACKGROUND``, ``HUD``, ``TOOLTIP``
|
||||||
|
- ``CONTENT_BACKGROUND``, ``UNDER_WINDOW_BACKGROUND``
|
||||||
|
|
||||||
|
**blending_mode** (BlendingMode)
|
||||||
|
How the effect blends with content:
|
||||||
|
|
||||||
|
- ``BEHIND_WINDOW``: Blurs content behind the window
|
||||||
|
- ``WITHIN_WINDOW``: Blurs content within the window
|
||||||
|
|
||||||
|
**padding** (tuple)
|
||||||
|
Inset from widget edges in points: (left, top, right, bottom). Default: (0, 0, 0, 0)
|
||||||
|
|
||||||
|
Preset Methods
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
For common use cases:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Full window glass (no corner radius, no padding)
|
||||||
|
options = GlassOptions.window()
|
||||||
|
|
||||||
|
# Sidebar glass (10pt radius, 9pt padding)
|
||||||
|
options = GlassOptions.sidebar()
|
||||||
|
|
||||||
|
# Custom sidebar
|
||||||
|
options = GlassOptions.sidebar(corner_radius=16.0, padding=12.0)
|
||||||
|
|
||||||
|
Coordinate Systems
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Qt and Cocoa use different coordinate systems:
|
||||||
|
|
||||||
|
- **Qt**: Origin at top-left, Y increases downward
|
||||||
|
- **Cocoa**: Origin at bottom-left, Y increases upward
|
||||||
|
|
||||||
|
pyqt-liquidglass handles this conversion internally. When specifying ``y_offset`` for traffic lights, positive values move the buttons down from center.
|
||||||
|
|
||||||
|
Traffic Lights
|
||||||
|
--------------
|
||||||
|
|
||||||
|
The traffic lights are the close, minimize, and zoom buttons in the window titlebar.
|
||||||
|
|
||||||
|
Positioning
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
``setup_traffic_lights_inset()`` repositions the buttons using ``NSLayoutConstraint``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
glass.setup_traffic_lights_inset(window, x_offset=20, y_offset=12)
|
||||||
|
|
||||||
|
- **x_offset**: Distance from the left edge in points
|
||||||
|
- **y_offset**: Vertical offset from center (positive = down)
|
||||||
|
|
||||||
|
This method survives window resizes because it uses Auto Layout constraints rather than absolute positioning.
|
||||||
|
|
||||||
|
Visibility
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
Hide the buttons while keeping window functionality:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
glass.hide_traffic_lights(window)
|
||||||
|
glass.show_traffic_lights(window)
|
||||||
|
|
||||||
|
The window remains closable, minimizable, and zoomable via keyboard shortcuts and menu commands.
|
||||||
|
|
||||||
|
Platform Detection
|
||||||
|
------------------
|
||||||
|
|
||||||
|
The library provides constants for platform detection:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from pyqt_liquidglass import (
|
||||||
|
IS_MACOS, # True if running on macOS
|
||||||
|
MACOS_VERSION, # Tuple like (15, 1, 0) or None
|
||||||
|
HAS_GLASS_EFFECT, # True if NSGlassEffectView is available
|
||||||
|
HAS_VISUAL_EFFECT, # True if NSVisualEffectView is available
|
||||||
|
)
|
||||||
|
|
||||||
|
All glass functions are safe to call on non-macOS platforms. They return ``None`` or ``False`` without side effects.
|
||||||
|
|
||||||
|
Effect Lifecycle
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Effect IDs
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
``apply_glass_to_window()`` and ``apply_glass_to_widget()`` return an effect ID:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
effect_id = glass.apply_glass_to_window(window)
|
||||||
|
|
||||||
|
Use this ID to remove the effect later:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
glass.remove_glass_effect(effect_id)
|
||||||
|
|
||||||
|
Cleanup
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
When a window is closed, Qt destroys the widget hierarchy. The glass effect views are removed automatically as part of the native view cleanup. You don't need to manually remove effects before closing windows.
|
||||||
544
docs/examples.rst
Normal file
544
docs/examples.rst
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
Examples
|
||||||
|
========
|
||||||
|
|
||||||
|
This page contains complete, runnable examples demonstrating various use cases.
|
||||||
|
|
||||||
|
Full Window Glass
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
The simplest example: glass filling the entire window content area.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
"""Full window glass effect example."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Window Glass")
|
||||||
|
self.resize(600, 400)
|
||||||
|
|
||||||
|
central = QWidget()
|
||||||
|
central.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(central)
|
||||||
|
layout.setContentsMargins(40, 60, 40, 40)
|
||||||
|
|
||||||
|
label = QLabel("Hello, Liquid Glass!")
|
||||||
|
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
label.setStyleSheet("""
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
|
||||||
|
layout.addWidget(label)
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
window = MainWindow()
|
||||||
|
|
||||||
|
# Prepare window BEFORE showing
|
||||||
|
glass.prepare_window_for_glass(window)
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# Apply glass AFTER showing
|
||||||
|
glass.apply_glass_to_window(window)
|
||||||
|
|
||||||
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
|
Sidebar Pattern
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Settings-style window with a glass sidebar and opaque content area. This is the most common pattern for macOS applications.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
"""Sidebar with glass effect example."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QListWidget,
|
||||||
|
QListWidgetItem,
|
||||||
|
QMainWindow,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
|
||||||
|
class Sidebar(QWidget):
|
||||||
|
"""Transparent sidebar widget for glass effect."""
|
||||||
|
|
||||||
|
def __init__(self, parent: QWidget | None = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setFixedWidth(250)
|
||||||
|
self.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(9, 18, 9, 9)
|
||||||
|
|
||||||
|
self.list_widget = QListWidget()
|
||||||
|
self.list_widget.setStyleSheet("""
|
||||||
|
QListWidget {
|
||||||
|
font-size: 13px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
QListWidget::item {
|
||||||
|
padding: 5px 2px;
|
||||||
|
margin: 0px 9px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QListWidget::item:selected {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.list_widget.viewport().setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
for item_text in ["General", "Appearance", "Sound", "Network", "Privacy"]:
|
||||||
|
self.list_widget.addItem(QListWidgetItem(item_text))
|
||||||
|
|
||||||
|
self.list_widget.setCurrentRow(0)
|
||||||
|
layout.addWidget(self.list_widget)
|
||||||
|
|
||||||
|
|
||||||
|
class Content(QWidget):
|
||||||
|
"""Opaque content area."""
|
||||||
|
|
||||||
|
def __init__(self, parent: QWidget | None = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setStyleSheet("background-color: #1e1e1e;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
|
||||||
|
self.title = QLabel("General")
|
||||||
|
self.title.setStyleSheet("""
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.description = QLabel("Configure general application settings.")
|
||||||
|
self.description.setStyleSheet("""
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888888;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
|
||||||
|
layout.addWidget(self.title)
|
||||||
|
layout.addSpacing(8)
|
||||||
|
layout.addWidget(self.description)
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Settings")
|
||||||
|
self.resize(720, 600)
|
||||||
|
|
||||||
|
central = QWidget()
|
||||||
|
layout = QHBoxLayout(central)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
|
||||||
|
self.sidebar = Sidebar()
|
||||||
|
self.content = Content()
|
||||||
|
|
||||||
|
layout.addWidget(self.sidebar)
|
||||||
|
layout.addWidget(self.content, 1)
|
||||||
|
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
window = MainWindow()
|
||||||
|
glass.prepare_window_for_glass(window)
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# Inset traffic lights to sit nicely on sidebar
|
||||||
|
glass.setup_traffic_lights_inset(window, x_offset=18, y_offset=12)
|
||||||
|
|
||||||
|
# Apply glass to sidebar with rounded corners
|
||||||
|
glass.apply_glass_to_widget(window.sidebar, options=glass.GlassOptions.sidebar())
|
||||||
|
|
||||||
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
|
Frameless Floating Panel
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
A frameless, draggable panel useful for HUDs, tool palettes, or popovers.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
"""Frameless window with glass effect example."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PySide6.QtCore import QPoint, Qt
|
||||||
|
from PySide6.QtGui import QMouseEvent
|
||||||
|
from PySide6.QtWidgets import QApplication, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
|
||||||
|
class FloatingPanel(QWidget):
|
||||||
|
"""A frameless, draggable floating panel with glass effect."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Floating Panel")
|
||||||
|
self.resize(300, 200)
|
||||||
|
|
||||||
|
self._drag_position: QPoint | None = None
|
||||||
|
|
||||||
|
self.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(24, 24, 24, 24)
|
||||||
|
layout.setSpacing(16)
|
||||||
|
|
||||||
|
title = QLabel("Floating Panel")
|
||||||
|
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
title.setStyleSheet("""
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
|
||||||
|
subtitle = QLabel("Drag anywhere to move")
|
||||||
|
subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
subtitle.setStyleSheet("""
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
|
||||||
|
close_button = QPushButton("Close")
|
||||||
|
close_button.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 24px;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
close_button.clicked.connect(self.close)
|
||||||
|
|
||||||
|
layout.addWidget(title)
|
||||||
|
layout.addWidget(subtitle)
|
||||||
|
layout.addStretch()
|
||||||
|
layout.addWidget(close_button)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event: QMouseEvent) -> None:
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
self._drag_position = (
|
||||||
|
event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
||||||
|
)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event: QMouseEvent) -> None:
|
||||||
|
if (
|
||||||
|
event.buttons() == Qt.MouseButton.LeftButton
|
||||||
|
and self._drag_position is not None
|
||||||
|
):
|
||||||
|
self.move(event.globalPosition().toPoint() - self._drag_position)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
|
||||||
|
self._drag_position = None
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
panel = FloatingPanel()
|
||||||
|
|
||||||
|
# Prepare with frameless=True to remove window decorations
|
||||||
|
glass.prepare_window_for_glass(panel, frameless=True)
|
||||||
|
|
||||||
|
panel.show()
|
||||||
|
|
||||||
|
# Apply glass with rounded corners for the floating look
|
||||||
|
glass.apply_glass_to_window(panel, options=glass.GlassOptions(corner_radius=16.0))
|
||||||
|
|
||||||
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
|
Custom GlassOptions
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Demonstrates different glass configurations side by side.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
"""Custom GlassOptions example."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QMainWindow,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
|
||||||
|
class GlassPanel(QWidget):
|
||||||
|
"""A panel that will have glass applied to it."""
|
||||||
|
|
||||||
|
def __init__(self, title: str, parent: QWidget | None = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
|
||||||
|
label = QLabel(title)
|
||||||
|
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
label.setStyleSheet("""
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
|
||||||
|
layout.addWidget(label)
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Custom Glass Options")
|
||||||
|
self.resize(800, 400)
|
||||||
|
|
||||||
|
central = QWidget()
|
||||||
|
central.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QHBoxLayout(central)
|
||||||
|
layout.setContentsMargins(20, 60, 20, 20)
|
||||||
|
layout.setSpacing(20)
|
||||||
|
|
||||||
|
self.panel_default = GlassPanel("Default\n(no radius)")
|
||||||
|
self.panel_default.setFixedWidth(200)
|
||||||
|
|
||||||
|
self.panel_rounded = GlassPanel("Rounded\n(radius: 16)")
|
||||||
|
self.panel_rounded.setFixedWidth(200)
|
||||||
|
|
||||||
|
self.panel_padded = GlassPanel("Padded\n(padding: 20)")
|
||||||
|
self.panel_padded.setFixedWidth(200)
|
||||||
|
|
||||||
|
layout.addWidget(self.panel_default)
|
||||||
|
layout.addWidget(self.panel_rounded)
|
||||||
|
layout.addWidget(self.panel_padded)
|
||||||
|
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
window = MainWindow()
|
||||||
|
|
||||||
|
glass.prepare_window_for_glass(window)
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# Apply window glass as background
|
||||||
|
glass.apply_glass_to_window(window)
|
||||||
|
|
||||||
|
# Apply different glass options to each panel
|
||||||
|
glass.apply_glass_to_widget(
|
||||||
|
window.panel_default,
|
||||||
|
options=glass.GlassOptions(corner_radius=0.0, padding=(8, 8, 8, 8)),
|
||||||
|
)
|
||||||
|
|
||||||
|
glass.apply_glass_to_widget(
|
||||||
|
window.panel_rounded,
|
||||||
|
options=glass.GlassOptions(corner_radius=16.0, padding=(8, 8, 8, 8)),
|
||||||
|
)
|
||||||
|
|
||||||
|
glass.apply_glass_to_widget(
|
||||||
|
window.panel_padded,
|
||||||
|
options=glass.GlassOptions(corner_radius=12.0, padding=(20, 20, 20, 20)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
|
Traffic Lights Control
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Demonstrates hiding, showing, and repositioning the macOS window buttons.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
"""Traffic lights control example."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QMainWindow,
|
||||||
|
QPushButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Traffic Lights Demo")
|
||||||
|
self.resize(500, 300)
|
||||||
|
|
||||||
|
self._lights_visible = True
|
||||||
|
|
||||||
|
central = QWidget()
|
||||||
|
central.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(central)
|
||||||
|
layout.setContentsMargins(40, 80, 40, 40)
|
||||||
|
layout.setSpacing(20)
|
||||||
|
|
||||||
|
title = QLabel("Traffic Lights Control")
|
||||||
|
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
title.setStyleSheet("""
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
|
||||||
|
description = QLabel(
|
||||||
|
"The traffic lights have been repositioned.\n"
|
||||||
|
"Use the button below to hide or show them."
|
||||||
|
)
|
||||||
|
description.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
description.setStyleSheet("""
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
|
||||||
|
buttons_layout = QHBoxLayout()
|
||||||
|
buttons_layout.setSpacing(12)
|
||||||
|
|
||||||
|
self.toggle_button = QPushButton("Hide Traffic Lights")
|
||||||
|
self.toggle_button.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.toggle_button.clicked.connect(self._toggle_traffic_lights)
|
||||||
|
|
||||||
|
buttons_layout.addStretch()
|
||||||
|
buttons_layout.addWidget(self.toggle_button)
|
||||||
|
buttons_layout.addStretch()
|
||||||
|
|
||||||
|
layout.addWidget(title)
|
||||||
|
layout.addWidget(description)
|
||||||
|
layout.addStretch()
|
||||||
|
layout.addLayout(buttons_layout)
|
||||||
|
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
|
||||||
|
def _toggle_traffic_lights(self) -> None:
|
||||||
|
if self._lights_visible:
|
||||||
|
glass.hide_traffic_lights(self)
|
||||||
|
self.toggle_button.setText("Show Traffic Lights")
|
||||||
|
else:
|
||||||
|
glass.show_traffic_lights(self)
|
||||||
|
self.toggle_button.setText("Hide Traffic Lights")
|
||||||
|
self._lights_visible = not self._lights_visible
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
window = MainWindow()
|
||||||
|
|
||||||
|
glass.prepare_window_for_glass(window)
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# Apply glass to window
|
||||||
|
glass.apply_glass_to_window(window)
|
||||||
|
|
||||||
|
# Reposition traffic lights with custom offset
|
||||||
|
glass.setup_traffic_lights_inset(window, x_offset=20, y_offset=16)
|
||||||
|
|
||||||
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
151
docs/getting_started.rst
Normal file
151
docs/getting_started.rst
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
Getting Started
|
||||||
|
===============
|
||||||
|
|
||||||
|
This guide covers installation and basic usage of pyqt-liquidglass.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Using pip
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip install pyqt-liquidglass
|
||||||
|
|
||||||
|
Using uv
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
uv add pyqt-liquidglass
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
- **Python**: 3.12 or higher
|
||||||
|
- **Operating System**: macOS (functions are safe no-ops on other platforms)
|
||||||
|
- **Qt Binding**: PySide6 or PyQt6
|
||||||
|
|
||||||
|
The library automatically detects your Qt binding.
|
||||||
|
|
||||||
|
Quick Start
|
||||||
|
-----------
|
||||||
|
|
||||||
|
The Three-Step Pattern
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Every glass effect follows the same pattern:
|
||||||
|
|
||||||
|
1. **Prepare** the window before showing
|
||||||
|
2. **Show** the window
|
||||||
|
3. **Apply** the glass effect
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
# 1. Prepare before showing
|
||||||
|
glass.prepare_window_for_glass(window)
|
||||||
|
|
||||||
|
# 2. Show the window
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# 3. Apply glass after showing
|
||||||
|
glass.apply_glass_to_window(window)
|
||||||
|
|
||||||
|
Full Window Example
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Glass Demo")
|
||||||
|
self.resize(600, 400)
|
||||||
|
|
||||||
|
central = QWidget()
|
||||||
|
central.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(central)
|
||||||
|
layout.setContentsMargins(40, 60, 40, 40)
|
||||||
|
|
||||||
|
label = QLabel("Hello, Liquid Glass!")
|
||||||
|
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
label.setStyleSheet("""
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
|
||||||
|
layout.addWidget(label)
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
window = MainWindow()
|
||||||
|
glass.prepare_window_for_glass(window)
|
||||||
|
window.show()
|
||||||
|
glass.apply_glass_to_window(window)
|
||||||
|
|
||||||
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
|
Key Points
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
- Set ``background: transparent`` on widgets that should show the glass through
|
||||||
|
- Call ``prepare_window_for_glass()`` before ``show()``
|
||||||
|
- Call ``apply_glass_to_window()`` after ``show()``
|
||||||
|
- The window needs to be visible for the native view hierarchy to exist
|
||||||
|
|
||||||
|
Widget Glass
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
For applying glass to specific widgets (like sidebars):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
sidebar = QWidget()
|
||||||
|
sidebar.setFixedWidth(250)
|
||||||
|
sidebar.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
# After window.show():
|
||||||
|
glass.apply_glass_to_widget(sidebar, options=glass.GlassOptions.sidebar())
|
||||||
|
|
||||||
|
Traffic Lights
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Reposition the macOS window buttons:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
glass.setup_traffic_lights_inset(window, x_offset=20, y_offset=12)
|
||||||
|
|
||||||
|
Hide or show them:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
glass.hide_traffic_lights(window)
|
||||||
|
glass.show_traffic_lights(window)
|
||||||
|
|
||||||
|
Next Steps
|
||||||
|
----------
|
||||||
|
|
||||||
|
- Learn about :doc:`core_concepts` to understand how glass effects work
|
||||||
|
- See :doc:`examples` for complete working examples
|
||||||
|
- Check the :doc:`api` for detailed function signatures
|
||||||
89
docs/index.rst
Normal file
89
docs/index.rst
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
pyqt-liquidglass
|
||||||
|
================
|
||||||
|
|
||||||
|
**macOS Liquid Glass effects for PySide6 and PyQt6**
|
||||||
|
|
||||||
|
pyqt-liquidglass brings Apple's Liquid Glass visual effects to your Qt applications on macOS. It provides a clean Python API to apply the native ``NSGlassEffectView`` (macOS 26+) or ``NSVisualEffectView`` (fallback) to windows and widgets.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- **Window Glass**: Apply glass effects to entire windows
|
||||||
|
- **Widget Glass**: Target specific widgets like sidebars or panels
|
||||||
|
- **Traffic Lights Control**: Reposition, hide, or show window buttons
|
||||||
|
- **GlassOptions**: Configure corner radius, padding, materials, and blending
|
||||||
|
- **Cross-Version**: Uses ``NSGlassEffectView`` on macOS 26+, falls back to ``NSVisualEffectView``
|
||||||
|
- **Safe No-ops**: All functions work on non-macOS platforms (return ``None`` or ``False``)
|
||||||
|
|
||||||
|
Quick Example
|
||||||
|
-------------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import QApplication, QMainWindow
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
window = QMainWindow()
|
||||||
|
window.resize(800, 600)
|
||||||
|
|
||||||
|
# Prepare before showing
|
||||||
|
glass.prepare_window_for_glass(window)
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# Apply glass after showing
|
||||||
|
glass.apply_glass_to_window(window)
|
||||||
|
|
||||||
|
app.exec()
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip install pyqt-liquidglass
|
||||||
|
|
||||||
|
Or with uv:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
uv add pyqt-liquidglass
|
||||||
|
|
||||||
|
**Requirements**: Python 3.12+, macOS, PySide6 or PyQt6
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: User Guide
|
||||||
|
|
||||||
|
getting_started
|
||||||
|
core_concepts
|
||||||
|
examples
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: API Reference
|
||||||
|
|
||||||
|
api
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Indices
|
||||||
|
=======
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
License
|
||||||
|
=======
|
||||||
|
|
||||||
|
MIT License. See LICENSE.md for details.
|
||||||
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
pushd %~dp0
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=sphinx-build
|
||||||
|
)
|
||||||
|
set SOURCEDIR=.
|
||||||
|
set BUILDDIR=_build
|
||||||
|
|
||||||
|
%SPHINXBUILD% >NUL 2>NUL
|
||||||
|
if errorlevel 9009 (
|
||||||
|
echo.
|
||||||
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||||
|
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||||
|
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||||
|
echo.may add the Sphinx directory to PATH.
|
||||||
|
echo.
|
||||||
|
echo.If you don't have Sphinx installed, grab it from
|
||||||
|
echo.https://www.sphinx-doc.org/
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
|
goto end
|
||||||
|
|
||||||
|
:help
|
||||||
|
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
|
|
||||||
|
:end
|
||||||
|
popd
|
||||||
3
docs/requirements.txt
Normal file
3
docs/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
sphinx
|
||||||
|
furo
|
||||||
|
sphinx-autodoc-typehints
|
||||||
1
examples/__init__.py
Normal file
1
examples/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Example scripts for pyqt-liquidglass."""
|
||||||
111
examples/custom_options.py
Normal file
111
examples/custom_options.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
"""Custom GlassOptions example.
|
||||||
|
|
||||||
|
Demonstrates how to configure glass effects with custom parameters
|
||||||
|
including corner radius, padding, and materials.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QMainWindow,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
|
||||||
|
class GlassPanel(QWidget):
|
||||||
|
"""A panel that will have glass applied to it."""
|
||||||
|
|
||||||
|
def __init__(self, title: str, parent: QWidget | None = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
|
||||||
|
label = QLabel(title)
|
||||||
|
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
label.setStyleSheet(
|
||||||
|
"""
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
layout.addWidget(label)
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Custom Glass Options")
|
||||||
|
self.resize(800, 400)
|
||||||
|
|
||||||
|
central = QWidget()
|
||||||
|
central.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QHBoxLayout(central)
|
||||||
|
layout.setContentsMargins(20, 60, 20, 20)
|
||||||
|
layout.setSpacing(20)
|
||||||
|
|
||||||
|
# Three panels with different glass options
|
||||||
|
self.panel_default = GlassPanel("Default\n(no radius)")
|
||||||
|
self.panel_default.setFixedWidth(200)
|
||||||
|
|
||||||
|
self.panel_rounded = GlassPanel("Rounded\n(radius: 16)")
|
||||||
|
self.panel_rounded.setFixedWidth(200)
|
||||||
|
|
||||||
|
self.panel_padded = GlassPanel("Padded\n(padding: 20)")
|
||||||
|
self.panel_padded.setFixedWidth(200)
|
||||||
|
|
||||||
|
layout.addWidget(self.panel_default)
|
||||||
|
layout.addWidget(self.panel_rounded)
|
||||||
|
layout.addWidget(self.panel_padded)
|
||||||
|
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
window = MainWindow()
|
||||||
|
|
||||||
|
glass.prepare_window_for_glass(window)
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# Apply window glass as background
|
||||||
|
glass.apply_glass_to_window(window)
|
||||||
|
|
||||||
|
# Apply different glass options to each panel
|
||||||
|
# Panel 1: Default options (no corner radius)
|
||||||
|
glass.apply_glass_to_widget(
|
||||||
|
window.panel_default,
|
||||||
|
options=glass.GlassOptions(corner_radius=0.0, padding=(8, 8, 8, 8)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Panel 2: Rounded corners
|
||||||
|
glass.apply_glass_to_widget(
|
||||||
|
window.panel_rounded,
|
||||||
|
options=glass.GlassOptions(corner_radius=16.0, padding=(8, 8, 8, 8)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Panel 3: Large padding
|
||||||
|
glass.apply_glass_to_widget(
|
||||||
|
window.panel_padded,
|
||||||
|
options=glass.GlassOptions(corner_radius=12.0, padding=(20, 20, 20, 20)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
115
examples/frameless.py
Normal file
115
examples/frameless.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"""Frameless window with glass effect example.
|
||||||
|
|
||||||
|
A floating panel without standard window decorations, useful for
|
||||||
|
custom UI elements like popovers, HUDs, or tool palettes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PySide6.QtCore import QPoint, Qt
|
||||||
|
from PySide6.QtGui import QMouseEvent
|
||||||
|
from PySide6.QtWidgets import QApplication, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
|
||||||
|
class FloatingPanel(QWidget):
|
||||||
|
"""A frameless, draggable floating panel with glass effect."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Floating Panel")
|
||||||
|
self.resize(300, 200)
|
||||||
|
|
||||||
|
self._drag_position: QPoint | None = None
|
||||||
|
|
||||||
|
self.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(24, 24, 24, 24)
|
||||||
|
layout.setSpacing(16)
|
||||||
|
|
||||||
|
title = QLabel("Floating Panel")
|
||||||
|
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
title.setStyleSheet(
|
||||||
|
"""
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
subtitle = QLabel("Drag anywhere to move")
|
||||||
|
subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
subtitle.setStyleSheet(
|
||||||
|
"""
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
background: transparent;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
close_button = QPushButton("Close")
|
||||||
|
close_button.setStyleSheet(
|
||||||
|
"""
|
||||||
|
QPushButton {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 24px;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
close_button.clicked.connect(self.close)
|
||||||
|
|
||||||
|
layout.addWidget(title)
|
||||||
|
layout.addWidget(subtitle)
|
||||||
|
layout.addStretch()
|
||||||
|
layout.addWidget(close_button)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event: QMouseEvent) -> None: # noqa: N802
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
self._drag_position = (
|
||||||
|
event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
||||||
|
)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event: QMouseEvent) -> None: # noqa: N802
|
||||||
|
if (
|
||||||
|
event.buttons() == Qt.MouseButton.LeftButton
|
||||||
|
and self._drag_position is not None
|
||||||
|
):
|
||||||
|
self.move(event.globalPosition().toPoint() - self._drag_position)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event: QMouseEvent) -> None: # noqa: N802, ARG002
|
||||||
|
self._drag_position = None
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
panel = FloatingPanel()
|
||||||
|
|
||||||
|
# Prepare with frameless=True to remove window decorations
|
||||||
|
glass.prepare_window_for_glass(panel, frameless=True)
|
||||||
|
|
||||||
|
panel.show()
|
||||||
|
|
||||||
|
# Apply glass with rounded corners for the floating look
|
||||||
|
glass.apply_glass_to_window(panel, options=glass.GlassOptions(corner_radius=16.0))
|
||||||
|
|
||||||
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
151
examples/sidebar.py
Normal file
151
examples/sidebar.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
"""Sidebar with glass effect example.
|
||||||
|
|
||||||
|
Settings-style window with a glass sidebar and opaque content area.
|
||||||
|
This is the most common pattern for using Liquid Glass.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QListWidget,
|
||||||
|
QListWidgetItem,
|
||||||
|
QMainWindow,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
|
||||||
|
class Sidebar(QWidget):
|
||||||
|
"""Transparent sidebar widget for glass effect."""
|
||||||
|
|
||||||
|
def __init__(self, parent: QWidget | None = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setFixedWidth(250)
|
||||||
|
self.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
# Top padding for traffic lights area
|
||||||
|
layout.setContentsMargins(9, 18, 9, 9)
|
||||||
|
|
||||||
|
self.list_widget = QListWidget()
|
||||||
|
self.list_widget.setStyleSheet("""
|
||||||
|
QListWidget {
|
||||||
|
font-size: 13px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
QListWidget::item {
|
||||||
|
padding: 5px 2px;
|
||||||
|
margin: 0px 9px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QListWidget::item:selected {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.list_widget.viewport().setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
for item_text in ["General", "Appearance", "Sound", "Network", "Privacy"]:
|
||||||
|
self.list_widget.addItem(QListWidgetItem(item_text))
|
||||||
|
|
||||||
|
self.list_widget.setCurrentRow(0)
|
||||||
|
layout.addWidget(self.list_widget)
|
||||||
|
|
||||||
|
|
||||||
|
class Content(QWidget):
|
||||||
|
"""Opaque content area."""
|
||||||
|
|
||||||
|
def __init__(self, parent: QWidget | None = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setStyleSheet("background-color: #1e1e1e;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(20, 0, 20, 20)
|
||||||
|
|
||||||
|
self.title = QLabel("General")
|
||||||
|
self.title.setStyleSheet(
|
||||||
|
"""
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
self.description = QLabel("Configure general application settings.")
|
||||||
|
self.description.setStyleSheet(
|
||||||
|
"""
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888888;
|
||||||
|
background: transparent;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
layout.addWidget(self.title)
|
||||||
|
layout.addSpacing(8)
|
||||||
|
layout.addWidget(self.description)
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Settings")
|
||||||
|
self.resize(720, 600)
|
||||||
|
|
||||||
|
central = QWidget()
|
||||||
|
layout = QHBoxLayout(central)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
|
||||||
|
self.sidebar = Sidebar()
|
||||||
|
self.content = Content()
|
||||||
|
|
||||||
|
layout.addWidget(self.sidebar)
|
||||||
|
layout.addWidget(self.content, 1)
|
||||||
|
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
|
||||||
|
self.sidebar.list_widget.currentRowChanged.connect(self._on_selection_changed)
|
||||||
|
|
||||||
|
def _on_selection_changed(self, row: int) -> None:
|
||||||
|
items = ["General", "Appearance", "Sound", "Network", "Privacy"]
|
||||||
|
descriptions = [
|
||||||
|
"Configure general application settings.",
|
||||||
|
"Customize colors, fonts, and themes.",
|
||||||
|
"Adjust volume and audio devices.",
|
||||||
|
"Manage network connections and proxy.",
|
||||||
|
"Control permissions and data sharing.",
|
||||||
|
]
|
||||||
|
if 0 <= row < len(items):
|
||||||
|
self.content.title.setText(items[row])
|
||||||
|
self.content.description.setText(descriptions[row])
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
window = MainWindow()
|
||||||
|
|
||||||
|
# Prepare window before showing
|
||||||
|
glass.prepare_window_for_glass(window)
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# Inset traffic lights to sit nicely on sidebar
|
||||||
|
glass.setup_traffic_lights_inset(window, x_offset=18, y_offset=12)
|
||||||
|
|
||||||
|
# Apply glass to sidebar with rounded corners
|
||||||
|
glass.apply_glass_to_widget(window.sidebar, options=glass.GlassOptions.sidebar())
|
||||||
|
|
||||||
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
128
examples/traffic_lights.py
Normal file
128
examples/traffic_lights.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
"""Traffic lights control example.
|
||||||
|
|
||||||
|
Demonstrates hiding, showing, and repositioning the macOS window
|
||||||
|
traffic lights (close, minimize, zoom buttons).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QMainWindow,
|
||||||
|
QPushButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Traffic Lights Demo")
|
||||||
|
self.resize(500, 300)
|
||||||
|
|
||||||
|
self._lights_visible = True
|
||||||
|
|
||||||
|
central = QWidget()
|
||||||
|
central.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(central)
|
||||||
|
layout.setContentsMargins(40, 80, 40, 40)
|
||||||
|
layout.setSpacing(20)
|
||||||
|
|
||||||
|
title = QLabel("Traffic Lights Control")
|
||||||
|
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
title.setStyleSheet(
|
||||||
|
"""
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
description = QLabel(
|
||||||
|
"The traffic lights have been repositioned.\n"
|
||||||
|
"Use the buttons below to hide or show them."
|
||||||
|
)
|
||||||
|
description.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
description.setStyleSheet(
|
||||||
|
"""
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
background: transparent;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
buttons_layout = QHBoxLayout()
|
||||||
|
buttons_layout.setSpacing(12)
|
||||||
|
|
||||||
|
self.toggle_button = QPushButton("Hide Traffic Lights")
|
||||||
|
self.toggle_button.setStyleSheet(self._button_style())
|
||||||
|
self.toggle_button.clicked.connect(self._toggle_traffic_lights)
|
||||||
|
|
||||||
|
buttons_layout.addStretch()
|
||||||
|
buttons_layout.addWidget(self.toggle_button)
|
||||||
|
buttons_layout.addStretch()
|
||||||
|
|
||||||
|
layout.addWidget(title)
|
||||||
|
layout.addWidget(description)
|
||||||
|
layout.addStretch()
|
||||||
|
layout.addLayout(buttons_layout)
|
||||||
|
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
|
||||||
|
def _button_style(self) -> str:
|
||||||
|
return """
|
||||||
|
QPushButton {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _toggle_traffic_lights(self) -> None:
|
||||||
|
if self._lights_visible:
|
||||||
|
glass.hide_traffic_lights(self)
|
||||||
|
self.toggle_button.setText("Show Traffic Lights")
|
||||||
|
else:
|
||||||
|
glass.show_traffic_lights(self)
|
||||||
|
self.toggle_button.setText("Hide Traffic Lights")
|
||||||
|
self._lights_visible = not self._lights_visible
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
window = MainWindow()
|
||||||
|
|
||||||
|
glass.prepare_window_for_glass(window)
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# Apply glass to window
|
||||||
|
glass.apply_glass_to_window(window)
|
||||||
|
|
||||||
|
# Reposition traffic lights with custom offset
|
||||||
|
glass.setup_traffic_lights_inset(window, x_offset=20, y_offset=16)
|
||||||
|
|
||||||
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
59
examples/window_glass.py
Normal file
59
examples/window_glass.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"""Full window glass effect example.
|
||||||
|
|
||||||
|
The simplest possible example: a window with glass filling the entire
|
||||||
|
content area.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Window Glass")
|
||||||
|
self.resize(600, 400)
|
||||||
|
|
||||||
|
central = QWidget()
|
||||||
|
central.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(central)
|
||||||
|
layout.setContentsMargins(40, 60, 40, 40)
|
||||||
|
|
||||||
|
label = QLabel("Hello, Liquid Glass!")
|
||||||
|
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
label.setStyleSheet(
|
||||||
|
"""
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
layout.addWidget(label)
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
window = MainWindow()
|
||||||
|
|
||||||
|
# Prepare window BEFORE showing
|
||||||
|
glass.prepare_window_for_glass(window)
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# Apply glass AFTER showing
|
||||||
|
glass.apply_glass_to_window(window)
|
||||||
|
|
||||||
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
45
pyproject.toml
Normal file
45
pyproject.toml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
[project]
|
||||||
|
name = "pyqt-liquidglass"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Simple library for PyQt to add macOS liquid glass effect"
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [
|
||||||
|
{ name = "h", email = "h@kotikot.com" }
|
||||||
|
]
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"pyobjc-framework-cocoa>=12.1",
|
||||||
|
"pyobjc-framework-quartz>=12.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["uv_build>=0.9.13,<0.10.0"]
|
||||||
|
build-backend = "uv_build"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pyside6-essentials>=6.10.1",
|
||||||
|
"pyside6-stubs>=6.7.3.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py313"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["ALL"]
|
||||||
|
ignore = ["CPY", "D1", "D203", "D212", "COM812", "RUF001", "RUF002", "RUF003"]
|
||||||
|
unfixable = ["F401"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"*.lock" = ["ALL"]
|
||||||
|
"docs/*" = ["ALL"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.pydocstyle]
|
||||||
|
convention = "google"
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
docstring-code-format = true
|
||||||
|
skip-magic-trailing-comma = true
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
split-on-trailing-comma = false
|
||||||
75
src/pyqt_liquidglass/__init__.py
Normal file
75
src/pyqt_liquidglass/__init__.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"""
|
||||||
|
pyqt-liquidglass: macOS Liquid Glass effects for PySide6/PyQt6.
|
||||||
|
|
||||||
|
This library provides a simple API to apply Apple's Liquid Glass visual
|
||||||
|
effects to Qt windows and widgets on macOS. On non-macOS platforms, all
|
||||||
|
functions are safe no-ops that return None or False.
|
||||||
|
|
||||||
|
Basic Usage::
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import QApplication, QMainWindow
|
||||||
|
import pyqt_liquidglass as glass
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
window = QMainWindow()
|
||||||
|
window.setWindowTitle("Glass Demo")
|
||||||
|
window.resize(800, 600)
|
||||||
|
|
||||||
|
glass.prepare_window_for_glass(window)
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
glass.apply_glass_to_window(window)
|
||||||
|
glass.setup_traffic_lights_inset(window, x_offset=20, y_offset=15)
|
||||||
|
|
||||||
|
app.exec()
|
||||||
|
|
||||||
|
Widget-specific glass (e.g., sidebar)::
|
||||||
|
|
||||||
|
sidebar = QWidget()
|
||||||
|
sidebar.setFixedWidth(200)
|
||||||
|
glass.prepare_widget_for_glass(sidebar)
|
||||||
|
|
||||||
|
# After window.show():
|
||||||
|
glass.apply_glass_to_widget(
|
||||||
|
sidebar, options=glass.GlassOptions.sidebar(corner_radius=12)
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
|
from ._platform import HAS_GLASS_EFFECT, HAS_VISUAL_EFFECT, IS_MACOS, MACOS_VERSION
|
||||||
|
from ._types import BlendingMode, GlassMaterial, GlassOptions
|
||||||
|
from .glass import apply_glass_to_widget, apply_glass_to_window, remove_glass_effect
|
||||||
|
from .helpers import (
|
||||||
|
prepare_widget_for_glass,
|
||||||
|
prepare_window_for_glass,
|
||||||
|
set_window_background_transparent,
|
||||||
|
)
|
||||||
|
from .traffic_lights import (
|
||||||
|
hide_traffic_lights,
|
||||||
|
setup_traffic_lights_inset,
|
||||||
|
show_traffic_lights,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"HAS_GLASS_EFFECT",
|
||||||
|
"HAS_VISUAL_EFFECT",
|
||||||
|
"IS_MACOS",
|
||||||
|
"MACOS_VERSION",
|
||||||
|
"BlendingMode",
|
||||||
|
"GlassMaterial",
|
||||||
|
"GlassOptions",
|
||||||
|
"__version__",
|
||||||
|
"apply_glass_to_widget",
|
||||||
|
"apply_glass_to_window",
|
||||||
|
"hide_traffic_lights",
|
||||||
|
"prepare_widget_for_glass",
|
||||||
|
"prepare_window_for_glass",
|
||||||
|
"remove_glass_effect",
|
||||||
|
"set_window_background_transparent",
|
||||||
|
"setup_traffic_lights_inset",
|
||||||
|
"show_traffic_lights",
|
||||||
|
]
|
||||||
97
src/pyqt_liquidglass/_bridge.py
Normal file
97
src/pyqt_liquidglass/_bridge.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"""Bridge utilities for converting Qt objects to native macOS Cocoa objects."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ctypes import c_void_p
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from ._compat import QtWidgets, get_window_id
|
||||||
|
from ._platform import IS_MACOS
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ._compat import QtCore
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"convert_qt_rect_to_ns_frame",
|
||||||
|
"get_nsview_from_widget",
|
||||||
|
"get_nswindow_from_widget",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_nsview_from_widget(widget: QtWidgets.QWidget) -> Any | None: # noqa: ANN401
|
||||||
|
"""
|
||||||
|
Get the NSView backing a QWidget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget: A Qt widget that has been realized (shown on screen).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The NSView object (as a PyObjC object), or None if unavailable
|
||||||
|
or not on macOS.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The widget must have a valid window ID, which typically requires
|
||||||
|
the widget to be shown before calling this function.
|
||||||
|
"""
|
||||||
|
if not IS_MACOS:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import objc # noqa: PLC0415 # ty: ignore
|
||||||
|
|
||||||
|
win_id = get_window_id(widget)
|
||||||
|
return objc.objc_object(c_void_p=c_void_p(win_id)) # ty: ignore
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_nswindow_from_widget(widget: QtWidgets.QWidget) -> Any | None: # noqa: ANN401
|
||||||
|
"""
|
||||||
|
Get the NSWindow containing a QWidget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget: A Qt widget that has been realized (shown on screen).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The NSWindow object (as a PyObjC object), or None if unavailable
|
||||||
|
or not on macOS.
|
||||||
|
"""
|
||||||
|
ns_view = get_nsview_from_widget(widget)
|
||||||
|
if ns_view is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return ns_view.window() # ty: ignore
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def convert_qt_rect_to_ns_frame(
|
||||||
|
qt_rect: QtCore.QRect, container_height: float, *, is_flipped: bool = False
|
||||||
|
) -> tuple[float, float, float, float]:
|
||||||
|
"""
|
||||||
|
Convert a Qt rectangle to Cocoa NSRect coordinates.
|
||||||
|
|
||||||
|
Qt uses a top-left origin coordinate system where Y increases downward.
|
||||||
|
Cocoa (when not flipped) uses a bottom-left origin where Y increases
|
||||||
|
upward.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
qt_rect: Rectangle in Qt coordinates.
|
||||||
|
container_height: Height of the container view for Y-axis conversion.
|
||||||
|
is_flipped: Whether the container view uses flipped coordinates
|
||||||
|
(top-left origin like Qt). Default is False (standard Cocoa).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple of (x, y, width, height) in Cocoa coordinates.
|
||||||
|
"""
|
||||||
|
x = float(qt_rect.x())
|
||||||
|
width = float(qt_rect.width())
|
||||||
|
height = float(qt_rect.height())
|
||||||
|
|
||||||
|
if is_flipped:
|
||||||
|
y = float(qt_rect.y())
|
||||||
|
else:
|
||||||
|
y = container_height - float(qt_rect.y()) - height
|
||||||
|
|
||||||
|
return (x, y, width, height)
|
||||||
58
src/pyqt_liquidglass/_compat.py
Normal file
58
src/pyqt_liquidglass/_compat.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"""Qt binding compatibility layer for PySide6 and PyQt6."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"PYQT6",
|
||||||
|
"PYSIDE6",
|
||||||
|
"QT_BINDING",
|
||||||
|
"QtCore",
|
||||||
|
"QtGui",
|
||||||
|
"QtWidgets",
|
||||||
|
"get_window_id",
|
||||||
|
]
|
||||||
|
|
||||||
|
PYSIDE6: bool = False
|
||||||
|
PYQT6: bool = False
|
||||||
|
QT_BINDING: Literal["PySide6", "PyQt6", "none"] = "none"
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PySide6 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
PYSIDE6 = True
|
||||||
|
QT_BINDING = "PySide6"
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
PYQT6 = True
|
||||||
|
QT_BINDING = "PyQt6"
|
||||||
|
except ImportError:
|
||||||
|
msg = (
|
||||||
|
"No Qt binding found. Install either PySide6 or PyQt6:\n"
|
||||||
|
" pip install PySide6\n"
|
||||||
|
" pip install PyQt6"
|
||||||
|
)
|
||||||
|
raise ImportError(msg) from None
|
||||||
|
|
||||||
|
|
||||||
|
def get_window_id(widget: QtWidgets.QWidget) -> int:
|
||||||
|
"""
|
||||||
|
Get the native window ID from a Qt widget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget: A Qt widget that has been realized (shown).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The native window ID as an integer.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
PySide6 returns an int directly, while PyQt6 returns a sip.voidptr
|
||||||
|
that needs to be converted.
|
||||||
|
"""
|
||||||
|
win_id = widget.winId()
|
||||||
|
if PYQT6:
|
||||||
|
return int(win_id) # type: ignore[arg-type]
|
||||||
|
return win_id # type: ignore[return-value]
|
||||||
104
src/pyqt_liquidglass/_platform.py
Normal file
104
src/pyqt_liquidglass/_platform.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
"""Platform detection and guards for macOS-specific functionality."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
from functools import wraps
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"HAS_GLASS_EFFECT",
|
||||||
|
"HAS_VISUAL_EFFECT",
|
||||||
|
"IS_MACOS",
|
||||||
|
"MACOS_VERSION",
|
||||||
|
"platform_guard",
|
||||||
|
"require_macos",
|
||||||
|
]
|
||||||
|
|
||||||
|
IS_MACOS: bool = sys.platform == "darwin"
|
||||||
|
|
||||||
|
MACOS_VERSION: tuple[int, int, int] | None = None
|
||||||
|
if IS_MACOS:
|
||||||
|
try:
|
||||||
|
version_str = platform.mac_ver()[0]
|
||||||
|
parts = version_str.split(".")
|
||||||
|
MACOS_VERSION = (
|
||||||
|
int(parts[0]) if len(parts) > 0 else 0,
|
||||||
|
int(parts[1]) if len(parts) > 1 else 0,
|
||||||
|
int(parts[2]) if len(parts) > 2 else 0, # noqa: PLR2004
|
||||||
|
)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
MACOS_VERSION = (0, 0, 0)
|
||||||
|
|
||||||
|
_MIN_VISUAL_EFFECT_VERSION = (10, 10, 0)
|
||||||
|
_MIN_GLASS_EFFECT_VERSION = (26, 0, 0)
|
||||||
|
|
||||||
|
HAS_VISUAL_EFFECT: bool = (
|
||||||
|
IS_MACOS
|
||||||
|
and MACOS_VERSION is not None
|
||||||
|
and MACOS_VERSION >= _MIN_VISUAL_EFFECT_VERSION
|
||||||
|
)
|
||||||
|
|
||||||
|
HAS_GLASS_EFFECT: bool = (
|
||||||
|
IS_MACOS
|
||||||
|
and MACOS_VERSION is not None
|
||||||
|
and MACOS_VERSION >= _MIN_GLASS_EFFECT_VERSION
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MacOSRequiredError(RuntimeError):
|
||||||
|
"""Raised when a macOS-only function is called on a non-macOS platform."""
|
||||||
|
|
||||||
|
def __init__(self, func_name: str) -> None:
|
||||||
|
super().__init__(f"{func_name} requires macOS")
|
||||||
|
|
||||||
|
|
||||||
|
def require_macos[**P, R](func: Callable[P, R]) -> Callable[P, R]:
|
||||||
|
"""
|
||||||
|
Decorator that raises an error if called on non-macOS platforms.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: The function to wrap.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A wrapped function that raises MacOSRequiredError on non-macOS.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
MacOSRequiredError: If called on a non-macOS platform.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||||
|
if not IS_MACOS:
|
||||||
|
raise MacOSRequiredError(func.__name__)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def platform_guard[**P, R](func: Callable[P, R | None]) -> Callable[P, R | None]:
|
||||||
|
"""
|
||||||
|
Decorator that makes a function a no-op on non-macOS platforms.
|
||||||
|
|
||||||
|
The wrapped function will return None without executing on non-macOS
|
||||||
|
platforms, allowing cross-platform code to call macOS-specific
|
||||||
|
functions safely.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: The function to wrap.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A wrapped function that returns None on non-macOS platforms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
|
||||||
|
if not IS_MACOS:
|
||||||
|
return None
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
100
src/pyqt_liquidglass/_types.py
Normal file
100
src/pyqt_liquidglass/_types.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
"""Type definitions for pyqt-liquidglass."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
__all__ = ["BlendingMode", "GlassMaterial", "GlassOptions"]
|
||||||
|
|
||||||
|
|
||||||
|
class GlassMaterial(IntEnum):
|
||||||
|
"""
|
||||||
|
Available materials for glass effects.
|
||||||
|
|
||||||
|
These map to NSVisualEffectMaterial values for the fallback
|
||||||
|
implementation on pre-macOS 26 systems.
|
||||||
|
"""
|
||||||
|
|
||||||
|
TITLEBAR = 3
|
||||||
|
SELECTION = 4
|
||||||
|
MENU = 5
|
||||||
|
POPOVER = 6
|
||||||
|
SIDEBAR = 7
|
||||||
|
HEADER_VIEW = 10
|
||||||
|
SHEET = 11
|
||||||
|
WINDOW_BACKGROUND = 12
|
||||||
|
HUD = 13
|
||||||
|
FULLSCREEN_UI = 15
|
||||||
|
TOOLTIP = 17
|
||||||
|
CONTENT_BACKGROUND = 18
|
||||||
|
UNDER_WINDOW_BACKGROUND = 21
|
||||||
|
UNDER_PAGE_BACKGROUND = 22
|
||||||
|
|
||||||
|
|
||||||
|
class BlendingMode(IntEnum):
|
||||||
|
"""
|
||||||
|
Blending modes for visual effect views.
|
||||||
|
|
||||||
|
These map to NSVisualEffectBlendingMode values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
BEHIND_WINDOW = 0
|
||||||
|
WITHIN_WINDOW = 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class GlassOptions:
|
||||||
|
"""
|
||||||
|
Configuration options for glass effects.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
corner_radius: Corner radius in points for rounded glass effects.
|
||||||
|
Only applies to NSGlassEffectView on macOS 26+.
|
||||||
|
material: The visual effect material to use. Only applies to
|
||||||
|
NSVisualEffectView fallback on older macOS versions.
|
||||||
|
blending_mode: How the effect blends with content. Only applies
|
||||||
|
to NSVisualEffectView fallback.
|
||||||
|
padding: Inset padding from widget edges in points (left, top, right, bottom).
|
||||||
|
"""
|
||||||
|
|
||||||
|
corner_radius: float = 0.0
|
||||||
|
material: GlassMaterial = GlassMaterial.UNDER_WINDOW_BACKGROUND
|
||||||
|
blending_mode: BlendingMode = BlendingMode.BEHIND_WINDOW
|
||||||
|
padding: tuple[float, float, float, float] = field(default=(0.0, 0.0, 0.0, 0.0))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sidebar(
|
||||||
|
cls, *, corner_radius: float = 10.0, padding: float = 9.0
|
||||||
|
) -> GlassOptions:
|
||||||
|
"""
|
||||||
|
Create options optimized for sidebar glass effects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
corner_radius: Corner radius for rounded corners.
|
||||||
|
padding: Uniform padding from all edges.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GlassOptions configured for sidebar use.
|
||||||
|
"""
|
||||||
|
return cls(
|
||||||
|
corner_radius=corner_radius,
|
||||||
|
material=GlassMaterial.SIDEBAR,
|
||||||
|
blending_mode=BlendingMode.BEHIND_WINDOW,
|
||||||
|
padding=(padding, padding, padding, padding),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def window(cls) -> GlassOptions:
|
||||||
|
"""
|
||||||
|
Create options for full window glass effects.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GlassOptions configured for window-wide glass.
|
||||||
|
"""
|
||||||
|
return cls(
|
||||||
|
corner_radius=0.0,
|
||||||
|
material=GlassMaterial.UNDER_WINDOW_BACKGROUND,
|
||||||
|
blending_mode=BlendingMode.BEHIND_WINDOW,
|
||||||
|
padding=(0.0, 0.0, 0.0, 0.0),
|
||||||
|
)
|
||||||
334
src/pyqt_liquidglass/glass.py
Normal file
334
src/pyqt_liquidglass/glass.py
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
"""Core glass effect implementation for macOS Liquid Glass."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from ._bridge import get_nsview_from_widget
|
||||||
|
from ._platform import HAS_GLASS_EFFECT, IS_MACOS, platform_guard
|
||||||
|
from ._types import BlendingMode, GlassOptions
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ._compat import QtWidgets
|
||||||
|
|
||||||
|
__all__ = ["apply_glass_to_widget", "apply_glass_to_window", "remove_glass_effect"]
|
||||||
|
|
||||||
|
_effect_registry: dict[int, tuple[Any, Any]] = {}
|
||||||
|
_next_effect_id: int = 0
|
||||||
|
|
||||||
|
_NS_WINDOW_BELOW: int = -1
|
||||||
|
_NS_VIEW_WIDTH_SIZABLE: int = 2
|
||||||
|
_NS_VIEW_HEIGHT_SIZABLE: int = 16
|
||||||
|
_NS_VIEW_MAX_X_MARGIN: int = 4
|
||||||
|
_NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK: int = 1 << 15
|
||||||
|
|
||||||
|
|
||||||
|
def _create_glass_view(frame: Any, options: GlassOptions) -> Any | None: # noqa: ANN401
|
||||||
|
"""
|
||||||
|
Create an NSGlassEffectView or NSVisualEffectView.
|
||||||
|
|
||||||
|
Attempts to use NSGlassEffectView on macOS 26+, falling back to
|
||||||
|
NSVisualEffectView on older versions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: NSRect for the view's frame.
|
||||||
|
options: Glass effect configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created view, or None on failure.
|
||||||
|
"""
|
||||||
|
import objc # noqa: PLC0415 # ty: ignore
|
||||||
|
|
||||||
|
if HAS_GLASS_EFFECT:
|
||||||
|
try:
|
||||||
|
glass_cls = objc.lookUpClass("NSGlassEffectView") # ty: ignore
|
||||||
|
glass = glass_cls.alloc().initWithFrame_(frame) # ty: ignore
|
||||||
|
if options.corner_radius > 0:
|
||||||
|
glass.setCornerRadius_(options.corner_radius) # ty: ignore
|
||||||
|
except objc.nosuchclass_error: # ty: ignore
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return glass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from AppKit import ( # noqa: PLC0415 # ty: ignore
|
||||||
|
NSVisualEffectBlendingModeBehindWindow,
|
||||||
|
NSVisualEffectBlendingModeWithinWindow,
|
||||||
|
NSVisualEffectStateActive,
|
||||||
|
NSVisualEffectView,
|
||||||
|
)
|
||||||
|
|
||||||
|
glass = NSVisualEffectView.alloc().initWithFrame_(frame) # ty: ignore
|
||||||
|
glass.setMaterial_(options.material.value) # ty: ignore
|
||||||
|
|
||||||
|
blending = (
|
||||||
|
NSVisualEffectBlendingModeBehindWindow # ty: ignore
|
||||||
|
if options.blending_mode == BlendingMode.BEHIND_WINDOW
|
||||||
|
else NSVisualEffectBlendingModeWithinWindow # ty: ignore
|
||||||
|
)
|
||||||
|
glass.setBlendingMode_(blending) # ty: ignore
|
||||||
|
glass.setState_(NSVisualEffectStateActive) # ty: ignore
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return glass
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_window_for_glass(ns_window: Any) -> None: # noqa: ANN401
|
||||||
|
"""Configure NSWindow properties for full window glass effect rendering."""
|
||||||
|
from AppKit import NSColor # noqa: PLC0415 # ty: ignore
|
||||||
|
|
||||||
|
ns_window.setOpaque_(False) # ty: ignore
|
||||||
|
ns_window.setBackgroundColor_(NSColor.clearColor()) # ty: ignore
|
||||||
|
|
||||||
|
current_mask = ns_window.styleMask() # ty: ignore
|
||||||
|
ns_window.setStyleMask_(current_mask | _NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK) # ty: ignore
|
||||||
|
ns_window.setTitlebarAppearsTransparent_(True) # ty: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_titlebar_for_glass(ns_window: Any) -> None: # noqa: ANN401
|
||||||
|
"""Configure only titlebar for widget-level glass (no window transparency)."""
|
||||||
|
current_mask = ns_window.styleMask() # ty: ignore
|
||||||
|
ns_window.setStyleMask_(current_mask | _NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK) # ty: ignore
|
||||||
|
ns_window.setTitlebarAppearsTransparent_(True) # ty: ignore
|
||||||
|
|
||||||
|
|
||||||
|
@platform_guard
|
||||||
|
def apply_glass_to_window(
|
||||||
|
window: QtWidgets.QWidget, options: GlassOptions | None = None
|
||||||
|
) -> int | None:
|
||||||
|
"""
|
||||||
|
Apply glass effect to an entire window.
|
||||||
|
|
||||||
|
Creates an NSGlassEffectView (macOS 26+) or NSVisualEffectView (fallback)
|
||||||
|
that fills the window's content area behind all Qt content.
|
||||||
|
|
||||||
|
Uses one of three strategies based on window configuration:
|
||||||
|
1. Sibling Injection: If root view has a superview, adds glass as sibling
|
||||||
|
2. Content Swap: For frameless windows, creates a container and swaps
|
||||||
|
3. Child Fallback: Adds glass inside root view at bottom of z-order
|
||||||
|
|
||||||
|
Args:
|
||||||
|
window: A top-level QWidget (typically QMainWindow).
|
||||||
|
options: Glass effect configuration. Uses defaults if None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An effect ID for later removal, or None if the effect could not
|
||||||
|
be applied.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The window should be shown before calling this function.
|
||||||
|
"""
|
||||||
|
if options is None:
|
||||||
|
options = GlassOptions.window()
|
||||||
|
|
||||||
|
root_view = get_nsview_from_widget(window)
|
||||||
|
if root_view is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ns_window = root_view.window() # ty: ignore
|
||||||
|
if ns_window is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
from AppKit import NSView # noqa: PLC0415 # ty: ignore
|
||||||
|
from Foundation import NSMakeRect # noqa: PLC0415 # ty: ignore
|
||||||
|
|
||||||
|
superview = root_view.superview() # ty: ignore
|
||||||
|
content_view = ns_window.contentView() # ty: ignore
|
||||||
|
|
||||||
|
container: Any = None
|
||||||
|
performed_swap = False
|
||||||
|
|
||||||
|
if superview is not None:
|
||||||
|
container = superview
|
||||||
|
elif root_view == content_view:
|
||||||
|
frame = root_view.frame() # ty: ignore
|
||||||
|
new_container = NSView.alloc().initWithFrame_(frame) # ty: ignore
|
||||||
|
new_container.setAutoresizingMask_( # ty: ignore
|
||||||
|
_NS_VIEW_WIDTH_SIZABLE | _NS_VIEW_HEIGHT_SIZABLE
|
||||||
|
)
|
||||||
|
new_container.setWantsLayer_(True) # ty: ignore
|
||||||
|
|
||||||
|
ns_window.setContentView_(new_container) # ty: ignore
|
||||||
|
|
||||||
|
root_view.setFrame_(new_container.bounds()) # ty: ignore
|
||||||
|
root_view.setAutoresizingMask_( # ty: ignore
|
||||||
|
_NS_VIEW_WIDTH_SIZABLE | _NS_VIEW_HEIGHT_SIZABLE
|
||||||
|
)
|
||||||
|
new_container.addSubview_(root_view) # ty: ignore
|
||||||
|
|
||||||
|
container = root_view.superview() # ty: ignore
|
||||||
|
performed_swap = True
|
||||||
|
else:
|
||||||
|
container = root_view
|
||||||
|
|
||||||
|
_configure_window_for_glass(ns_window)
|
||||||
|
|
||||||
|
if container == root_view.superview(): # ty: ignore
|
||||||
|
frame_rect = root_view.frame() # ty: ignore
|
||||||
|
else:
|
||||||
|
frame_rect = root_view.bounds() # ty: ignore
|
||||||
|
|
||||||
|
if performed_swap:
|
||||||
|
frame_rect = container.bounds() # ty: ignore
|
||||||
|
|
||||||
|
pad_left, pad_top, pad_right, pad_bottom = options.padding
|
||||||
|
frame_rect = NSMakeRect( # ty: ignore
|
||||||
|
frame_rect.origin.x + pad_left, # ty: ignore
|
||||||
|
frame_rect.origin.y + pad_bottom, # ty: ignore
|
||||||
|
frame_rect.size.width - pad_left - pad_right, # ty: ignore
|
||||||
|
frame_rect.size.height - pad_top - pad_bottom, # ty: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
glass = _create_glass_view(frame_rect, options)
|
||||||
|
if glass is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
glass.setAutoresizingMask_(_NS_VIEW_WIDTH_SIZABLE | _NS_VIEW_HEIGHT_SIZABLE) # ty: ignore
|
||||||
|
|
||||||
|
if container == root_view.superview(): # ty: ignore
|
||||||
|
container.addSubview_positioned_relativeTo_( # ty: ignore
|
||||||
|
glass, _NS_WINDOW_BELOW, root_view
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
container.addSubview_positioned_relativeTo_( # ty: ignore
|
||||||
|
glass, _NS_WINDOW_BELOW, None
|
||||||
|
)
|
||||||
|
|
||||||
|
global _next_effect_id # noqa: PLW0603
|
||||||
|
effect_id = _next_effect_id
|
||||||
|
_next_effect_id += 1
|
||||||
|
_effect_registry[effect_id] = (glass, container)
|
||||||
|
|
||||||
|
window._glass_view = glass # type: ignore[attr-defined] # noqa: SLF001 # ty: ignore
|
||||||
|
|
||||||
|
return effect_id
|
||||||
|
|
||||||
|
|
||||||
|
@platform_guard
|
||||||
|
def apply_glass_to_widget(
|
||||||
|
widget: QtWidgets.QWidget, options: GlassOptions | None = None
|
||||||
|
) -> int | None:
|
||||||
|
"""
|
||||||
|
Apply glass effect to a specific widget.
|
||||||
|
|
||||||
|
Creates a glass effect view sized and positioned to match the widget's
|
||||||
|
geometry within its parent window.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget: The widget to apply the glass effect to.
|
||||||
|
options: Glass effect configuration. Uses GlassOptions.sidebar()
|
||||||
|
defaults if None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An effect ID for later removal, or None if the effect could not
|
||||||
|
be applied.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The widget must be visible and part of a shown window.
|
||||||
|
The effect view tracks widget resize and move events.
|
||||||
|
"""
|
||||||
|
from ._compat import QtCore # noqa: PLC0415
|
||||||
|
|
||||||
|
if options is None:
|
||||||
|
options = GlassOptions.sidebar()
|
||||||
|
|
||||||
|
widget.setAttribute(QtCore.Qt.WidgetAttribute.WA_NativeWindow)
|
||||||
|
widget.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
ns_view = get_nsview_from_widget(widget)
|
||||||
|
if ns_view is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ns_window = ns_view.window() # ty: ignore
|
||||||
|
if ns_window is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
superview = ns_view.superview() # ty: ignore
|
||||||
|
if superview is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
from Foundation import NSMakeRect # noqa: PLC0415 # ty: ignore
|
||||||
|
|
||||||
|
_configure_titlebar_for_glass(ns_window)
|
||||||
|
|
||||||
|
content_view = ns_window.contentView() # ty: ignore
|
||||||
|
sidebar_frame = ns_view.frame() # ty: ignore
|
||||||
|
content_frame = content_view.frame() # ty: ignore
|
||||||
|
|
||||||
|
pad_left, pad_top, pad_right, pad_bottom = options.padding
|
||||||
|
|
||||||
|
glass_frame = NSMakeRect( # ty: ignore
|
||||||
|
sidebar_frame.origin.x + pad_left, # ty: ignore
|
||||||
|
pad_bottom,
|
||||||
|
sidebar_frame.size.width - pad_left - pad_right, # ty: ignore
|
||||||
|
content_frame.size.height - pad_top - pad_bottom, # ty: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
glass = _create_glass_view(glass_frame, options)
|
||||||
|
if glass is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
glass.setAutoresizingMask_(_NS_VIEW_HEIGHT_SIZABLE | _NS_VIEW_MAX_X_MARGIN) # ty: ignore
|
||||||
|
content_view.addSubview_positioned_relativeTo_(glass, _NS_WINDOW_BELOW, None) # ty: ignore
|
||||||
|
|
||||||
|
def update_glass_frame() -> None:
|
||||||
|
sf = ns_view.frame() # ty: ignore
|
||||||
|
cf = content_view.frame() # ty: ignore
|
||||||
|
new_frame = NSMakeRect( # ty: ignore
|
||||||
|
sf.origin.x + pad_left, # ty: ignore
|
||||||
|
pad_bottom,
|
||||||
|
sf.size.width - pad_left - pad_right, # ty: ignore
|
||||||
|
cf.size.height - pad_top - pad_bottom, # ty: ignore
|
||||||
|
)
|
||||||
|
glass.setFrame_(new_frame) # ty: ignore
|
||||||
|
|
||||||
|
original_resize = widget.resizeEvent
|
||||||
|
original_move = widget.moveEvent
|
||||||
|
|
||||||
|
def on_resize(event: Any) -> None: # noqa: ANN401
|
||||||
|
update_glass_frame()
|
||||||
|
original_resize(event)
|
||||||
|
|
||||||
|
def on_move(event: Any) -> None: # noqa: ANN401
|
||||||
|
update_glass_frame()
|
||||||
|
original_move(event)
|
||||||
|
|
||||||
|
widget.resizeEvent = on_resize # type: ignore[method-assign]
|
||||||
|
widget.moveEvent = on_move # type: ignore[method-assign]
|
||||||
|
|
||||||
|
global _next_effect_id # noqa: PLW0603
|
||||||
|
effect_id = _next_effect_id
|
||||||
|
_next_effect_id += 1
|
||||||
|
_effect_registry[effect_id] = (glass, content_view)
|
||||||
|
|
||||||
|
widget._glass_view = glass # type: ignore[attr-defined] # noqa: SLF001
|
||||||
|
widget._update_glass_frame = update_glass_frame # type: ignore[attr-defined] # noqa: SLF001
|
||||||
|
|
||||||
|
return effect_id
|
||||||
|
|
||||||
|
|
||||||
|
def remove_glass_effect(effect_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
Remove a previously applied glass effect.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
effect_id: The identifier returned by apply_glass_to_window or
|
||||||
|
apply_glass_to_widget.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the effect was successfully removed, False if the effect
|
||||||
|
ID was not found.
|
||||||
|
"""
|
||||||
|
if effect_id not in _effect_registry:
|
||||||
|
return False
|
||||||
|
|
||||||
|
glass_view, _ = _effect_registry.pop(effect_id)
|
||||||
|
|
||||||
|
if IS_MACOS:
|
||||||
|
try:
|
||||||
|
glass_view.removeFromSuperview() # ty: ignore
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
133
src/pyqt_liquidglass/helpers.py
Normal file
133
src/pyqt_liquidglass/helpers.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
"""Helper functions for preparing Qt widgets for glass effects."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from ._bridge import get_nswindow_from_widget
|
||||||
|
from ._platform import IS_MACOS
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ._compat import QtWidgets
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"prepare_widget_for_glass",
|
||||||
|
"prepare_window_for_glass",
|
||||||
|
"set_window_background_transparent",
|
||||||
|
]
|
||||||
|
|
||||||
|
_NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK: int = 1 << 15
|
||||||
|
_NS_WINDOW_TITLE_HIDDEN: int = 1
|
||||||
|
_NS_WINDOW_STYLE_MASK_BORDERLESS: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_window_for_glass(
|
||||||
|
window: QtWidgets.QWidget,
|
||||||
|
*,
|
||||||
|
frameless: bool = False,
|
||||||
|
transparent_titlebar: bool = True,
|
||||||
|
full_size_content: bool = True,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Prepare a window for glass effects.
|
||||||
|
|
||||||
|
Sets the necessary Qt widget attributes and configures the native
|
||||||
|
NSWindow properties for glass effect rendering.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
window: The window widget to prepare.
|
||||||
|
frameless: If True, remove the window frame entirely using
|
||||||
|
Qt.FramelessWindowHint.
|
||||||
|
transparent_titlebar: If True, make the titlebar transparent
|
||||||
|
on macOS so glass can extend underneath.
|
||||||
|
full_size_content: If True, extend content view to cover the
|
||||||
|
titlebar area.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Call this before showing the window for best results.
|
||||||
|
"""
|
||||||
|
from ._compat import QtCore # noqa: PLC0415
|
||||||
|
|
||||||
|
window.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
if frameless:
|
||||||
|
window.setWindowFlag(QtCore.Qt.WindowType.FramelessWindowHint)
|
||||||
|
|
||||||
|
if not IS_MACOS:
|
||||||
|
return
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
ns_window = get_nswindow_from_widget(window)
|
||||||
|
if ns_window is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if frameless:
|
||||||
|
ns_window.setHasShadow_(False) # ty: ignore # Keep shadow for depth
|
||||||
|
return
|
||||||
|
|
||||||
|
if full_size_content:
|
||||||
|
current_mask = ns_window.styleMask() # ty: ignore
|
||||||
|
ns_window.setStyleMask_( # ty: ignore
|
||||||
|
current_mask | _NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK
|
||||||
|
)
|
||||||
|
|
||||||
|
if transparent_titlebar:
|
||||||
|
ns_window.setTitlebarAppearsTransparent_(True) # ty: ignore
|
||||||
|
ns_window.setTitleVisibility_(_NS_WINDOW_TITLE_HIDDEN) # ty: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_widget_for_glass(widget: QtWidgets.QWidget) -> None:
|
||||||
|
"""
|
||||||
|
Prepare a widget for having glass effect applied.
|
||||||
|
|
||||||
|
Sets the necessary Qt attributes for the widget to render correctly
|
||||||
|
with a glass effect behind it. The widget's content will be visible
|
||||||
|
on top of the glass effect.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget: The widget to prepare.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This sets WA_TranslucentBackground which makes the widget
|
||||||
|
background transparent. Ensure your widget's stylesheet or
|
||||||
|
paint event handles the transparent background appropriately.
|
||||||
|
"""
|
||||||
|
from ._compat import QtCore # noqa: PLC0415
|
||||||
|
|
||||||
|
widget.setAttribute(QtCore.Qt.WidgetAttribute.WA_NativeWindow)
|
||||||
|
widget.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
|
||||||
|
def set_window_background_transparent(window: QtWidgets.QWidget) -> None:
|
||||||
|
"""
|
||||||
|
Make a window's background fully transparent.
|
||||||
|
|
||||||
|
This is useful when you want complete control over the window
|
||||||
|
appearance, such as creating a fully custom-drawn window with
|
||||||
|
glass effects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
window: The window to make transparent.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
After calling this, the window will have no visible background.
|
||||||
|
You must provide your own background through stylesheets or
|
||||||
|
painting.
|
||||||
|
"""
|
||||||
|
from ._compat import QtCore, QtGui # noqa: PLC0415
|
||||||
|
|
||||||
|
window.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
window.setAttribute(QtCore.Qt.WidgetAttribute.WA_NoSystemBackground)
|
||||||
|
|
||||||
|
palette = window.palette()
|
||||||
|
palette.setColor(QtGui.QPalette.ColorRole.Window, QtGui.QColor(0, 0, 0, 0))
|
||||||
|
window.setPalette(palette)
|
||||||
|
|
||||||
|
if IS_MACOS:
|
||||||
|
ns_window = get_nswindow_from_widget(window)
|
||||||
|
if ns_window is not None:
|
||||||
|
from AppKit import NSColor # noqa: PLC0415 # ty: ignore
|
||||||
|
|
||||||
|
ns_window.setOpaque_(False) # ty: ignore
|
||||||
|
ns_window.setBackgroundColor_(NSColor.clearColor()) # ty: ignore
|
||||||
0
src/pyqt_liquidglass/py.typed
Normal file
0
src/pyqt_liquidglass/py.typed
Normal file
223
src/pyqt_liquidglass/traffic_lights.py
Normal file
223
src/pyqt_liquidglass/traffic_lights.py
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
"""Traffic light (window button) positioning for macOS windows."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from ._bridge import get_nsview_from_widget
|
||||||
|
from ._platform import platform_guard
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ._compat import QtWidgets
|
||||||
|
|
||||||
|
__all__ = ["hide_traffic_lights", "setup_traffic_lights_inset", "show_traffic_lights"]
|
||||||
|
|
||||||
|
_NS_WINDOW_CLOSE_BUTTON: int = 0
|
||||||
|
_NS_WINDOW_MINIATURIZE_BUTTON: int = 1
|
||||||
|
_NS_WINDOW_ZOOM_BUTTON: int = 2
|
||||||
|
_NS_WINDOW_TITLE_HIDDEN: int = 1
|
||||||
|
_NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK: int = 1 << 15
|
||||||
|
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_LEADING: int = 5
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_TRAILING: int = 6
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_CENTER_Y: int = 10
|
||||||
|
_NS_LAYOUT_RELATION_EQUAL: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
def _get_traffic_light_buttons(ns_window: Any) -> tuple[Any, Any, Any]: # noqa: ANN401
|
||||||
|
"""Get the close, minimize, and zoom buttons from an NSWindow."""
|
||||||
|
close_btn = ns_window.standardWindowButton_(_NS_WINDOW_CLOSE_BUTTON) # ty: ignore
|
||||||
|
minimize_btn = ns_window.standardWindowButton_(_NS_WINDOW_MINIATURIZE_BUTTON) # ty: ignore
|
||||||
|
zoom_btn = ns_window.standardWindowButton_(_NS_WINDOW_ZOOM_BUTTON) # ty: ignore
|
||||||
|
return close_btn, minimize_btn, zoom_btn
|
||||||
|
|
||||||
|
|
||||||
|
@platform_guard
|
||||||
|
def setup_traffic_lights_inset(
|
||||||
|
window: QtWidgets.QWidget, x_offset: float = 0.0, y_offset: float = 0.0
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Reposition the traffic light buttons (close, minimize, zoom).
|
||||||
|
|
||||||
|
Uses NSLayoutConstraint to position the buttons with an offset from
|
||||||
|
their default location. This method is more robust than frame-based
|
||||||
|
positioning as it survives window resizes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
window: The window whose traffic lights to reposition.
|
||||||
|
x_offset: Horizontal offset in points from the left edge.
|
||||||
|
y_offset: Vertical offset in points from the center.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the traffic lights were successfully repositioned,
|
||||||
|
False otherwise.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This function configures the window for full-size content view
|
||||||
|
and transparent titlebar automatically.
|
||||||
|
"""
|
||||||
|
ns_view = get_nsview_from_widget(window)
|
||||||
|
if ns_view is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ns_window = ns_view.window() # ty: ignore
|
||||||
|
if ns_window is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
from AppKit import NSLayoutConstraint # noqa: PLC0415 # ty: ignore
|
||||||
|
|
||||||
|
current_mask = ns_window.styleMask() # ty: ignore
|
||||||
|
ns_window.setStyleMask_( # ty: ignore
|
||||||
|
current_mask | _NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK
|
||||||
|
)
|
||||||
|
ns_window.setTitlebarAppearsTransparent_(True) # ty: ignore
|
||||||
|
ns_window.setTitleVisibility_(_NS_WINDOW_TITLE_HIDDEN) # ty: ignore
|
||||||
|
|
||||||
|
close_btn, minimize_btn, zoom_btn = _get_traffic_light_buttons(ns_window)
|
||||||
|
|
||||||
|
if close_btn is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
close_btn.setTranslatesAutoresizingMaskIntoConstraints_(False) # ty: ignore
|
||||||
|
if minimize_btn is not None:
|
||||||
|
minimize_btn.setTranslatesAutoresizingMaskIntoConstraints_(False) # ty: ignore
|
||||||
|
if zoom_btn is not None:
|
||||||
|
zoom_btn.setTranslatesAutoresizingMaskIntoConstraints_(False) # ty: ignore
|
||||||
|
|
||||||
|
superview = close_btn.superview() # ty: ignore
|
||||||
|
if superview is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
button_spacing = 6.0
|
||||||
|
make_constraint = ( # ty: ignore
|
||||||
|
NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_
|
||||||
|
)
|
||||||
|
|
||||||
|
constraint_close_x = make_constraint(
|
||||||
|
close_btn,
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_LEADING,
|
||||||
|
_NS_LAYOUT_RELATION_EQUAL,
|
||||||
|
superview,
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_LEADING,
|
||||||
|
1.0,
|
||||||
|
x_offset,
|
||||||
|
)
|
||||||
|
superview.addConstraint_(constraint_close_x) # ty: ignore
|
||||||
|
|
||||||
|
constraint_close_y = make_constraint(
|
||||||
|
close_btn,
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_CENTER_Y,
|
||||||
|
_NS_LAYOUT_RELATION_EQUAL,
|
||||||
|
superview,
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_CENTER_Y,
|
||||||
|
1.0,
|
||||||
|
y_offset,
|
||||||
|
)
|
||||||
|
superview.addConstraint_(constraint_close_y) # ty: ignore
|
||||||
|
|
||||||
|
if minimize_btn is not None:
|
||||||
|
constraint_min_x = make_constraint(
|
||||||
|
minimize_btn,
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_LEADING,
|
||||||
|
_NS_LAYOUT_RELATION_EQUAL,
|
||||||
|
close_btn,
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_TRAILING,
|
||||||
|
1.0,
|
||||||
|
button_spacing,
|
||||||
|
)
|
||||||
|
superview.addConstraint_(constraint_min_x) # ty: ignore
|
||||||
|
|
||||||
|
constraint_min_y = make_constraint(
|
||||||
|
minimize_btn,
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_CENTER_Y,
|
||||||
|
_NS_LAYOUT_RELATION_EQUAL,
|
||||||
|
superview,
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_CENTER_Y,
|
||||||
|
1.0,
|
||||||
|
y_offset,
|
||||||
|
)
|
||||||
|
superview.addConstraint_(constraint_min_y) # ty: ignore
|
||||||
|
|
||||||
|
if zoom_btn is not None and minimize_btn is not None:
|
||||||
|
constraint_zoom_x = make_constraint(
|
||||||
|
zoom_btn,
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_LEADING,
|
||||||
|
_NS_LAYOUT_RELATION_EQUAL,
|
||||||
|
minimize_btn,
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_TRAILING,
|
||||||
|
1.0,
|
||||||
|
button_spacing,
|
||||||
|
)
|
||||||
|
superview.addConstraint_(constraint_zoom_x) # ty: ignore
|
||||||
|
|
||||||
|
constraint_zoom_y = make_constraint(
|
||||||
|
zoom_btn,
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_CENTER_Y,
|
||||||
|
_NS_LAYOUT_RELATION_EQUAL,
|
||||||
|
superview,
|
||||||
|
_NS_LAYOUT_ATTRIBUTE_CENTER_Y,
|
||||||
|
1.0,
|
||||||
|
y_offset,
|
||||||
|
)
|
||||||
|
superview.addConstraint_(constraint_zoom_y) # ty: ignore
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@platform_guard
|
||||||
|
def hide_traffic_lights(window: QtWidgets.QWidget) -> bool:
|
||||||
|
"""
|
||||||
|
Hide the traffic light buttons while keeping window functionality.
|
||||||
|
|
||||||
|
The buttons are hidden but the window remains closable, minimizable,
|
||||||
|
and zoomable via keyboard shortcuts and menu commands.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
window: The window whose traffic lights to hide.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise.
|
||||||
|
"""
|
||||||
|
ns_view = get_nsview_from_widget(window)
|
||||||
|
if ns_view is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ns_window = ns_view.window() # ty: ignore
|
||||||
|
if ns_window is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
close_btn, minimize_btn, zoom_btn = _get_traffic_light_buttons(ns_window)
|
||||||
|
|
||||||
|
for btn in (close_btn, minimize_btn, zoom_btn):
|
||||||
|
if btn is not None:
|
||||||
|
btn.setHidden_(True) # ty: ignore
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@platform_guard
|
||||||
|
def show_traffic_lights(window: QtWidgets.QWidget) -> bool:
|
||||||
|
"""
|
||||||
|
Show previously hidden traffic light buttons.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
window: The window whose traffic lights to show.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise.
|
||||||
|
"""
|
||||||
|
ns_view = get_nsview_from_widget(window)
|
||||||
|
if ns_view is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ns_window = ns_view.window() # ty: ignore
|
||||||
|
if ns_window is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
close_btn, minimize_btn, zoom_btn = _get_traffic_light_buttons(ns_window)
|
||||||
|
|
||||||
|
for btn in (close_btn, minimize_btn, zoom_btn):
|
||||||
|
if btn is not None:
|
||||||
|
btn.setHidden_(False) # ty: ignore
|
||||||
|
|
||||||
|
return True
|
||||||
6
ty.toml
Normal file
6
ty.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[environment]
|
||||||
|
python = ".venv"
|
||||||
|
|
||||||
|
[rules]
|
||||||
|
unresolved-import = "ignore"
|
||||||
|
unresolved-attribute = "ignore"
|
||||||
241
uv.lock
generated
Normal file
241
uv.lock
generated
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "librt"
|
||||||
|
version = "0.7.8"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy"
|
||||||
|
version = "1.19.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
|
{ name = "mypy-extensions" },
|
||||||
|
{ name = "pathspec" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathspec"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyobjc-core"
|
||||||
|
version = "12.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyobjc-framework-cocoa"
|
||||||
|
version = "12.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyobjc-core" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyobjc-framework-quartz"
|
||||||
|
version = "12.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyobjc-core" },
|
||||||
|
{ name = "pyobjc-framework-cocoa" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860", size = 219206, upload-time = "2025-11-14T10:00:15.623Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/43/b1f0ad3b842ab150a7e6b7d97f6257eab6af241b4c7d14cb8e7fde9214b8/pyobjc_framework_quartz-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:53b84e880c358ba1ddcd7e8d5ea0407d760eca58b96f0d344829162cda5f37b3", size = 224317, upload-time = "2025-11-14T10:00:30.703Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/00/96249c5c7e5aaca5f688ca18b8d8ad05cd7886ebd639b3c71a6a4cadbe75/pyobjc_framework_quartz-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:42d306b07f05ae7d155984503e0fb1b701fecd31dcc5c79fe8ab9790ff7e0de0", size = 219558, upload-time = "2025-11-14T10:00:45.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580, upload-time = "2025-11-14T10:01:00.091Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyqt-liquidglass"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyobjc-framework-cocoa" },
|
||||||
|
{ name = "pyobjc-framework-quartz" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pyside6-essentials" },
|
||||||
|
{ name = "pyside6-stubs" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "pyobjc-framework-cocoa", specifier = ">=12.1" },
|
||||||
|
{ name = "pyobjc-framework-quartz", specifier = ">=12.1" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "pyside6-essentials", specifier = ">=6.10.1" },
|
||||||
|
{ name = "pyside6-stubs", specifier = ">=6.7.3.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyside6"
|
||||||
|
version = "6.10.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyside6-addons" },
|
||||||
|
{ name = "pyside6-essentials" },
|
||||||
|
{ name = "shiboken6" },
|
||||||
|
]
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/22/f82cfcd1158be502c5741fe67c3fa853f3c1edbd3ac2c2250769dd9722d1/pyside6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:d0e70dd0e126d01986f357c2a555722f9462cf8a942bf2ce180baf69f468e516", size = 558169, upload-time = "2025-11-20T10:09:08.79Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/eb/54afe242a25d1c33b04ecd8321a549d9efb7b89eef7690eed92e98ba1dc9/pyside6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4053bf51ba2c2cb20e1005edd469997976a02cec009f7c46356a0b65c137f1fa", size = 557818, upload-time = "2025-11-20T10:09:10.132Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/af/5706b1b33587dc2f3dfa3a5000424befba35e4f2d5889284eebbde37138b/pyside6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:7d3ca20a40139ca5324a7864f1d91cdf2ff237e11bd16354a42670f2a4eeb13c", size = 558358, upload-time = "2025-11-20T10:09:11.288Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/41/3f48d724ecc8e42cea8a8442aa9b5a86d394b85093275990038fd1020039/pyside6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9f89ff994f774420eaa38cec6422fddd5356611d8481774820befd6f3bb84c9e", size = 564424, upload-time = "2025-11-20T10:09:12.677Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/30/395411473b433875a82f6b5fdd0cb28f19a0e345bcaac9fbc039400d7072/pyside6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:9c5c1d94387d1a32a6fae25348097918ef413b87dfa3767c46f737c6d48ae437", size = 548866, upload-time = "2025-11-20T10:09:14.174Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyside6-addons"
|
||||||
|
version = "6.10.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyside6-essentials" },
|
||||||
|
{ name = "shiboken6" },
|
||||||
|
]
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/f9/b72a2578d7dbef7741bb90b5756b4ef9c99a5b40148ea53ce7f048573fe9/pyside6_addons-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:4d2b82bbf9b861134845803837011e5f9ac7d33661b216805273cf0c6d0f8e82", size = 322639446, upload-time = "2025-11-20T09:54:50.75Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/3b/3ed951c570a15570706a89d39bfd4eaaffdf16d5c2dca17e82fc3ec8aaa6/pyside6_addons-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:330c229b58d30083a7b99ed22e118eb4f4126408429816a4044ccd0438ae81b4", size = 170678293, upload-time = "2025-11-20T09:56:40.991Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/77/4c780b204d0bf3323a75c184e349d063e208db44c993f1214aa4745d6f47/pyside6_addons-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:56864b5fecd6924187a2d0f7e98d968ed72b6cc267caa5b294cd7e88fff4e54c", size = 166365011, upload-time = "2025-11-20T09:57:20.261Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/14/58239776499e6b279fa6ca2e0d47209531454b99f6bd2ad7c96f11109416/pyside6_addons-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:b6e249d15407dd33d6a2ffabd9dc6d7a8ab8c95d05f16a71dad4d07781c76341", size = 164864664, upload-time = "2025-11-20T09:57:54.815Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/cd/1b74108671ba4b1ebb2661330665c4898b089e9c87f7ba69fe2438f3d1b6/pyside6_addons-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:0de303c0447326cdc6c8be5ab066ef581e2d0baf22560c9362d41b8304fdf2db", size = 34191225, upload-time = "2025-11-20T09:58:04.184Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyside6-essentials"
|
||||||
|
version = "6.10.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "shiboken6" },
|
||||||
|
]
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/b0/c43209fecef79912e9b1c70a1b5172b1edf76caebcc885c58c60a09613b0/pyside6_essentials-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:cd224aff3bb26ff1fca32c050e1c4d0bd9f951a96219d40d5f3d0128485b0bbe", size = 105461499, upload-time = "2025-11-20T09:59:23.733Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/8e/b69ba7fa0c701f3f4136b50460441697ec49ee6ea35c229eb2a5ee4b5952/pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e9ccbfb58c03911a0bce1f2198605b02d4b5ca6276bfc0cbcf7c6f6393ffb856", size = 76764617, upload-time = "2025-11-20T09:59:38.831Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/83/569d27f4b6c6b9377150fe1a3745d64d02614021bea233636bc936a23423/pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:ec8617c9b143b0c19ba1cc5a7e98c538e4143795480cb152aee47802c18dc5d2", size = 75850373, upload-time = "2025-11-20T09:59:56.082Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/64/a8df6333de8ccbf3a320e1346ca30d0f314840aff5e3db9b4b66bf38e26c/pyside6_essentials-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9555a48e8f0acf63fc6a23c250808db841b28a66ed6ad89ee0e4df7628752674", size = 74491180, upload-time = "2025-11-20T10:00:11.215Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/da/65cc6c6a870d4ea908c59b2f0f9e2cf3bfc6c0710ebf278ed72f69865e4e/pyside6_essentials-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:4d1d248644f1778f8ddae5da714ca0f5a150a5e6f602af2765a7d21b876da05c", size = 55190458, upload-time = "2025-11-20T10:00:26.226Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyside6-stubs"
|
||||||
|
version = "6.7.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mypy" },
|
||||||
|
{ name = "pyside6" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/2b/d2a3fdb212475682b11e8b6330f2cada60dcbbaf151f73f86844b11d1db6/pyside6_stubs-6.7.3.0.tar.gz", hash = "sha256:db8def2bb9c74091bfa2a4df7610d3ea344ecd00ebd4283995eeb5fda8383603", size = 509931, upload-time = "2025-03-04T05:49:15.755Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/2e/3488fa3baca56ff494b9502424675f5b14b11e966eb74d5adaf1811cd651/pyside6_stubs-6.7.3.0-py3-none-any.whl", hash = "sha256:7a2ef0d7486939f240e745829f80887ec3eab280e52c9930d64b48b3de95042a", size = 550616, upload-time = "2025-03-04T05:49:13.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shiboken6"
|
||||||
|
version = "6.10.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/8b/e5db743d505ceea3efc4cd9634a3bee22a3e2bf6e07cefd28c9b9edabcc6/shiboken6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:9f2990f5b61b0b68ecadcd896ab4441f2cb097eef7797ecc40584107d9850d71", size = 478483, upload-time = "2025-11-20T10:08:52.411Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/ba/b50c1a44b3c4643f482afbf1a0ea58f393827307100389ce29404f9ad3b0/shiboken6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4221a52dfb81f24a0d20cc4f8981cb6edd810d5a9fb28287ce10d342573a0e4", size = 271993, upload-time = "2025-11-20T10:08:54.093Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/b8/939c24ebd662b0aa5c945443d0973145b3fb7079f0196274ef7bb4b98f73/shiboken6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:c095b00f4d6bf578c0b2464bb4e264b351a99345374478570f69e2e679a2a1d0", size = 268691, upload-time = "2025-11-20T10:08:55.639Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/a6/8c65ee0fa5e172ebcca03246b1bc3bd96cdaf1d60537316648536b7072a5/shiboken6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:c1601d3cda1fa32779b141663873741b54e797cb0328458d7466281f117b0a4e", size = 1234704, upload-time = "2025-11-20T10:08:57.417Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/6a/c0fea2f2ac7d9d96618c98156500683a4d1f93fea0e8c5a2bc39913d7ef1/shiboken6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:5cf800917008587b551005a45add2d485cca66f5f7ecd5b320e9954e40448cc9", size = 1795567, upload-time = "2025-11-20T10:08:59.184Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user