"""
Provides a user interface object that can render to an Qt host.
"""
from __future__ import annotations

# standard libraries
import abc
import asyncio
import collections
import concurrent.futures
import copy
import dataclasses
import enum
import functools
import logging
import numbers
import operator
import pathlib
import threading
import typing
import weakref

# third party libraries
import numpy

# local libraries
from nion.ui import Bitmap
from nion.ui import CanvasItem
from nion.ui import DrawingContext
from nion.utils import Binding
from nion.utils import Converter
from nion.utils import Geometry
from nion.utils import Model

if typing.TYPE_CHECKING:
    from nion.ui import Application
    from nion.ui import Window as WindowModule


def notnone(s: typing.Any) -> str:
    return str(s) if s is not None else str()


FontMetrics = collections.namedtuple("FontMetrics", ["width", "height", "ascent", "descent", "leading"])

MeasureTextFn = typing.Callable[[str, str], FontMetrics]

MenuItemState = collections.namedtuple("MenuItemState", ["title", "enabled", "checked"])


class KeyboardModifiers(typing.Protocol):

    def __str__(self) -> str:
        return "shift:{} control:{} alt:{} option:{} meta:{}".format(self.shift, self.control, self.alt, self.option, self.meta)

    @property
    def any_modifier(self) -> bool:
        return self.shift or self.control or self.alt or self.meta

    # shift
    @property
    @abc.abstractmethod
    def shift(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def only_shift(self) -> bool:
        ...

    # control (command key on mac)
    @property
    @abc.abstractmethod
    def control(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def only_control(self) -> bool:
        ...

    # alt (option key on mac)
    @property
    @abc.abstractmethod
    def alt(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def only_alt(self) -> bool:
        ...

    # option (alt key on windows)
    @property
    @abc.abstractmethod
    def option(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def only_option(self) -> bool:
        ...

    # meta (control key on mac)
    @property
    @abc.abstractmethod
    def meta(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def only_meta(self) -> bool:
        ...

    # control key (all platforms)
    @property
    @abc.abstractmethod
    def native_control(self) -> bool:
        ...

    # keypad
    @property
    @abc.abstractmethod
    def keypad(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def only_keypad(self) -> bool:
        ...


class Key(typing.Protocol):

    @property
    @abc.abstractmethod
    def text(self) -> str:
        ...

    @property
    @abc.abstractmethod
    def key(self) -> int:
        ...

    @property
    @abc.abstractmethod
    def modifiers(self) -> KeyboardModifiers:
        ...

    @property
    @abc.abstractmethod
    def is_delete(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_enter_or_return(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_escape(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_tab(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_insert(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_home(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_end(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_move_to_start_of_line(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_move_to_end_of_line(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_delete_to_end_of_line(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_arrow(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_left_arrow(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_up_arrow(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_right_arrow(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_down_arrow(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_page_up(self) -> bool:
        ...

    @property
    @abc.abstractmethod
    def is_page_down(self) -> bool:
        ...

    @property
    def key_sequence_str(self) -> str:
        # the composite key strokes are not allowed to have modifiers.
        # these are move_to_start_of_line, move_to_end_of_line, delete_to_end_of_line
        # also, they take precedence and are checked first.
        if self.is_move_to_start_of_line:
            return "move_to_start_of_line"
        elif self.is_move_to_end_of_line:
            return "move_to_end_of_line"
        elif self.is_delete_to_end_of_line:
            return "delete_to_end_of_line"

        # check the named keys
        if self.is_delete:
            ks = "delete"
        elif self.is_enter_or_return:
            ks = "enter"
        elif self.is_escape:
            ks = "esc"
        elif self.is_tab:
            ks = "tab"
        elif self.is_insert:
            ks = "insert"
        elif self.is_home:
            ks = "home"
        elif self.is_end:
            ks = "end"
        elif self.is_left_arrow:
            ks = "left_arrow"
        elif self.is_up_arrow:
            ks = "up_arrow"
        elif self.is_right_arrow:
            ks = "right_arrow"
        elif self.is_down_arrow:
            ks = "down_arrow"
        elif self.is_page_up:
            ks = "page_up"
        elif self.is_page_down:
            ks = "page_down"
        else:
            ks = self.text if self.text else "none"

        # add the modifiers. order is Ctrl+Alt+Shift+<key>
        if self.modifiers.shift:
            ks = "Shift+" + ks
        if self.modifiers.alt:
            ks = "Alt+" + ks
        if self.modifiers.control:
            ks = "Ctrl+" + ks

        return ks


class KeySequenceMatch(enum.Enum):
    NONE = 0
    PARTIAL = 1
    EXACT = 2


class KeySequence:
    def __init__(self, key_sequence_str: str):
        self.key_sequence_str = key_sequence_str

    def matches(self, key: Key) -> KeySequenceMatch:
        if key.key_sequence_str == self.key_sequence_str:
            return KeySequenceMatch.EXACT
        else:
            return KeySequenceMatch.NONE


class MimeData(abc.ABC):

    @property
    @abc.abstractmethod
    def formats(self) -> typing.Sequence[str]:
        ...

    def has_format(self, format: str) -> bool:
        return format in self.formats

    @property
    def has_urls(self) -> bool:
        return "text/uri-list" in self.formats

    @property
    def has_file_paths(self) -> bool:
        return "text/uri-list" in self.formats

    @property
    def urls(self) -> typing.Sequence[str]:
        raw_urls = self.data_as_string("text/uri-list")
        return raw_urls.splitlines() if raw_urls and len(raw_urls) > 0 else []

    @property
    @abc.abstractmethod
    def file_paths(self) -> typing.Sequence[str]:
        ...

    @abc.abstractmethod
    def data_as_string(self, format: str) -> str:
        ...

    @abc.abstractmethod
    def set_data_as_string(self, format: str, text: str) -> None:
        ...


@dataclasses.dataclass
class _TaskAndFuture:
    task: typing.Optional[asyncio.Task[None]] = None
    future: typing.Optional[concurrent.futures.Future[None]] = None
    lock = threading.RLock()

    def cancel(self) -> None:
        with self.lock:
            if self.task:
                self.task.cancel()
                self.task = None
            if self.future:
                self.future.cancel()
                self.future = None

    def clear(self) -> None:
        with self.lock:
            self.task = None
            self.future = None


T = typing.TypeVar('T')

class BindablePropertyHelper(typing.Generic[T]):
    def __init__(self,
                 value_getter: typing.Optional[typing.Callable[[], T]],
                 value_setter: typing.Callable[[T], None],
                 value_validator: typing.Optional[typing.Callable[[T, T], T]] = None,
                 value_cmp: typing.Optional[typing.Callable[[T, T], bool]] = None) -> None:
        self.__value_initialized = False
        self.__value = typing.cast(T, None)
        self.__pending_value = typing.cast(T, None)
        self.__value_getter = value_getter
        self.__value_setter = value_setter
        self.__value_validator = value_validator if callable(value_validator) else lambda x, y: x
        self.__value_cmp = value_cmp if callable(value_cmp) else typing.cast(typing.Callable[[T, T], bool], operator.eq)
        self.__binding: typing.Optional[Binding.Binding] = None

        # in order that we can shut down properly in finalize without holding any reference to self, store
        # both task and future in a single object that can be passed to finalize and will always hold the latest
        # task and future. the lock will ensure everything shuts down cleanly.
        self.__task_and_future = _TaskAndFuture()

        def finalize(task_and_future: _TaskAndFuture) -> None:
            task_and_future.cancel()

        weakref.finalize(self, finalize, self.__task_and_future)

    def close(self) -> None:
        self.__task_and_future.cancel()

    @property
    def value(self) -> T:
        return self.__value_getter() if callable(self.__value_getter) else self.__value

    @value.setter
    def value(self, value: T) -> None:
        # when the high level user interface element changes programmatically, this method
        # should be called with the new value. it updates the low level and the binding.
        validated_value = self.__value_validator(value, self.__value)
        if not self.__value_initialized or not self.__value_cmp(validated_value, self.__value):
            self.__value_initialized = True
            self.__value = validated_value
            self.__value_setter(validated_value)
            if self.__binding:
                self.__binding.update_source(validated_value)

    def set_value(self, value: T) -> None:
        # when the binding source changes, this method should be called with the new value.
        # it updates the low level, but does not update the binding.
        validated_value = self.__value_validator(value, self.__value)
        if not self.__value_initialized or not self.__value_cmp(validated_value, self.__value):
            self.__value_initialized = True
            self.__value = validated_value
            self.__value_setter(validated_value)

    def value_changed(self, value: T) -> None:
        # when the target value changes due to a user action, this method
        # should be called with the new value. it updates the binding, but does
        # not update the low level.
        validated_value = self.__value_validator(value, self.__value)
        self.__value_initialized = True
        self.__value = validated_value
        if self.__binding:
            self.__binding.update_source(validated_value)

    # bind to value. takes ownership of binding.
    def bind_value(self, binding: Binding.Binding) -> None:
        # close the old binding
        if self.__binding:
            self.__binding.close()
            self.__binding = None

        # grab the initial value from the binding. use str method to convert value to text.
        self.value = typing.cast(T, binding.get_target_value())

        # save the binding and configure the target setter
        # which will set the text when the binding changes
        self.__binding = binding

        async def update_value_inner(bph: typing.Any) -> None:
            # this will always be called on the main thread. avoid holding any references to self so that
            # dealloc works naturally.
            try:
                self_ = typing.cast(typing.Optional[BindablePropertyHelper[T]], bph())
                if self_:
                    try:
                        self_.set_value(self_.__pending_value)
                    finally:
                        self_.__task_and_future.clear()
            except Exception as e:
                import traceback
                traceback.print_exc()
                logging.debug(e)

        def update_value(bph: typing.Any, event_loop: asyncio.AbstractEventLoop, thread: threading.Thread, value: T) -> None:
            # threadsafe. target setter may be called from a thread.
            # to avoid repeated cancel/new-task situations that starve the execution during tests,
            # update the pending value and only create a new task if required.
            self_ = typing.cast(typing.Optional[BindablePropertyHelper[T]], bph())
            if self_:
                self_.__pending_value = value
                if threading.current_thread() != thread:
                    with self_.__task_and_future.lock:
                        if not self_.__task_and_future.future:
                            self_.__task_and_future.future = asyncio.run_coroutine_threadsafe(update_value_inner(bph), event_loop)
                else:
                    self_.set_value(self_.__pending_value)

        # the target setter may come in on a thread. call update_value, which is threadsafe. then either dispatch
        # a future (different thread), a task (same thread), or just update the pending value (pending update).
        # this ensures the value is updated in update_value_inner in all cases. be careful to use locks for anything
        # that may have thread contention.
        self.__binding.target_setter = functools.partial(update_value, weakref.ref(self), asyncio.get_event_loop_policy().get_event_loop(), threading.current_thread())

    def unbind_value(self) -> None:
        if self.__binding:
            self.__binding.close()
            self.__binding = None


class WidgetBehavior(typing.Protocol):
    # note: behaviors are generally responsible for closing widgets that get added to them.
    # this is so behaviors which insert added widgets into another widget get closed in a
    # predictable manner (i.e. when the enclosing widget gets closed).

    on_ui_activity: typing.Optional[typing.Callable[[], None]]
    on_context_menu_event: typing.Optional[typing.Callable[[int, int, int, int], bool]]
    on_focus_changed: typing.Optional[typing.Callable[[bool], None]]

    @property
    def focused(self) -> bool: raise NotImplementedError()

    @focused.setter
    def focused(self, value: bool) -> None: ...

    @property
    def does_retain_focus(self) -> bool:  raise NotImplementedError()

    @does_retain_focus.setter
    def does_retain_focus(self, value: bool) -> None: ...

    @property
    def visible(self) -> bool:  raise NotImplementedError()

    @visible.setter
    def visible(self, value: bool) -> None: ...

    @property
    def enabled(self) -> bool:  raise NotImplementedError()

    @enabled.setter
    def enabled(self, value: bool) -> None: ...

    @property
    def size(self) -> Geometry.IntSize: raise NotImplementedError()

    @size.setter
    def size(self, value: Geometry.IntSize) -> None: ...

    @property
    def tool_tip(self) -> typing.Optional[str]: raise NotImplementedError()

    @tool_tip.setter
    def tool_tip(self, value: typing.Optional[str]) -> None: ...

    # low level UI specific widget
    @property
    def widget(self) -> typing.Any: raise NotImplementedError()

    def close(self) -> None: ...
    def periodic(self) -> None: pass
    def _set_root_container(self, window: typing.Optional[Window]) -> None: ...
    def _get_content_widget(self) -> typing.Optional[Widget]: return None
    def set_property(self, key: str, value: typing.Any) -> None: ...
    def map_to_global(self, p: Geometry.IntPoint) -> Geometry.IntPoint: ...
    def drag(self, mime_data: MimeData, thumbnail: typing.Optional[Bitmap.Bitmap] = None,
             hot_spot_x: typing.Optional[int] = None, hot_spot_y: typing.Optional[int] = None,
             drag_finished_fn: typing.Optional[typing.Callable[[str], None]] = None) -> None: ...
    def set_background_color(self, value: typing.Optional[typing.Union[str, DrawingContext.LinearGradient]]) -> None: ...
    def set_border_color(self, value: typing.Optional[str]) -> None: ...


class Widget:

    def __init__(self, widget_behavior: WidgetBehavior) -> None:
        self.__behavior = widget_behavior
        self.__behavior.on_ui_activity = self._register_ui_activity
        self.__root_container: typing.Optional[Window] = None  # the document window
        self.__pending_keyed_tasks: typing.List[typing.Tuple[str, typing.Callable[[], None]]] = list()
        self.__pending_queued_tasks: typing.List[typing.Callable[[], None]] = list()
        self.on_context_menu_event: typing.Optional[typing.Callable[[int, int, int, int], bool]] = None
        self.on_focus_changed: typing.Optional[typing.Callable[[bool], None]] = None
        self.widget_id: typing.Optional[str] = None

        def handle_context_menu_event(x: int, y: int, gx: int, gy: int) -> bool:
            if callable(self.on_context_menu_event):
                return self.on_context_menu_event(x, y, gx, gy)
            return False

        def handle_focus_changed(focused: bool) -> None:
            if callable(self.on_focus_changed):
                self.on_focus_changed(focused)

        self._behavior.on_context_menu_event = handle_context_menu_event
        self._behavior.on_focus_changed = handle_focus_changed

        def set_visible(value: bool) -> None:
            self._behavior.visible = value

        def set_enabled(value: bool) -> None:
            self._behavior.enabled = value

        def set_tool_tip(value: typing.Optional[str]) -> None:
            self._behavior.tool_tip = value

        def set_background_color(value: typing.Optional[typing.Union[str, DrawingContext.LinearGradient]]) -> None:
            self._behavior.set_background_color(value)

        def set_border_color(value: typing.Optional[str]) -> None:
            self._behavior.set_border_color(value)

        self.__visible_binding_helper = BindablePropertyHelper[bool](None, set_visible)
        self.__enabled_binding_helper = BindablePropertyHelper[bool](None, set_enabled)
        self.__tool_tip_binding_helper = BindablePropertyHelper[typing.Optional[str]](None, set_tool_tip)
        self.__background_color_binding_helper = BindablePropertyHelper[typing.Optional[typing.Union[str, DrawingContext.LinearGradient]]](None, set_background_color)
        self.__border_color_binding_helper = BindablePropertyHelper[typing.Optional[str]](None, set_border_color)

        self.visible = True
        self.enabled = True
        self.tool_tip = None

    def close(self) -> None:
        self.__visible_binding_helper.close()
        self.__visible_binding_helper = typing.cast(typing.Any, None)
        self.__enabled_binding_helper.close()
        self.__enabled_binding_helper = typing.cast(typing.Any, None)
        self.__tool_tip_binding_helper.close()
        self.__tool_tip_binding_helper = typing.cast(typing.Any, None)
        self.__background_color_binding_helper.close()
        self.__background_color_binding_helper = typing.cast(typing.Any, None)
        self.__border_color_binding_helper.close()
        self.__border_color_binding_helper = typing.cast(typing.Any, None)
        content_widget = self._behavior._get_content_widget()
        if content_widget:
            content_widget.close()
        self.__behavior.close()
        self.__behavior = typing.cast(typing.Any, None)
        self.on_context_menu_event = None
        self.on_focus_changed = None
        self.__root_container = None

    @property
    def _behavior(self) -> WidgetBehavior:
        return self.__behavior

    @property
    def root_container(self) -> typing.Optional[Window]:
        return self.__root_container

    def _set_root_container(self, root_container: typing.Optional[Window]) -> None:
        self.__root_container = root_container
        self._behavior._set_root_container(root_container)
        if self.__root_container:
            pending_keyed_tasks = self.__pending_keyed_tasks
            self.__pending_keyed_tasks = list()
            for key, task in pending_keyed_tasks:
                self.add_task(key, task)
            pending_queued_tasks = self.__pending_queued_tasks
            self.__pending_queued_tasks = list()
            for task in pending_queued_tasks:
                self.queue_task(task)

    def _register_ui_activity(self) -> None:
        if self.__root_container:
            self.__root_container._register_ui_activity()

    @property
    def _contained_widgets(self) -> typing.List[Widget]:
        content_widget = self._behavior._get_content_widget()
        return [content_widget] if content_widget else list()

    @property
    def content_widget(self) -> Widget:
        # this is a compromise method for backwards compatibility for code expecting the widgets to be
        # based on CompositeWidgetBase.
        content_widget = self._behavior._get_content_widget()
        assert content_widget
        return content_widget

    def find_widget_by_id(self, widget_id: str) -> typing.Optional[Widget]:
        if self.widget_id == widget_id:
            return self
        for contained_widget in self._contained_widgets:
            found_widget = contained_widget.find_widget_by_id(widget_id)
            if found_widget:
                return found_widget
        return None

    # not thread safe
    def periodic(self) -> None:
        self._behavior.periodic()

    def redraw(self) -> None:
        # forces a draw of the widget in response to dpi change. this is not thread safe.
        # calls all contained widgets to redraw themselves (bottom-up).
        # then calls _redraw, which is this widget specific method.
        for contained_widget in self._contained_widgets:
            contained_widget.redraw()
        self._redraw()

    def _redraw(self) -> None:
        # redraw this particular widget. default is to do nothing since the normal container based widgets will
        # redraw themselves automatically. this will typically be implemented by custom widgets like canvas.
        pass

    def run_pending_keyed_tasks(self) -> None:
        # used for testing
        pending_keyed_tasks = copy.copy(self.__pending_keyed_tasks)
        self.__pending_keyed_tasks.clear()
        for key, task in pending_keyed_tasks:
            if callable(task):
                task()

    @property
    def pending_keyed_tasks(self) -> typing.List[typing.Tuple[str, typing.Callable[[], None]]]:
        # used for testing
        return copy.copy(self.__pending_keyed_tasks)

    @property
    def pending_queued_tasks(self) -> typing.List[typing.Callable[[], None]]:
        # used for testing
        return copy.copy(self.__pending_queued_tasks)

    # thread safe
    # tasks are run periodically. if another task causes a widget to close,
    # the outstanding task may try to use a closed widget. any methods called
    # in a task need to verify that the widget is not yet closed. this can be
    # mitigated in several ways: 1) clear the task if possible; 2) do not queue
    # the task if widget is already closed; 3) check during task to make sure
    # widget was not already closed.
    def add_task(self, key: str, task: typing.Callable[[], None]) -> None:
        root_container = self.root_container
        if root_container:
            root_container.add_task(key + str(id(self)), task)
        else:
            self.__pending_keyed_tasks.append((key, task))

    # thread safe
    def clear_task(self, key: str) -> None:
        root_container = self.root_container
        if root_container:
            root_container.clear_task(key + str(id(self)))
        else:
            pending_keyed_tasks = copy.copy(self.__pending_keyed_tasks)
            self.__pending_keyed_tasks = list()
            for pending_key, task in pending_keyed_tasks:
                if key != pending_key:
                    self.__pending_keyed_tasks.append((pending_key, task))

    # thread safe
    def queue_task(self, task: typing.Callable[[], None]) -> None:
        root_container = self.root_container
        if root_container:
            root_container.queue_task(task)
        else:
            self.__pending_queued_tasks.append(task)

    def clear_queued_tasks(self) -> None:
        root_container = self.root_container
        if root_container:
            root_container.clear_queued_tasks()
        else:
            self.__pending_queued_tasks.clear()

    @property
    def focused(self) -> bool:
        return self._behavior.focused

    @focused.setter
    def focused(self, focused: bool) -> None:
        self._behavior.focused = focused

    def refocus(self) -> None:
        pass

    def request_refocus(self) -> None:
        root_container = self.root_container
        if root_container:
            root_container.refocus_widget(self)

    @property
    def does_retain_focus(self) -> bool:
        return self._behavior.does_retain_focus

    @does_retain_focus.setter
    def does_retain_focus(self, value: bool) -> None:
        self._behavior.does_retain_focus = value

    @property
    def visible(self) -> bool:
        return self.__visible_binding_helper.value

    @visible.setter
    def visible(self, visible: bool) -> None:
        self.__visible_binding_helper.value = visible

    @property
    def enabled(self) -> bool:
        return self.__enabled_binding_helper.value

    @enabled.setter
    def enabled(self, enabled: bool) -> None:
        self.__enabled_binding_helper.value = enabled

    @property
    def size(self) -> Geometry.IntSize:
        return self._behavior.size

    @size.setter
    def size(self, size: Geometry.IntSize) -> None:
        self._behavior.size = size

    @property
    def tool_tip(self) -> typing.Optional[str]:
        return self.__tool_tip_binding_helper.value

    @tool_tip.setter
    def tool_tip(self, tool_tip: typing.Optional[str]) -> None:
        self.__tool_tip_binding_helper.value = tool_tip

    @property
    def background_color(self) -> typing.Optional[typing.Union[str, DrawingContext.LinearGradient]]:
        return self.__background_color_binding_helper.value

    @background_color.setter
    def background_color(self, value: typing.Optional[typing.Union[str, DrawingContext.LinearGradient]]) -> None:
        self.__background_color_binding_helper.value = value

    @property
    def border_color(self) -> typing.Optional[str]:
        return self.__border_color_binding_helper.value

    @border_color.setter
    def border_color(self, value: typing.Optional[str]) -> None:
        self.__border_color_binding_helper.value = value

    def set_property(self, key: str, value: typing.Any) -> None:
        self._behavior.set_property(key, value)

    def drag(self, mime_data: MimeData, thumbnail: typing.Optional[Bitmap.BitmapOrArray] = None,
             hot_spot_x: typing.Optional[int] = None, hot_spot_y: typing.Optional[int] = None,
             drag_finished_fn: typing.Optional[typing.Callable[[str], None]] = None) -> None:
        self._behavior.drag(mime_data, Bitmap.promote_bitmap(thumbnail), hot_spot_x, hot_spot_y, drag_finished_fn)

    def map_to_global(self, p: Geometry.IntPoint) -> Geometry.IntPoint:
        return self._behavior.map_to_global(p)

    def _dispatch_any(self, method: str, *args: typing.Any, **kwargs: typing.Any) -> bool:
        if hasattr(self, method):
            return typing.cast(bool, getattr(self, method)(*args, **kwargs))
        return False

    def _can_dispatch_any(self, method: str) -> bool:
        return False

    def _get_menu_item_state(self, command_id: str) -> typing.Optional[MenuItemState]:
        handle_method = "handle_" + command_id
        menu_item_state_method = "get_" + command_id + "_menu_item_state"
        if hasattr(self, menu_item_state_method):
            menu_item_state = typing.cast(typing.Optional[MenuItemState], getattr(self, menu_item_state_method)())
            if menu_item_state:
                return menu_item_state
        if hasattr(self, handle_method):
            return MenuItemState(title=None, enabled=True, checked=False)
        return None

    def bind_enabled(self, binding: Binding.Binding) -> None:
        self.__enabled_binding_helper.bind_value(binding)

    def unbind_enabled(self) -> None:
        self.__enabled_binding_helper.unbind_value()

    def bind_visible(self, binding: Binding.Binding) -> None:
        self.__visible_binding_helper.bind_value(binding)

    def unbind_visible(self) -> None:
        self.__visible_binding_helper.unbind_value()

    def bind_tool_tip(self, binding: Binding.Binding) -> None:
        self.__tool_tip_binding_helper.bind_value(binding)

    def unbind_tool_tip(self) -> None:
        self.__tool_tip_binding_helper.unbind_value()

    def bind_background_color(self, binding: Binding.Binding) -> None:
        self.__background_color_binding_helper.bind_value(binding)

    def unbind_background_color(self) -> None:
        self.__background_color_binding_helper.unbind_value()

    def bind_border_color(self, binding: Binding.Binding) -> None:
        self.__border_color_binding_helper.bind_value(binding)

    def unbind_border_color(self) -> None:
        self.__border_color_binding_helper.unbind_value()


class BoxWidgetBehavior(WidgetBehavior, typing.Protocol):
    def insert(self, child: Widget, before: int, fill: bool, alignment: typing.Optional[str]) -> None: ...
    def remove(self, child: Widget) -> None: ...
    def remove_all(self) -> None: ...
    def add_stretch(self) -> Widget: ...
    def add_spacing(self, spacing: int) -> Widget: ...


class BoxWidget(Widget):

    def __init__(self, widget_behavior: BoxWidgetBehavior, alignment: typing.Optional[str]) -> None:
        super().__init__(widget_behavior)
        self.alignment = alignment
        self.children: typing.List[Widget] = []

    def close(self) -> None:
        for child in self.children:
            child.close()
        self.children = typing.cast(typing.Any, None)
        super().close()

    @property
    def _behavior(self) -> BoxWidgetBehavior:
        return typing.cast(BoxWidgetBehavior, super()._behavior)

    def _set_root_container(self, root_container: typing.Optional[Window]) -> None:
        super()._set_root_container(root_container)
        for child in self.children:
            child._set_root_container(root_container)

    @property
    def _contained_widgets(self) -> typing.List[Widget]:
        return super()._contained_widgets + copy.copy(self.children)

    def periodic(self) -> None:
        super().periodic()
        for child in self.children:
            child.periodic()

    @property
    def child_count(self) -> int:
        return len(self.children)

    def index(self, child: Widget) -> int:
        assert child in self.children
        return self.children.index(child)

    def insert(self, child: Widget, before: typing.Optional[typing.Union[Widget, int]], fill: bool = False, alignment: typing.Optional[str] = None) -> None:
        assert child
        if isinstance(before, numbers.Integral):
            index = before
        elif isinstance(before, Widget):
            index = self.index(before)
        else:
            index = self.child_count
        if alignment is None:
            alignment = self.alignment
        self.children.insert(index, child)
        child._set_root_container(self.root_container)
        self._behavior.insert(child, index, fill, alignment)

    def add(self, child: Widget, fill: bool = False, alignment: typing.Optional[str] = None) -> None:
        self.insert(child, None, fill, alignment)

    def remove(self, child: typing.Union[Widget, int]) -> None:
        child_widget = child if isinstance(child, Widget) else self.children[int(child)]
        child_widget._set_root_container(None)
        self.children.remove(child_widget)
        # closing the child should remove it from the layout but give the behavior a chance to remote it too
        self._behavior.remove(child_widget)
        child_widget.close()

    def remove_all(self) -> None:
        for child in reversed(copy.copy(self.children)):
            self.remove(child)
        self._behavior.remove_all()  # spacing and stretches for Qt

    def add_stretch(self) -> None:
        child = self._behavior.add_stretch()
        self.children.append(child)

    def add_spacing(self, spacing: int) -> None:
        child = self._behavior.add_spacing(spacing)
        self.children.append(child)


class SplitterWidgetBehavior(WidgetBehavior, typing.Protocol):
    orientation: typing.Optional[str]
    def add(self, child: Widget) -> None: ...
    def restore_state(self, tag: str) -> None: ...
    def save_state(self, tag: str) -> None: ...
    def set_sizes(self, sizes: typing.Sequence[int]) -> None: ...


class SplitterWidget(Widget):

    def __init__(self, widget_behavior: WidgetBehavior, orientation: typing.Optional[str]) -> None:
        super().__init__(widget_behavior)
        self.children: typing.List[Widget] = []
        self.orientation = orientation

    def close(self) -> None:
        # note: behavior is responsible for closing the children so that behavior can put children in another widget.
        self.children = typing.cast(typing.Any, None)
        super().close()

    @property
    def _behavior(self) -> SplitterWidgetBehavior:
        return typing.cast(SplitterWidgetBehavior, super()._behavior)

    def _set_root_container(self, root_container: typing.Optional[Window]) -> None:
        super()._set_root_container(root_container)
        for child in self.children:
            child._set_root_container(root_container)

    @property
    def _contained_widgets(self) -> typing.List[Widget]:
        return super()._contained_widgets + copy.copy(self.children)

    def periodic(self) -> None:
        super().periodic()
        for child in self.children:
            child.periodic()

    @property
    def orientation(self) -> typing.Optional[str]:
        return self._behavior.orientation

    @orientation.setter
    def orientation(self, value: typing.Optional[str]) -> None:
        self._behavior.orientation = value

    def add(self, child: Widget) -> None:
        self._behavior.add(child)
        self.children.append(child)
        child._set_root_container(self.root_container)

    def restore_state(self, tag: str) -> None:
        self._behavior.restore_state(tag)

    def save_state(self, tag: str) -> None:
        self._behavior.save_state(tag)

    def set_sizes(self, sizes: typing.Sequence[int]) -> None:
        self._behavior.set_sizes(sizes)


class TabWidgetBehavior(WidgetBehavior, typing.Protocol):
    current_index: int
    on_current_index_changed: typing.Optional[typing.Callable[[int], None]]

    def add(self, child: Widget, label: str) -> None: ...
    def restore_state(self, tag: str) -> None: ...
    def save_state(self, tag: str) -> None: ...


class TabWidget(Widget):

    def __init__(self, widget_behavior: TabWidgetBehavior) -> None:
        super().__init__(widget_behavior)
        self.children: typing.List[Widget] = []
        self.on_current_index_changed: typing.Optional[typing.Callable[[int], None]] = None

        def handle_current_index_changed(index: int) -> None:
            self.__current_index_binding_helper.value_changed(index)
            if callable(self.on_current_index_changed):
                self.on_current_index_changed(index)

        self._behavior.on_current_index_changed = handle_current_index_changed

        def set_current_index(value: int) -> None:
            self._behavior.current_index = value

        self.__current_index_binding_helper = BindablePropertyHelper[int](None, set_current_index)

        self.current_index = 0

    def close(self) -> None:
        # note: behavior is responsible for closing the children so that behavior can put children in another widget.
        self.children = typing.cast(typing.Any, None)
        self.__current_index_binding_helper.close()
        self.__current_index_binding_helper = typing.cast(typing.Any, None)
        self.on_current_index_changed = None
        super().close()

    @property
    def _behavior(self) -> TabWidgetBehavior:
        return typing.cast(TabWidgetBehavior, super()._behavior)

    def _set_root_container(self, root_container: typing.Optional[Window]) -> None:
        super()._set_root_container(root_container)
        for child in self.children:
            child._set_root_container(root_container)

    @property
    def _contained_widgets(self) -> typing.List[Widget]:
        return super()._contained_widgets + copy.copy(self.children)

    def periodic(self) -> None:
        super().periodic()
        for child in self.children:
            child.periodic()

    def add(self, child: Widget, label: str) -> None:
        self._behavior.add(child, label)
        self.children.append(child)
        child._set_root_container(self.root_container)

    def restore_state(self, tag: str) -> None:
        self._behavior.restore_state(tag)

    def save_state(self, tag: str) -> None:
        self._behavior.save_state(tag)

    @property
    def current_index(self) -> int:
        return self.__current_index_binding_helper.value

    @current_index.setter
    def current_index(self, index: int) -> None:
        self.__current_index_binding_helper.value = index

    def bind_current_index(self, binding: Binding.Binding) -> None:
        self.__current_index_binding_helper.bind_value(binding)

    def unbind_current_index(self) -> None:
        self.__current_index_binding_helper.unbind_value()


class StackWidgetBehavior(WidgetBehavior, typing.Protocol):
    current_index: int

    def insert(self, child: Widget, before: int) -> None: ...
    def remove(self, child: Widget) -> None: ...


class StackWidget(Widget):

    def __init__(self, widget_behavior: StackWidgetBehavior) -> None:
        super().__init__(widget_behavior)
        self.children: typing.List[Widget] = []

        def set_current_index(value: int) -> None:
            self._behavior.current_index = value

        self.__current_index_binding_helper = BindablePropertyHelper[int](None, set_current_index)

        self.current_index = 0

    def close(self) -> None:
        # note: behavior is responsible for closing the children so that behavior can put children in another widget.
        self.children = typing.cast(typing.Any, None)
        self.__current_index_binding_helper.close()
        self.__current_index_binding_helper = typing.cast(typing.Any, None)
        super().close()

    @property
    def _behavior(self) -> StackWidgetBehavior:
        return typing.cast(StackWidgetBehavior, super()._behavior)

    def _set_root_container(self, root_container: typing.Optional[Window]) -> None:
        super()._set_root_container(root_container)
        for child in self.children:
            child._set_root_container(root_container)

    @property
    def _contained_widgets(self) -> typing.List[Widget]:
        return super()._contained_widgets + copy.copy(self.children)

    def periodic(self) -> None:
        super().periodic()
        for child in self.children:
            child.periodic()

    @property
    def child_count(self) -> int:
        return len(self.children)

    def index(self, child: Widget) -> int:
        assert child in self.children
        return self.children.index(child)

    def insert(self, child: Widget, before: typing.Optional[typing.Union[Widget, int]]) -> None:
        if isinstance(before, numbers.Integral):
            index = before
        elif isinstance(before, Widget):
            index = self.index(before)
        else:
            index = self.child_count
        self._behavior.insert(child, index)
        self.children.insert(index, child)
        child._set_root_container(self.root_container)

    def add(self, child: Widget) -> None:
        self.insert(child, None)

    def remove(self, child: typing.Union[Widget, int]) -> None:
        child_widget = child if isinstance(child, Widget) else self.children[int(child)]
        child_widget._set_root_container(None)
        self.children.remove(child_widget)
        # note: behavior is responsible for closing the children so that behavior can put children in another widget.
        self._behavior.remove(child_widget)

    def remove_all(self) -> None:
        while len(self.children) > 0:
            self.remove(self.children[-1])

    @property
    def current_index(self) -> int:
        return self.__current_index_binding_helper.value

    @current_index.setter
    def current_index(self, index: int) -> None:
        self.__current_index_binding_helper.value = index

    def bind_current_index(self, binding: Binding.Binding) -> None:
        self.__current_index_binding_helper.bind_value(binding)

    def unbind_current_index(self) -> None:
        self.__current_index_binding_helper.unbind_value()


class GroupWidgetBehavior(WidgetBehavior, typing.Protocol):
    title: typing.Optional[str]

    def add(self, child: Widget) -> None: ...
    def remove(self, child: Widget) -> None: ...


class GroupWidget(Widget):

    def __init__(self, widget_behavior: GroupWidgetBehavior) -> None:
        super().__init__(widget_behavior)
        self.children: typing.List[Widget] = []
        self.__title: typing.Optional[str] = None

    def close(self) -> None:
        # note: behavior is responsible for closing the children so that behavior can put children in another widget.
        self.children = typing.cast(typing.Any, None)
        super().close()

    @property
    def _behavior(self) -> GroupWidgetBehavior:
        return typing.cast(GroupWidgetBehavior, super()._behavior)

    def _set_root_container(self, root_container: typing.Optional[Window]) -> None:
        super()._set_root_container(root_container)
        for child in self.children:
            child._set_root_container(root_container)

    @property
    def _contained_widgets(self) -> typing.List[Widget]:
        return super()._contained_widgets + copy.copy(self.children)

    def periodic(self) -> None:
        super().periodic()
        for child in self.children:
            child.periodic()

    def add(self, child: Widget) -> None:
        self._behavior.add(child)
        self.children.append(child)
        child._set_root_container(self.root_container)

    def remove(self, child: Widget) -> None:
        child._set_root_container(None)
        self.children.remove(child)
        # note: behavior is responsible for closing the children so that behavior can put children in another widget.
        self._behavior.remove(child)

    def remove_all(self) -> None:
        while len(self.children) > 0:
            self.remove(self.children[-1])

    @property
    def title(self) -> typing.Optional[str]:
        return self.__title

    @title.setter
    def title(self, value: typing.Optional[str]) -> None:
        self.__title = value
        self._behavior.title = value


class ScrollAreaWidgetBehavior(WidgetBehavior, typing.Protocol):
    on_size_changed: typing.Optional[typing.Callable[[int, int], None]]
    on_viewport_changed: typing.Optional[typing.Callable[[Geometry.RectIntTuple], None]]

    def set_content(self, content: typing.Optional[Widget]) -> None: ...
    def scroll_to(self, x: int, y: int) -> None: ...
    def set_scrollbar_policies(self, horizontal_policy: str, vertical_policy: str) -> None: ...
    def info(self) -> None: ...


class ScrollAreaWidget(Widget):

    def __init__(self, widget_behavior: ScrollAreaWidgetBehavior) -> None:
        super().__init__(widget_behavior)
        self.__content: typing.Optional[Widget] = None
        self.on_size_changed: typing.Optional[typing.Callable[[int, int], None]] = None
        self.on_viewport_changed: typing.Optional[typing.Callable[[Geometry.IntRect], None]] = None
        self.viewport: Geometry.IntRect = Geometry.IntRect.empty_rect()
        self.width = 0
        self.height = 0

        def handle_size_changed(width: int, height: int) -> None:
            self.width = width
            self.height = height
            if callable(self.on_size_changed):
                self.on_size_changed(width, height)

        self._behavior.on_size_changed = handle_size_changed

        def handle_viewport_changed(viewport: Geometry.RectIntTuple) -> None:
            self.viewport = Geometry.IntRect.make(viewport)
            if callable(self.on_viewport_changed):
                self.on_viewport_changed(self.viewport)

        self._behavior.on_viewport_changed = handle_viewport_changed

    def close(self) -> None:
        if self.__content:
            self.__content.close()
            self.__content = None
        self.on_size_changed = None
        self.on_viewport_changed = None
        super().close()

    @property
    def _behavior(self) -> ScrollAreaWidgetBehavior:
        return typing.cast(ScrollAreaWidgetBehavior, super()._behavior)

    def _set_root_container(self, root_container: typing.Optional[Window]) -> None:
        super()._set_root_container(root_container)
        if self.__content:
            self.__content._set_root_container(root_container)

    @property
    def _contained_widgets(self) -> typing.List[Widget]:
        return super()._contained_widgets + ([self.__content] if self.__content else list())

    def periodic(self) -> None:
        super().periodic()
        if self.__content:
            self.__content.periodic()

    @property
    def content(self) -> typing.Optional[Widget]:
        return self.__content

    @content.setter
    def content(self, content: typing.Optional[Widget]) -> None:
        self._behavior.set_content(content)
        self.__content = content
        if self.__content:
            self.__content._set_root_container(self.root_container)

    def restore_state(self, tag: str) -> None:
        pass

    def save_state(self, tag: str) -> None:
        pass

    def scroll_to(self, x: int, y: int) -> None:
        self._behavior.scroll_to(x, y)

    def set_scrollbar_policies(self, horizontal_policy: str, vertical_policy: str) -> None:
        self._behavior.set_scrollbar_policies(horizontal_policy, vertical_policy)

    def info(self) -> None:
        self._behavior.info()


class ComboBoxWidgetBehavior(WidgetBehavior, typing.Protocol):
    current_text: str
    on_current_text_changed: typing.Optional[typing.Callable[[str], None]]

    def set_item_strings(self, strings: typing.Sequence[str]) -> None: ...



class ComboBoxWidget(Widget):

    def __init__(self, widget_behavior: ComboBoxWidgetBehavior, items: typing.Sequence[typing.Any], item_getter: typing.Callable[[typing.Any], str]) -> None:
        super().__init__(widget_behavior)
        self.on_items_changed : typing.Optional[typing.Callable[[typing.Sequence[typing.Any]], None]] = None
        self.on_current_text_changed : typing.Optional[typing.Callable[[str], None]]= None
        self.on_current_item_changed : typing.Optional[typing.Callable[[typing.Any], None]] = None
        self.on_current_index_changed : typing.Optional[typing.Callable[[typing.Optional[int]], None]] = None
        self.item_getter = item_getter
        self.__items_binding: typing.Optional[Binding.Binding] = None

        def handle_current_text_changed(text: str) -> None:
            self.__current_index_binding_helper.value_changed(self.current_index)
            if callable(self.on_current_text_changed):
                self.on_current_text_changed(text)
            if callable(self.on_current_item_changed):
                self.on_current_item_changed(self.current_item)
            if callable(self.on_current_index_changed):
                self.on_current_index_changed(self.current_index)

        self._behavior.on_current_text_changed = handle_current_text_changed

        def set_items(items: typing.Sequence[typing.Any]) -> None:
            current_index = self.current_index
            item_strings = list()
            for item in items:
                item_string = notnone(self.item_getter(item) if callable(self.item_getter) else item)
                item_strings.append(item_string)
            self._behavior.set_item_strings(item_strings)
            if callable(self.on_items_changed):
                self.on_items_changed(self.__items)
            self.current_index = current_index

        def set_current_index(value: typing.Optional[int]) -> None:
            self.current_item = self.items[value] if value is not None else None

        def validate_current_index(new_value: typing.Optional[int], old_value: typing.Optional[int]) -> typing.Optional[int]:
            return new_value if new_value is not None and new_value >= 0 and new_value < len(self.items) else None

        self.__current_index_binding_helper = BindablePropertyHelper[typing.Optional[int]](None, set_current_index, validate_current_index)
        self.__items_binding_helper = BindablePropertyHelper[typing.Sequence[typing.Any]](None, set_items)

        self.items = list(items) if items else list()
        self.current_index = 0

    def close(self) -> None:
        self.__current_index_binding_helper.close()
        self.__current_index_binding_helper = typing.cast(typing.Any, None)
        self.__items_binding_helper.close()
        self.__items_binding_helper = typing.cast(typing.Any, None)
        self.item_getter = typing.cast(typing.Any, None)
        self.__items = typing.cast(typing.Any, None)
        self.on_items_changed = None
        self.on_current_text_changed = None
        self.on_current_item_changed = None
        self.on_current_index_changed = None
        super().close()

    @property
    def _behavior(self) -> ComboBoxWidgetBehavior:
        return typing.cast(ComboBoxWidgetBehavior, super()._behavior)

    @property
    def current_text(self) -> str:
        return self._behavior.current_text

    @current_text.setter
    def current_text(self, value: str) -> None:
        self._behavior.current_text = value

    @property
    def current_item(self) -> typing.Optional[typing.Any]:
        current_text = self.current_text
        for item in self.items or list():
            if current_text == notnone(self.item_getter(item) if callable(self.item_getter) else item):
                return item
        return None

    @current_item.setter
    def current_item(self, value: typing.Optional[typing.Any]) -> None:
        item_string = notnone(self.item_getter(value) if callable(self.item_getter) and value is not None else value)
        self.current_text = item_string

    @property
    def current_index(self) -> typing.Optional[int]:
        current_item = self.current_item
        return self.items.index(current_item) if current_item in self.items else None

    @current_index.setter
    def current_index(self, value: typing.Optional[int]) -> None:
        self.__current_index_binding_helper.value = value

    @property
    def items(self) -> typing.Sequence[typing.Any]:
        return self.__items_binding_helper.value

    @items.setter
    def items(self, items: typing.Sequence[typing.Any]) -> None:
        self.__items_binding_helper.value = items

    def bind_items(self, binding: Binding.Binding) -> None:
        self.__items_binding_helper.bind_value(binding)

    def unbind_items(self) -> None:
        self.__items_binding_helper.unbind_value()

    def bind_current_index(self, binding: Binding.Binding) -> None:
        self.__current_index_binding_helper.bind_value(binding)

    def unbind_current_index(self) -> None:
        self.__current_index_binding_helper.unbind_value()


class PushButtonWidgetBehavior(WidgetBehavior, typing.Protocol):
    text: typing.Optional[str]
    icon: typing.Optional[Bitmap.Bitmap]
    on_clicked: typing.Optional[typing.Callable[[], None]]


class PushButtonWidget(Widget):

    def __init__(self, widget_behavior: PushButtonWidgetBehavior, text: typing.Optional[str]) -> None:
        super().__init__(widget_behavior)
        self.on_clicked: typing.Optional[typing.Callable[[], None]] = None

        def handle_clicked() -> None:
            if callable(self.on_clicked):
                self.on_clicked()

        self._behavior.on_clicked = handle_clicked

        def set_text(value: typing.Optional[str]) -> None:
            self._behavior.text = str(value) if value is not None else None

        def set_icon(value: typing.Optional[Bitmap.BitmapOrArray]) -> None:
            self._behavior.icon = Bitmap.promote_bitmap(value)

        self.__text_binding_helper = BindablePropertyHelper[typing.Optional[str]](None, set_text)
        self.__icon_binding_helper = BindablePropertyHelper[typing.Optional[Bitmap.BitmapOrArray]](None, set_icon, None, typing.cast(typing.Any, Bitmap.bitmap_or_array_equal))

        self.text = text
        self.icon = None

    def close(self) -> None:
        self.__text_binding_helper.close()
        self.__text_binding_helper = typing.cast(typing.Any, None)
        self.__icon_binding_helper.close()
        self.__icon_binding_helper = typing.cast(typing.Any, None)
        self.on_clicked = None
        super().close()

    @property
    def _behavior(self) -> PushButtonWidgetBehavior:
        return typing.cast(PushButtonWidgetBehavior, super()._behavior)

    @property
    def text(self) -> typing.Optional[str]:
        return self.__text_binding_helper.value

    @text.setter
    def text(self, text: typing.Optional[str]) -> None:
        self.__text_binding_helper.value = text

    @property
    def icon(self) -> typing.Optional[Bitmap.BitmapOrArray]:  # hack for type checking until all packages switched to Bitmap
        return Bitmap.promote_bitmap(self.__icon_binding_helper.value)

    @icon.setter
    def icon(self, bitmap: typing.Optional[Bitmap.BitmapOrArray]) -> None:
        self.__icon_binding_helper.value = bitmap

    def bind_text(self, binding: Binding.Binding) -> None:
        self.__text_binding_helper.bind_value(binding)

    def unbind_text(self) -> None:
        self.__text_binding_helper.unbind_value()

    def bind_icon(self, binding: Binding.Binding) -> None:
        self.__icon_binding_helper.bind_value(binding)

    def unbind_icon(self) -> None:
        self.__icon_binding_helper.unbind_value()


class RadioButtonWidgetBehavior(WidgetBehavior, typing.Protocol):
    text: typing.Optional[str]
    icon: typing.Optional[Bitmap.Bitmap]
    checked: bool
    on_clicked: typing.Optional[typing.Callable[[], None]]


class RadioButtonWidget(Widget):

    def __init__(self, widget_behavior: RadioButtonWidgetBehavior, text: typing.Optional[str]) -> None:
        super().__init__(widget_behavior)
        self.on_clicked: typing.Optional[typing.Callable[[], None]] = None
        self.__value: typing.Optional[int] = None

        def set_text(value: typing.Optional[str]) -> None:
            self._behavior.text = str(value) if value is not None else None

        def set_icon(value: typing.Optional[Bitmap.BitmapOrArray]) -> None:
            self._behavior.icon = Bitmap.promote_bitmap(value)

        def set_group_value(group_value: typing.Optional[int]) -> None:
            self.checked = group_value == self.__value

        self.__text_binding_helper = BindablePropertyHelper[typing.Optional[str]](None, set_text)
        self.__icon_binding_helper = BindablePropertyHelper[typing.Optional[Bitmap.BitmapOrArray]](None, set_icon, None, typing.cast(typing.Any, Bitmap.bitmap_or_array_equal))
        self.__group_value_binding_helper = BindablePropertyHelper[typing.Optional[typing.Optional[int]]](None, set_group_value)

        def handle_clicked() -> None:
            if self.__value is not None:
                self.group_value = self.__value
            if callable(self.on_clicked):
                self.on_clicked()

        self._behavior.on_clicked = handle_clicked

        self.text = text
        self.icon = None
        self.group_value = None

    def close(self) -> None:
        self.__text_binding_helper.close()
        self.__text_binding_helper = typing.cast(typing.Any, None)
        self.__icon_binding_helper.close()
        self.__icon_binding_helper = typing.cast(typing.Any, None)
        self.__group_value_binding_helper.close()
        self.__group_value_binding_helper = typing.cast(typing.Any, None)
        super().close()

    @property
    def _behavior(self) -> RadioButtonWidgetBehavior:
        return typing.cast(RadioButtonWidgetBehavior, super()._behavior)

    @property
    def text(self) -> typing.Optional[str]:
        return self.__text_binding_helper.value

    @text.setter
    def text(self, text: typing.Optional[str]) -> None:
        self.__text_binding_helper.value = text

    @property
    def icon(self) -> typing.Optional[Bitmap.Bitmap]:
        return Bitmap.promote_bitmap(self.__icon_binding_helper.value)

    @icon.setter
    def icon(self, bitmap: typing.Optional[Bitmap.BitmapOrArray]) -> None:
        self.__icon_binding_helper.value = bitmap

    @property
    def checked(self) -> bool:
        return self._behavior.checked

    @checked.setter
    def checked(self, value: bool) -> None:
        self._behavior.checked = value

    @property
    def value(self) -> typing.Optional[int]:
        return self.__value

    @value.setter
    def value(self, value: typing.Optional[int]) -> None:
        self.__value = value
        self.checked = self.group_value == self.__value

    @property
    def group_value(self) -> typing.Optional[int]:
        return self.__group_value_binding_helper.value

    @group_value.setter
    def group_value(self, group_value: typing.Optional[int]) -> None:
        self.__group_value_binding_helper.value = group_value

    def bind_group_value(self, binding: Binding.Binding) -> None:
        self.__group_value_binding_helper.bind_value(binding)

    def unbind_group_value(self) -> None:
        self.__group_value_binding_helper.unbind_value()

    def bind_text(self, binding: Binding.Binding) -> None:
        self.__text_binding_helper.bind_value(binding)

    def unbind_text(self) -> None:
        self.__text_binding_helper.unbind_value()

    def bind_icon(self, binding: Binding.Binding) -> None:
        self.__icon_binding_helper.bind_value(binding)

    def unbind_icon(self) -> None:
        self.__icon_binding_helper.unbind_value()


class CheckBoxWidgetBehavior(WidgetBehavior, typing.Protocol):
    text: typing.Optional[str]
    check_state: str
    tristate: bool
    on_check_state_changed: typing.Optional[typing.Callable[[str], None]]


class CheckBoxWidget(Widget):

    def __init__(self, widget_behavior: CheckBoxWidgetBehavior, text: typing.Optional[str]) -> None:
        super().__init__(widget_behavior)
        self.on_checked_changed: typing.Optional[typing.Callable[[bool], None]] = None
        self.on_check_state_changed: typing.Optional[typing.Callable[[str], None]] = None

        def set_text(value: typing.Optional[str]) -> None:
            self._behavior.text = str(value) if value is not None else None

        def get_check_state() -> str:
            return self._behavior.check_state

        def set_check_state(value: str) -> None:
            self._behavior.check_state = value

        def get_checked() -> bool:
            return self._behavior.check_state == "checked"

        def set_checked(value: bool) -> None:
            set_check_state("checked" if value else "unchecked")

        self.__text_binding_helper = BindablePropertyHelper[typing.Optional[str]](None, set_text)

        # check state and checked are not independent, so use low level getters
        self.__check_state_binding_helper = BindablePropertyHelper[str](get_check_state, set_check_state)
        self.__checked_binding_helper = BindablePropertyHelper[bool](get_checked, set_checked)

        def handle_check_state_changed(check_state: str) -> None:
            self.__checked_binding_helper.value_changed(check_state == "checked")
            self.__check_state_binding_helper.value_changed(check_state)
            if callable(self.on_checked_changed):
                self.on_checked_changed(check_state == "checked")
            if callable(self.on_check_state_changed):
                self.on_check_state_changed(check_state)

        self._behavior.on_check_state_changed = handle_check_state_changed

        self.text = text
        self.check_state = "unchecked"
        self.checked = False

    def close(self) -> None:
        self.__text_binding_helper.close()
        self.__text_binding_helper = typing.cast(typing.Any, None)
        self.__check_state_binding_helper.close()
        self.__check_state_binding_helper = typing.cast(typing.Any, None)
        self.__checked_binding_helper.close()
        self.__checked_binding_helper = typing.cast(typing.Any, None)
        self.on_checked_changed = None
        self.on_check_state_changed = None
        super().close()

    @property
    def _behavior(self) -> CheckBoxWidgetBehavior:
        return typing.cast(CheckBoxWidgetBehavior, super()._behavior)

    @property
    def text(self) -> typing.Optional[str]:
        return self.__text_binding_helper.value

    @text.setter
    def text(self, text: typing.Optional[str]) -> None:
        self.__text_binding_helper.value = text

    @property
    def checked(self) -> bool:
        return self.__checked_binding_helper.value

    @checked.setter
    def checked(self, value: bool) -> None:
        self.__checked_binding_helper.value = value

    @property
    def tristate(self) -> bool:
        return self._behavior.tristate

    @tristate.setter
    def tristate(self, value: bool) -> None:
        self._behavior.tristate = value

    @property
    def check_state(self) -> str:
        return self.__check_state_binding_helper.value

    @check_state.setter
    def check_state(self, value: str) -> None:
        self.__check_state_binding_helper.value = value

    def bind_text(self, binding: Binding.Binding) -> None:
        self.__text_binding_helper.bind_value(binding)

    def unbind_text(self) -> None:
        self.__text_binding_helper.unbind_value()

    def bind_checked(self, binding: Binding.Binding) -> None:
        self.__checked_binding_helper.bind_value(binding)

    def unbind_checked(self) -> None:
        self.__checked_binding_helper.unbind_value()

    def bind_check_state(self, binding: Binding.Binding) -> None:
        self.__check_state_binding_helper.bind_value(binding)

    def unbind_check_state(self) -> None:
        self.__check_state_binding_helper.unbind_value()


class LabelWidgetBehavior(WidgetBehavior, typing.Protocol):
    text: typing.Optional[str]
    word_wrap: bool
    def set_text_color(self, value: typing.Optional[str]) -> None: ...
    def set_text_font(self, value: typing.Optional[str]) -> None: ...


class LabelWidget(Widget):

    def __init__(self, widget_behavior: LabelWidgetBehavior, text: typing.Optional[str]) -> None:
        super().__init__(widget_behavior)

        def set_text(value: typing.Optional[str]) -> None:
            self._behavior.text = str(value) if value is not None else str()

        def set_text_font(value: typing.Optional[str]) -> None:
            self._behavior.set_text_font(str(value) if value is not None else None)

        def set_text_color(value: typing.Optional[str]) -> None:
            self._behavior.set_text_color(str(value) if value is not None else None)

        self.__text_binding_helper = BindablePropertyHelper[typing.Optional[str]](None, set_text)
        self.__text_font_binding_helper = BindablePropertyHelper[typing.Optional[str]](None, set_text_font)
        self.__text_color_binding_helper = BindablePropertyHelper[typing.Optional[str]](None, set_text_color)

        self.text = text
        self.text_font = None
        self.text_color = "black"
        self.background_color = None

    def close(self) -> None:
        self.__text_binding_helper.close()
        self.__text_binding_helper = typing.cast(typing.Any, None)
        self.__text_font_binding_helper.close()
        self.__text_font_binding_helper = typing.cast(typing.Any, None)
        self.__text_color_binding_helper.close()
        self.__text_color_binding_helper = typing.cast(typing.Any, None)
        super().close()

    @property
    def _behavior(self) -> LabelWidgetBehavior:
        return typing.cast(LabelWidgetBehavior, super()._behavior)

    @property
    def text(self) -> typing.Optional[str]:
        return self.__text_binding_helper.value

    @text.setter
    def text(self, text: typing.Optional[str]) -> None:
        self.__text_binding_helper.value = text

    @property
    def text_color(self) -> typing.Optional[str]:
        return self.__text_color_binding_helper.value

    @text_color.setter
    def text_color(self, value: typing.Optional[str]) -> None:
        self.__text_color_binding_helper.value = value

    @property
    def text_font(self) -> typing.Optional[str]:
        return self.__text_font_binding_helper.value

    @text_font.setter
    def text_font(self, value: typing.Optional[str]) -> None:
        self.__text_font_binding_helper.value = value

    @property
    def word_wrap(self) -> bool:
        return self._behavior.word_wrap

    @word_wrap.setter
    def word_wrap(self, value: bool) -> None:
        self._behavior.word_wrap = value

    def bind_text(self, binding: Binding.Binding) -> None:
        self.__text_binding_helper.bind_value(binding)

    def unbind_text(self) -> None:
        self.__text_binding_helper.unbind_value()

    def bind_text_font(self, binding: Binding.Binding) -> None:
        self.__text_font_binding_helper.bind_value(binding)

    def unbind_text_font(self) -> None:
        self.__text_font_binding_helper.unbind_value()

    def bind_text_color(self, binding: Binding.Binding) -> None:
        self.__text_color_binding_helper.bind_value(binding)

    def unbind_text_color(self) -> None:
        self.__text_color_binding_helper.unbind_value()


class SliderWidgetBehavior(WidgetBehavior, typing.Protocol):
    value: int
    minimum: int
    maximum: int

    @property
    def pressed(self) -> bool: raise NotImplementedError()

    on_value_changed: typing.Optional[typing.Callable[[int], None]]
    on_slider_pressed: typing.Optional[typing.Callable[[], None]]
    on_slider_released: typing.Optional[typing.Callable[[], None]]
    on_slider_moved: typing.Optional[typing.Callable[[int], None]]


class SliderWidget(Widget):
    # note: sliders with exactly the same configuration have problems on macOS.
    # see https://bugreports.qt.io/browse/QTBUG-77368
    # ensure different sliders by setting different min/max. argh.

    def __init__(self, widget_behavior: SliderWidgetBehavior) -> None:
        super().__init__(widget_behavior)
        self.on_value_changed: typing.Optional[typing.Callable[[int], None]] = None
        self.on_slider_pressed: typing.Optional[typing.Callable[[], None]] = None
        self.on_slider_released: typing.Optional[typing.Callable[[], None]] = None
        self.on_slider_moved: typing.Optional[typing.Callable[[int], None]] = None
        self.minimum = 0
        self.maximum = 0

        def set_value(value: int) -> None:
            old_value_changed = self.on_value_changed
            self.on_value_changed = None
            try:
                self._behavior.value = value
                if callable(self.on_value_changed):
                    self.on_value_changed(value)
            finally:
                self.on_value_changed = old_value_changed

        def validate_value(new_value: int, old_value: int) -> int:
            return min(max(new_value, self.minimum), self.maximum)

        self.__value_binding_helper = BindablePropertyHelper[int](None, set_value, validate_value)

        def handle_value_changed(value: int) -> None:
            self.__value_binding_helper.value_changed(value)
            if callable(self.on_value_changed):
                self.on_value_changed(value)

        self._behavior.on_value_changed = handle_value_changed

        def handle_slider_pressed() -> None:
            if callable(self.on_slider_pressed):
                self.on_slider_pressed()

        self._behavior.on_slider_pressed = handle_slider_pressed

        def handle_slider_released() -> None:
            if callable(self.on_slider_released):
                self.on_slider_released()

        self._behavior.on_slider_released = handle_slider_released

        def handle_slider_moved(value: int) -> None:
            if callable(self.on_slider_moved):
                self.on_slider_moved(value)

        self._behavior.on_slider_moved = handle_slider_moved

        self.value = 0

    def close(self) -> None:
        self.__value_binding_helper.close()
        self.__value_binding_helper = typing.cast(typing.Any, None)
        self.on_value_changed = None
        self.on_slider_pressed = None
        self.on_slider_released = None
        self.on_slider_moved = None
        super().close()

    @property
    def _behavior(self) -> SliderWidgetBehavior:
        return typing.cast(SliderWidgetBehavior, super()._behavior)

    @property
    def value(self) -> int:
        return self.__value_binding_helper.value

    @value.setter
    def value(self, value: int) -> None:
        self.__value_binding_helper.value = value

    @property
    def minimum(self) -> int:
        return self._behavior.minimum

    @minimum.setter
    def minimum(self, minimum: int) -> None:
        self._behavior.minimum = minimum

    @property
    def maximum(self) -> int:
        return self._behavior.maximum

    @maximum.setter
    def maximum(self, maximum: int) -> None:
        self._behavior.maximum = maximum

    @property
    def pressed(self) -> bool:
        return self._behavior.pressed

    def bind_value(self, binding: Binding.Binding) -> None:
        self.__value_binding_helper.bind_value(binding)

    def unbind_value(self) -> None:
        self.__value_binding_helper.unbind_value()


class LineEditWidgetBehavior(WidgetBehavior, typing.Protocol):
    text: typing.Optional[str]
    placeholder_text: typing.Optional[str]
    editable: bool
    clear_button_enabled: bool
    on_editing_finished: typing.Optional[typing.Callable[[str], None]]
    on_escape_pressed: typing.Optional[typing.Callable[[], bool]]
    on_return_pressed: typing.Optional[typing.Callable[[], bool]]
    on_key_pressed: typing.Optional[typing.Callable[[Key], bool]]
    on_text_edited: typing.Optional[typing.Callable[[str], None]]

    def editing_finished(self, text: str) -> None: ...

    @property
    def selected_text(self) -> typing.Optional[str]: raise NotImplementedError()

    def select_all(self) -> None: ...


class LineEditWidget(Widget):

    def __init__(self, widget_behavior: LineEditWidgetBehavior) -> None:
        super().__init__(widget_behavior)
        self.on_editing_finished: typing.Optional[typing.Callable[[str], None]] = None
        self.on_escape_pressed: typing.Optional[typing.Callable[[], bool]] = None
        self.on_return_pressed: typing.Optional[typing.Callable[[], bool]] = None
        self.on_key_pressed: typing.Optional[typing.Callable[[Key], bool]] = None
        self.on_text_edited: typing.Optional[typing.Callable[[str], None]] = None
        self.__last_text = None

        def handle_editing_finished(text: str) -> None:
            if text != self.__last_text:
                self.__text_binding_helper.value_changed(text)
            if callable(self.on_editing_finished):
                self.on_editing_finished(text)
            self.__last_text = text

        self._behavior.on_editing_finished = handle_editing_finished

        def handle_escape_pressed() -> bool:
            self.__text_binding_helper.value_changed(self.__last_text)
            self.request_refocus()
            if callable(self.on_escape_pressed):
                return self.on_escape_pressed()
            return False

        self._behavior.on_escape_pressed = handle_escape_pressed

        def handle_return_pressed() -> bool:
            self.__text_binding_helper.value_changed(self.text)
            self.request_refocus()
            if callable(self.on_return_pressed):
                return self.on_return_pressed()
            return False

        self._behavior.on_return_pressed = handle_return_pressed

        def handle_key_pressed(key: Key) -> bool:
            if callable(self.on_key_pressed):
                return self.on_key_pressed(key)
            return False

        self._behavior.on_key_pressed = handle_key_pressed

        def handle_text_edited(text: str) -> None:
            if callable(self.on_text_edited):
                self.on_text_edited(text)

        self._behavior.on_text_edited = handle_text_edited

        def get_text() -> typing.Optional[str]:
            return self._behavior.text

        def set_text(value: typing.Optional[str]) -> None:
            str_ = str(value) if value is not None else str()
            self.__last_text = str_
            self._behavior.text = value
            # if focused, select all text. if no text, un-focus.
            if self.focused:
                if self.text:
                    self.select_all()
                else:
                    self.focused = False

        def get_placeholder_text() -> typing.Optional[str]:
            return self._behavior.placeholder_text

        def set_placeholder_text(value: typing.Optional[str]) -> None:
            str_ = str(value) if value is not None else str()
            self.__last_text = str_
            self._behavior.placeholder_text = value
            # if focused and no text, un-focus.
            if self.focused and not self.text:
                self.focused = False

        self.__text_binding_helper = BindablePropertyHelper[typing.Optional[str]](get_text, set_text)

        self.__placeholder_text_binding_helper = BindablePropertyHelper[typing.Optional[str]](get_placeholder_text, set_placeholder_text)

    def close(self) -> None:
        self.__placeholder_text_binding_helper.close()
        self.__placeholder_text_binding_helper = typing.cast(typing.Any, None)
        self.__text_binding_helper.close()
        self.__text_binding_helper = typing.cast(typing.Any, None)
        self.on_editing_finished = None
        self.on_escape_pressed = None
        self.on_return_pressed = None
        self.on_text_edited = None
        super().close()

    @property
    def _behavior(self) -> LineEditWidgetBehavior:
        return typing.cast(LineEditWidgetBehavior, super()._behavior)

    @property
    def text(self) -> typing.Optional[str]:
        return self.__text_binding_helper.value

    @text.setter
    def text(self, text: typing.Optional[str]) -> None:
        self.__text_binding_helper.value = text

    @property
    def placeholder_text(self) -> typing.Optional[str]:
        return self.__placeholder_text_binding_helper.value

    @placeholder_text.setter
    def placeholder_text(self, text: typing.Optional[str]) -> None:
        self.__placeholder_text_binding_helper.value = text

    @property
    def selected_text(self) -> typing.Optional[str]:
        return self._behavior.selected_text

    @property
    def clear_button_enabled(self) -> bool:
        return self._behavior.clear_button_enabled

    @clear_button_enabled.setter
    def clear_button_enabled(self, enabled: bool) -> None:
        self._behavior.clear_button_enabled = enabled

    @property
    def editable(self) -> bool:
        return self._behavior.editable

    @editable.setter
    def editable(self, editable: bool) -> None:
        self._behavior.editable = editable

    def select_all(self) -> None:
        self._behavior.select_all()

    def handle_select_all(self) -> bool:
        self.select_all()
        return True

    def refocus(self) -> None:
        self.select_all()

    def bind_text(self, binding: Binding.Binding) -> None:
        self.__text_binding_helper.bind_value(binding)

    def unbind_text(self) -> None:
        self.__text_binding_helper.unbind_value()

    def bind_placeholder_text(self, binding: Binding.Binding) -> None:
        self.__placeholder_text_binding_helper.bind_value(binding)

    def unbind_placeholder_text(self) -> None:
        self.__placeholder_text_binding_helper.unbind_value()

    def editing_finished(self, text: str) -> None:
        self._behavior.editing_finished(text)


class TextBrowserWidgetBehavior(WidgetBehavior, typing.Protocol):
    on_anchor_clicked: typing.Optional[typing.Callable[[str], None]]
    on_load_image_resource: typing.Optional[typing.Callable[[str], typing.Optional[DrawingContext.RGBA32Type]]]
    on_escape_pressed: typing.Optional[typing.Callable[[], bool]]
    on_return_pressed: typing.Optional[typing.Callable[[], bool]]
    on_key_pressed: typing.Optional[typing.Callable[[Key], bool]]

    def set_html(self, str: typing.Optional[str]) -> None: ...
    def set_markdown(self, str: typing.Optional[str]) -> None: ...
    def set_text(self, str: typing.Optional[str]) -> None: ...
    def set_text_background_color(self, color: typing.Optional[str]) -> None: ...
    def set_text_color(self, color: typing.Optional[str]) -> None: ...
    def set_text_font(self, font_str: typing.Optional[str]) -> None: ...
    def scroll_to_anchor(self, anchor: str) -> None: ...


Selection = collections.namedtuple("Selection", ["start", "end"])

CursorPosition = collections.namedtuple("CursorPosition", ["position", "block_number", "column_number"])


class TextEditWidgetBehavior(WidgetBehavior, typing.Protocol):
    text: typing.Optional[str]
    placeholder: typing.Optional[str]
    editable: bool
    word_wrap_mode: str

    on_cursor_position_changed: typing.Optional[typing.Callable[[CursorPosition], None]]
    on_selection_changed: typing.Optional[typing.Callable[[Selection], None]]
    on_text_changed: typing.Optional[typing.Callable[[typing.Optional[str]], None]]
    on_text_edited: typing.Optional[typing.Callable[[typing.Optional[str]], None]]
    on_escape_pressed: typing.Optional[typing.Callable[[], bool]]
    on_return_pressed: typing.Optional[typing.Callable[[], bool]]
    on_key_pressed: typing.Optional[typing.Callable[[Key], bool]]
    on_insert_mime_data: typing.Optional[typing.Callable[[MimeData], None]]

    @property
    def selected_text(self) -> typing.Optional[str]:  raise NotImplementedError()

    @property
    def cursor_position(self) -> CursorPosition: raise NotImplementedError()

    @property
    def selection(self) -> Selection: raise NotImplementedError()

    def append_text(self, value: str) -> None: ...
    def insert_text(self, value: str) -> None: ...
    def clear_selection(self) -> None: ...
    def remove_selected_text(self) -> None: ...
    def select_all(self) -> None: ...
    def move_cursor_position(self, operation: str, mode: typing.Optional[str] = None, n: int = 1) -> None: ...
    def set_line_height_proportional(self, proportional_line_height: float) -> None: ...
    def set_text_background_color(self, color: typing.Optional[str]) -> None: ...
    def set_text_color(self, color: typing.Optional[str]) -> None: ...
    def set_text_font(self, font_str: typing.Optional[str]) -> None: ...


class TextBrowserWidget(Widget):

    def __init__(self, widget_behavior: TextBrowserWidgetBehavior) -> None:
        super().__init__(widget_behavior)
        self.on_anchor_clicked: typing.Optional[typing.Callable[[str], bool]] = None
        self.on_load_image_resource: typing.Optional[typing.Callable[[str], typing.Optional[DrawingContext.RGBA32Type]]] = None
        self.on_escape_pressed: typing.Optional[typing.Callable[[], bool]] = None
        self.on_return_pressed: typing.Optional[typing.Callable[[], bool]] = None
        self.on_key_pressed: typing.Optional[typing.Callable[[Key], bool]] = None

        def handle_anchor_clicked(anchor: str) -> None:
            if callable(self.on_anchor_clicked):
                if self.on_anchor_clicked(anchor):
                    return
            if anchor.startswith("#"):
                self.scroll_to_anchor(anchor)

        self._behavior.on_anchor_clicked = handle_anchor_clicked

        def handle_load_image_resource(name: str) -> typing.Optional[DrawingContext.RGBA32Type]:
            if callable(self.on_load_image_resource):
                return self.on_load_image_resource(name)
            return None

        self._behavior.on_load_image_resource = handle_load_image_resource

        def handle_escape_pressed() -> bool:
            if callable(self.on_escape_pressed):
                return self.on_escape_pressed()
            return False

        self._behavior.on_escape_pressed = handle_escape_pressed

        def handle_return_pressed() -> bool:
            if callable(self.on_return_pressed):
                return self.on_return_pressed()
            return False

        self._behavior.on_return_pressed = handle_return_pressed

        def handle_key_pressed(key: Key) -> bool:
            if callable(self.on_key_pressed):
                return self.on_key_pressed(key)
            return False

        self._behavior.on_key_pressed = handle_key_pressed

        def set_markdown(value: typing.Optional[str]) -> None:
            self._behavior.set_markdown(str(value) if value is not None else str())

        self.__markdown_binding_helper = BindablePropertyHelper[typing.Optional[str]](None, set_markdown)

        def set_text(value: typing.Optional[str]) -> None:
            self._behavior.set_text(str(value) if value is not None else str())

        self.__text_binding_helper = BindablePropertyHelper[typing.Optional[str]](None, set_text)

    def close(self) -> None:
        self.__markdown_binding_helper.close()
        self.__markdown_binding_helper = typing.cast(typing.Any, None)
        self.__text_binding_helper.close()
        self.__text_binding_helper = typing.cast(typing.Any, None)
        self.on_anchor_clicked = None
        self.on_load_image_resource = None
        self.on_escape_pressed = None
        self.on_return_pressed = None
        self.on_key_pressed = None
        super().close()

    @property
    def _behavior(self) -> TextBrowserWidgetBehavior:
        return typing.cast(TextBrowserWidgetBehavior, super()._behavior)

    @property
    def markdown(self) -> typing.Optional[str]:
        return self.__markdown_binding_helper.value

    @markdown.setter
    def markdown(self, markdown: typing.Optional[str]) -> None:
        self.__markdown_binding_helper.value = markdown

    @property
    def text(self) -> typing.Optional[str]:
        return self.__text_binding_helper.value

    @text.setter
    def text(self, text: typing.Optional[str]) -> None:
        self.__text_binding_helper.value = text

    def scroll_to_anchor(self, anchor: str) -> None:
        self._behavior.scroll_to_anchor(anchor)

    def set_text_background_color(self, color: typing.Optional[str]) -> None:
        self._behavior.set_text_background_color(color)

    def set_text_color(self, color: typing.Optional[str]) -> None:
        self._behavior.set_text_color(color)

    def set_text_font(self, font_str: typing.Optional[str]) -> None:
        self._behavior.set_text_font(font_str)

    def bind_markdown(self, binding: Binding.Binding) -> None:
        self.__markdown_binding_helper.bind_value(binding)

    def unbind_markdown(self) -> None:
        self.__markdown_binding_helper.unbind_value()

    def bind_text(self, binding: Binding.Binding) -> None:
        self.__text_binding_helper.bind_value(binding)

    def unbind_text(self) -> None:
        self.__text_binding_helper.unbind_value()


class TextEditWidget(Widget):

    def __init__(self, widget_behavior: TextEditWidgetBehavior) -> None:
        super().__init__(widget_behavior)
        self.on_cursor_position_changed: typing.Optional[typing.Callable[[CursorPosition], None]] = None
        self.on_selection_changed: typing.Optional[typing.Callable[[Selection], None]] = None
        self.on_text_changed: typing.Optional[typing.Callable[[typing.Optional[str]], None]] = None  # backwards compatibility
        self.on_text_edited: typing.Optional[typing.Callable[[typing.Optional[str]], None]] = None
        self.on_escape_pressed: typing.Optional[typing.Callable[[], bool]] = None
        self.on_return_pressed: typing.Optional[typing.Callable[[], bool]] = None
        self.on_key_pressed: typing.Optional[typing.Callable[[Key], bool]] = None
        self.on_insert_mime_data: typing.Optional[typing.Callable[[MimeData], None]] = None

        def handle_cursor_position_changed(cursor_position: CursorPosition) -> None:
            if callable(self.on_cursor_position_changed):
                self.on_cursor_position_changed(cursor_position)

        self._behavior.on_cursor_position_changed = handle_cursor_position_changed

        def handle_selection_changed(selection: Selection) -> None:
            if callable(self.on_selection_changed):
                self.on_selection_changed(selection)

        self._behavior.on_selection_changed = handle_selection_changed

        def handle_text_changed(text: typing.Optional[str]) -> None:
            self.__text_binding_helper.value_changed(text)
            if callable(self.on_text_changed):
                self.on_text_changed(text)
            if callable(self.on_text_edited):
                self.on_text_edited(text)

        self._behavior.on_text_changed = handle_text_changed

        def handle_escape_pressed() -> bool:
            if callable(self.on_escape_pressed):
                return self.on_escape_pressed()
            return False

        self._behavior.on_escape_pressed = handle_escape_pressed

        def handle_return_pressed() -> bool:
            if callable(self.on_return_pressed):
                return self.on_return_pressed()
            return False

        self._behavior.on_return_pressed = handle_return_pressed

        def handle_key_pressed(key: Key) -> bool:
            if callable(self.on_key_pressed):
                return self.on_key_pressed(key)
            return False

        self._behavior.on_key_pressed = handle_key_pressed

        def handle_insert_mime_data(mime_data: MimeData) -> None:
            if callable(self.on_insert_mime_data):
                self.on_insert_mime_data(mime_data)
            else:
                text = mime_data.data_as_string("text/plain")
                self.insert_text(text)

        self._behavior.on_insert_mime_data = handle_insert_mime_data

        def set_text(value: typing.Optional[str]) -> None:
            self._behavior.text = str(value) if value is not None else str()

        self.__text_binding_helper = BindablePropertyHelper[typing.Optional[str]](None, set_text)

    def close(self) -> None:
        self.__text_binding_helper.close()
        self.__text_binding_helper = typing.cast(typing.Any, None)
        self.on_cursor_position_changed = None
        self.on_selection_changed = None
        self.on_text_changed = None
        self.on_text_edited = None
        self.on_escape_pressed = None
        self.on_return_pressed = None
        self.on_key_pressed = None
        self.on_insert_mime_data = None
        super().close()

    @property
    def _behavior(self) -> TextEditWidgetBehavior:
        return typing.cast(TextEditWidgetBehavior, super()._behavior)

    @property
    def text(self) -> typing.Optional[str]:
        return self.__text_binding_helper.value

    @text.setter
    def text(self, text: typing.Optional[str]) -> None:
        self.__text_binding_helper.value = text

    @property
    def placeholder(self) -> typing.Optional[str]:
        return self._behavior.placeholder

    @placeholder.setter
    def placeholder(self, text: typing.Optional[str]) -> None:
        self._behavior.placeholder = text

    @property
    def editable(self) -> bool:
        return self._behavior.editable

    @editable.setter
    def editable(self, value: bool) -> None:
        self._behavior.editable = value

    @property
    def selected_text(self) -> typing.Optional[str]:
        return self._behavior.selected_text

    @property
    def cursor_position(self) -> CursorPosition:
        return self._behavior.cursor_position

    @property
    def selection(self) -> Selection:
        return self._behavior.selection

    def append_text(self, value: str) -> None:
        self._behavior.append_text(value)

    def insert_text(self, value: str) -> None:
        self._behavior.insert_text(value)

    def clear_selection(self) -> None:
        self._behavior.clear_selection()

    def remove_selected_text(self) -> None:
        self._behavior.remove_selected_text()

    def select_all(self) -> None:
        self._behavior.select_all()

    def refocus(self) -> None:
        self.select_all()

    def move_cursor_position(self, operation: str, mode: typing.Optional[str] = None, n: int = 1) -> None:
        self._behavior.move_cursor_position(operation, mode, n)

    def handle_select_all(self) -> None:
        self.select_all()

    def set_line_height_proportional(self, proportional_line_height: float) -> None:
        self._behavior.set_line_height_proportional(proportional_line_height)

    def set_text_background_color(self, color: typing.Optional[str]) -> None:
        self._behavior.set_text_background_color(color)

    def set_text_color(self, color: typing.Optional[str]) -> None:
        self._behavior.set_text_color(color)

    def set_text_font(self, font_str: typing.Optional[str]) -> None:
        self._behavior.set_text_font(font_str)

    @property
    def word_wrap_mode(self) -> str:
        return self._behavior.word_wrap_mode

    @word_wrap_mode.setter
    def word_wrap_mode(self, value: str) -> None:
        self._behavior.word_wrap_mode = value

    def bind_text(self, binding: Binding.Binding) -> None:
        self.__text_binding_helper.bind_value(binding)

    def unbind_text(self) -> None:
        self.__text_binding_helper.unbind_value()


class CanvasWidgetBehavior(WidgetBehavior, typing.Protocol):
    on_mouse_entered: typing.Optional[typing.Callable[[], None]]
    on_mouse_exited: typing.Optional[typing.Callable[[], None]]
    on_mouse_clicked: typing.Optional[typing.Callable[[int, int, KeyboardModifiers], bool]]
    on_mouse_double_clicked: typing.Optional[typing.Callable[[int, int, KeyboardModifiers], bool]]
    on_mouse_pressed: typing.Optional[typing.Callable[[int, int, KeyboardModifiers], bool]]
    on_mouse_released: typing.Optional[typing.Callable[[int, int, KeyboardModifiers], bool]]
    on_mouse_position_changed: typing.Optional[typing.Callable[[int, int, KeyboardModifiers], None]]
    on_grabbed_mouse_position_changed: typing.Optional[typing.Callable[[int, int, KeyboardModifiers], None]]
    on_wheel_changed: typing.Optional[typing.Callable[[int, int, int, int, bool], bool]]
    on_size_changed: typing.Optional[typing.Callable[[int, int], None]]
    on_key_pressed: typing.Optional[typing.Callable[[Key], bool]]
    on_key_released: typing.Optional[typing.Callable[[Key], bool]]
    on_drag_enter: typing.Optional[typing.Callable[[MimeData], str]]
    on_drag_leave: typing.Optional[typing.Callable[[], str]]
    on_drag_move: typing.Optional[typing.Callable[[MimeData, int, int], str]]
    on_drop: typing.Optional[typing.Callable[[MimeData, int, int], str]]
    on_tool_tip: typing.Optional[typing.Callable[[int, int, int, int], bool]]
    on_pan_gesture: typing.Optional[typing.Callable[[int, int], bool]]

    @property
    def focusable(self) -> bool: raise NotImplementedError()

    @focusable.setter
    def focusable(self, value: bool) -> None: ...

    def periodic(self) -> None: ...
    def draw(self, drawing_context: DrawingContext.DrawingContext) -> None: ...
    def draw_section(self, section_id: int, drawing_context: DrawingContext.DrawingContext, canvas_rect: Geometry.IntRect) -> None: ...
    def remove_section(self, section_id: int) -> None: ...
    def set_cursor_shape(self, cursor_shape: typing.Optional[str]) -> None: ...
    def grab_gesture(self, gesture_type: str) -> None: ...
    def release_gesture(self, gesture_type: str) -> None: ...
    def grab_mouse(self, gx: int, gy: int) -> None: ...
    def release_mouse(self) -> None: ...
    def show_tool_tip_text(self, text: str, gx: int, gy: int) -> None: ...
    def hide_tool_tip_text(self) -> None: ...
    def _set_canvas_item(self, canvas_item: CanvasItem.AbstractCanvasItem) -> None: ...
    def _create_composition_canvas_item(self, canvas_widget: CanvasWidget, layout_render: typing.Optional[str]) -> CanvasItem.CanvasWidgetCanvasItem: ...


class CanvasWidget(Widget):

    def __init__(self, widget_behavior: CanvasWidgetBehavior, *, layout_render: typing.Optional[str] = None) -> None:
        super().__init__(widget_behavior)
        self.on_periodic: typing.Optional[typing.Callable[[], None]] = None
        self.on_dispatch_any: typing.Optional[typing.Callable[..., typing.Any]] = None
        self.on_can_dispatch_any: typing.Optional[typing.Callable[[str], bool]] = None
        self.on_get_menu_item_state: typing.Optional[typing.Callable[[str], typing.Optional[MenuItemState]]] = None
        self.on_mouse_entered: typing.Optional[typing.Callable[[], None]] = None
        self.on_mouse_exited: typing.Optional[typing.Callable[[], None]] = None
        self.on_mouse_clicked: typing.Optional[typing.Callable[[int, int, KeyboardModifiers], bool]] = None
        self.on_mouse_double_clicked: typing.Optional[typing.Callable[[int, int, KeyboardModifiers], bool]] = None
        self.on_mouse_pressed: typing.Optional[typing.Callable[[int, int, KeyboardModifiers], bool]] = None
        self.on_mouse_released: typing.Optional[typing.Callable[[int, int, KeyboardModifiers], bool]] = None
        self.on_mouse_position_changed: typing.Optional[typing.Callable[[int, int, KeyboardModifiers], None]] = None
        self.on_grabbed_mouse_position_changed: typing.Optional[typing.Callable[[int, int, KeyboardModifiers], None]] = None
        self.on_wheel_changed: typing.Optional[typing.Callable[[int, int, int, int, bool], bool]] = None
        self.on_key_pressed: typing.Optional[typing.Callable[[Key], bool]] = None
        self.on_key_released: typing.Optional[typing.Callable[[Key], bool]] = None
        self.on_size_changed: typing.Optional[typing.Callable[[int, int], None]] = None
        self.on_drag_enter: typing.Optional[typing.Callable[[MimeData], str]] = None
        self.on_drag_leave: typing.Optional[typing.Callable[[], str]] = None
        self.on_drag_move: typing.Optional[typing.Callable[[MimeData, int, int], str]] = None
        self.on_drop: typing.Optional[typing.Callable[[MimeData, int, int], str]] = None
        self.on_tool_tip: typing.Optional[typing.Callable[[int, int, int, int], bool]] = None
        self.on_pan_gesture: typing.Optional[typing.Callable[[int, int], bool]] = None
        self.width = 0
        self.height = 0
        self.position_info = None

        def handle_mouse_entered() -> None:
            if callable(self.on_mouse_entered):
                self.on_mouse_entered()

        self._behavior.on_mouse_entered = handle_mouse_entered

        def handle_mouse_exited() -> None:
            if callable(self.on_mouse_exited):
                self.on_mouse_exited()
                # when the mouse exits, position info may still be pending
                # since it is serviced during periodic. clear it here so
                # nothing else gets processed. the mouse has already exited.
                self.position_info = None

        self._behavior.on_mouse_exited = handle_mouse_exited

        def handle_mouse_clicked(x: int, y: int, modifiers: KeyboardModifiers) -> bool:
            if callable(self.on_mouse_clicked):
                return self.on_mouse_clicked(x, y, modifiers)
            return False

        self._behavior.on_mouse_clicked = handle_mouse_clicked

        def handle_mouse_double_clicked(x: int, y: int, modifiers: KeyboardModifiers) -> bool:
            try:
                if callable(self.on_mouse_double_clicked):
                    return self.on_mouse_double_clicked(x, y, modifiers)
                return False
            except Exception as e:
                import traceback
                traceback.print_exc()
                return False

        self._behavior.on_mouse_double_clicked = handle_mouse_double_clicked

        def handle_mouse_pressed(x: int, y: int, modifiers: KeyboardModifiers) -> bool:
            if callable(self.on_mouse_pressed):
                return self.on_mouse_pressed(x, y, modifiers)
            return False

        self._behavior.on_mouse_pressed = handle_mouse_pressed

        def handle_mouse_released(x: int, y: int, modifiers: KeyboardModifiers) -> bool:
            if callable(self.on_mouse_released):
                return self.on_mouse_released(x, y, modifiers)
            return False

        self._behavior.on_mouse_released = handle_mouse_released

        def handle_mouse_position_changed(x: int, y: int, modifiers: KeyboardModifiers) -> None:
            # mouse tracking takes priority over timer events in newer
            # versions of Qt, so during mouse tracking, make sure periodic
            # gets called regularly.
            self.position_info = x, y, modifiers

        self._behavior.on_mouse_position_changed = handle_mouse_position_changed

        def handle_grabbed_mouse_position_changed(dx: int, dy: int, modifiers: KeyboardModifiers) -> None:
            if callable(self.on_grabbed_mouse_position_changed):
                self.on_grabbed_mouse_position_changed(dx, dy, modifiers)

        self._behavior.on_grabbed_mouse_position_changed = handle_grabbed_mouse_position_changed

        def handle_wheel_changed(x: int, y: int, dx: int, dy: int, is_horizontal: bool) -> bool:
            if callable(self.on_wheel_changed):
                return self.on_wheel_changed(x, y, dx, dy, is_horizontal)
            return False

        self._behavior.on_wheel_changed = handle_wheel_changed

        def handle_size_changed(width: int, height: int) -> None:
            self.width = width
            self.height = height
            if callable(self.on_size_changed):
                self.on_size_changed(width, height)

        self._behavior.on_size_changed = handle_size_changed

        def handle_key_pressed(key: Key) -> bool:
            if callable(self.on_key_pressed):
                return self.on_key_pressed(key)
            return False

        self._behavior.on_key_pressed = handle_key_pressed

        def handle_key_released(key: Key) -> bool:
            if callable(self.on_key_released):
                return self.on_key_released(key)
            return False

        self._behavior.on_key_released = handle_key_released

        def handle_drag_enter(mime_data: MimeData) -> str:
            if callable(self.on_drag_enter):
                return self.on_drag_enter(mime_data)
            return "ignore"

        self._behavior.on_drag_enter = handle_drag_enter

        def handle_drag_leave() -> str:
            if callable(self.on_drag_leave):
                return self.on_drag_leave()
            return "ignore"

        self._behavior.on_drag_leave = handle_drag_leave

        def handle_drag_move(mime_data: MimeData, x: int, y: int) -> str:
            if callable(self.on_drag_move):
                return self.on_drag_move(mime_data, x, y)
            return "ignore"

        self._behavior.on_drag_move = handle_drag_move

        def handle_drop(mime_data: MimeData, x: int, y: int) -> str:
            if callable(self.on_drop):
                return self.on_drop(mime_data, x, y)
            return "ignore"

        self._behavior.on_drop = handle_drop

        def handle_tool_tip(x: int, y: int, gx: int, gy: int) -> bool:
            if callable(self.on_tool_tip):
                return self.on_tool_tip(x, y, gx, gy)
            return False

        self._behavior.on_tool_tip = handle_tool_tip

        def handle_pan_gesture(delta_x: int, delta_y: int) -> bool:
            if callable(self.on_pan_gesture):
                return self.on_pan_gesture(delta_x, delta_y)
            return False

        self._behavior.on_pan_gesture = handle_pan_gesture

        self.__canvas_item = self._behavior._create_composition_canvas_item(self, layout_render)
        self._behavior._set_canvas_item(self.__canvas_item)

    def close(self) -> None:
        if self.__canvas_item:
            self.__canvas_item.close()
            self.__canvas_item = typing.cast(typing.Any, None)
        # messages generated from this class
        self.on_periodic = None
        self.on_dispatch_any = None
        self.on_can_dispatch_any = None
        self.on_get_menu_item_state = None
        # messages passed on from the behavior
        self.on_mouse_entered = None
        self.on_mouse_exited = None
        self.on_mouse_clicked = None
        self.on_mouse_double_clicked = None
        self.on_mouse_pressed = None
        self.on_mouse_released = None
        self.on_mouse_position_changed = None
        self.on_grabbed_mouse_position_changed = None
        self.on_wheel_changed = None
        self.on_key_pressed = None
        self.on_key_released = None
        self.on_size_changed = None
        self.on_drag_enter = None
        self.on_drag_leave = None
        self.on_drag_move = None
        self.on_drop = None
        self.on_tool_tip = None
        self.on_pan_gesture = None
        super().close()

    @property
    def _behavior(self) -> CanvasWidgetBehavior:
        return typing.cast(CanvasWidgetBehavior, super()._behavior)

    def periodic(self) -> None:
        super().periodic()
        self._behavior.periodic()
        if self.on_periodic:
            self.on_periodic()
        if self.position_info is not None:
            if callable(self.on_mouse_position_changed):
                self.on_mouse_position_changed(*self.position_info)
            self.position_info = None

    def _redraw(self) -> None:
        self.__canvas_item.redraw()

    @property
    def canvas_item(self) -> CanvasItem.CanvasWidgetCanvasItem:
        return self.__canvas_item

    @property
    def canvas_size(self) -> Geometry.IntSize:
        return Geometry.IntSize(h=self.height, w=self.width)

    @property
    def focusable(self) -> bool:
        return self._behavior.focusable

    @focusable.setter
    def focusable(self, focusable: bool) -> None:
        self._behavior.focusable = focusable

    def draw(self, drawing_context: DrawingContext.DrawingContext) -> None:
        self._behavior.draw(drawing_context)

    def draw_section(self, section_id: int, drawing_context: DrawingContext.DrawingContext, canvas_rect: Geometry.IntRect) -> None:
        self._behavior.draw_section(section_id, drawing_context, canvas_rect)

    def remove_section(self, section_id: int) -> None:
        self._behavior.remove_section(section_id)

    def set_cursor_shape(self, cursor_shape: typing.Optional[str]) -> None:
        self._behavior.set_cursor_shape(cursor_shape)

    def simulate_mouse_click(self, x: int, y: int, modifiers: KeyboardModifiers) -> None:
        if self.on_mouse_pressed:
            self.on_mouse_pressed(x, y, modifiers)
        if self.on_mouse_released:
            self.on_mouse_released(x, y, modifiers)
        if self.on_mouse_clicked:
            self.on_mouse_clicked(x, y, modifiers)

    def grab_gesture(self, gesture_type: str) -> None:
        self._behavior.grab_gesture(gesture_type)

    def release_gesture(self, gesture_type: str) -> None:
        self._behavior.release_gesture(gesture_type)

    def grab_mouse(self, gx: int, gy: int) -> None:
        self._behavior.grab_mouse(gx, gy)

    def release_mouse(self) -> None:
        self._behavior.release_mouse()

    def show_tool_tip_text(self, text: str, gx: int, gy: int) -> None:
        self._behavior.show_tool_tip_text(text, gx, gy)

    def hide_tool_tip_text(self) -> None:
        self._behavior.hide_tool_tip_text()

    def _dispatch_any(self, method: str, *args: typing.Any, **kwargs: typing.Any) -> bool:
        if callable(self.on_dispatch_any):
            return typing.cast(bool, self.on_dispatch_any(method, *args, **kwargs))
        return False

    def _can_dispatch_any(self, method: str) -> bool:
        if callable(self.on_can_dispatch_any):
            return self.on_can_dispatch_any(method)
        return False

    def _get_menu_item_state(self, command_id: str) -> typing.Optional[MenuItemState]:
        if callable(self.on_get_menu_item_state):
            menu_item_state = self.on_get_menu_item_state(command_id)
            if menu_item_state:
                return menu_item_state
        return None


class ProgressBarWidgetBehavior(CanvasWidgetBehavior, typing.Protocol):
    pass


class ProgressBarWidget(CanvasWidget):

    def __init__(self, widget_behavior: ProgressBarWidgetBehavior) -> None:
        super().__init__(widget_behavior)
        self.__minimum = 0
        self.__maximum = 0
        self.on_value_changed: typing.Optional[typing.Callable[[int], None]] = None

        def set_value(value: int) -> None:
            old_value_changed = self.on_value_changed
            self.on_value_changed = None
            try:
                self.__progress_bar_canvas_item.progress = (value - self.__minimum) / (self.__maximum - self.__minimum) if self.__maximum != self.__minimum else 0.0
                if callable(self.on_value_changed):
                    self.on_value_changed(value)
            finally:
                self.on_value_changed = old_value_changed

        def validate_value(new_value: int, old_value: int) -> int:
            return min(max(new_value, self.__minimum), self.__maximum)

        self.__value_binding_helper = BindablePropertyHelper[int](None, set_value, validate_value)

        self.__progress_bar_canvas_item = CanvasItem.ProgressBarCanvasItem()
        self.__progress_bar_canvas_item.update_sizing(self.__progress_bar_canvas_item.sizing.with_fixed_size(Geometry.IntSize(w=500, h=20)))
        self.canvas_item.add_canvas_item(self.__progress_bar_canvas_item)

        self.value = 0

    def close(self) -> None:
        self.__value_binding_helper.close()
        self.__value_binding_helper = typing.cast(typing.Any, None)
        self.on_value_changed = None
        super().close()

    @property
    def _behavior(self) -> ProgressBarWidgetBehavior:
        return typing.cast(ProgressBarWidgetBehavior, super()._behavior)

    @property
    def value(self) -> int:
        return self.__value_binding_helper.value

    @value.setter
    def value(self, value: int) -> None:
        self.__value_binding_helper.value = value

    @property
    def minimum(self) -> int:
        return self.__minimum

    @minimum.setter
    def minimum(self, minimum: int) -> None:
        self.__minimum = minimum

    @property
    def maximum(self) -> int:
        return self.__maximum

    @maximum.setter
    def maximum(self, maximum: int) -> None:
        self.__maximum = maximum

    def bind_value(self, binding: Binding.Binding) -> None:
        self.__value_binding_helper.bind_value(binding)

    def unbind_value(self) -> None:
        self.__value_binding_helper.unbind_value()


class TreeWidgetBehavior(WidgetBehavior, typing.Protocol):
    selection_mode: str
    item_model_controller: typing.Any
    on_key_pressed: typing.Optional[typing.Callable[[typing.Sequence[int], Key], bool]]
    on_tree_selection_changed: typing.Optional[typing.Callable[[typing.Sequence[typing.Tuple[int, int, int]]], None]]
    on_tree_item_changed: typing.Optional[typing.Callable[[int, int, int], None]]
    on_tree_item_clicked: typing.Optional[typing.Callable[[int, int, int], bool]]
    on_tree_item_double_clicked: typing.Optional[typing.Callable[[int, int, int], bool]]
    on_tree_item_key_pressed: typing.Optional[typing.Callable[[int, int, int, Key], bool]]
    def set_current_row(self, index: int, parent_row: int, parent_id: int) -> None: ...
    def clear_current_row(self) -> None: ...
    def size_to_content(self) -> None: ...


class TreeWidget(Widget):

    def __init__(self, widget_behavior: TreeWidgetBehavior) -> None:
        super().__init__(widget_behavior)
        self.on_key_pressed: typing.Optional[typing.Callable[[typing.Sequence[int], Key], bool]] = None
        self.on_selection_changed: typing.Optional[typing.Callable[[typing.Sequence[typing.Tuple[int, int, int]]], None]] = None
        self.on_current_item_changed: typing.Optional[typing.Callable[[int, int, int], None]] = None
        self.on_item_clicked: typing.Optional[typing.Callable[[int, int, int], bool]] = None
        self.on_item_double_clicked: typing.Optional[typing.Callable[[int, int, int], bool]] = None
        self.on_item_key_pressed: typing.Optional[typing.Callable[[int, int, int, Key], bool]] = None

        def handle_key_pressed(indexes: typing.Sequence[int], key: Key) -> bool:
            if callable(self.on_key_pressed):
                return self.on_key_pressed(indexes, key)
            return False

        self._behavior.on_key_pressed = handle_key_pressed

        def handle_tree_item_changed(index: int, parent_row: int, parent_id: int) -> None:
            if callable(self.on_current_item_changed):
                self.on_current_item_changed(index, parent_row, parent_id)

        self._behavior.on_tree_item_changed = handle_tree_item_changed

        def handle_tree_selection_changed(selected_indexes: typing.Sequence[typing.Tuple[int, int, int]]) -> None:
            if callable(self.on_selection_changed):
                self.on_selection_changed(selected_indexes)

        self._behavior.on_tree_selection_changed = handle_tree_selection_changed

        def handle_tree_item_key_pressed(index: int, parent_row: int, parent_id: int, key: Key) -> bool:
            if callable(self.on_item_key_pressed):
                return self.on_item_key_pressed(index, parent_row, parent_id, key)
            return False

        self._behavior.on_tree_item_key_pressed = handle_tree_item_key_pressed

        def handle_tree_item_clicked(index: int, parent_row: int, parent_id: int) -> bool:
            if callable(self.on_item_clicked):
                return self.on_item_clicked(index, parent_row, parent_id)
            return False

        self._behavior.on_tree_item_clicked = handle_tree_item_clicked

        def handle_tree_item_double_clicked(index: int, parent_row: int, parent_id: int) -> bool:
            if callable(self.on_item_double_clicked):
                return self.on_item_double_clicked(index, parent_row, parent_id)
            return False

        self._behavior.on_tree_item_double_clicked = handle_tree_item_double_clicked

    def close(self) -> None:
        self.__item_model_controller = None
        self.on_key_pressed = None
        self.on_selection_changed = None
        self.on_current_item_changed = None
        self.on_item_clicked = None
        self.on_item_double_clicked = None
        self.on_item_key_pressed = None
        super().close()

    @property
    def _behavior(self) -> TreeWidgetBehavior:
        return typing.cast(TreeWidgetBehavior, super()._behavior)

    @property
    def selection_mode(self) -> str:
        return self._behavior.selection_mode

    @selection_mode.setter
    def selection_mode(self, value: str) -> None:
        self._behavior.selection_mode = value

    @property
    def item_model_controller(self) -> typing.Any:
        return self._behavior.item_model_controller

    @item_model_controller.setter
    def item_model_controller(self, value: typing.Any) -> None:
        self._behavior.item_model_controller = value

    def set_current_row(self, index: int, parent_row: int, parent_id: int) -> None:
        self._behavior.set_current_row(index, parent_row, parent_id)

    def clear_current_row(self) -> None:
        self._behavior.clear_current_row()

    def size_to_content(self) -> None:
        self._behavior.size_to_content()


class MenuAction:

    def __init__(self, action_id: typing.Optional[str] = None) -> None:
        self.action_id = action_id
        self.on_triggered: typing.Optional[typing.Callable[[], None]] = None
        self.on_ui_activity: typing.Optional[typing.Callable[[], None]] = None

    def close(self) -> None:
        self.on_triggered = None
        self.on_ui_activity = None

    def _register_ui_activity(self) -> None:
        if callable(self.on_ui_activity):
            self.on_ui_activity()

    @property
    def title(self) -> str:
        raise NotImplementedError()

    @title.setter
    def title(self, value: str) -> None:
        raise NotImplementedError()

    @property
    def checked(self) -> bool:
        raise NotImplementedError()

    @checked.setter
    def checked(self, checked: bool) -> None:
        raise NotImplementedError()

    @property
    def enabled(self) -> bool:
        raise NotImplementedError()

    @enabled.setter
    def enabled(self, enabled: bool) -> None:
        raise NotImplementedError()

    def trigger(self) -> None:
        # there is a choice whether to handle top level exceptions here (for all callers) or
        # in the caller. it is placed here because it may eliminate duplicated code.
        # if there is a reason to hoist this to the caller at some point, this should be considered
        # a relatively arbitrary decision that it is here.
        try:
            self._register_ui_activity()
            if callable(self.on_triggered):
                self.on_triggered()
        except Exception as e:
            import traceback
            traceback.print_exc()

    def create(self, document_window: Window, title: str, key_sequence: typing.Optional[str], role: typing.Optional[str]) -> None:
        raise NotImplementedError()

    def apply_state(self, menu_item_state: MenuItemState) -> None:
        if menu_item_state and menu_item_state.title is not None:
            self.title = menu_item_state.title
        else:
            self.title = self.title
        if menu_item_state and menu_item_state.checked is not None:
            self.checked = menu_item_state.checked
        else:
            self.checked = False
        if menu_item_state and menu_item_state.enabled is not None:
            self.enabled = menu_item_state.enabled
        else:
            self.enabled = False


class MenuItem:

    def __init__(self, *, action: typing.Optional[MenuAction] = None, is_separator: bool = False,
                 sub_menu: typing.Optional[Menu] = None) -> None:
        self.action = action
        self.is_separator = is_separator
        self.sub_menu = sub_menu

    def close(self) -> None:
        if self.action:
            self.action.close()
        if self.sub_menu:
            self.sub_menu.close()
        self.action = None
        self.sub_menu = None

    @property
    def title(self) -> str:
        # NOTE: for backwards compatibility only (tests 0.14)
        if self.action:
            return self.action.title
        elif self.is_separator:
            return "separator"
        elif self.sub_menu:
            return self.sub_menu.title if self.sub_menu.title else "sub_menu"
        else:
            return "item"

    @property
    def callback(self) -> typing.Optional[typing.Callable[[], None]]:
        # NOTE: for backwards compatibility only (tests 0.14)
        return typing.cast(typing.Optional[typing.Callable[[], None]], getattr(self.action, "callback", None))

    def __str__(self) -> str:
        if self.action:
            return f"action {self.action.title}"
        elif self.is_separator:
            return "separator"
        elif self.sub_menu:
            return self.sub_menu.title if self.sub_menu.title else "sub_menu"
        else:
            return "?item"


class Menu:

    def __init__(self, document_window: Window, title: str, menu_id: str) -> None:
        self.document_window = document_window
        self.title = title
        self.menu_id = menu_id
        self.on_about_to_show: typing.Optional[typing.Callable[[], None]] = None
        self.on_about_to_hide: typing.Optional[typing.Callable[[], None]] = None
        self.__items: typing.List[MenuItem] = list()

    def close(self) -> None:
        for item in self.__items:
            item.close()
        self.__items = typing.cast(typing.Any, None)
        self.document_window = typing.cast(typing.Any, None)
        self.on_about_to_show = None
        self.on_about_to_hide = None

    def destroy(self) -> None:
        # for backwards compatibility
        if self.__items:
            self.close()

    @property
    def items(self) -> typing.Sequence[MenuItem]:
        return self.__items

    def get_menu_actions(self) -> typing.Sequence[MenuAction]:
        return [item.action for item in self.__items if item.action]

    def _register_ui_activity(self) -> None:
        self.document_window._register_ui_activity()

    def about_to_show(self) -> None:
        self._register_ui_activity()
        if callable(self.on_about_to_show):
            self.on_about_to_show()

    def about_to_hide(self) -> None:
        self._register_ui_activity()
        if callable(self.on_about_to_hide):
            self.on_about_to_hide()

    def _prepare_action(self, action: MenuAction, title: str, action_id: typing.Optional[str],
                        callback: typing.Callable[[], None], key_sequence: typing.Optional[str] = None,
                        role: typing.Optional[str] = None) -> None:
        # subclasses should call this to prepare a newly created action
        action.create(self.document_window, title, key_sequence, role)
        action.action_id = action_id
        action.on_triggered = callback
        action.on_ui_activity = self._register_ui_activity

    def add_menu_item(self, title: str, callback: typing.Callable[[], None], key_sequence: typing.Optional[str] = None,
                      role: typing.Optional[str] = None, action_id: typing.Optional[str] = None) -> MenuAction:
        raise NotImplementedError()

    def add_action(self, action: MenuAction) -> None:
        raise NotImplementedError()

    def add_sub_menu(self, title: str, menu: Menu) -> None:
        raise NotImplementedError()

    def add_separator(self) -> None:
        raise NotImplementedError()

    def insert_menu_item(self, title: str, before_action: MenuAction, callback: typing.Callable[[], None],
                         key_sequence: typing.Optional[str] = None, role: typing.Optional[str] = None,
                         action_id: typing.Optional[str] = None) -> None:
        raise NotImplementedError()

    def insert_separator(self, before_action: MenuAction) -> None:
        raise NotImplementedError()

    def remove_action(self, action: MenuAction) -> None:
        raise NotImplementedError()

    def _item_added(self, *, action: typing.Optional[MenuAction] = None, is_separator: bool = False,
                    sub_menu: typing.Optional[Menu] = None) -> None:
        # subclasses should call this when adding a menu item
        item = MenuItem(action=action, is_separator=is_separator, sub_menu=sub_menu)
        self.__items.append(item)
        self.document_window._menu_item_added(item)

    def _item_inserted(self, before_action: MenuAction, *, action: typing.Optional[MenuAction] = None,
                       is_separator: bool = False, sub_menu: typing.Optional[Menu] = None) -> None:
        # subclasses should call this when adding a menu item
        index = 0
        for index, item in enumerate(self.__items):
            if before_action == item.action:
                break
        item = MenuItem(action=action, is_separator=is_separator, sub_menu=sub_menu)
        self.__items.insert(index, item)
        self.document_window._menu_item_added(item)

    def _item_removed(self, action: MenuAction) -> None:
        # subclasses should call this when inserting a menu.
        index = 0
        for index, item in enumerate(self.__items):
            if action == item.action:
                self.document_window._menu_item_removed(self.__items.pop(index))
                break

    def popup(self, gx: int, gy: int) -> None:
        raise NotImplementedError()


class DockWidget:

    def __init__(self, document_window: Window, widget: Widget, panel_id: str, title: str, positions: typing.Sequence[str], position: str) -> None:
        self.document_window = document_window
        self.document_window.register_dock_widget(self)
        self.widget = widget
        self.widget._set_root_container(typing.cast("Window", self))
        self.panel_id = panel_id
        self.title = title
        self.positions = positions
        self.position = position
        self.on_will_close: typing.Optional[typing.Callable[[], None]] = None
        self.on_will_hide: typing.Optional[typing.Callable[[], None]] = None
        self.on_will_show: typing.Optional[typing.Callable[[], None]] = None
        self.on_size_changed: typing.Optional[typing.Callable[[int, int], None]] = None
        self.on_focus_changed: typing.Optional[typing.Callable[[bool], None]] = None
        self.on_ui_activity: typing.Optional[typing.Callable[[], None]] = None
        self.on_logical_dpi_changed: typing.Optional[typing.Callable[[float], None]] = None
        self.on_physical_dpi_changed: typing.Optional[typing.Callable[[float], None]] = None
        self.on_screen_changed: typing.Optional[typing.Callable[[], None]] = None
        self.on_color_scheme_changed: typing.Optional[typing.Callable[[str], None]] = None
        self.size: typing.Optional[Geometry.IntSize] = None

    def close(self) -> None:
        self.widget.close()
        self.widget = typing.cast(typing.Any, None)
        self.document_window.unregister_dock_widget(self)
        self.document_window = typing.cast(typing.Any, None)
        self.on_will_close = None
        self.on_will_hide = None
        self.on_will_show = None
        self.on_size_changed = None
        self.on_focus_changed = None
        self.on_ui_activity = None
        self.on_logical_dpi_changed = None
        self.on_physical_dpi_changed = None
        self.on_screen_changed = None
        self.on_color_scheme_changed = None

    def _register_ui_activity(self) -> None:
        if callable(self.on_ui_activity):
            self.on_ui_activity()

    @property
    def width(self) -> int:
        size = self.size
        if size is not None:
            return size.width
        return 0

    @property
    def height(self) -> int:
        size = self.size
        if size is not None:
            return size.height
        return 0

    def refocus_widget(self, widget: Widget) -> None:
        # if the widget is one that retains focus, refocus it. this ensures that pressing return
        # on a line edit widget in a dock widget selects all for a nice user experience.
        if widget.does_retain_focus:
            widget.refocus()
        else:
            self.document_window.refocus_widget(widget)

    @property
    def focus_widget(self) -> typing.Optional[Widget]:
        def match_native_widget(widget: Widget) -> typing.Optional[Widget]:
            if widget.focused:
                return widget
            for child_widget in widget._contained_widgets:
                matched_widget = match_native_widget(child_widget)
                if matched_widget:
                    return matched_widget
            return None

        return match_native_widget(self.widget)

    def queue_task(self, task: typing.Callable[[], None]) -> None:
        self.document_window.queue_task(task)

    def clear_queued_tasks(self) -> None:
        self.document_window.clear_queued_tasks()

    def add_task(self, key: str, task: typing.Callable[[], None]) -> None:
        self.document_window.add_task(key + str(id(self)), task)

    def clear_task(self, key: str) -> None:
        self.document_window.clear_task(key + str(id(self)))

    def periodic(self) -> None:
        self.widget.periodic()

    @property
    def toggle_action(self) -> MenuAction:
        raise NotImplementedError()

    @property
    def visible(self) -> bool:
        return False

    def show(self) -> None:
        self._register_ui_activity()

    def hide(self) -> None:
        self._register_ui_activity()

    def _handle_close(self) -> None:
        if callable(self.on_will_close):
            self.on_will_close()

    def _handle_hide(self) -> None:
        if callable(self.on_will_hide):
            self.on_will_hide()

    def _handle_show(self) -> None:
        if callable(self.on_will_show):
            self.on_will_show()

    def _handle_size_changed(self, size: Geometry.IntSize) -> None:
        self._register_ui_activity()
        self.size = size
        if callable(self.on_size_changed):
            self.on_size_changed(self.width, self.height)

    def _handle_focus_in(self) -> None:
        self._register_ui_activity()
        if callable(self.on_focus_changed):
            self.on_focus_changed(True)

    def _handle_focus_out(self) -> None:
        self._register_ui_activity()
        if callable(self.on_focus_changed):
            self.on_focus_changed(False)

    def _handle_logical_dpi_changed(self, dpi: float) -> None:
        if callable(self.on_logical_dpi_changed):
            self.on_logical_dpi_changed(dpi)

    def _handle_physical_dpi_changed(self, dpi: float) -> None:
        if callable(self.on_physical_dpi_changed):
            self.on_physical_dpi_changed(dpi)

    def _handle_screen_changed(self) -> None:
        if callable(self.on_screen_changed):
            self.on_screen_changed()

    def _handle_color_scheme_changed(self, color_scheme: str) -> None:
        if callable(self.on_color_scheme_changed):
            self.on_color_scheme_changed(color_scheme)


class Window:

    def __init__(self, parent_window: typing.Optional[Window], title: str) -> None:
        self.parent_window = parent_window
        self.root_widget: typing.Optional[Widget] = None
        self.has_event_loop = True
        self.window_style = "window"
        # Python 3.9+: weakref.ReferenceType[DockWidget]
        self.__dock_widget_weak_refs: typing.List[typing.Any] = list()
        self.on_periodic: typing.Optional[typing.Callable[[], None]] = None
        self.on_queue_task: typing.Optional[typing.Callable[[typing.Callable[[], None]], None]] = None
        self.on_clear_queued_tasks: typing.Optional[typing.Callable[[], None]] = None
        self.on_add_task: typing.Optional[typing.Callable[[str, typing.Callable[[], None]], None]] = None
        self.on_clear_task: typing.Optional[typing.Callable[[str], None]] = None
        self.on_about_to_show: typing.Optional[typing.Callable[[], None]] = None
        self.on_about_to_close: typing.Optional[typing.Callable[[str, str], None]] = None
        self.on_key_pressed: typing.Optional[typing.Callable[[Key], bool]] = None
        self.on_key_released: typing.Optional[typing.Callable[[Key], bool]] = None
        self.on_activation_changed: typing.Optional[typing.Callable[[bool], None]] = None
        self.on_size_changed: typing.Optional[typing.Callable[[int, int], None]] = None
        self.on_position_changed: typing.Optional[typing.Callable[[int, int], None]] = None
        self.on_refocus_widget: typing.Optional[typing.Callable[[Widget], None]] = None
        self.on_ui_activity: typing.Optional[typing.Callable[[], None]] = None
        self.on_logical_dpi_changed: typing.Optional[typing.Callable[[float], None]] = None
        self.on_physical_dpi_changed: typing.Optional[typing.Callable[[float], None]] = None
        self.on_screen_changed: typing.Optional[typing.Callable[[], None]] = None
        self.on_color_scheme_changed: typing.Optional[typing.Callable[[str], None]] = None
        self.pos_x: typing.Optional[int] = None
        self.pos_y: typing.Optional[int] = None
        self.width: typing.Optional[int] = None
        self.height: typing.Optional[int] = None
        self.__title = title if title is not None else str()
        self.__window_file_path: typing.Optional[pathlib.Path] = None
        self.__menus: typing.List[Menu] = list()
        self.__menu_map: typing.Dict[str, Menu] = dict()
        self.__menu_actions: typing.Dict[str, MenuAction] = dict()

    def close(self) -> None:
        # this is a callback and should not be invoked directly from Python;
        # call request_close instead.
        self.parent_window = None
        if self.root_widget:
            # care must be taken here that this method isn't somehow called while handling
            # the click in a widget. closing the root widget will destroy the widget hierarchy
            # which will potentially crash if the widget in the hierarchy triggered this call
            # directly. using request_close from a separate window will mitigate this.
            self.root_widget.close()
            self.root_widget = None
        for menu in reversed(self.__menus):
            menu.close()
        self.__menus = typing.cast(typing.Any, None)
        self.__menu_map = typing.cast(typing.Any, None)
        self.__menu_actions = typing.cast(typing.Any, None)
        self.on_periodic = None
        self.on_queue_task = None
        self.on_clear_queued_tasks = None
        self.on_add_task = None
        self.on_clear_task = None
        self.on_about_to_show = None
        self.on_about_to_close = None
        self.on_key_pressed = None
        self.on_key_released = None
        self.on_activation_changed = None
        self.on_size_changed = None
        self.on_position_changed = None
        self.on_refocus_widget = None
        self.on_ui_activity = None
        self.on_logical_dpi_changed = None
        self.on_physical_dpi_changed = None
        self.on_screen_changed = None
        self.on_color_scheme_changed = None

    def request_close(self) -> None:
        raise NotImplementedError()

    def _register_ui_activity(self) -> None:
        if callable(self.on_ui_activity):
            self.on_ui_activity()

    # attach the root widget to this window
    # the root widget must respond to _set_root_container
    def attach(self, root_widget: Widget) -> None:
        self.root_widget = root_widget
        self.root_widget._set_root_container(self)
        self._attach_root_widget(root_widget)

    def _attach_root_widget(self, root_widget: typing.Optional[Widget]) -> None:
        raise NotImplementedError()

    def detach(self) -> None:
        assert self.root_widget is not None
        self.root_widget.close()
        self.root_widget = None

    @property
    def dock_widgets(self) -> typing.List[DockWidget]:
        return [dock_widget_weak_ref() for dock_widget_weak_ref in self.__dock_widget_weak_refs]

    def register_dock_widget(self, dock_widget: DockWidget) -> None:
        self.__dock_widget_weak_refs.append(weakref.ref(dock_widget))

    def unregister_dock_widget(self, dock_widget: DockWidget) -> None:
        self.__dock_widget_weak_refs.remove(weakref.ref(dock_widget))

    def queue_task(self, task: typing.Callable[[], None]) -> None:
        if self.on_queue_task:
            self.on_queue_task(task)

    def clear_queued_tasks(self) -> None:
        if self.on_clear_queued_tasks:
            self.on_clear_queued_tasks()

    def add_task(self, key: str, task: typing.Callable[[], None]) -> None:
        if self.on_add_task:
            self.on_add_task(key + str(id(self)), task)

    def clear_task(self, key: str) -> None:
        if self.on_clear_task:
            self.on_clear_task(key + str(id(self)))

    @property
    def focus_widget(self) -> typing.Optional[Widget]:
        return self._get_focus_widget()

    def _get_focus_widget(self) -> typing.Optional[Widget]:
        raise NotImplementedError()

    def refocus_widget(self, widget: Widget) -> None:
        if callable(self.on_refocus_widget):
            self.on_refocus_widget(widget)

    def get_file_paths_dialog(self, title: str, directory: str, filter: str, selected_filter: typing.Optional[str] = None) -> typing.Tuple[typing.List[str], str, str]:
        raise NotImplementedError()

    def get_file_path_dialog(self, title: str, directory: str, filter: str, selected_filter: typing.Optional[str] = None) -> typing.Tuple[typing.List[str], str, str]:
        raise NotImplementedError()

    def get_color_dialog(self, title: str, color: typing.Optional[str], show_alpha: bool) -> typing.Optional[str]:
        raise NotImplementedError()

    def get_save_file_path(self, title: str, directory: str, filter: str, selected_filter: typing.Optional[str] = None) -> typing.Tuple[str, str, str]:
        raise NotImplementedError()

    def create_dock_widget(self, widget: Widget, panel_id: str, title: str, positions: typing.Sequence[str], position: str) -> DockWidget:
        raise NotImplementedError()

    def tabify_dock_widgets(self, dock_widget1: DockWidget, dock_widget2: DockWidget) -> None:
        raise NotImplementedError()

    @property
    def screen_size(self) -> Geometry.IntSize:
        return self._get_screen_size()

    def _get_screen_size(self) -> Geometry.IntSize:
        raise NotImplementedError()

    @property
    def screen_logical_dpi(self) -> float:
        return self._get_screen_logical_dpi()

    def _get_screen_logical_dpi(self) -> float:
        raise NotImplementedError()

    @property
    def screen_physical_dpi(self) -> float:
        return self._get_screen_physical_dpi()

    def _get_screen_physical_dpi(self) -> float:
        raise NotImplementedError()

    @property
    def display_scaling(self) -> float:
        return self._get_display_scaling()

    def _get_display_scaling(self) -> float:
        raise NotImplementedError()

    # call show to display the window.
    def show(self, size: typing.Optional[Geometry.IntSize] = None, position: typing.Optional[Geometry.IntPoint] = None) -> None:
        raise NotImplementedError()

    def activate(self) -> None:
        raise NotImplementedError()

    def fill_screen(self) -> None:
        raise NotImplementedError()

    @property
    def title(self) -> str:
        return self.__title

    @title.setter
    def title(self, value: str) -> None:
        self.__title = value
        self._set_title(value)

    def _set_title(self, value: str) -> None:
        raise NotImplementedError()

    @property
    def window_file_path(self) -> typing.Optional[pathlib.Path]:
        return self.__window_file_path

    @window_file_path.setter
    def window_file_path(self, value: typing.Optional[pathlib.Path]) -> None:
        self.__window_file_path = value
        self._set_window_file_path(value)

    def _set_window_file_path(self, value: typing.Optional[pathlib.Path]) -> None:
        raise NotImplementedError()

    def set_palette_color(self, role: str, r: int, g: int, b: int, a: int) -> None:
        raise NotImplementedError()

    def set_window_style(self, styles: typing.Sequence[str]) -> None:
        raise NotImplementedError()

    def set_attributes(self, attributes: typing.Sequence[str]) -> None:
        raise NotImplementedError()

    def _handle_periodic(self) -> None:
        # there is a choice whether to handle top level exceptions here (for all callers) or
        # in the caller. it is placed here because it may eliminate duplicated code.
        # if there is a reason to hoist this to the caller at some point, this should be considered
        # a relatively arbitrary decision that it is here.
        try:
            if self.root_widget:
                self.root_widget.periodic()
            if self.on_periodic:
                self.on_periodic()
        except Exception as e:
            import traceback
            traceback.print_exc()

    def _handle_about_to_show(self) -> None:
        if self.on_about_to_show:
            self.on_about_to_show()

    def _handle_activation_changed(self, activated: bool) -> None:
        if self.on_activation_changed:
            self.on_activation_changed(activated)

    def _handle_about_to_close(self, geometry: str, state: str) -> None:
        if self.on_about_to_close:
            self.on_about_to_close(geometry, state)

    def _handle_key_pressed(self, key: Key) -> bool:
        if callable(self.on_key_pressed):
            return self.on_key_pressed(key)
        return False

    def _handle_key_released(self, key: Key) -> bool:
        if callable(self.on_key_released):
            return self.on_key_released(key)
        return False

    def add_menu(self, title: str, menu_id: typing.Optional[str] = None) -> Menu:
        raise NotImplementedError()

    def insert_menu(self, title: str, before_menu: Menu, menu_id: typing.Optional[str] = None) -> Menu:
        raise NotImplementedError()

    def _menu_added(self, menu: Menu) -> None:
        # subclasses should call this when adding a menu
        self.__menus.append(menu)
        if menu.menu_id:
            self.__menu_map[menu.menu_id] = menu

    def _menu_inserted(self, menu: Menu, before_menu: Menu) -> None:
        # subclasses should call this when inserting a menu.
        self.__menus.insert(self.__menus.index(before_menu), menu)
        if menu.menu_id:
            self.__menu_map[menu.menu_id] = menu

    def _menu_item_added(self, item: MenuItem) -> None:
        if item.action and item.action.action_id:
            self.__menu_actions[item.action.action_id] = item.action

    def _menu_item_removed(self, item: MenuItem) -> None:
        if item.action and item.action.action_id:
            self.__menu_actions.pop(item.action.action_id)

    def get_menu_action(self, action_id: str) -> MenuAction:
        return self.__menu_actions.get(action_id, MenuAction())

    def get_menu_actions(self) -> typing.Sequence[MenuAction]:
        return list(self.__menu_actions.values())

    @property
    def menus(self) -> typing.List[Menu]:
        return self.__menus

    def get_menu(self, menu_id: str) -> typing.Optional[Menu]:
        return self.__menu_map.get(menu_id)

    def save(self) -> typing.Tuple[str, str]:
        raise NotImplementedError()

    def restore(self, geometry: str, state: str) -> None:
        raise NotImplementedError()

    def _handle_size_changed(self, width: int, height: int) -> None:
        self.width = width
        self.height = height
        if callable(self.on_size_changed):
            self.on_size_changed(self.width, self.height)

    def _handle_position_changed(self, x: int, y: int) -> None:
        self.pos_x = x
        self.pos_y = y
        if callable(self.on_position_changed):
            self.on_position_changed(self.pos_x, self.pos_y)

    @property
    def position(self) -> Geometry.IntPoint:
        raise NotImplementedError()

    @property
    def size(self) -> Geometry.IntSize:
        raise NotImplementedError()

    def _handle_logical_dpi_changed(self, dpi: float) -> None:
        if callable(self.on_logical_dpi_changed):
            self.on_logical_dpi_changed(dpi)

    def _handle_physical_dpi_changed(self, dpi: float) -> None:
        if callable(self.on_physical_dpi_changed):
            self.on_physical_dpi_changed(dpi)

    def _handle_screen_changed(self) -> None:
        if callable(self.on_screen_changed):
            self.on_screen_changed()

    def _handle_color_scheme_changed(self, color_scheme: str) -> None:
        if callable(self.on_color_scheme_changed):
            self.on_color_scheme_changed(color_scheme)


class ToleranceType(enum.IntEnum):
    CURSOR = 1


class TruncateModeType(enum.IntEnum):
    LEFT = 0
    RIGHT = 1
    MIDDLE = 2
    NONE = 3


class PersistenceHandler(abc.ABC):
    # interface for handling backwards compatibility of persistent values from a UserInterface object

    @abc.abstractmethod
    def get_string(self, key: str) -> typing.Tuple[bool, str]: ...

    @abc.abstractmethod
    def set_string(self, key: str, value: str) -> bool: ...

    @abc.abstractmethod
    def remove_key(self, key: str) -> bool: ...


class StringPersistentModel(Model.PropertyModel[str]):
    def __init__(self, ui: UserInterface, storage_key: str, value: typing.Optional[str] = None):
        self.__storage_key = storage_key
        self.__ui = ui
        super().__init__(self.__ui.get_persistent_string(self.__storage_key, value))

    def _set_value(self, value: typing.Optional[str]) -> None:
        super()._set_value(value)
        if value is not None:
            self.__ui.set_persistent_string(self.__storage_key, str(value))
        else:
            self.__ui.remove_persistent_key(self.__storage_key)


class FloatPersistentModel(Model.PropertyModel[float]):
    def __init__(self, ui: UserInterface, storage_key: str, value: typing.Optional[float] = None):
        self.__storage_key = storage_key
        self.__ui = ui
        value_str = self.__ui.get_persistent_string(self.__storage_key, None)
        value = Converter.FloatToStringConverter(pass_none=True).convert_back(value_str)
        super().__init__(value)

    def _set_value(self, value: typing.Optional[float]) -> None:
        super()._set_value(value)
        if value is not None:
            self.__ui.set_persistent_string(self.__storage_key, str(value))
        else:
            self.__ui.remove_persistent_key(self.__storage_key)


class ButtonGroup(typing.Protocol):
    on_button_clicked: typing.Optional[typing.Callable[[str], None]]
    def close(self) -> None: ...
    def add_button(self, radio_button: RadioButtonWidget, button_id: str) -> None: ...
    def remove_button(self, radio_button: RadioButtonWidget) -> None: ...
    def clicked(self, button_id: str) -> None: ...


class UserInterface(abc.ABC):

    @abc.abstractmethod
    def close(self) -> None:
        ...

    @abc.abstractmethod
    def run(self, app: Application.BaseApplication) -> None:
        ...

    @abc.abstractmethod
    def request_quit(self) -> None:
        ...

    # data objects

    @abc.abstractmethod
    def create_mime_data(self) -> MimeData:
        ...

    @abc.abstractmethod
    def create_item_model_controller(self) -> typing.Any:
        ...

    @abc.abstractmethod
    def create_button_group(self) -> ButtonGroup:
        ...

    # window elements

    @abc.abstractmethod
    def create_document_window(self, title: typing.Optional[str] = None, parent_window: typing.Optional[Window] = None) -> Window:
        ...

    @abc.abstractmethod
    def destroy_document_window(self, document_window: Window) -> None:
        ...

    # user interface elements

    @abc.abstractmethod
    def create_row_widget(self, alignment: typing.Optional[str] = None, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> BoxWidget:
        ...

    @abc.abstractmethod
    def create_column_widget(self, alignment: typing.Optional[str] = None, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> BoxWidget:
        ...

    @abc.abstractmethod
    def create_splitter_widget(self, orientation: typing.Optional[str] = None, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> SplitterWidget:
        ...

    @abc.abstractmethod
    def create_tab_widget(self, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> TabWidget:
        ...

    @abc.abstractmethod
    def create_stack_widget(self, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> StackWidget:
        ...

    @abc.abstractmethod
    def create_group_widget(self, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> GroupWidget:
        ...

    @abc.abstractmethod
    def create_scroll_area_widget(self, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> ScrollAreaWidget:
        ...

    @abc.abstractmethod
    def create_combo_box_widget(self, items: typing.Optional[typing.Sequence[typing.Any]] = None, item_getter: typing.Optional[typing.Callable[[typing.Any], str]] = None, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> ComboBoxWidget:
        ...

    @abc.abstractmethod
    def create_push_button_widget(self, text: typing.Optional[str] = None, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> PushButtonWidget:
        ...

    @abc.abstractmethod
    def create_radio_button_widget(self, text: typing.Optional[str] = None, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> RadioButtonWidget:
        ...

    @abc.abstractmethod
    def create_check_box_widget(self, text: typing.Optional[str] = None, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> CheckBoxWidget:
        ...

    @abc.abstractmethod
    def create_label_widget(self, text: typing.Optional[str] = None, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> LabelWidget:
        ...

    @abc.abstractmethod
    def create_slider_widget(self, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> SliderWidget:
        ...

    @abc.abstractmethod
    def create_progress_bar_widget(self, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> ProgressBarWidget:
        ...

    @abc.abstractmethod
    def create_line_edit_widget(self, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> LineEditWidget:
        ...

    @abc.abstractmethod
    def create_text_browser_widget(self, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> TextBrowserWidget:
        ...

    @abc.abstractmethod
    def create_text_edit_widget(self, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> TextEditWidget:
        ...

    @abc.abstractmethod
    def create_canvas_widget(self, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None, *, layout_render: typing.Optional[str] = None) -> CanvasWidget:
        ...

    @abc.abstractmethod
    def create_tree_widget(self, properties: typing.Optional[typing.Mapping[str, typing.Any]] = None) -> TreeWidget:
        ...

    # file i/o

    @abc.abstractmethod
    def load_rgba_data_from_file(self, filename: str) -> typing.Optional[DrawingContext.RGBA32Type]:
        ...

    def load_bitmap_from_file(self, filename: str) -> typing.Optional[Bitmap.Bitmap]:
        return Bitmap.Bitmap(rgba_bitmap_data=self.load_rgba_data_from_file(filename))

    @abc.abstractmethod
    def save_rgba_data_to_file(self, data: DrawingContext.RGBA32Type, filename: str, format: typing.Optional[str]) -> None:
        ...

    def save_bitmap_to_file(self, bitmap: Bitmap.Bitmap, filename: str, format: typing.Optional[str]) -> None:
        assert bitmap.rgba_bitmap_data is not None
        self.save_rgba_data_to_file(bitmap.rgba_bitmap_data, filename, format)

    @abc.abstractmethod
    def get_existing_directory_dialog(self, title: str, directory: str) -> typing.Tuple[str, str]:
        ...

    @abc.abstractmethod
    def get_file_paths_dialog(self, title: str, directory: str, filter: str, selected_filter: typing.Optional[str] = None) -> typing.Tuple[typing.List[str], str, str]:
        ...

    @abc.abstractmethod
    def get_file_path_dialog(self, title: str, directory: str, filter: str, selected_filter: typing.Optional[str] = None) -> typing.Tuple[typing.List[str], str, str]:
        ...

    @abc.abstractmethod
    def get_save_file_path(self, title: str, directory: str, filter: str, selected_filter: typing.Optional[str] = None) -> typing.Tuple[str, str, str]:
        ...

    # persistence (associated with application)

    @abc.abstractmethod
    def get_data_location(self) -> str:
        ...

    @abc.abstractmethod
    def get_document_location(self) -> str:
        ...

    @abc.abstractmethod
    def get_temporary_location(self) -> str:
        ...

    @abc.abstractmethod
    def get_configuration_location(self) -> str:
        ...

    @abc.abstractmethod
    def set_persistence_handler(self, handler: PersistenceHandler) -> None:
        ...

    @abc.abstractmethod
    def get_persistent_string(self, key: str, default_value: typing.Optional[str] = None) -> str:
        ...

    @abc.abstractmethod
    def set_persistent_string(self, key: str, value: str) -> None:
        ...

    @abc.abstractmethod
    def get_persistent_object(self, key: str, default_value: typing.Any=None) -> typing.Any:
        ...

    @abc.abstractmethod
    def set_persistent_object(self, key: str, value: typing.Any) -> None:
        ...

    @abc.abstractmethod
    def remove_persistent_key(self, key: str) -> None:
        ...

    def create_persistent_string_model(self, key: str, default_value: typing.Optional[str] = None) -> Model.PropertyModel[str]:
        return StringPersistentModel(self, key, default_value)

    def create_persistent_float_model(self, key: str, default_value: typing.Optional[float] = None) -> Model.PropertyModel[float]:
        return FloatPersistentModel(self, key, default_value)

    # clipboard

    @abc.abstractmethod
    def clipboard_clear(self) -> None:
        ...

    @abc.abstractmethod
    def clipboard_mime_data(self) -> MimeData:
        ...

    @abc.abstractmethod
    def clipboard_set_mime_data(self, mime_data: MimeData) -> None:
        ...

    @abc.abstractmethod
    def clipboard_set_text(self, text: str) -> None:
        ...

    @abc.abstractmethod
    def clipboard_text(self) -> str:
        ...

    # misc

    @abc.abstractmethod
    def set_application_info(self, application_name: str, organization_name: str, organization_domain: str) -> None:
        ...

    @abc.abstractmethod
    def create_rgba_image(self, drawing_context: DrawingContext.DrawingContext, width: int, height: int) -> typing.Optional[DrawingContext.RGBA32Type]:
        ...

    def create_bitmap(self, drawing_context: DrawingContext.DrawingContext, shape: Geometry.IntSize) -> typing.Optional[Bitmap.Bitmap]:
        rgba_bitmap_data = self.create_rgba_image(drawing_context, shape.width, shape.height)
        return Bitmap.Bitmap(rgba_bitmap_data=rgba_bitmap_data)

    @abc.abstractmethod
    def get_font_metrics(self, font: str, text: str) -> FontMetrics:
        ...

    @abc.abstractmethod
    def truncate_string_to_width(self, font_str: str, text: str, pixel_width: int, mode: TruncateModeType) -> str:
        ...

    @abc.abstractmethod
    def get_qt_version(self) -> str:
        ...

    @abc.abstractmethod
    def get_tolerance(self, tolerance_type: ToleranceType) -> float:
        ...

    @abc.abstractmethod
    def create_context_menu(self, document_window: Window) -> Menu:
        ...

    @abc.abstractmethod
    def create_sub_menu(self, document_window: Window, title: typing.Optional[str] = None, menu_id: typing.Optional[str] = None) -> Menu:
        ...

    @abc.abstractmethod
    def get_color_dialog(self, title: str, color: typing.Optional[str], show_alpha: bool) -> typing.Optional[str]:
        ...

    @abc.abstractmethod
    def get_keyboard_modifiers(self, query: bool = False) -> KeyboardModifiers:
        ...
