Files
Forge-Engine/screens/widgets/registry.py
T
william 343732d7aa Turn free-typed field/object names into dropdowns in the Répéteur and Donnée liée filters
Two settings previously required typing an exact field or object name by hand:

- "champ" (Répéteur's filtre_champ/filtre2_champ, Donnée liée's data_filtre_champ/data_filtre2_champ) is now a dropdown of the actual fields on the object already chosen for that widget — pre-populated server-side (controls_with_values.py) and live-updated client-side without a reload when you change the object (bindDefinitionFieldSelects(), generalizing the existing Jauge champ/row_id pattern).

- "valeur" (the comparison value, which could already reference another object's field via a "{{Objet.champ}}" string typed by hand) is now a "Valeur fixe" / "Donnée d'un autre objet" toggle — the dynamic mode shows an object dropdown and a dependent field dropdown, and picking from them reconstructs that internal reference string automatically (initFilterValuePickers()/updateFilterValueFromDynamic()). The underlying stored value and _resolve_filter_value's parsing are unchanged, so existing elements using the old typed syntax still load correctly and populate the pickers on open.

Guards against writing a literal "{{...}}" pair directly in the Jinja template source (caught by a real render error while building this — Jinja parses {{/}} anywhere in the file, including inside comments) by building that syntax from split string literals in JS instead.

Verified end to end: repeater panel renders the pickers, saving with a dynamic reference filters correctly against live data (current_level changing which row shows), and reloading pre-selects the right object/field in the dropdowns. Full suite green (89).
2026-08-25 06:32:21 +02:00

337 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from .controls.c_text import c_text
from .controls.c_textarea import c_textarea
from .controls.c_url import c_url
from .controls.c_color import c_color
from .controls.c_slider import c_slider
from .controls.c_checkbox import c_checkbox
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 .control_groups.bulma_controls import (
BULMA_BUTTON_CONTROLS, BULMA_TITLE_CONTROLS, BULMA_PROGRESS_CONTROLS,
BULMA_TABLE_CONTROLS, BULMA_TABS_CONTROLS, BULMA_FIELD_CONTROLS,
)
from .font_options import FONT_OPTIONS
from ..flow.constants import CONDITION_OPERATORS
WIDGETS = {
"texte": {
"label": "Texte", "icon": "📝", "tag": "p", "has_content": True,
"w": 40, "h": 10, "default_content": "Texte du paragraphe.",
"controls": [
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"),
c_checkbox("bold", "Gras", "style:font-weight", "bold", "normal", group="Texte"),
c_checkbox("italic", "Italique", "style:font-style", "italic", "normal", group="Texte"),
c_toggle_token("underline", "Souligné", "style:text-decoration", "underline", group="Texte"),
c_toggle_token("strikethrough", "Barré", "style:text-decoration", "line-through", group="Texte"),
] + TEXT_STYLE_CONTROLS_EXTRA,
},
"titre": {
"label": "Titre", "icon": "🔠", "tag": None, "has_content": True,
"w": 55, "h": 10, "default_content": "Titre",
# class:"title" — poids de police et espacement Bulma par défaut ; la
# taille reste pilotée par le réglage "Taille du texte" ci-dessous
# comme avant (non affecté), Bulma ne fournissant ici que l'habillage.
"fixed_attrs": {"class": "title"},
"controls": [
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 + BULMA_TITLE_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"),
c_checkbox("italic", "Italique", "style:font-style", "italic", "normal", group="Texte"),
c_toggle_token("underline", "Souligné", "style:text-decoration", "underline", group="Texte"),
] + TEXT_STYLE_CONTROLS_EXTRA,
},
"bouton": {
"label": "Bouton", "icon": "🔘", "tag": "button", "has_content": True,
"w": 30, "h": 8, "default_content": "Valider",
# class:"button" — donne au Bouton l'apparence Bulma par défaut
# (padding, poids de police, transitions...) tant qu'aucune couleur/
# taille "libre" n'est personnalisée ci-dessous — voir
# BULMA_BUTTON_CONTROLS et _visible_attrs.py.
"fixed_attrs": {"class": "button"},
"controls": [
c_text("content", "Texte du bouton", "content", default="Valider", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#ffffff", group="Texte"),
c_color("background", "Couleur de fond", "style:background-color", "#5b8cff", group="Texte"),
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=40, default=15, group="Texte", no_freeze=True),
c_slider("border-radius", "Arrondi des coins", "style:border-radius", min=0, max=40, default=8, group="Texte", no_freeze=True),
c_checkbox("submit", "Valide un formulaire", "attr:type", "submit", "button", group="Contenu"),
] + BULMA_BUTTON_CONTROLS + TEXT_STYLE_CONTROLS_EXTRA,
},
"lien": {
"label": "Lien", "icon": "🔗", "tag": "a", "has_content": True,
"w": 25, "h": 6, "default_content": "Cliquez ici",
"controls": [
c_text("content", "Texte du lien", "content", default="Cliquez ici", group="Contenu"),
c_url("href", "Destination", "attr:href", default="#", group="Contenu"),
c_checkbox("new_tab", "Ouvrir dans un nouvel onglet", "attr:target", "_blank", "", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#5b8cff", group="Texte"),
c_toggle_token("underline", "Souligné", "style:text-decoration", "underline", group="Texte"),
] + TEXT_STYLE_CONTROLS_EXTRA,
},
"image": {
"label": "Image", "icon": "🖼️", "tag": "img", "has_content": False, "self_closing": True,
"w": 35, "h": 22, "default_content": "",
"controls": [
c_url("src", "Source de l'image", "attr:src", allow_upload=True, upload_accept="image/*", group="Contenu"),
c_text("alt", "Texte alternatif", "attr:alt", placeholder="décrit l'image pour l'accessibilité", group="Contenu"),
c_select("object-fit", "Ajustement dans son cadre", "style:object-fit",
[("cover", "Remplir en recadrant"), ("contain", "Tout montrer sans déformer"), ("fill", "Étirer pour remplir tout le cadre")],
default="cover", group="Média"),
c_slider("border-radius", "Arrondi des coins", "style:border-radius", min=0, max=60, default=0, group="Média"),
],
},
"video": {
"label": "Vidéo", "icon": "🎬", "tag": "video", "has_content": False,
"w": 40, "h": 25, "default_content": "",
"controls": [
c_url("src", "Source de la vidéo", "attr:src", allow_upload=True, upload_accept="video/*", group="Contenu"),
c_checkbox("controls", "Afficher les contrôles de lecture", "attr:controls", "controls", "", default=True, group="Contenu"),
c_checkbox("autoplay", "Lecture automatique", "attr:autoplay", "autoplay", "", group="Contenu"),
c_checkbox("loop", "Lecture en boucle", "attr:loop", "loop", "", group="Contenu"),
],
},
"champ_texte": {
"label": "Champ — texte", "icon": "⌨️", "tag": "input", "has_content": False, "self_closing": True,
"w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "text", "class": "input"},
"controls": [
c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. Votre réponse", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. reponse1", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
] + BULMA_FIELD_CONTROLS,
},
"champ_email": {
"label": "Champ — email", "icon": "📧", "tag": "input", "has_content": False, "self_closing": True,
"w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "email", "class": "input"},
"controls": [
c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. votre@email.com", default="votre@email.com", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. email", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
] + BULMA_FIELD_CONTROLS,
},
"champ_mot_de_passe": {
"label": "Champ — mot de passe", "icon": "🔒", "tag": "input", "has_content": False, "self_closing": True,
"w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "password", "class": "input"},
"controls": [
c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. Mot de passe", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. mot_de_passe", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
] + BULMA_FIELD_CONTROLS,
},
"case_a_cocher": {
"label": "Case à cocher", "icon": "☑️", "tag": "input", "has_content": True, "special_render": "checkbox_label",
"w": 35, "h": 8, "default_content": "Option",
"controls": [
c_text("content", "Texte affiché", "content", default="Option", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. accepte_cgu", group="Contenu"),
c_text("value", "Valeur envoyée", "attr:value", placeholder="ex. oui", group="Contenu"),
c_checkbox("checked", "Coché par défaut", "attr:checked", "checked", "", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
],
},
"bouton_radio": {
"label": "Bouton radio", "icon": "🔘", "tag": "input", "has_content": True, "special_render": "checkbox_label",
"w": 35, "h": 8, "default_content": "Option",
"controls": [
c_text("content", "Texte affiché", "content", default="Option", group="Contenu"),
c_text("name", "Nom du groupe", "attr:name", placeholder="ex. choix1", group="Contenu"),
c_text("value", "Valeur envoyée", "attr:value", placeholder="ex. optionA", group="Contenu"),
c_checkbox("checked", "Sélectionné par défaut", "attr:checked", "checked", "", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
],
},
"zone_texte": {
"label": "Zone de texte", "icon": "🗒️", "tag": "textarea", "has_content": False,
"w": 40, "h": 18, "default_content": "", "fixed_attrs": {"class": "textarea"},
"controls": [
c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. Votre message", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. message", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
] + BULMA_FIELD_CONTROLS,
},
"liste_deroulante": {
"label": "Liste déroulante", "icon": "🔽", "tag": "select", "has_content": True, "special_render": "select_options",
"w": 35, "h": 8, "default_content": "Option 1\nOption 2\nOption 3",
"controls": [
c_textarea("content", "Options — une par ligne", "content", default="Option 1\nOption 2\nOption 3", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. choix", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
] + BULMA_FIELD_CONTROLS,
},
"groupe_champs": {
"label": "Groupe de champs", "icon": "🗃️", "tag": "fieldset", "has_content": True, "special_render": "fieldset_legend",
"w": 55, "h": 30, "default_content": "Groupe de champs",
"controls": [
c_text("content", "Titre du groupe", "content", default="Groupe de champs", group="Contenu"),
],
},
"liste": {
"label": "Liste à puces", "icon": "📋", "tag": "ul", "has_content": True,
"w": 40, "h": 22, "default_content": "Premier élément\nDeuxième élément\nTroisième élément",
"controls": [
c_textarea("content", "Éléments de la liste — un par ligne", "content",
default="Premier élément\nDeuxième élément\nTroisième élément", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=40, default=16, group="Texte"),
],
},
"liste_numerotee": {
"label": "Liste numérotée", "icon": "🔢", "tag": "ol", "has_content": True,
"w": 40, "h": 22, "default_content": "Premier élément\nDeuxième élément\nTroisième élément",
"controls": [
c_textarea("content", "Éléments de la liste — un par ligne", "content",
default="Premier élément\nDeuxième élément\nTroisième élément", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=40, default=16, group="Texte"),
],
},
"tableau": {
"label": "Tableau", "icon": "🗂️", "tag": "table", "has_content": True, "special_render": "table",
"w": 55, "h": 25, "default_content": "Colonne 1;Colonne 2\nValeur A;Valeur B",
"fixed_attrs": {"class": "table"},
"controls": [
c_textarea("content", "Contenu — une ligne par rangée, colonnes séparées par ;", "content",
default="Colonne 1;Colonne 2\nValeur A;Valeur B", group="Contenu"),
c_checkbox("header_row", "Première ligne = en-tête", "attr:_header_row", "1", "", default=True, group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
] + BULMA_TABLE_CONTROLS,
},
"conteneur": {
"label": "Conteneur / décor", "icon": "⬛", "tag": "div", "has_content": True,
"w": 50, "h": 30, "default_content": "",
# class:"box" — ce widget EST une carte Bulma nativement (fond clair,
# ombre, coins arrondis), pas un habillage qu'on choisit dans un
# menu : voir bouton/titre ci-dessus, même logique. Une couleur de
# fond personnalisée (réglage ci-dessous) reste prioritaire dès
# qu'elle est choisie, exactement comme avant.
"fixed_attrs": {"class": "box"},
"controls": [
c_text("content", "Texte", "content", group="Contenu"),
c_color("background", "Couleur de fond", "style:background-color", "#1f2430", group="Texte"),
c_slider("border-radius", "Arrondi des coins", "style:border-radius", min=0, max=60, default=10, group="Texte", no_freeze=True),
c_slider("opacity", "Opacité", "style:opacity", unit="", min=0, max=1, step=0.05, default=1, group="Texte"),
],
},
"separateur": {
"label": "Séparateur", "icon": "➖", "tag": "hr", "has_content": False, "self_closing": True,
"w": 50, "h": 2, "default_content": "",
"controls": [
c_color("color", "Couleur", "style:border-color", "#2b3140", group="Texte"),
],
},
"repeteur_donnees": {
"label": "Répéteur de données", "icon": "🔁", "tag": "div", "has_content": True, "special_render": "repeater",
"w": 55, "h": 35, "default_content": "{{nom}}",
"controls": [
c_select("definition_id", "Objet de données à afficher", "attr:_definition_id",
[("", "Sélectionner un objet")], default="", group="Contenu"),
c_select("template_type_id", "Modèle de ligne — élément de jeu", "attr:_template_type_id",
[("", "Aucun — utiliser le modèle de texte ci-dessous")], default="", group="Contenu"),
c_textarea("content", "Modèle d'affichage texte — utilise {{nom_du_champ}}", "content", default="{{nom}}", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=40, default=16, group="Texte"),
c_select("filtre_champ", "Filtrer : n'afficher que les lignes où ce champ",
"attr:_filtre_champ", [("", "(sélectionner un objet d'abord)")], default="", group="Filtre"),
c_select("filtre_operateur", "...est", "attr:_filtre_operateur", CONDITION_OPERATORS, default="egal", group="Filtre"),
c_text("filtre_valeur", "...cette valeur",
"attr:_filtre_valeur", placeholder="ex. 3", default="", group="Filtre"),
c_select("filtre2_champ", "ET (optionnel) : ce 2ᵉ champ",
"attr:_filtre2_champ", [("", "(sélectionner un objet d'abord)")], default="", group="Filtre"),
c_select("filtre2_operateur", "...est", "attr:_filtre2_operateur", CONDITION_OPERATORS, default="egal", group="Filtre"),
c_text("filtre2_valeur", "...cette valeur",
"attr:_filtre2_valeur", placeholder="ex. 1", default="", group="Filtre"),
],
},
"jauge": {
"label": "Jauge — liée à une donnée", "icon": "📊", "tag": "div", "has_content": False, "special_render": "jauge",
"w": 60, "h": 12, "default_content": "",
"controls": [
# La clé "definition_id" est reconnue spécifiquement dans
# controls_with_values.py pour être peuplée avec les VRAIS
# objets de données du jeu — même mécanisme que le Répéteur.
c_select("definition_id", "Objet de données à suivre", "attr:_definition_id",
[("", "Sélectionner un objet")], default="", group="Donnée"),
# row_id/champ/champ_nom : options peuplées dynamiquement une
# fois "definition_id" choisi — voir controls_with_values.py
# (premier affichage) et onJaugeDefinitionChange() dans
# screen_edit.html (changement en direct dans le panneau).
c_select("row_id", "Enregistrement (ligne) à suivre", "attr:_row_id",
[("", "Sélectionner un objet d'abord")], default="", group="Donnée"),
c_select("champ", "Champ numérique à afficher", "attr:_champ",
[("", "Sélectionner un objet d'abord")], default="", group="Donnée"),
c_select("champ_nom", "Champ contenant le nom (affiché au-dessus, optionnel)", "attr:_champ_nom",
[("", "— aucun —")], default="", group="Donnée"),
c_text("min", "Valeur mini", "attr:_min", default="0", group="Donnée"),
c_text("max", "Valeur maxi", "attr:_max", default="100", group="Donnée"),
c_color("couleur_basse", "Couleur côté mini", "attr:_couleur_basse", "#e05252", group="Apparence"),
c_color("couleur_haute", "Couleur côté maxi", "attr:_couleur_haute", "#4caf50", group="Apparence"),
c_checkbox("afficher_valeur", "Afficher la valeur en chiffres sur la jauge", "attr:_afficher_valeur", "1", "", default=False, group="Apparence"),
c_select("nom_position", "Position du nom", "attr:_nom_position",
[("dessus", "Au-dessus de la jauge"), ("cote", "À côté (à gauche) de la jauge")],
default="dessus", group="Apparence"),
c_select("nom_alignement", "Alignement du nom (si au-dessus)", "attr:_nom_alignement",
[("centre", "Centré"), ("gauche", "Aligné à gauche")],
default="centre", group="Apparence"),
c_select("nom_police", "Police du nom", "attr:_nom_police", FONT_OPTIONS, default="inherit", group="Apparence"),
c_slider("nom_taille", "Taille du texte du nom", "attr:_nom_taille", unit="", min=8, max=32, default=12, group="Apparence"),
c_checkbox("nom_gras", "Nom en gras", "attr:_nom_gras", "1", "", default=False, group="Apparence"),
c_checkbox("nom_italique", "Nom en italique", "attr:_nom_italique", "1", "", default=False, group="Apparence"),
] + BULMA_PROGRESS_CONTROLS,
},
"superposition": {
# 3.4 (Confort) — overlay/modale réutilisable : couvre TOUJOURS tout
# l'écran (position:fixed, indépendant de x/y/width/height — voir
# render_overlay.py), avec un voile semi-transparent + une boîte
# centrée qui contient les éléments posés à l'intérieur. Fermeture
# manuelle uniquement : réutilise l'action existante "Modifier un
# élément → Visibilité" (masquer/rendre visible), comme n'importe
# quel autre élément — pas de mécanisme de fermeture au clic externe.
"label": "Superposition / boîte de dialogue", "icon": "🪟", "tag": "div", "has_content": True,
"special_render": "overlay",
"w": 50, "h": 40, "default_content": "",
"controls": [
c_text("content", "Texte — en plus des éléments posés dans la boîte", "content", group="Contenu"),
c_color("couleur_boite", "Couleur de fond de la boîte", "attr:_couleur_boite", "#1f2430", group="Apparence"),
c_slider("arrondi", "Arrondi des coins de la boîte", "attr:_arrondi", unit="", min=0, max=40, default=12, group="Apparence"),
],
},
"onglets": {
# Chaque onglet est un vrai élément "conteneur" posé comme enfant
# (voir screens/elements/add_tab.py et render_onglets.py) — pas de
# gestion dédiée ici : ajouter/renommer/supprimer un onglet se fait
# via le bloc "Onglets" du panneau de propriétés (screen_edit.html),
# qui réutilise les routes existantes (element_rename, element_delete).
"label": "Onglets", "icon": "🗂️", "tag": "div", "has_content": False, "special_render": "onglets",
"w": 60, "h": 40, "default_content": "",
"controls": BULMA_TABS_CONTROLS,
},
"icone": {
# Un SVG affiché en silhouette via "mask-image" (voir .icon-svg dans
# style.css et render_icone.py) plutôt qu'une police d'icônes —
# certains navigateurs/extensions bloquent le chargement de polices
# web tierces (protection anti-fingerprinting), ce qui rendait
# invisible toute icône Font Awesome classique quelle que soit sa
# source (CDN ou même hébergée ici). Le nom de l'icône (ex. "star")
# est posé soit en cliquant/glissant depuis la galerie du panneau de
# propriétés (voir routes/elements/element_add.py), soit choisi via
# le sélecteur visuel du réglage "Icône" — jamais tapé à la main.
"label": "Icône", "icon": "🔤", "tag": "i", "has_content": False, "special_render": "icone",
"w": 15, "h": 15, "default_content": "",
"controls": [
c_text("icon_class", "Icône", "attr:_icon_slug", default="star", group="Contenu"),
c_slider("font-size", "Taille", "style:font-size", min=10, max=160, default=32, group="Texte"),
c_color("color", "Couleur", "style:color", "#e8eaf0", group="Texte"),
],
},
}