From 0e5db97180b2dac76926dbc0e0eee521fe1d6ccf Mon Sep 17 00:00:00 2001 From: william Date: Mon, 24 Aug 2026 11:02:54 +0200 Subject: [PATCH] Let Texte/Titre elements bind directly to a single row of another object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "Donnée liée" settings group to Texte/Titre: pick an object, optionally match it to the current game state via 1-2 filter conditions (same engine as the data-repeater's filter, including {{Objet.champ}} cross-references), and use {{champ}} in the text content to show a field from that one matching row. Unlike the data repeater — built for showing a list of rows — this covers displaying a single computed value (e.g. the objective of the level matching the game's current parcours/level) without wrapping it in a repeater. --- screens/rendering/render_element_html.py | 4 ++ screens/rendering/resolve_bound_row.py | 38 +++++++++++++++++++ .../control_groups/data_binding_controls.py | 26 +++++++++++++ screens/widgets/controls_with_values.py | 2 +- screens/widgets/registry.py | 7 +++- 5 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 screens/rendering/resolve_bound_row.py create mode 100644 screens/widgets/control_groups/data_binding_controls.py diff --git a/screens/rendering/render_element_html.py b/screens/rendering/render_element_html.py index bd978cae..213790c5 100644 --- a/screens/rendering/render_element_html.py +++ b/screens/rendering/render_element_html.py @@ -14,6 +14,7 @@ from .render_repeater import _render_repeater from .render_jauge import _render_jauge from .render_overlay import _render_overlay from .render_onglets import _render_onglets +from .resolve_bound_row import _resolve_bound_row_ctx def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_direction=None): @@ -29,6 +30,9 @@ def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_ pose physiquement cet élément, s'il y en a un — elle influence la largeur par défaut de cet élément (voir _style_string).""" meta = widget_meta(el.get("widget")) + bound_ctx = _resolve_bound_row_ctx(slug, el.get("attributes") or {}) + if bound_ctx: + ctx = {**(ctx or {}), **bound_ctx} special = meta.get("special_render") if special == "table": return _render_table(el, meta, slug, children_map, ctx, parent_flex_direction) diff --git a/screens/rendering/resolve_bound_row.py b/screens/rendering/resolve_bound_row.py new file mode 100644 index 00000000..cc706783 --- /dev/null +++ b/screens/rendering/resolve_bound_row.py @@ -0,0 +1,38 @@ +import db + +from .filter_repeater_rows import filter_repeater_rows + + +def _resolve_bound_row_ctx(slug, attrs): + """Si cet élément est lié à un objet de données (attr:_data_definition_id — + réglage "Lier à un objet de données" des widgets Texte/Titre, voir + DATA_BINDING_CONTROLS), retrouve LA ligne qui correspond aux 1 ou 2 + filtres réglés (_data_filtre*/_data_filtre2*) — même moteur que le + filtre du Répéteur de données, voir filter_repeater_rows.py — et + renvoie ses champs sous forme de {nom_du_champ: valeur en texte}, + exploitable par {{champ}} dans le contenu. Permet d'afficher UNE seule + valeur calculée (ex: l'objectif du niveau en cours) sans avoir besoin de + poser un Répéteur, qui affiche lui plusieurs lignes. Renvoie None si + rien n'est lié ou si aucune ligne ne correspond.""" + definition_id = attrs.get("_data_definition_id") + if not definition_id or not slug: + return None + try: + definition = db.get_definition(slug, int(definition_id)) + except (TypeError, ValueError): + return None + if not definition: + return None + rows = db.list_rows(slug, definition) + rows = filter_repeater_rows(slug, definition, rows, { + "_filtre_champ": attrs.get("_data_filtre_champ"), + "_filtre_operateur": attrs.get("_data_filtre_operateur"), + "_filtre_valeur": attrs.get("_data_filtre_valeur"), + "_filtre2_champ": attrs.get("_data_filtre2_champ"), + "_filtre2_operateur": attrs.get("_data_filtre2_operateur"), + "_filtre2_valeur": attrs.get("_data_filtre2_valeur"), + }) + if not rows: + return None + row = rows[0] + return {f["name"]: str(row.get(db.slugify(f["name"]).replace("-", "_"), "") or "") for f in definition["fields"]} diff --git a/screens/widgets/control_groups/data_binding_controls.py b/screens/widgets/control_groups/data_binding_controls.py new file mode 100644 index 00000000..6207c70f --- /dev/null +++ b/screens/widgets/control_groups/data_binding_controls.py @@ -0,0 +1,26 @@ +from ..controls.c_text import c_text +from ..controls.c_select import c_select +from ...flow.constants import CONDITION_OPERATORS + +# Permet à un Texte/Titre d'afficher directement la valeur d'un champ d'un +# AUTRE objet de données, sans avoir besoin de le poser dans un Répéteur — +# utile pour une valeur unique calculée (ex: l'objectif du niveau en cours) +# plutôt qu'une liste. La ligne affichée est celle qui correspond aux 1 ou 2 +# filtres réglés ci-dessous (mêmes réglages, même moteur que ceux du +# Répéteur de données — voir filter_repeater_rows.py) ; sans filtre, c'est la +# ligne la plus récente de l'objet choisi (convention déjà utilisée par la +# Jauge pour un objet "état de partie" qui ne garde qu'une seule ligne). +DATA_BINDING_CONTROLS = [ + c_select("data_definition_id", "Lier à un objet de données (pour {{champ}} ci-dessus)", + "attr:_data_definition_id", [("", "(sélectionner un objet)")], default="", group="Donnée liée"), + c_text("data_filtre_champ", "Trouver la ligne où ce champ", + "attr:_data_filtre_champ", placeholder="ex. number", default="", group="Donnée liée"), + c_select("data_filtre_operateur", "...est", "attr:_data_filtre_operateur", CONDITION_OPERATORS, default="egal", group="Donnée liée"), + c_text("data_filtre_valeur", "...cette valeur, ou {{NomObjet.champ}} pour une donnée qui change en cours de partie", + "attr:_data_filtre_valeur", placeholder="ex. 3 ou {{game.current_level}}", default="", group="Donnée liée"), + c_text("data_filtre2_champ", "ET (optionnel) : ce 2ᵉ champ", + "attr:_data_filtre2_champ", placeholder="ex. parcour_id", default="", group="Donnée liée"), + c_select("data_filtre2_operateur", "...est", "attr:_data_filtre2_operateur", CONDITION_OPERATORS, default="egal", group="Donnée liée"), + c_text("data_filtre2_valeur", "...cette valeur, ou {{NomObjet.champ}}", + "attr:_data_filtre2_valeur", placeholder="ex. 1 ou {{game.current_parcours}}", default="", group="Donnée liée"), +] diff --git a/screens/widgets/controls_with_values.py b/screens/widgets/controls_with_values.py index 1cc3b500..b2cc63c2 100644 --- a/screens/widgets/controls_with_values.py +++ b/screens/widgets/controls_with_values.py @@ -24,7 +24,7 @@ def controls_with_values(slug, el): result = [] for control in meta.get("controls", []): control = dict(control) - if control["key"] == "definition_id": + if control["key"] in ("definition_id", "data_definition_id"): defs = db.list_definitions(slug) control["options"] = [("", "(sélectionner un objet)")] + [(str(d["id"]), d["name"]) for d in defs] if control["key"] == "template_type_id": diff --git a/screens/widgets/registry.py b/screens/widgets/registry.py index 0184bd59..757de5c8 100644 --- a/screens/widgets/registry.py +++ b/screens/widgets/registry.py @@ -8,6 +8,7 @@ from .controls.c_toggle_token import c_toggle_token from .controls.c_align import c_align from .controls.c_select import c_select from .control_groups.text_style_controls_extra import TEXT_STYLE_CONTROLS_EXTRA +from .control_groups.data_binding_controls import DATA_BINDING_CONTROLS from .font_options import FONT_OPTIONS from ..flow.constants import CONDITION_OPERATORS @@ -16,7 +17,8 @@ WIDGETS = { "label": "Texte", "icon": "📝", "tag": "p", "has_content": True, "w": 40, "h": 10, "default_content": "Texte du paragraphe.", "controls": [ - c_textarea("content", "Texte", "content", default="Texte du paragraphe.", group="Contenu"), + c_textarea("content", "Texte — {{champ}} affiche la valeur de ce champ sur la ligne trouvée ci-dessous", "content", default="Texte du paragraphe.", group="Contenu"), + ] + DATA_BINDING_CONTROLS + [ c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=64, default=16, group="Texte"), c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"), c_align("text-align", "Alignement", "style:text-align", group="Texte"), @@ -30,9 +32,10 @@ WIDGETS = { "label": "Titre", "icon": "🔠", "tag": None, "has_content": True, "w": 55, "h": 10, "default_content": "Titre", "controls": [ - c_textarea("content", "Texte", "content", default="Titre", group="Contenu"), + c_textarea("content", "Texte — {{champ}} affiche la valeur de ce champ sur la ligne trouvée ci-dessous", "content", default="Titre", group="Contenu"), c_select("niveau", "Importance du titre", "attr:_niveau", [("1", "Très grand — titre principal"), ("2", "Grand"), ("3", "Moyen")], default="2", group="Contenu"), + ] + DATA_BINDING_CONTROLS + [ c_slider("font-size", "Taille du texte", "style:font-size", min=14, max=80, default=32, group="Texte"), c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"), c_align("text-align", "Alignement", "style:text-align", default="left", group="Texte"),