Consolidate the properties panel: Position & taille, no more Taille/Survol/duplicate Disposition
- Merge "Taille dans le conteneur" into "Position & taille": a child element (posé dans un conteneur/répéteur/groupe de champs) now shows greyed-out X/Y (not applicable, it follows its parent's layout) and a single Largeur/Hauteur field per axis with a unit selector (%/px) instead of the old fixed-px-only sliders capped at 1000 — which was the root cause of a landscape-mode bug where a child couldn't be made wider than 1000px even though the actual screen was wider. New c_size control type (one style key per axis, not two competing sliders — an earlier px+% two-slider attempt let the untouched slider silently clobber the other's value on every autosave). - Merge the two "Disposition" groups (visibility + scale, previously split apart in UNIVERSAL_CONTROLS by unrelated groups) into one. - Remove the "Survol" panel: hovering is conceptually a flow trigger, not a static element property — to be reintroduced there. The underlying data-hover-text/bindHoverTexts runtime is untouched.
This commit is contained in:
@@ -64,6 +64,25 @@ def save_element_controls(slug, element_id, form):
|
||||
style["transform"] = f"scale({val})"
|
||||
continue
|
||||
|
||||
if ctype == "size":
|
||||
# Un seul champ CSS ("width"/"height") mais deux entrées de
|
||||
# formulaire (valeur + unité) — voir size_override_controls.py
|
||||
# pour pourquoi ce n'est PAS deux contrôles slider séparés.
|
||||
_kind, name = control["target"].split(":", 1)
|
||||
try:
|
||||
num = float(form.get(field_name + "_value") or 0)
|
||||
except (TypeError, ValueError):
|
||||
num = 0.0
|
||||
unit = form.get(field_name + "_unit", "%")
|
||||
if unit not in ("px", "%"):
|
||||
unit = "%"
|
||||
if num <= 0:
|
||||
style.pop(name, None)
|
||||
else:
|
||||
num_str = str(int(num)) if num == int(num) else str(num)
|
||||
style[name] = f"{num_str}{unit}"
|
||||
continue
|
||||
|
||||
target = control["target"]
|
||||
if ctype == "checkbox":
|
||||
checked = form.get(field_name) is not None
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
from ..controls.c_slider import c_slider
|
||||
from ..controls.c_size import c_size
|
||||
|
||||
# Taille fixe (en pixels), surtout utile pour un élément posé À L'INTÉRIEUR
|
||||
# d'un conteneur/répéteur/groupe de champs : par défaut, ces éléments
|
||||
# remplissent la largeur disponible et s'ajustent en hauteur à leur contenu
|
||||
# (voir _style_string) — 0 garde ce comportement automatique ; une valeur
|
||||
# non nulle impose une taille fixe (ex: une image de 120px de large dans une
|
||||
# ligne, pour que "Espacement égal entre eux"/"autour"/"uniforme" sur le
|
||||
# conteneur parent ait un effet visible, ce qui n'est pas le cas tant que
|
||||
# chaque élément remplit 100% de la largeur).
|
||||
# Taille d'un élément posé À L'INTÉRIEUR d'un conteneur/répéteur/groupe de
|
||||
# champs : par défaut, ces éléments remplissent la largeur disponible et
|
||||
# s'ajustent en hauteur à leur contenu (voir _style_string) — 0 garde ce
|
||||
# comportement automatique ; une valeur non nulle impose une taille fixe,
|
||||
# dans l'unité choisie par l'utilisateur (% du parent, qui suit
|
||||
# naturellement n'importe quelle taille d'écran — ou px, pour une taille
|
||||
# précise, ex: une image de 120px de large dans une ligne, pour que
|
||||
# "Espacement égal entre eux"/"autour"/"uniforme" sur le conteneur parent
|
||||
# ait un effet visible, ce qui n'est pas le cas tant que chaque élément
|
||||
# remplit 100% de la largeur).
|
||||
#
|
||||
# UN SEUL contrôle par axe (pas un slider % + un slider px séparés) : les
|
||||
# deux cibleraient la même propriété CSS "width"/"height", et le mécanisme
|
||||
# de save_element_controls.py qui préserve un réglage "jamais personnalisé"
|
||||
# ne protège que le PREMIER contrôle à écrire une clé donnée — un second
|
||||
# contrôle non touché par l'utilisateur mais ciblant la même clé
|
||||
# l'écraserait quand même à chaque sauvegarde (bug constaté en pratique).
|
||||
#
|
||||
# Ces réglages ne sont PAS rendus via le mécanisme générique de groupes du
|
||||
# panneau de propriétés (voir screen_edit.html) : ils sont affichés à la
|
||||
# main dans le bloc "Position & taille", à la place de X/Y (non applicables
|
||||
# à un élément qui suit le flux de son parent plutôt qu'une position
|
||||
# absolue) — voir selected_controls / size_ctrls dans le template.
|
||||
SIZE_OVERRIDE_CONTROLS = [
|
||||
c_slider("child-width", "Largeur fixe", "style:width",
|
||||
min=0, max=1000, default=0, group="Taille dans le conteneur"),
|
||||
c_slider("child-height", "Hauteur fixe", "style:height",
|
||||
min=0, max=1000, default=0, group="Taille dans le conteneur"),
|
||||
c_size("child-width", "Largeur", "style:width", default=0, group="Position & taille"),
|
||||
c_size("child-height", "Hauteur", "style:height", default=0, group="Position & taille"),
|
||||
]
|
||||
|
||||
@@ -3,6 +3,12 @@ from .visibility_control import VISIBILITY_CONTROL
|
||||
from .size_override_controls import SIZE_OVERRIDE_CONTROLS
|
||||
from .spacing_controls import SPACING_CONTROLS
|
||||
from .border_controls import BORDER_CONTROLS
|
||||
from .hover_controls import HOVER_CONTROLS
|
||||
|
||||
UNIVERSAL_CONTROLS = [VISIBILITY_CONTROL] + SIZE_OVERRIDE_CONTROLS + SPACING_CONTROLS + BORDER_CONTROLS + [c_scale()] + HOVER_CONTROLS
|
||||
# Le panneau "Survol" (HOVER_CONTROLS) a été retiré du panneau de propriétés :
|
||||
# survoler un élément est conceptuellement un DÉCLENCHEUR (voir la Logique de
|
||||
# la scène), pas une propriété statique de l'élément — il sera réintroduit
|
||||
# là-bas. Le mécanisme data-hover-text sous-jacent (voir render_element_html
|
||||
# et bindHoverTexts() dans play.html) reste fonctionnel pour les éléments qui
|
||||
# en avaient déjà un, seul le moyen de le RÉGLER depuis ce panneau disparaît
|
||||
# pour l'instant.
|
||||
UNIVERSAL_CONTROLS = [VISIBILITY_CONTROL, c_scale()] + SIZE_OVERRIDE_CONTROLS + SPACING_CONTROLS + BORDER_CONTROLS
|
||||
|
||||
@@ -23,6 +23,14 @@ def _control_value(el, control):
|
||||
m = re.search(r"scale\(([\d.]+)\)", transform)
|
||||
return float(m.group(1)) if m else control.get("default", 1)
|
||||
|
||||
if ctype == "size":
|
||||
_kind, name = control["target"].split(":", 1)
|
||||
raw = style.get(name)
|
||||
m = re.match(r"^([\d.]+)(px|%)$", str(raw or ""))
|
||||
if m:
|
||||
return {"value": float(m.group(1)), "unit": m.group(2)}
|
||||
return {"value": control.get("default", 0), "unit": "%"}
|
||||
|
||||
target = control["target"]
|
||||
if target == "content":
|
||||
return el.get("content") or control.get("default", "")
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
def c_size(key, label, target, default=0, group=None):
|
||||
"""Taille avec unité choisie par l'utilisateur (% ou px) — voir
|
||||
size_override_controls.py. 0 = comportement automatique, quelle que soit
|
||||
l'unité affichée."""
|
||||
return {"key": key, "label": label, "type": "size", "target": target, "default": default, "group": group}
|
||||
@@ -1,14 +1,5 @@
|
||||
from .widget_meta import widget_meta
|
||||
|
||||
# Ces deux réglages utilisent 0 comme valeur sentinelle "automatique" (voir
|
||||
# size_override_controls.py / _style_string) : contrairement aux autres
|
||||
# sliders, 0 n'y est PAS une vraie valeur par défaut à figer, donc on ne les
|
||||
# fige jamais à la création — les autres sliders à 0 par défaut (ex:
|
||||
# épaisseur de bordure) doivent au contraire être figés explicitement, sinon
|
||||
# un border-style baké sans border-width associé afficherait une bordure par
|
||||
# défaut du navigateur (~3px, "medium") au lieu de rester invisible.
|
||||
_AUTO_SIZE_KEYS = {"child-width", "child-height"}
|
||||
|
||||
# Réglages "par côté" (padding/margin/bordure individuels) : contrairement
|
||||
# aux réglages "tous les côtés" équivalents (qui, eux, DOIVENT être figés à
|
||||
# la création — voir plus bas), ceux-ci ne doivent jamais l'être. Une fois
|
||||
@@ -47,6 +38,11 @@ def default_style_and_attributes(widget_key):
|
||||
ctype = control["type"]
|
||||
if ctype in ("checkbox", "toggle_token", "preset", "scale", "color"):
|
||||
continue
|
||||
if ctype == "size":
|
||||
# 0 est TOUJOURS la valeur sentinelle "automatique" pour ce
|
||||
# type de réglage (voir size_override_controls.py) — jamais une
|
||||
# vraie valeur par défaut à figer à la création.
|
||||
continue
|
||||
target = control.get("target")
|
||||
if not target or target == "content":
|
||||
continue
|
||||
@@ -54,8 +50,6 @@ def default_style_and_attributes(widget_key):
|
||||
if default in (None, ""):
|
||||
continue
|
||||
if ctype == "slider":
|
||||
if control["key"] in _AUTO_SIZE_KEYS and str(default) in ("0",):
|
||||
continue # 0 = comportement automatique, rien à figer
|
||||
if control["key"] in _PER_SIDE_KEYS:
|
||||
continue # réglage par côté : jamais figé (voir commentaire plus haut)
|
||||
unit = control.get("unit", "")
|
||||
|
||||
@@ -176,6 +176,10 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
|
||||
.posGrid{ display:grid; grid-template-columns:1fr 1fr; gap:8px 12px; margin-bottom:14px; }
|
||||
.posGrid label{ font-size:11px; color:var(--text-dim); display:flex; flex-direction:column; gap:4px; }
|
||||
.posGrid input{ background:var(--panel2); border:1px solid var(--border); border-radius:6px; color:var(--text); padding:6px 8px; }
|
||||
.posGrid input:disabled{ opacity:0.4; cursor:not-allowed; }
|
||||
.sizeField{ display:flex; gap:4px; }
|
||||
.sizeField input{ flex:1; min-width:0; }
|
||||
.sizeField select{ width:auto; flex:0 0 auto; padding:6px 4px; }
|
||||
|
||||
.controlRow{ margin-bottom:12px; }
|
||||
.controlRow > label{ display:block; font-size:12px; color:var(--text-dim); margin-bottom:4px; }
|
||||
|
||||
@@ -188,21 +188,49 @@
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
{% set size_ctrl_keys = ['child-width', 'child-height'] %}
|
||||
{% set size_ctrls = {} %}
|
||||
{% for c in selected_controls if c.key in size_ctrl_keys %}{% set _ = size_ctrls.update({c.key: c}) %}{% endfor %}
|
||||
|
||||
<form method="post" action="{{ url_for('element_save', slug=game.slug, element_id=el.id) }}" id="propsForm" onsubmit="event.preventDefault(); return false;">
|
||||
<details class="propGroup" open>
|
||||
<summary>Position & taille</summary>
|
||||
<div class="propGroupBody">
|
||||
<div class="posGrid">
|
||||
<label>X (%)<input type="number" id="pos-x" min="0" max="100" value="{{ el.x }}"></label>
|
||||
<label>Y (%)<input type="number" id="pos-y" min="0" max="100" value="{{ el.y }}"></label>
|
||||
<label>X (%)<input type="number" id="pos-x" min="0" max="100" value="{{ el.x }}" {{ 'disabled' if el.parent_id else '' }}></label>
|
||||
<label>Y (%)<input type="number" id="pos-y" min="0" max="100" value="{{ el.y }}" {{ 'disabled' if el.parent_id else '' }}></label>
|
||||
{% if el.parent_id %}
|
||||
<label>Largeur
|
||||
<div class="sizeField">
|
||||
<input type="number" name="ctrl_child-width_value" min="0" value="{{ size_ctrls['child-width'].value.value or '' }}" placeholder="auto">
|
||||
<select name="ctrl_child-width_unit">
|
||||
<option value="%" {{ 'selected' if size_ctrls['child-width'].value.unit == '%' else '' }}>%</option>
|
||||
<option value="px" {{ 'selected' if size_ctrls['child-width'].value.unit == 'px' else '' }}>px</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label>Hauteur
|
||||
<div class="sizeField">
|
||||
<input type="number" name="ctrl_child-height_value" min="0" value="{{ size_ctrls['child-height'].value.value or '' }}" placeholder="auto">
|
||||
<select name="ctrl_child-height_unit">
|
||||
<option value="%" {{ 'selected' if size_ctrls['child-height'].value.unit == '%' else '' }}>%</option>
|
||||
<option value="px" {{ 'selected' if size_ctrls['child-height'].value.unit == 'px' else '' }}>px</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
{% else %}
|
||||
<label>Largeur (%)<input type="number" id="pos-w" min="1" max="100" value="{{ el.width }}"></label>
|
||||
<label>Hauteur (%)<input type="number" id="pos-h" min="1" max="100" value="{{ el.height }}"></label>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if el.parent_id %}
|
||||
<p class="hint">X/Y ne s'appliquent qu'à un élément posé directement sur l'écran — celui-ci suit la disposition de son conteneur. Largeur/Hauteur : laisser vide garde la taille automatique ; "%" (relatif au conteneur) convient à la plupart des cas, "px" pour une taille précise.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<form method="post" action="{{ url_for('element_save', slug=game.slug, element_id=el.id) }}" id="propsForm" onsubmit="event.preventDefault(); return false;">
|
||||
{% set ns = namespace(last_group=None, started=False) %}
|
||||
{% for c in selected_controls %}
|
||||
{% for c in selected_controls if c.key not in size_ctrl_keys %}
|
||||
{% if c.group != ns.last_group %}
|
||||
{% if ns.started %}
|
||||
</div>
|
||||
@@ -767,7 +795,11 @@ function initBuilderPanel() {
|
||||
(function () {
|
||||
var xEl = document.getElementById('pos-x'), yEl = document.getElementById('pos-y');
|
||||
var wEl = document.getElementById('pos-w'), hEl = document.getElementById('pos-h');
|
||||
if (!xEl) return;
|
||||
// pos-w/pos-h n'existent pas pour un élément posé dans un conteneur (sa
|
||||
// taille se règle via les champs "Largeur/Hauteur (%)"/"fixe (px)" du
|
||||
// formulaire général à la place, voir size_ctrls plus haut) — X/Y non
|
||||
// plus, mais ils restent dans le DOM (juste désactivés) pour cet élément.
|
||||
if (!xEl || !wEl) return;
|
||||
function apply() {
|
||||
var canvasEl = document.getElementById('el-' + CURRENT_SELECTED_ID);
|
||||
var x = parseFloat(xEl.value) || 0, y = parseFloat(yEl.value) || 0;
|
||||
|
||||
@@ -18,7 +18,13 @@ def _add_element(client, slug, screen_id, widget):
|
||||
|
||||
# ---------- 3.1 — Interactions au survol ----------
|
||||
|
||||
def test_hover_text_control_saved_and_rendered(client, game):
|
||||
def test_hover_text_control_removed_from_properties_panel(client, game):
|
||||
"""Le réglage "Survol" a été retiré du panneau de propriétés (voir
|
||||
universal_controls.py) : survoler un élément est conceptuellement un
|
||||
déclencheur de la Logique de la scène, pas une propriété statique —
|
||||
il sera réintroduit là-bas. Poster ctrl_survol_texte ne doit donc plus
|
||||
avoir d'effet (le mécanisme data-hover-text/bindHoverTexts sous-jacent
|
||||
reste en place pour un usage futur, voir les tests ci-dessous)."""
|
||||
screen_id = _create_screen(client, game)
|
||||
el_id = _add_element(client, game, screen_id, "titre")
|
||||
client.post(f"/game/{game}/elements/{el_id}/save", data={
|
||||
@@ -26,7 +32,7 @@ def test_hover_text_control_saved_and_rendered(client, game):
|
||||
})
|
||||
resp = client.get(f"/game/{game}/play")
|
||||
html = resp.data.decode()
|
||||
assert 'data-hover-text="mathilde.d@forgebase.fr"' in html
|
||||
assert 'data-hover-text="' not in html
|
||||
|
||||
|
||||
def test_hover_text_absent_by_default_no_regression(client, game):
|
||||
|
||||
Reference in New Issue
Block a user