Quellcode für miniworlds.worlds.gui.toolbar
from collections import OrderedDict
from typing import Union
import miniworlds.worlds.gui.gui as gui
import miniworlds.actors.widgets.widget_base as widget_base
import miniworlds.actors.widgets.widget_parts as widget_parts
import miniworlds.worlds.gui.toolbar_mainloop_manager as toolbar_mainloop_manager
[Doku]
class Toolbar(gui.GUI):
"""A panel that holds buttons, labels, and other widgets.
Toolbars are usually docked beside the main world using
`world.camera.add_right(toolbar)` or `world.camera.add_bottom(toolbar)`.
Widgets are added with `toolbar.add(widget)` (or by simply creating them -
they register themselves automatically when their world is set to the toolbar).
When a button inside a toolbar is clicked it broadcasts its label text as
a message. The main world responds via `on_message`.
Examples:
::
toolbar = Toolbar()
toolbar.add(Button("Restart"))
world.camera.add_right(toolbar)
@world.register
def on_message(self, message):
if message == "Restart":
world.reset()
"""
[Doku]
def __init__(self):
"""Create a toolbar.
Examples:
::
toolbar = Toolbar()
toolbar.add(Button("Start Rocket"))
world.camera.add_right(toolbar)
rocket.started = False
@world.register
def on_message(self, message):
if message == "Start Rocket":
rocket.started = True
@rocket.register
def act(self):
if self.started:
self.move()
"""
super().__init__()
self.widgets: OrderedDict[Union[int, str], "widget_base.BaseWidget"] = OrderedDict()
self.timed_widgets: dict[int, "widget_base.BaseWidget"] = dict()
self._padding_top = 10
self._padding_bottom = 0
self._padding_left = 10
self._padding_right = 10
self.row_height = 26
self._background_color = (255, 255, 255, 255)
self.max_row_height = 0
self.fixed_camera = False
@staticmethod
def _get_mainloopmanager_class():
return toolbar_mainloop_manager.ToolbarMainloopManager
[Doku]
def on_change(self):
"""Reflows all widgets after the toolbar size or layout has changed.
This keeps widgets aligned when the toolbar is resized or docked in a
different place.
"""
if hasattr(self, "widgets"):
self.reorder()
@property
def background_color(self):
"""tuple: Toolbar background color.
Examples:
::
toolbar.background_color = (255, 255, 255)
"""
return self.background
@background_color.setter
def background_color(self, value):
self.set_background(value)
@property
def padding_left(self):
"""Defines left margin"""
return self._padding_left
@padding_left.setter
def padding_left(self, value):
self._padding_left = value
self.dirty = 1
@property
def padding_right(self):
"""Defines right margin"""
return self._padding_right
@padding_right.setter
def padding_right(self, value):
self._padding_right = value
self.dirty = 1
@property
def padding_top(self):
"""Defines top margin"""
return self._padding_top
@padding_top.setter
def padding_top(self, value):
self._padding_top = value
self.dirty = 1
@property
def padding_bottom(self):
"""Defines bottom margin"""
return self._padding_bottom
@padding_bottom.setter
def padding_bottom(self, value):
self._padding_bottom = value
self.dirty = 1
def _add_widget(
self,
widget: "widget_base.BaseWidget",
key: Union[int, str, None] = None,
) -> "widget_base.BaseWidget":
widget._resize = False
widget.row_height = self.row_height
if key is None:
# Auto keys are always integers, so custom string keys
# cannot break the max() computation.
key = max((k for k in self.widgets if isinstance(k, int)), default=-1) + 1
elif key in self.widgets:
# Replace the widget under an existing key: remove the old
# widget completely so it does not stay in the world as an
# unreachable, still rendered actor.
old_widget = self.widgets.pop(key)
self._unregister_widget(old_widget)
old_widget.remove()
self.widgets[key] = widget
if getattr(widget, "timed", False):
self.timed_widgets[id(widget)] = widget
# Reorder must be called before camera position is updated
self.reorder()
if not self.fixed_camera and widget.y + widget.height > self.camera.y + self.camera.height:
self.camera.world_size_y = widget.y + widget.height
self.camera.y = widget.y + widget.height - self.camera.height
widget._resize = True
return widget
def _unregister_widget(self, widget: "widget_base.BaseWidget"):
self.timed_widgets.pop(id(widget), None)
[Doku]
def remove(self, item: Union[int, str, "widget_base.BaseWidget"]):
"""
Removes a widget from the toolbar.
The widget is also removed from the toolbar world, so it is no
longer rendered and no longer receives events. The camera is
clamped to the remaining content, so the view does not stay
scrolled past the removed widget.
Args:
item: The widget to remove – either the widget object itself, its
integer index, or its string key.
Returns:
bool: True if the widget was removed, False if no matching
widget or key was found.
.. warning::
Be careful when calling this inside a loop over ``self.widgets``.
"""
if type(item) in [int, str]:
if item in self.widgets:
removed_widget = self.widgets.pop(item)
self._unregister_widget(removed_widget)
removed_widget.remove()
self.reorder()
self._clamp_camera_to_content()
return True
else:
return False
elif isinstance(item, widget_base.BaseWidget):
search_key = None
for key, value in self.widgets.items():
if value == item:
search_key = key
break
if search_key is None:
return False
removed_widget = self.widgets.pop(search_key)
self._unregister_widget(removed_widget)
removed_widget.remove()
self.reorder()
self._clamp_camera_to_content()
return True
else:
raise TypeError(f"item must be of type [int, str, Widget], found {type(item)}")
[Doku]
def has_widget(self, key: Union[int, str]):
"""Checks whether the toolbar contains a widget under *key*.
Args:
key: The widget key (integer auto key or custom string key).
Returns:
bool: True if the widget exists, otherwise False.
"""
if key in self.widgets:
return True
else:
return False
[Doku]
def get_widget(self, key: Union[int, str]) -> "widget_base.BaseWidget":
"""Gets widget by key
Args:
key: The key of the widget (integer auto key or custom string key).
Returns:
The widget stored under *key*.
Raises:
KeyError: If no widget with the given key exists.
"""
if key in self.widgets:
return self.widgets[key]
else:
raise KeyError(f"Toolbar widgets does not contain key {key!r}")
[Doku]
def remove_all_widgets(self):
"""Removes all widgets from the toolbar at once.
The widgets are also removed from the toolbar world. Use this
when rebuilding a menu or replacing all controls. The camera is
reset to the top of the (now empty) toolbar.
"""
for widget in list(self.widgets.values()):
self._unregister_widget(widget)
widget.remove()
self.widgets = OrderedDict()
self._clamp_camera_to_content()
self.dirty = 1
[Doku]
def reorder(self):
"""Recomputes the positions of all toolbar widgets.
Widgets are stacked from top to bottom with the configured paddings and
margins.
"""
if hasattr(self, "widgets") and self.widgets:
actual_height = self.padding_top
for widget in self.widgets.values():
actual_height += widget.margin_top
self._set_widget_width(widget)
widget.topleft = (self.padding_left + widget.margin_left, actual_height)
if self.max_row_height != 0:
widget.height = self.max_row_height
actual_height += widget.height + widget.margin_bottom
def _widgets_total_height(self):
height = self.padding_top
for name, widget in self.widgets.items():
height += widget.margin_top
height += widget.height + widget.margin_bottom
return height + self.padding_bottom
def _clamp_camera_to_content(self):
"""Clamps the camera to the remaining widget content.
After widgets are removed, the world size and the camera position
are reduced to the remaining content, so the view does not stay
scrolled past removed widgets. Toolbars with ``fixed_camera``
are left untouched.
"""
if self.fixed_camera:
return
total_height = self._widgets_total_height()
self.camera.world_size_y = max(self.camera.height, total_height)
max_camera_y = max(0, total_height - self.camera.height)
if self.camera.y > max_camera_y:
self.camera.y = max_camera_y
def _set_widget_width(self, widget):
new_width = self.camera.width - self.padding_left - self.padding_right - widget.margin_left - widget.margin_right
if new_width < 0:
new_width = 0
widget.width = new_width
[Doku]
def send_message(self, text: str) -> None:
"""Sends a broadcast message to the world and all actors.
The message is dispatched through the event system and can be handled
by any registered method in the world or its actors.
Args:
text: The name of the message/event to send.
Example:
>>> toolbar.send_message("open_menu")
"""
self.app.event_manager.to_event_queue("message", text)
[Doku]
def scroll_up(self, value):
"""Scrolls the toolbar view upward by *value* pixels.
Args:
value: Number of pixels to scroll.
"""
if self.can_scroll_up(value):
self.camera.y -= value
self.camera.y = max(0, self.camera.y)
[Doku]
def scroll_down(self, value):
"""Scrolls the toolbar view downward by *value* pixels.
Args:
value: Number of pixels to scroll.
"""
if self.can_scroll_down(value):
self.camera.y += value
[Doku]
def can_scroll_down(self, value):
"""Returns True if the toolbar can scroll downward by *value* pixels.
Args:
value: Number of pixels to scroll by.
Returns:
bool: True if scrolling is possible, False otherwise.
"""
if self._widgets_total_height() < self.camera.y + self.camera.height:
return False
if self.camera.y + value > self.world_size_y - self.camera.height:
return False
else:
return True
[Doku]
def can_scroll_up(self, value):
"""Returns True if the toolbar can scroll upward by *value* pixels.
Args:
value: Number of pixels to scroll by.
Returns:
bool: True if upward scrolling is possible, otherwise False.
"""
if self.camera.y == 0:
return False
else:
return True
[Doku]
def on_new_actor(self, actor):
"""Handles actors that are added to this toolbar world.
Widget actors are registered in the internal widget list and then
resized/reordered so the layout stays consistent.
A widget that is already registered is not registered again: a
world transfer can remove and re-add a widget in the same toolbar,
and a duplicate entry would render the widget twice and inflate
the computed toolbar height.
Args:
actor: The actor that has been added.
"""
if isinstance(actor, widget_parts.WidgetPart):
return
if isinstance(actor, widget_base.BaseWidget):
if any(widget is actor for widget in self.widgets.values()):
return
self._add_widget(actor)
actor.resize()
self.reorder()
[Doku]
def on_remove_actor(self, actor):
"""Handles actors that are removed from this toolbar world.
Widgets can also be removed directly via ``widget.remove()`` (not
only through ``Toolbar.remove()``); this keeps the internal widget
list consistent in that case.
Args:
actor: The actor that has been removed.
"""
if not isinstance(actor, widget_base.BaseWidget):
return
if not hasattr(self, "widgets"):
return
search_key = None
for key, value in self.widgets.items():
if value == actor:
search_key = key
break
if search_key is None:
return
self.widgets.pop(search_key)
self._unregister_widget(actor)
self.reorder()
self._clamp_camera_to_content()