"""
GUI app constants and helper functions
--------------------------------------
"""
import re
from math import cos, sin, sqrt
from typing import Any, Callable, Optional, Type, Union
from ae.base import NAME_PARTS_SEP, norm_path, os_path_dirname, os_path_join, snake_to_camel # type: ignore
from ae.system import os_platform, stack_var # type: ignore
from ae.paths import path_name, placeholder_path, FilesRegister # type: ignore
from ae.i18n import get_text, translation # type: ignore
APP_STATE_SECTION_NAME = 'aeAppState' #: config section name to store app state
APP_STATE_VERSION_VAR_NAME = 'app_state_version' #: config variable name to store the current application state version
MIN_FONT_SIZE = 15.0 #: minimum (see :attr:`~ae.kivy.apps.FrameworkApp.min_font_size`) and
MAX_FONT_SIZE = 99.0 #: .. maximum font size in pixels
# inks for the non-colors black, gray and white (!= 0/1 to differentiate from the framework's pure black/white colors)
COLOR_BLACK = [0.009, 0.006, 0.003, 1.0] #: black ink
COLOR_DARK_GREY = [0.309, 0.306, 0.303, 1.0] #: dark grey ink
COLOR_GREY = [0.509, 0.506, 0.503, 1.0] #: grey ink
COLOR_LIGHT_GREY = [0.699, 0.696, 0.693, 1.0] #: light grey ink
COLOR_WHITE = [0.999, 0.996, 0.993, 1.0] #: white ink
RELIEF_ANGLE_BEG = 69 #: beginning angle for ellipse drawings via :func:`relief_colors`
RELIEF_ANGLE_END = 249 #: ending angle for ellipse drawings via :func:`relief_colors`
THEME_DARK_BACKGROUND_COLOR = COLOR_BLACK #: dark theme background color in rgba(0.0 ... 1.0)
THEME_DARK_FONT_COLOR = COLOR_WHITE #: dark theme font color in rgba(0.0 ... 1.0)
THEME_LIGHT_BACKGROUND_COLOR = COLOR_WHITE #: light theme background color in rgba(0.0 ... 1.0)
THEME_LIGHT_FONT_COLOR = COLOR_BLACK #: light theme font color in rgba(0.0 ... 1.0)
THEME_SECTION_PREFIX = 'aeTheme_' #: config-files section name prefix for to store app theme vars
THEME_VARIABLE_PREFIX = 'MUSASV_' #: mangle app state var names to not be interpreted as user-specific
FLOW_KEY_SEP = ':' #: separator character between flow action/object and flow key
FLOW_ACTION_RE = re.compile("[a-z0-9]+") #: regular expression detecting invalid characters in flow action string
FLOW_OBJECT_RE = re.compile("[A-Za-z0-9_]+") #: regular expression detecting invalid characters in flow object string
APP_STATE_HELP_ID_PREFIX = 'help_app_state#' #: message id prefix for app state change help texts
FLOW_HELP_ID_PREFIX = 'help_flow#' #: message id prefix for flow change help texts
TOUR_PAGE_HELP_ID_PREFIX = 'tour_page#' #: message id prefix of tour page text/dict
TOUR_START_DELAY_DEF = 0.15 #: default value of tour start delay in seconds
TOUR_EXIT_DELAY_DEF = 0.45 #: default value of tour exit delay in seconds
PORTIONS_IMAGES = FilesRegister() #: app image files register
PORTIONS_SOUNDS = FilesRegister() #: app audio/sound files register
REGISTERED_TOURS: dict[str, Type] = {} #: map(name: class) of all registered tour classes
AnchorSpecType = tuple[float, float, str] #: (see return value of :func:`anchor_spec`)
AppStatesType = dict[str, Any] #: app state config variable type
# pylint: disable=invalid-name
ColorRGB = Union[tuple[float, float, float], list[float]] #: color red, green and blue parts
# pylint: disable=invalid-name
ColorRGBA = Union[tuple[float, float, float, float], list[float]] #: ink is rgb color and alpha
ColorOrInk = Union[ColorRGB, ColorRGBA] #: color or ink type
EventKwargsType = dict[str, Any] #: change flow event kwargs type
ExplainedMatcherType = Union[Callable[[Any], bool], str] #: single explained widget matcher type
HelpVarsType = dict[str, Any] #: help context variables for help text rendering
PopupsToCloseType = Union[int, tuple] #: popups to close on button-press/flow-change
ReliefColors = Union[tuple[ColorRGB, ColorRGB], tuple] #: tuple of top/bottom relief colors or empty tuple
[docs]
def anchor_layout_x(anchor_spe: AnchorSpecType, layout_width: float, win_width: float) -> float:
""" calculate the anchor's x position of the layout box.
:param anchor_spe: :data:`AnchorSpecType` instance (:func:`anchor_spec` return) with anchor position/direction.
:param layout_width: anchor layout width.
:param win_width: app window width.
:return: absolute x coordinate within the app window of anchor layout.
"""
anchor_x, _anchor_y, anchor_dir = anchor_spe
if anchor_dir == 'l':
return anchor_x - layout_width
if anchor_dir == 'r':
return anchor_x
return min(max(0.0, anchor_x - layout_width / 2), win_width - layout_width)
[docs]
def anchor_layout_y(anchor_spe: AnchorSpecType, layout_height: float, win_height: float) -> float:
""" calculate the layout box y position of an anchor.
:param anchor_spe: :data:`AnchorSpecType` tuple with anchor position and direction.
:param layout_height: anchor layout height.
:param win_height: app window height.
:return: the absolute y coordinate in the app window of anchor layout.
"""
_anchor_x, anchor_y, anchor_dir = anchor_spe
if anchor_dir == 'i':
return anchor_y
if anchor_dir == 'd':
return anchor_y - layout_height
return min(max(0.0, anchor_y - layout_height / 2), win_height - layout_height)
[docs]
def anchor_points(font_size: float, anchor_spe: AnchorSpecType) -> tuple[float, ...]:
""" recalculate points of the anchor triangle drawing.
:param font_size: font_size to calculate the size (radius == hypotenuse / 2) of the anchor triangle.
:param anchor_spe: anchor specification tuple: x/y coordinates and direction - see :func:`anchor_spec` return.
:return: 6-item-tuple with the three x and y coordinates of the anchor triangle edges.
"""
if not anchor_spe:
return () # return empty tuple to prevent run-time-error at kv build/init
radius = font_size * 0.69
anchor_x, anchor_y, anchor_dir = anchor_spe
return (anchor_x - (radius if anchor_dir in 'id' else 0),
anchor_y - (radius if anchor_dir in 'lr' else 0),
anchor_x + (0 if anchor_dir in 'id' else radius * (-1 if anchor_dir == 'r' else 1)),
anchor_y + (0 if anchor_dir in 'lr' else radius * (-1 if anchor_dir == 'i' else 1)),
anchor_x + (radius if anchor_dir in 'id' else 0),
anchor_y + (radius if anchor_dir in 'lr' else 0),
)
# pylint: disable=too-many-arguments,too-many-positional-arguments
[docs]
def anchor_spec(wid_x: float, wid_y: float, wid_width: float, wid_height: float, win_width: float, win_height: float
) -> AnchorSpecType:
""" calculate anchor center pos (x, y) and anchor direction to the targeted widget.
:param wid_x: the absolute x coordinate in the main app window of the targeted widget.
:param wid_y: the absolute y coordinate in the main app window of the targeted widget.
:param wid_width: width of targeted widget.
:param wid_height: height of targeted widget.
:param win_width: app window width.
:param win_height: app window height.
:return: tooltip anchor specification tuple (:data:`AnchorSpecType`) with the three items:
* anchor_x (the absolute anchor center x-coordinate in the app main window),
* anchor_y (the absolute anchor center y-coordinate in the app main window) and
* anchor_dir (anchor direction: 'r'=right, 'i'=increase-y, 'l'=left, 'd'=decrease-y)
.. note::
the direction in the y-axis got named increase for higher y values and `decrease` for
lower y values to support different coordinate systems of the GUI frameworks.
e.g., Kivy has the y-axis zero value at the bottom of the app window, whereas in
enaml/Qt it is at the top.
"""
max_width = win_width - wid_x - wid_width
if max_width < wid_x:
max_width = wid_x
anchor_dir_x = 'l'
else:
anchor_dir_x = 'r'
max_height = win_height - wid_y - wid_height
if max_height < wid_y:
max_height = wid_y
anchor_dir_y = 'd'
else:
anchor_dir_y = 'i'
if max_width > max_height:
anchor_dir = anchor_dir_x
anchor_x = wid_x + (0 if anchor_dir_x == 'l' else wid_width)
anchor_y = wid_y + wid_height / 2
else:
anchor_dir = anchor_dir_y
anchor_x = wid_x + wid_width / 2
anchor_y = wid_y + (0 if anchor_dir_y == 'd' else wid_height)
return anchor_x, anchor_y, anchor_dir
[docs]
def brighten_color(color_or_ink: ColorOrInk, factor: float = 0.3) -> ColorOrInk:
""" brightens the specified color/ink without changing an optionally passed alpha/occupancy value.
:param color_or_ink: the color or ink to be brightened.
:param factor: the factor to brighten the color or ink by. its value must range between -1 and 1,
0 results in no brightening at all (original color), positive values are using the
more complex HSV algorithm, and negative values the efficient RGB brightening algorithm.
so +1 returns the brightest value of the specified color, whereas -1 results as white.
:return: the brightened color or ink, as the same type as the input.
"""
if factor > 0:
hsv = color_to_hsv(color_or_ink)
hsv = hsv[0], hsv[1] * (1.0 - factor), min(1.0, hsv[2] * (1.0 + factor))
rgb = list(color_from_hsv(hsv))
else:
rgb = list(min(1.0, _ + (1.0 - _) * -factor) for _ in color_or_ink[:3])
return rgb + list(color_or_ink[3:])
[docs]
def color_from_hsv(hsv: tuple[float, float, float]) -> ColorRGB:
""" convert HSV-color (H: 0-360, S: 0-1, V: 0-1) into its corresponding RGB color.
:param hsv: HSV color tuple to convert into its corresponding RGB value.
:return: the corresponding RGB color, with normalized color channel values between 0 and 1.
"""
h, s, v = hsv
h_i = int(h / 60)
f = (h / 60) - h_i
p = v * (1 - s)
q = v * (1 - f * s)
t = v * (1 - (1 - f) * s)
if h_i == 0:
r, g, b = v, t, p
elif h_i == 1:
r, g, b = q, v, p
elif h_i == 2:
r, g, b = p, v, t
elif h_i == 3:
r, g, b = p, q, v
elif h_i == 4:
r, g, b = t, p, v
else: # if h_i == 5:
r, g, b = v, p, q
return r, g, b
[docs]
def color_to_hsv(color_or_ink: ColorOrInk) -> tuple[float, float, float]:
""" convert color/ink to its HSV values.
:param color_or_ink: the color or ink to convert into its hsv value.
:return: the HSV values (H: 0-360, S: 0-1, V: 0-1) of the specified color or ink.
"""
r, g, b, *_a = color_or_ink
max_wert: float = max(r, g, b)
delta = max_wert - min(r, g, b)
if delta == 0:
h: float = 0
elif max_wert == r:
h = 60 * (((g - b) / delta) % 6)
elif max_wert == g:
h = 60 * (((b - r) / delta) + 2)
else: # if max_wert == b:
h = 60 * (((r - g) / delta) + 4)
return h, delta / max_wert if max_wert else 0, max_wert
[docs]
def complementary_color(color_or_ink: ColorOrInk, delta_h: float = 180.0) -> ColorOrInk:
""" determine the complementary color or ink without changing an optionally passed alpha/occupancy value.
:param color_or_ink: the color or ink to convert into its complementary value.
:param delta_h: specify a value between -360 and +360 to get a complementary color. specify -180 or 180
to get the opposite value using the HSV algorithm, or a
zero value to get the opposite color with the more efficient RGB algorithm.
any other value will use the HSV algorithm, which adds this value as a delta angle in
the HSV color circle. some harmonic delta angles are:
* 30: analogous colors
* 90, 180 and 270: tetradic colors 2, 3 and 4
* 120 and 240: triadic color 2 and 3
* 180: opposite color
:return: the complementary color or ink, as the same type as the input. note that the HSV algorithm
does not change any monochrom/greyscale colors; pass a zero value to the delta_h parameter
to get an "opposite"-like color from a gray-scale color (see the unit tests on how
the RGB algorithm does behave in relation to the HSV algorithm).
"""
if delta_h:
hsv = color_to_hsv(color_or_ink)
hsv = (hsv[0] + delta_h + 360) % 360, hsv[1], hsv[2]
rgb = list(color_from_hsv(hsv))
else:
rgb = list(1.0 - _ for _ in color_or_ink[:3])
return rgb + list(color_or_ink[3:])
[docs]
def darken_color(color_or_ink: ColorOrInk, factor: float = 0.3) -> ColorOrInk:
""" darkens the specified color/ink without changing an optionally passed alpha/occupancy value.
:param color_or_ink: the color or ink to be darkened.
:param factor: the factor to darken the color or ink by. its value must range between -1 and 1,
where 0 results in no darkening (original color), positive values are using the
more complex HSV algorithm, and negative values the efficient RGB darkening algorithm.
so +1 returns the darkest value of the specified color, whereas -1 results in a black color.
:return: the darkened color or ink, as the same type as the input.
"""
if factor > 0:
hsv = color_to_hsv(color_or_ink)
hsv = hsv[0], min(1.0, hsv[1] * (1.0 + factor)), hsv[2] * (1.0 - factor)
rgb = list(color_from_hsv(hsv))
else:
rgb = list(_ * (1.0 + factor) for _ in color_or_ink[:3])
return rgb + list(color_or_ink[3:])
[docs]
def ellipse_polar_radius(ell_a: float, ell_b: float, radian: float) -> float:
""" calculate the radius from polar for the given ellipse and radian.
:param ell_a: ellipse x-radius.
:param ell_b: ellipse y-radius.
:param radian: angle radian.
:return: ellipse radius at the angle specified by :paramref:`~ellipse_polar_radius.radian`.
"""
return ell_a * ell_b / sqrt((ell_a * sin(radian)) ** 2 + (ell_b * cos(radian)) ** 2)
[docs]
def ensure_tap_kwargs_refs(init_kwargs: EventKwargsType, tap_widget: Any):
""" ensure that the passed widget.__init__ kwargs dict contains a reference to itself within kwargs['tap_kwargs'].
:param init_kwargs: kwargs of the widgets __init__ method.
:param tap_widget: reference to the tap widget.
this alternative version is only 10 % faster but much less clean than the current implementation::
if 'tap_kwargs' not in init_kwargs:
init_kwargs['tap_kwargs'] = {}
tap_kwargs = init_kwargs['tap_kwargs']
if 'tap_widget' not in tap_kwargs:
tap_kwargs['tap_widget'] = tap_widget
if 'popup_kwargs' not in tap_kwargs:
tap_kwargs['popup_kwargs'] = {}
popup_kwargs = tap_kwargs['popup_kwargs']
if 'opener' not in popup_kwargs:
popup_kwargs['opener'] = tap_kwargs['tap_widget']
"""
init_kwargs['tap_kwargs'] = tap_kwargs = init_kwargs.get('tap_kwargs', {})
tap_kwargs['tap_widget'] = tap_widget = tap_kwargs.get('tap_widget', tap_widget)
tap_kwargs['popup_kwargs'] = popup_kwargs = tap_kwargs.get('popup_kwargs', {})
popup_kwargs['opener'] = popup_kwargs.get('opener', tap_widget)
[docs]
def flow_action(flow_id: str) -> str:
""" determine the action string of a flow_id.
:param flow_id: flow id.
:return: flow action string.
"""
return flow_action_split(flow_id)[0]
[docs]
def flow_action_split(flow_id: str) -> tuple[str, str]:
""" split flow id string into the action part and the rest.
:param flow_id: flow id.
:return: tuple of (flow action string, flow obj and key string)
"""
idx = flow_id.find(NAME_PARTS_SEP)
if idx != -1:
return flow_id[:idx], flow_id[idx + 1:]
return flow_id, ""
[docs]
def flow_change_confirmation_event_name(flow_id: str) -> str:
""" determine the name of the event method for the change confirmation of the passed flow_id.
:param flow_id: flow id.
:return: tuple with 2 items containing the flow action and the object name (and id).
"""
flow, _index = flow_key_split(flow_id)
action, obj = flow_action_split(flow)
return f'on_{obj}_{action}'
[docs]
def flow_class_name(flow_id: str, name_suffix: str) -> str:
""" determine the class name for the given flow id and class name suffix.
:param flow_id: flow id.
:param name_suffix: class name suffix.
:return: name of the class. please note that the flow action `open` will not be added
to the returned class name.
"""
flow, _index = flow_key_split(flow_id)
action, obj = flow_action_split(flow)
if action == 'open':
action = ''
return f'{snake_to_camel(obj)}{action.capitalize()}{name_suffix}'
[docs]
def flow_key(flow_id: str) -> str:
""" return the key of a flow id.
:param flow_id: flow id string.
:return: flow key string.
"""
_action_object, index = flow_key_split(flow_id)
return index
[docs]
def flow_key_split(flow_id: str) -> tuple[str, str]:
""" split flow id into an action, object and flow key.
:param flow_id: flow id to split.
:return: tuple of (flow action and object string, flow key string).
"""
idx = flow_id.find(FLOW_KEY_SEP)
if idx != -1:
return flow_id[:idx], flow_id[idx + 1:]
return flow_id, ""
[docs]
def flow_object(flow_id: str) -> str:
""" determine the object string of the passed flow_id.
:param flow_id: flow id.
:return: flow object string.
"""
return flow_action_split(flow_key_split(flow_id)[0])[1]
[docs]
def flow_path_id(flow_path: list[str], path_index: int = -1) -> str:
""" determine the flow id of the newest/last entry in the flow_path.
:param flow_path: flow path to determine the flow id from its newest/latest entry.
:param path_index: index in the flow_path.
:return: flow id string or empty string if the flow path is empty or index does not exist.
"""
if len(flow_path) >= (abs(path_index) if path_index < 0 else path_index + 1):
return flow_path[path_index]
return ''
[docs]
def flow_path_strip(flow_path: list[str]) -> list[str]:
""" return a copy of the specified flow_path with all non-enter actions stripped from the end.
:param flow_path: flow path list to strip.
:return: stripped flow path copy.
"""
deep = len(flow_path)
while deep and flow_action(flow_path_id(flow_path, path_index=deep - 1)) != 'enter':
deep -= 1
return flow_path[:deep]
[docs]
def help_id_tour_class(help_id: str) -> Optional[Any]:
""" determine the tour class if passed help id has attached tour pages.
:param help_id: help id to determine the tour class from.
:return: tour class of an existing tour for the passed help id or None if no associated tour exists.
"""
tour_id = help_sub_id(help_id)
if tour_id:
return tour_id_class(tour_id)
return None
[docs]
def help_sub_id(help_id: str) -> str:
""" determine sub id (flow id, tour id or app state name) of the current/specified/passed help id.
opposite of :func:`id_of_flow_help` / :func:`id_of_state_help` / :func:`id_of_tour_help`.
:param help_id: help id to extract the sub id from.
:return: flow id, tour id, app state name or empty string if help id does not contain a sub id.
"""
if help_id.startswith(APP_STATE_HELP_ID_PREFIX):
return help_id[len(APP_STATE_HELP_ID_PREFIX):]
if help_id.startswith(FLOW_HELP_ID_PREFIX):
return help_id[len(FLOW_HELP_ID_PREFIX):]
if help_id.startswith(TOUR_PAGE_HELP_ID_PREFIX):
return help_id[len(TOUR_PAGE_HELP_ID_PREFIX):]
return ''
[docs]
def id_of_flow(action: str, obj: str = '', key: str = '') -> str:
""" create flow id string.
:param action: flow action string.
:param obj: flow object (defined by app project).
:param key: flow index/item_id/field_id/... (defined by app project).
:return: complete flow_id string.
"""
assert action == '' or FLOW_ACTION_RE.fullmatch(action), \
f"flow action only allows lowercase letters and digits: got '{action}'"
assert obj == '' or FLOW_OBJECT_RE.fullmatch(obj), \
f"flow object only allows letters, digits and underscores: got '{obj}'"
cid = f'{action}{NAME_PARTS_SEP if action and obj else ""}{obj}'
if key:
cid += f'{FLOW_KEY_SEP}{key}'
return cid
[docs]
def id_of_flow_help(flow_id: str) -> str:
""" compose help id for specified flow id.
:param flow_id: flow id to make help id for.
:return: help id for the specified flow id.
"""
return f'{FLOW_HELP_ID_PREFIX}{flow_id}'
[docs]
def id_of_state_help(app_state_name: str) -> str:
""" compose help id for app state name/key.
:param app_state_name: name of the app state variable.
:return: help id for the specified app state.
"""
return f'{APP_STATE_HELP_ID_PREFIX}{app_state_name}'
[docs]
def id_of_tour_help(page_id: str) -> str:
""" compose help id for specified tour page id.
:param page_id: tour page id to make help id for.
:return: help id for the specified tour page.
"""
return f'{TOUR_PAGE_HELP_ID_PREFIX}{page_id}'
[docs]
def mix_colors(*colors: ColorOrInk) -> ColorOrInk:
""" mix multiple colors or inks into a single one.
:param colors: colors or inks to mix.
:return: mixed color or ink.
"""
return [sum(_) / len(_) for _ in zip(*colors)]
[docs]
def register_package_images():
""" call from the module scope of the package to register/add the image/img resources path.
no parameters needed because we use here :func:`~ae.system.stack_var` helper function to determine the
module file path via the `__file__` module variable of the caller module in the call stack. in this call
we have to overwrite the default value (:data:`~ae.system.SKIPPED_MODULES`) of the
:paramref:`~ae.system.stack_var.skip_modules` parameter to not skip ae portions that are providing
package resources and are listed in the :data:`~ae.system.SKIPPED_MODULES`, like e.g., :mod:`ae.gui.app` and
:mod:`ae.gui.utils` (passing empty string '' to overwrite the default skip list).
"""
package_path = os_path_dirname(norm_path(stack_var('__file__', '')))
search_path = os_path_join(package_path, 'img/**')
PORTIONS_IMAGES.add_paths(search_path)
[docs]
def register_package_sounds():
""" call from the module scope of the package to register/add sound file resources.
no parameters needed because we use here :func:`~ae.system.stack_var` helper function to determine the
module file path via the `__file__` module variable of the caller module in the call stack. in this call,
we have to overwrite the default value (:data:`~ae.system.SKIPPED_MODULES`) of the
:paramref:`~ae.system.stack_var.skip_modules` parameter to not skip ae portions that are providing
package resources and are listed in the :data:`~ae.system.SKIPPED_MODULES`.
"""
package_path = os_path_dirname(norm_path(stack_var('__file__', '')))
search_path = os_path_join(package_path, 'snd/**')
PORTIONS_SOUNDS.add_paths(search_path)
[docs]
def relief_colors(color_or_ink: Optional[ColorOrInk] = None, top_factor: float = 0.6, bottom_factor: float = 0.3,
sunken: bool = False) -> ReliefColors:
""" calculate the top-left and bottom-right colors used for square/ellipse relief effects.
:param color_or_ink: optional color used to calculate the relief colors from. if not specified, then
the :data:`COLOR_GREY` constant will be used to draw the relief effect.
:param top_factor: factor to brighten/darken the top-left part of a square/ellipse with a relief effect,
via the HSV calculation method. pass a negative factor to use the more efficient
RGB method.
:param bottom_factor: factor to brighten/darken the bottom part of a square/ellipse with a relief effect,
via tbe HSV calculation method. pass a negative factor to use the more efficient
RGB method.
:param sunken: if True, then the top-left part of the relief colors will be darkened instead of
brightened. a raised relief effect will be produced if False (default).
:return: tuple with the brightened/darkened top and bottom colors, calculated from the argument
specified in :paramref:`~relief_colors.color_or_ink`,
or an empty tuple if the alpha value of :paramref:`~relief_colors.color_or_ink` is zero.
"""
if not color_or_ink:
color_or_ink = COLOR_GREY
elif len(color_or_ink) > 3 and not color_or_ink[3]:
return ()
top_fun, bot_fun = (darken_color, brighten_color) if sunken else (brighten_color, darken_color)
return top_fun(color_or_ink[:3], factor=top_factor), bot_fun(color_or_ink[:3], factor=bottom_factor)
[docs]
def replace_flow_action(flow_id: str, new_action: str):
""" replace action in the given flow id.
:param flow_id: flow id.
:param new_action: action to be set/replaced within passed flow id.
:return: flow id with the new action and object/key from passed flow id.
"""
return id_of_flow(new_action, *flow_key_split(flow_action_split(flow_id)[1]))
[docs]
def tour_help_translation(page_id: str) -> Optional[Union[str, dict[str, str]]]:
""" determine help translation for the passed page id (flow id or app state name).
:param page_id: tour page id.
:return: help translation text/dict (if exists) or None if translation is not found.
"""
return (translation_short_help_id(id_of_flow_help(page_id))[0] or
translation_short_help_id(id_of_state_help(page_id))[0])
[docs]
def tour_id_class(tour_id: str) -> Optional[Any]:
""" determine the tour class of the passed tour id.
:param tour_id: tour/flow id to determine tour class for.
:return: tour class of an existing tour for the passed tour id or None if no tour exists.
"""
return REGISTERED_TOURS.get(flow_class_name(tour_id, 'Tour'))
[docs]
def translation_short_help_id(help_id: str) -> tuple[Optional[Union[str, dict[str, str]]], str]:
""" check if a help text exists for the passed help id.
:param help_id: help id to check if a translation/help with texts exists.
:return: tuple of translation text/dict (if exists) and maybe shortened help id(removed detail)
or tuple of (None, help_id) if translation is not found.
"""
trans_text_or_dict = translation(help_id)
short_help_id = help_id
if not trans_text_or_dict and FLOW_KEY_SEP in help_id:
short_help_id = help_id[:help_id.index(FLOW_KEY_SEP)] # remove detail (e.g. flow key or app state value)
trans_text_or_dict = translation(short_help_id)
return trans_text_or_dict, short_help_id
[docs]
def update_tap_kwargs(widget_or_kwargs: Union[EventKwargsType, Any], popup_kwargs: Optional[EventKwargsType] = None,
**tap_kwargs) -> EventKwargsType:
""" update or simulate a widget's tap_kwargs property and return the updated dictionary (for kv rule of tap_kwargs).
:param widget_or_kwargs: either the tap widget (with optional tap_kwargs property, to be extended),
or a tap_kwargs dict to be updated (returning an extended shallow copy of it).
:param popup_kwargs: dict with items to update popup_kwargs key of.
:param tap_kwargs: additional tap_kwargs items to update.
:return: tap_kwargs dict extended with the specified argument values.
if the :paramref:`~update_tap_kwargs.widget` parameter is a widget, then the
'opener' and 'tap_widget' keys will be set to this widget if they are not already set.
if the :paramref:`~update_tap_kwargs.tap_kwargs` parameter as well as widget.tap_kwargs
are having the key 'popups_to_close', then both values will be returned merged.
"""
if isinstance(widget_or_kwargs, dict):
new_kwargs = widget_or_kwargs.copy() # .copy prevents endless-recursion; Kivy property don't support deepcopy
else:
ini_kwargs = {'tap_kwargs': widget_or_kwargs.tap_kwargs} if hasattr(widget_or_kwargs, 'tap_kwargs') else {}
ensure_tap_kwargs_refs(ini_kwargs, widget_or_kwargs)
new_kwargs = ini_kwargs['tap_kwargs']
if popup_kwargs:
new_kwargs['popup_kwargs'].update(popup_kwargs)
if tap_kwargs:
if popups_to_close := merge_popups_to_close(new_kwargs, tap_kwargs):
tap_kwargs['popups_to_close'] = popups_to_close
new_kwargs.update(tap_kwargs)
return new_kwargs
[docs]
def widget_page_id(wid: Optional[Any]) -> str:
""" determine tour page id of passed widget.
:param wid: widget to determine tour page id from (can be None).
:return: tour page id or empty string if the widget has no page id or is None.
"""
page_id = getattr(wid, 'tap_flow_id', '')
if not page_id:
page_id = getattr(wid, 'app_state_name', '')
if not page_id:
page_id = getattr(wid, 'focus_flow_id', '')
return page_id
# reference imported but unused names in pseudo variable `_d_`, to be available in :meth:`MainAppBase.global_variables`
_d_ = (os_platform, path_name, placeholder_path)
module_globals = globals()
""" used. e.g., for execution/evaluation of dynamic code, expressions and f-strings of the app tour and help systems """