exteraGram

Plugin Settings

Learn how to build settings pages, sub-pages, and custom setting items for your plugin.

You can build a plugin settings screen by implementing create_settings(self) -> List[Any] on your BasePlugin.

The method should return a list of dataclass instances from ui.settings.

General Example

This example intentionally uses almost every built-in setting type so you can see how the pieces fit together.

from typing import Any, List
 
from android.view import Gravity, View
from android.widget import LinearLayout
 
from org.telegram.messenger import AndroidUtilities, MediaDataController, UserConfig
from org.telegram.ui.Components import BackupImageView, LayoutHelper, UItem
 
from base_plugin import BasePlugin
from ui.settings import (
    Custom,
    Divider,
    EditText,
    Header,
    Input,
    Selector,
    SimpleSettingFactory,
    Switch,
    Text,
)
 
 
def create_sticker_view(context, list_view, current_account: int, class_guid: int, resources_provider):
    main_layout = LinearLayout(context)
    main_layout.setOrientation(LinearLayout.VERTICAL)
    main_layout.setGravity(Gravity.CENTER)
    main_layout.setPadding(
        AndroidUtilities.dp(20),
        AndroidUtilities.dp(20),
        AndroidUtilities.dp(20),
        AndroidUtilities.dp(20),
    )
 
    image_view = BackupImageView(context)
    image_view.setRoundRadius(AndroidUtilities.dp(45))
 
    # The view is created here once. Do not bind item-specific data here yet.
    main_layout.addView(image_view, LayoutHelper.createLinear(130, 130, Gravity.CENTER, 0, 0, 0, 16))
    return main_layout
 
 
def bind_sticker_view(view, item, divider: bool, adapter, list_view):
    sticker_set_name = "CactusPlugins"
    sticker_index = 0
 
    # This runs when the view should display actual content.
    MediaDataController.getInstance(UserConfig.selectedAccount).setPlaceholderImageByIndex(
        view.getChildAt(0),
        sticker_set_name,
        sticker_index,
        "130_130",
    )
 
 
StickerSettingFactory = SimpleSettingFactory(
    create_sticker_view,
    bind_sticker_view,
    is_clickable=False,
    is_shadow=False,
)
 
 
class MyPlugin(BasePlugin):
    def _on_test_switch_change(self, new_value: bool):
        self.log(f"Test switch changed to: {new_value}")
 
    def _on_test_input_change(self, new_value: str):
        self.log(f"Test input changed to: {new_value}")
 
    def _on_test_selector_change(self, new_index: int):
        self.log(f"Test selector changed to index: {new_index}")
 
    def _on_text_click(self, view: View):
        self.log("Text item clicked!")
 
    def _create_sub_page(self) -> List[Any]:
        return [
            Header(text="This is a Sub-Page"),
            Text(text="You can nest settings pages."),
        ]
 
    def create_settings(self) -> List[Any]:
        return [
            Header(text="General Settings"),
            Switch(
                key="test_switch_key",
                text="Test Switch",
                default=True,
                subtext="This is a sample switch control.",
                icon="msg_settings",
                on_change=self._on_test_switch_change,
                link_alias="test_switch",
            ),
            Selector(
                key="test_selector_key",
                text="Test Selector",
                default=1,
                items=["Option A", "Option B", "Option C"],
                icon="msg_list",
                on_change=self._on_test_selector_change,
            ),
            Input(
                key="test_input_key",
                text="Test Input",
                default="Hello, World!",
                subtext="A simple single-line text input.",
                icon="msg_text",
                on_change=self._on_test_input_change,
            ),
            EditText(
                key="multiline_key",
                hint="Enter multiple lines of text here...",
                default="",
                multiline=True,
                max_length=1000,
            ),
            Divider(text="This is a divider with text."),
            Text(
                text="Click for Sub-Page",
                icon="msg_arrow_forward",
                on_click=self._on_text_click,
                create_sub_fragment=self._create_sub_page,
                link_alias="sub_page_link",
            ),
            Text(
                text="This is red text",
                icon="msg_error",
                red=True,
            ),
            Divider(),
            Header(text="Custom Controls"),
            Custom(factory=StickerSettingFactory.instance.java),
            Custom(item=UItem.asShadow("This is a custom UItem shadow.")),
        ]

Accessing and Modifying Settings

Use the helper methods from BasePlugin.

# Get the value of "test_switch_key", defaulting to False if it was never saved.
is_enabled = self.get_setting("test_switch_key", False)

To save a value:

# Example: toggle a boolean setting.
current_value = self.get_setting("test_switch_key", False)
self.set_setting("test_switch_key", not current_value)
 
# If changing one setting should rebuild the whole page,
# ask the plugin controller to reload the settings screen.
self.set_setting("main_option", "A", reload_settings=True)

You can also export or import all settings for the current plugin:

# Export all settings for backup or debugging.
all_my_settings = self.export_settings()
self.log(f"My settings: {all_my_settings}")
 
# Import a whole dictionary of settings.
new_settings = {
    "test_switch_key": False,
    "test_input_key": "New Value",
}
 
# By default the settings UI is reloaded after import.
self.import_settings(new_settings)
 
# You can skip reload when doing batch work.
self.import_settings(new_settings, reload_settings=False)

Built-in Setting Types

The SDK currently exposes these public dataclasses from ui.settings:

  • Header
  • Divider
  • Switch
  • Selector
  • Input
  • Text
  • EditText
  • Custom

Supported Controls

ControlkeytextdefaultOther Important Parameters
Header-Required-text: section title
Divider---text: optional caption shown on the divider
SwitchRequiredRequiredRequired (bool)subtext, icon, on_change, on_long_click, link_alias
SelectorRequiredRequiredRequired (int)items, icon, on_change, on_long_click, link_alias
InputRequiredRequiredoptional strsubtext, icon, on_change, on_long_click, link_alias
Text-Required-subtext, icon, accent, red, on_click, on_long_click, create_sub_fragment, link_alias
EditTextRequired-optional strhint (required), multiline, max_length, mask, on_change
Custom---item, view, factory, factory_args, on_click, on_long_click, create_sub_fragment, link_alias

Parameter Details

ParameterTypeDescription
keystrUnique key for persisted settings. Used with get_setting() and set_setting().
textstrMain visible text for the setting row.
defaultAnyDefault value used if the setting was never saved.
subtextstrOptional second line of text.
iconstrOptional drawable name such as "msg_settings".
on_changeCallableRuns when the value changes. Signature depends on control type.
on_clickCallableRuns when the row is clicked. Usually receives the clicked View.
on_long_clickCallableRuns on long press. Usually receives the clicked View.
link_aliasstrEnables a copyable deeplink alias for the setting row.
itemsList[str]Selector option labels.
create_sub_fragmentCallable[[], List[Any]]Builds a nested settings page when the item is tapped.
accentboolApplies accent coloring to Text.
redboolApplies red coloring to Text.
hintstrPlaceholder text for EditText.
multilineboolAllows multiple lines in EditText.
max_lengthintMaximum allowed input length.
maskstrRegex-like input restriction for EditText.
factoryCustomSetting.FactoryJava factory used for advanced custom items.
itemUItemPrebuilt Telegram UItem instance.
viewViewPlain Android View instance.

Custom Settings

Custom is the flexible entry point for settings rows which do not fit the built-in controls.

There are three main ways to use it.

1. Provide an existing UItem

This is the lightest solution when Telegram already has a suitable row type.

from org.telegram.ui.Components import UItem
from ui.settings import Custom
 
 
setting = Custom(
    item=UItem.asShadow("This is a custom UItem shadow.")
)

2. Provide a ready Android View

This is useful for static or already-constructed views.

from android.widget import TextView
from ui.settings import Custom
 
 
label = TextView(context)
label.setText("Hello from a custom View")
 
setting = Custom(view=label)

This form is usually best for simple one-off views. If the row needs recycling, binding, click handling, or per-item behavior, prefer a factory.

3. Use SimpleSettingFactory

SimpleSettingFactory is the recommended high-level API for custom settings.

It internally creates a Java CustomSetting.Factory, handles binding, and exposes Python callbacks for the parts you usually want to customize.

from android.view import Gravity
from android.widget import LinearLayout, TextView
 
from org.telegram.messenger import AndroidUtilities
from org.telegram.ui.Components import LayoutHelper
 
from ui.bulletin import BulletinHelper
from ui.settings import Custom, SimpleSettingFactory
 
 
def create_view(context, list_view, current_account: int, class_guid: int, resources_provider):
    main_layout = LinearLayout(context)
    main_layout.setOrientation(LinearLayout.VERTICAL)
    main_layout.setGravity(Gravity.CENTER)
    main_layout.setPadding(
        AndroidUtilities.dp(20),
        AndroidUtilities.dp(20),
        AndroidUtilities.dp(20),
        AndroidUtilities.dp(20),
    )
 
    # Build the row skeleton once.
    title = TextView(context)
    title.setText("Tap me")
    main_layout.addView(title, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT))
    return main_layout
 
 
def bind_view(view, item, divider: bool, adapter, list_view):
    # Bind current data here.
    title = view.getChildAt(0)
    title.setText("Tap me")
 
 
def on_click(plugin, item, view):
    BulletinHelper.show_info(f"Hello from {plugin.getName()}!")
 
 
InteractiveRowFactory = SimpleSettingFactory(
    create_view,
    bind_view,
    is_clickable=True,
    on_click=on_click,
)
 
 
setting = Custom(factory=InteractiveRowFactory.instance.java)

Factory callbacks

SimpleSettingFactory(...) supports these callbacks:

  • create_view(context, list_view, current_account, class_guid, resources_provider) -> View
  • bind_view(view, item, divider, adapter, list_view) -> None
  • create_item(plugin, setting, args) -> UItem
  • on_click(plugin, item, view) -> None
  • on_long_click(plugin, item, view) -> bool
  • attached_view(list_view, view, item) -> None
  • equals(a, b) -> bool
  • content_equals(a, b) -> bool

Constructor options:

SimpleSettingFactory(
    create_view,
    bind_view,
    is_clickable=False,
    is_shadow=False,
    create_item=None,
    on_click=None,
    on_long_click=None,
    attached_view=None,
    equals=None,
    content_equals=None,
)

A few important notes:

  • create_view(...) builds the row view itself.
  • bind_view(...) fills the current data into that view.
  • is_clickable and is_shadow are optimized Java-side flags used in hot paths.
  • if create_item is omitted, the factory creates a default UItem for you
  • if equals or content_equals are omitted, Telegram's default UItem comparison logic is used

Using SimpleSettingFactory as a callable

SimpleSettingFactory also implements __call__, so you can use it as a small builder for Custom(...).

StickerFactory = SimpleSettingFactory(create_sticker_view, bind_sticker_view)
 
 
setting = StickerFactory(link_alias="sticker_preview")

You can also pass positional arguments:

setting = StickerFactory("CactusPlugins", 0)

Those values are stored in factory_args for the created custom row and can be used by advanced factory logic.

Advanced Custom Factories

If SimpleSettingFactory is not enough, you can generate your own CustomSetting.Factory subclass through the class proxy system.

That route is useful when you need:

  • a real Java subclass
  • custom Java fields
  • super() support
  • overload-specific behavior
  • MVEL-backed hot-path methods

For that API, see Class Proxy.