Skip to main content

Property sheet

Project description


tags: [gradio-custom-component, ui, form, settings, dataclass] title: gradio_propertysheet short_description: Property Sheet Component for Gradio colorFrom: blue colorTo: green sdk: gradio pinned: true app_file: space.py

gradio_propertysheet

PyPI - Version

Property sheet

Installation

pip install gradio_propertysheet

Usage

import os
import json
import gradio as gr
from dataclasses import dataclass, field, asdict
from typing import Literal
from gradio_propertysheet import PropertySheet
from gradio_htmlinjector import HTMLInjector


# --- 1. Dataclass Definitions (unchanged) ---
@dataclass
class ModelSettings:
    model_type: Literal["SD 1.5", "SDXL", "Pony", "Custom"] = field(
        default="SDXL", metadata={"component": "dropdown", "label": "Base Model"}
    )
    custom_model_path: str = field(
        default="/path/to/default.safetensors",
        metadata={
            "label": "Custom Model Path",
            "interactive_if": {"field": "model_type", "value": "Custom"},
        },
    )
    vae_path: str = field(default="", metadata={"label": "VAE Path (optional)"})


@dataclass
class SamplingSettings:
    scheduler: Literal["Karras", "Simple", "Exponential"] = field(
        default="Karras",
        metadata={"component": "radio", "label": "Scheduler"}
    )
    sampler_name: Literal["Euler", "Euler a", "DPM++ 2M Karras", "UniPC"] = field(
        default="DPM++ 2M Karras",
        metadata={"component": "dropdown", "label": "Sampler"},
    )
    steps: int = field(
        default=25,
        metadata={"component": "slider", "minimum": 1, "maximum": 150, "step": 1},
    )
    cfg_scale: float = field(
        default=7.0,
        metadata={"component": "slider", "minimum": 1.0, "maximum": 30.0, "step": 0.5},
    )
    enable_advanced: bool = field(
        default=False,
        metadata={"label": "Enable Advanced Settings"}
    )
    advanced_option: float = field(
        default=0.5,
        metadata={
            "label": "Advanced Option",
            "component": "slider",
            "minimum": 0.0,
            "maximum": 1.0,
            "step": 0.01,
            "interactive_if": {"field": "enable_advanced", "value": True},
        },
    )
    temperature: float = field(
        default=1.0,
        metadata={
            "label": "Sampling Temperature",
            "component": "number_float",
            "minimum": 0.1,
            "maximum": 2.0,
            "step": 0.1
        }
    )

@dataclass
class RenderConfig:
    randomize_seed: bool = field(default=True, metadata={"label": "Randomize Seed"})
    seed: int = field(
        default=-1,
        metadata={"component": "number_integer", "label": "Seed (-1 for random)"},
    )
    model: ModelSettings = field(default_factory=ModelSettings, metadata={"label": "Model Settings"})
    sampling: SamplingSettings = field(default_factory=SamplingSettings)

@dataclass
class Lighting:
    sun_intensity: float = field(
        default=1.0,
        metadata={"component": "slider", "minimum": 0, "maximum": 5, "step": 0.1},
    )
    color: str = field(
        default="#FFDDBB", metadata={"component": "colorpicker", "label": "Sun Color"}
    )


@dataclass
class EnvironmentConfig:
    background: Literal["Sky", "Color", "Image"] = field(
        default="Sky", metadata={"component": "dropdown"}
    )
    lighting: Lighting = field(default_factory=Lighting)


@dataclass
class EulerSettings:
    s_churn: float = field(
        default=0.0,
        metadata={"component": "slider", "minimum": 0.0, "maximum": 1.0, "step": 0.01},
    )


@dataclass
class DPM_Settings:
    karras_style: bool = field(
        default=True, metadata={"label": "Use Karras Sigma Schedule"}
    )


# --- 2. Data Mappings and Initial Instances (unchanged) ---
initial_render_config = RenderConfig()
initial_env_config = EnvironmentConfig()
sampler_settings_map_py = {
    "Euler": EulerSettings(),
    "DPM++ 2M Karras": DPM_Settings(),
    "UniPC": None,
    "CustomSampler": SamplingSettings()
}
model_settings_map_py = {
    "SDXL 1.0": DPM_Settings(),
    "Stable Diffusion 1.5": EulerSettings(),
    "Pony": None,
}


# --- 3. CSS & JS Injection function (unchanged) ---
def inject_assets():
    """
    This function prepares the payload of CSS, JS, and Body HTML for injection.
    """
    popup_html = """<div id="injected_flyout_container" class="flyout-sheet" style="display: none;"></div>"""
    css_code = ""
    js_code = ""

    try:
        with open("custom.css", "r", encoding="utf-8") as f:
            css_code += f.read() + "\n"
        with open("custom.js", "r", encoding="utf-8") as f:
            js_code += f.read() + "\n"
    except FileNotFoundError as e:
        print(f"Warning: Could not read asset file: {e}")
    return {"js": js_code, "css": css_code, "body_html": popup_html}


# --- 4. Gradio App Build ---
with gr.Blocks(title="PropertySheet Demos") as demo:
    html_injector = HTMLInjector()
    gr.Markdown("# PropertySheet Component Demos")

    with gr.Row():
        # --- Flyout popup ---
        with gr.Column(
            elem_id="flyout_panel_source", elem_classes=["flyout-source-hidden"]
        ) as flyout_panel_source:
            close_btn = gr.Button("×", elem_classes=["flyout-close-btn"])
            flyout_sheet = PropertySheet(
                visible=True,
                container=False,
                label="Settings",
                show_group_name_only_one=False,
                disable_accordion=True,
            )

    with gr.Tabs():
        with gr.TabItem("Original Sidebar Demo"):
            gr.Markdown(
                "An example of using the `PropertySheet` component as a traditional sidebar for settings."
            )
            render_state = gr.State(value=initial_render_config)
            env_state = gr.State(value=initial_env_config)
            sidebar_visible = gr.State(False)
            with gr.Row():
                with gr.Column(scale=3):
                    generate = gr.Button("Show Settings", variant="primary")
                    with gr.Row():
                        output_render_json = gr.JSON(label="Live Render State")
                        output_env_json = gr.JSON(label="Live Environment State")
                with gr.Column(scale=1):
                    render_sheet = PropertySheet(
                        value=initial_render_config,
                        label="Render Settings",
                        width=400,
                        height=550,
                        visible=False,
                        root_label="Generator",
                        interactive=True                        
                    )
                    environment_sheet = PropertySheet(
                        value=initial_env_config,
                        label="Environment Settings",
                        width=400,
                        open=False,
                        visible=False,
                        root_label="General",
                        interactive=True,
                        root_properties_first=False
                    )

            def change_visibility(is_visible, render_cfg, env_cfg):
                new_visibility = not is_visible
                button_text = "Hide Settings" if new_visibility else "Show Settings"
                return (
                    new_visibility,
                    gr.update(visible=new_visibility, value=render_cfg),
                    gr.update(visible=new_visibility, value=env_cfg),
                    gr.update(value=button_text),
                )

            def handle_render_change(
                updated_config: RenderConfig, current_state: RenderConfig
            ):
                if updated_config is None:
                    return current_state, asdict(current_state), current_state
                if updated_config.model.model_type != "Custom":
                    updated_config.model.custom_model_path = "/path/to/default.safetensors"                
                return updated_config, asdict(updated_config), updated_config

            def handle_env_change(
                updated_config: EnvironmentConfig, current_state: EnvironmentConfig
            ):
                if updated_config is None:
                    return current_state, asdict(current_state), current_state
                return updated_config, asdict(updated_config), current_state

            generate.click(
                fn=change_visibility,
                inputs=[sidebar_visible, render_state, env_state],
                outputs=[sidebar_visible, render_sheet, environment_sheet, generate],
            )
            render_sheet.change(
                fn=handle_render_change,
                inputs=[render_sheet, render_state],
                outputs=[render_sheet, output_render_json, render_state],
            )
            environment_sheet.change(
                fn=handle_env_change,
                inputs=[environment_sheet, env_state],
                outputs=[environment_sheet, output_env_json, env_state],
            )
            
            #In version 0.0.7, I moved the undo function to a new `undo` event. This was necessary to avoid conflict with the `change` event where it was previously implemented. 
            # Now you need to implement the undo event for the undo button to work. You can simply receive the component as input and set it as output.
            def render_undo(updated_config: RenderConfig, current_state: RenderConfig):
                if updated_config is None:
                    return current_state, asdict(current_state), current_state
                return updated_config, asdict(updated_config), current_state
            
            def environment_undo(updated_config: EnvironmentConfig, current_state: EnvironmentConfig):
                if updated_config is None:
                    return current_state, asdict(current_state), current_state
                return updated_config, asdict(updated_config), current_state
            
            render_sheet.undo(fn=render_undo, 
                              inputs=[render_sheet, render_state], 
                              outputs=[render_sheet, output_render_json, render_state]
            )
            environment_sheet.undo(fn=environment_undo, 
                            inputs=[environment_sheet, env_state],
                            outputs=[environment_sheet, output_env_json, env_state],
            )
            
            
            demo.load(
                fn=lambda r_cfg, e_cfg: (asdict(r_cfg), asdict(e_cfg)),
                inputs=[render_state, env_state],
                outputs=[output_render_json, output_env_json],
            )

        with gr.TabItem("Flyout Popup Demo"):
            gr.Markdown(
                "An example of attaching a `PropertySheet` as a flyout panel to other components."
            )

            # --- State Management ---
            flyout_visible = gr.State(False)
            active_anchor_id = gr.State(None)
            js_data_bridge = gr.Textbox(visible=False, elem_id="js_data_bridge")

            with gr.Column(elem_classes=["flyout-context-area"]):
                with gr.Row(
                    elem_classes=["fake-input-container", "no-border-dropdown"]
                ):
                    sampler_dd = gr.Dropdown(
                        choices=list(sampler_settings_map_py.keys()),
                        label="Sampler",
                        value="Euler",
                        elem_id="sampler_dd",
                        scale=10,
                    )
                    sampler_ear_btn = gr.Button(
                        "⚙️",
                        elem_id="sampler_ear_btn",
                        scale=1,
                        elem_classes=["integrated-ear-btn"],
                    )

                with gr.Row(
                    elem_classes=["fake-input-container", "no-border-dropdown"]
                ):
                    model_dd = gr.Dropdown(
                        choices=list(model_settings_map_py.keys()),
                        label="Model",
                        value="SDXL 1.0",
                        elem_id="model_dd",
                        scale=10,
                    )
                    model_ear_btn = gr.Button(
                        "⚙️",
                        elem_id="model_ear_btn",
                        scale=1,
                        elem_classes=["integrated-ear-btn"],
                    )

            # --- Event Logic ---
            def handle_flyout_toggle(
                is_vis, current_anchor, clicked_dropdown_id, settings_obj
            ):
                if is_vis and current_anchor == clicked_dropdown_id:
                    js_data = json.dumps({"isVisible": False, "anchorId": None})
                    return False, None, gr.update(), gr.update(value=js_data)
                else:
                    js_data = json.dumps(
                        {"isVisible": True, "anchorId": clicked_dropdown_id}
                    )
                    return (
                        True,
                        clicked_dropdown_id,
                        gr.update(value=settings_obj),
                        gr.update(value=js_data),
                    )

            def update_ear_visibility(selection, settings_map):
                has_settings = settings_map.get(selection) is not None
                return gr.update(visible=has_settings)

            def on_flyout_change(updated_settings, active_id, sampler_val, model_val):
                if updated_settings is None or active_id is None:
                    return
                if active_id == sampler_dd.elem_id:
                    sampler_settings_map_py[sampler_val] = updated_settings
                elif active_id == model_dd.elem_id:
                    model_settings_map_py[model_val] = updated_settings

            def close_the_flyout():
                js_data = json.dumps({"isVisible": False, "anchorId": None})
                return False, None, gr.update(value=js_data)

            js_update_flyout = "(jsonData) => { update_flyout_from_state(jsonData); }"

            sampler_dd.change(
                fn=lambda sel: update_ear_visibility(sel, sampler_settings_map_py),
                inputs=[sampler_dd],
                outputs=[sampler_ear_btn],
            ).then(
                fn=close_the_flyout,
                outputs=[flyout_visible, active_anchor_id, js_data_bridge],
            ).then(
                fn=None, inputs=[js_data_bridge], js=js_update_flyout
            )

            sampler_ear_btn.click(
                fn=lambda is_vis, anchor, sel: handle_flyout_toggle(
                    is_vis, anchor, sampler_dd.elem_id, sampler_settings_map_py.get(sel)
                ),
                inputs=[flyout_visible, active_anchor_id, sampler_dd],
                outputs=[
                    flyout_visible,
                    active_anchor_id,
                    flyout_sheet,
                    js_data_bridge,
                ],
            ).then(fn=None, inputs=[js_data_bridge], js=js_update_flyout)

            model_dd.change(
                fn=lambda sel: update_ear_visibility(sel, model_settings_map_py),
                inputs=[model_dd],
                outputs=[model_ear_btn],
            ).then(
                fn=close_the_flyout,
                outputs=[flyout_visible, active_anchor_id, js_data_bridge],
            ).then(
                fn=None, inputs=[js_data_bridge], js=js_update_flyout
            )

            model_ear_btn.click(
                fn=lambda is_vis, anchor, sel: handle_flyout_toggle(
                    is_vis, anchor, model_dd.elem_id, model_settings_map_py.get(sel)
                ),
                inputs=[flyout_visible, active_anchor_id, model_dd],
                outputs=[
                    flyout_visible,
                    active_anchor_id,
                    flyout_sheet,
                    js_data_bridge,
                ],
            ).then(fn=None, inputs=[js_data_bridge], js=js_update_flyout)

            flyout_sheet.change(
                fn=on_flyout_change,
                inputs=[flyout_sheet, active_anchor_id, sampler_dd, model_dd],
                outputs=None,
            )

            close_btn.click(
                fn=close_the_flyout,
                inputs=None,
                outputs=[flyout_visible, active_anchor_id, js_data_bridge],
            ).then(fn=None, inputs=[js_data_bridge], js=js_update_flyout)

            def initial_flyout_setup(sampler_val, model_val):
                return {
                    sampler_ear_btn: update_ear_visibility(
                        sampler_val, sampler_settings_map_py
                    ),
                    model_ear_btn: update_ear_visibility(
                        model_val, model_settings_map_py
                    ),
                }

            # --- App Load ---
            demo.load(fn=inject_assets, inputs=None, outputs=[html_injector]).then(
                fn=initial_flyout_setup,
                inputs=[sampler_dd, model_dd],
                outputs=[sampler_ear_btn, model_ear_btn],
            ).then(
                fn=None,
                inputs=None,
                outputs=None,
                js="() => { setTimeout(reparent_flyout, 200); }",
            )

if __name__ == "__main__":
    demo.launch()

PropertySheet

Initialization

name type default description
value
typing.Optional[typing.Any][Any, None]
None The initial dataclass instance to render.
label
str | None
None The main label for the component, displayed in the accordion header.
root_label
str
"General" The label for the root group of properties.
show_group_name_only_one
bool
True If True, only the group name is shown when there is a single group.
root_properties_first
bool
True If True (default), root-level properties are rendered before nested groups. If False, they are rendered after.
disable_accordion
bool
False If True, disables the accordion functionality.
visible
bool
True If False, the component will be hidden.
open
bool
True If False, the accordion will be collapsed by default.
elem_id
str | None
None An optional string that is assigned as the id of this component in the DOM.
scale
int | None
None The relative size of the component in its container.
width
int | str | None
None The width of the component in pixels.
height
int | str | None
None The maximum height of the component's content area in pixels before scrolling.
min_width
int | None
None The minimum width of the component in pixels.
container
bool
True If True, wraps the component in a container with a background.
elem_classes
list[str] | str | None
None An optional list of strings that are assigned as the classes of this component in the DOM.

Events

name description
change Triggered when the value of the PropertySheet changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See .input() for a listener that is only triggered by user input.
input This listener is triggered when the user changes the value of the PropertySheet.
expand This listener is triggered when the PropertySheet is expanded.
collapse This listener is triggered when the PropertySheet is collapsed.
undo This listener is triggered when the user clicks the undo button in component.

User function

The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).

  • When used as an Input, the component only impacts the input signature of the user function.
  • When used as an output, the component only impacts the return signature of the user function.

The code snippet below is accurate in cases where the component is used as both an input and an output.

  • As output: Is passed, a new, updated instance of the dataclass.
  • As input: Should return, the dataclass instance to process.
def predict(
    value: Any
) -> Any:
    return value

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gradio_propertysheet-0.0.10.tar.gz (180.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gradio_propertysheet-0.0.10-py3-none-any.whl (106.1 kB view details)

Uploaded Python 3

File details

Details for the file gradio_propertysheet-0.0.10.tar.gz.

File metadata

  • Download URL: gradio_propertysheet-0.0.10.tar.gz
  • Upload date:
  • Size: 180.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.10.11

File hashes

Hashes for gradio_propertysheet-0.0.10.tar.gz
Algorithm Hash digest
SHA256 e85c0ff978f105586af2468bd11d3e3f00333633d5b4aacb4896c61d8d015c84
MD5 9ea5e974592fccc48d0fc7ee4266fb76
BLAKE2b-256 d2529be7ead4114655bd51141acb5f7e280ed42dc9dfb7ef27fce4317643a70c

See more details on using hashes here.

File details

Details for the file gradio_propertysheet-0.0.10-py3-none-any.whl.

File metadata

File hashes

Hashes for gradio_propertysheet-0.0.10-py3-none-any.whl
Algorithm Hash digest
SHA256 232e7c954268a836b003352ca7eda43850ba1af26309e50fdbb79bc0e6f3c612
MD5 43587e324d96295d0b25d85b99ef2d0a
BLAKE2b-256 2913e02c9cb891daee68f727c502dc5456791f7733349a1297aea354efebffc4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page