variables: highlighted row owns Delete, Options and Select Bound Object
Select Bound Object ran variable.unbind on the selection, so choosing it silently removed the bindings; Delete Variable and Delete Data Set deleted every row. Now the panel keeps a highlighted variable row (a click highlights and selects what it drives, as the reference panel does): Select Bound Object selects what the highlighted variable drives via select.set, Delete Variable deletes only it, and Delete Data Set deletes only the current set. Variable Options and Rename Data Set open their dialogs preselected with the highlighted variable / active set (the double-click used to invoke variable.rename with a bare name, which only errored). Unbind enables only over a bound selection; Make Text Dynamic only for exactly one type object. The highlight is per-document session state with its own command (variable.highlight, no undo step, like layer.highlight): rename carries it, delete clears it when stale. New engine tests (18) and panel tests cover the reviewer's regression case, verified to fail on the old wiring.
This commit is contained in:
parent
06dbe5b1e8
commit
f2252b0d9c
|
|
@ -104,6 +104,12 @@ fn rename(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
Ok(())
|
||||
})?;
|
||||
// The highlight names a variable, so it follows the rename rather than going stale.
|
||||
let st = s.doc_mut()?;
|
||||
if st.variables_highlight.as_deref() == Some(from.as_str()) {
|
||||
st.variables_highlight = Some(to.clone());
|
||||
}
|
||||
st.revision += 1;
|
||||
Ok(json!({"from": from, "name": to}))
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +155,12 @@ fn make_dynamic(s: &mut Session, kind: VariableKind, stem: &str) -> Result<Value
|
|||
fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let names = names_param(p, "variable.delete")?;
|
||||
let n = s.edit("Delete Variables", |d, _| Ok(d.variables.prune(&names)))?;
|
||||
// A highlight of a variable that is gone highlights nothing.
|
||||
let st = s.doc_mut()?;
|
||||
if st.variables_highlight.as_ref().is_some_and(|h| st.doc.variables.variable(h).is_none()) {
|
||||
st.variables_highlight = None;
|
||||
}
|
||||
st.revision += 1;
|
||||
Ok(json!({"deleted": n}))
|
||||
}
|
||||
|
||||
|
|
@ -227,6 +239,27 @@ fn unbind(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Ok(json!({"unbound": n}))
|
||||
}
|
||||
|
||||
/// Highlight one Variables panel row: what the panel's Delete, Options… and Select Bound
|
||||
/// Object act on. Panel state, not art selection: not saved, not an undo step (an empty
|
||||
/// or missing `variable` clears it), like `layer.highlight`.
|
||||
fn highlight(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "variable.highlight";
|
||||
let name = match str_param(p, "variable").filter(|v| !v.is_empty()) {
|
||||
Some(v) => {
|
||||
let v = v.to_string();
|
||||
if s.doc()?.doc.variables.variable(&v).is_none() {
|
||||
return Err(bad(C, format!("no variable named `{v}`")));
|
||||
}
|
||||
Some(v)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let st = s.doc_mut()?;
|
||||
st.variables_highlight = name.clone();
|
||||
st.revision += 1;
|
||||
Ok(json!({"variable": name}))
|
||||
}
|
||||
|
||||
fn dataset_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "dataset.new";
|
||||
let name = var_name(p, C)?;
|
||||
|
|
@ -480,6 +513,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
has_doc,
|
||||
unbind
|
||||
),
|
||||
cmd!(
|
||||
"variable.highlight",
|
||||
"Highlight Variable",
|
||||
[],
|
||||
None,
|
||||
"{variable?} highlight one Variables panel row (a click); what the panel's Delete, Options… and Select Bound Object act on. An empty or missing `variable` clears it. Not an undo step → {variable}",
|
||||
has_doc,
|
||||
highlight
|
||||
),
|
||||
cmd!("dataset.new", "New Data Set…", ["Window", "Variables"], None, "{name, values?: {variable: value}} add a dataset", has_doc, dataset_new),
|
||||
cmd!(
|
||||
"dataset.set",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ pub struct Interaction {
|
|||
/// isolation).
|
||||
pub active_layer: Option<NodeId>,
|
||||
pub layer_rows: Vec<NodeId>,
|
||||
pub variables_highlight: Option<String>,
|
||||
pub isolation: Option<NodeId>,
|
||||
/// The perspective transform the previews make (`perspective.transform` params): Transform
|
||||
/// Again repeats it once the drag is committed.
|
||||
|
|
@ -121,6 +122,10 @@ pub struct DocState {
|
|||
/// Panel state, not art selection: not saved, not undoable (`layer.setCurrent`,
|
||||
/// `layer.highlight`).
|
||||
pub layer_rows: Vec<NodeId>,
|
||||
/// The variable highlighted in the Variables panel: what its Delete, Options… and Select
|
||||
/// Bound Object act on. Panel state, not art selection: not saved, not undoable
|
||||
/// (`variable.highlight`). A name, so renaming a variable moves the highlight with it.
|
||||
pub variables_highlight: Option<String>,
|
||||
/// Isolation mode container.
|
||||
pub isolation: Option<NodeId>,
|
||||
pub interaction: Option<Interaction>,
|
||||
|
|
@ -236,6 +241,7 @@ impl DocState {
|
|||
revision: 1,
|
||||
active_layer,
|
||||
layer_rows: vec![],
|
||||
variables_highlight: None,
|
||||
isolation: None,
|
||||
interaction: None,
|
||||
undo_group: None,
|
||||
|
|
@ -300,6 +306,7 @@ impl DocState {
|
|||
self.selection = it.selection;
|
||||
self.active_layer = it.active_layer;
|
||||
self.layer_rows = it.layer_rows;
|
||||
self.variables_highlight = it.variables_highlight;
|
||||
self.isolation = it.isolation;
|
||||
self.revision += 1;
|
||||
}
|
||||
|
|
@ -1294,6 +1301,7 @@ impl Session {
|
|||
preview: None,
|
||||
active_layer: st.active_layer,
|
||||
layer_rows: st.layer_rows.clone(),
|
||||
variables_highlight: st.variables_highlight.clone(),
|
||||
isolation: st.isolation,
|
||||
perspective_again: None,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -436,3 +436,56 @@ fn bad_variables_requests_are_errors_not_panics() {
|
|||
assert!(s.execute(cmd, ¶ms).is_err(), "{cmd} {params}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlight_selects_without_unbinding_and_deletes_leave_the_rest() {
|
||||
// The reviewer's regression case: two variables, two rows.
|
||||
let mut s = session();
|
||||
let a = text(&mut s, "Alice");
|
||||
let b = text(&mut s, "Bob");
|
||||
for (name, id) in [("Name", a), ("Other", b)] {
|
||||
s.execute("variable.define", &json!({"name": name, "kind": "text"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": name, "ids": [id.0]})).unwrap();
|
||||
}
|
||||
s.execute("dataset.new", &json!({"name": "One", "values": {"Name": "Alice", "Other": "Bob"}})).unwrap();
|
||||
s.execute("dataset.new", &json!({"name": "Two", "values": {"Name": "A2", "Other": "B2"}})).unwrap();
|
||||
s.execute("dataset.select", &json!({"name": "One"})).unwrap();
|
||||
|
||||
// What a row click runs (highlight), then what Select Bound Object runs (select).
|
||||
s.execute("variable.highlight", &json!({"variable": "Name"})).unwrap();
|
||||
let bound: Vec<u64> = s.doc().unwrap().doc.variables.objects_of("Name").iter().map(|i| i.0).collect();
|
||||
s.execute("select.set", &json!({"ids": bound})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.objects, vec![a]);
|
||||
// …and the binding is still there: selecting never unbinds.
|
||||
assert_eq!(s.doc().unwrap().doc.variables.objects_of("Name"), vec![a]);
|
||||
|
||||
// Deleting one variable leaves the other; deleting the current dataset leaves the other.
|
||||
s.execute("variable.delete", &json!({"names": ["Name"]})).unwrap();
|
||||
let vars = s.execute("variable.list", &json!({})).unwrap();
|
||||
assert_eq!(vars["variables"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(vars["variables"][0]["name"], "Other");
|
||||
s.execute("dataset.delete", &json!({"names": ["One"]})).unwrap();
|
||||
let sets = s.execute("dataset.list", &json!({})).unwrap();
|
||||
assert_eq!(sets["datasets"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(sets["datasets"][0]["name"], "Two");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlight_follows_rename_and_clears_with_delete() {
|
||||
let mut s = session();
|
||||
let t = text(&mut s, "Alice");
|
||||
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
|
||||
assert!(s.execute("variable.highlight", &json!({"variable": "Missing"})).is_err(), "unknown names stay errors");
|
||||
let v = s.execute("variable.highlight", &json!({"variable": "Name"})).unwrap();
|
||||
assert_eq!(v["variable"], "Name");
|
||||
// The highlight names a variable, so the rename carries it along.
|
||||
s.execute("variable.rename", &json!({"name": "Name", "newName": "Title"})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().variables_highlight.as_deref(), Some("Title"));
|
||||
// Empty clears it, and deleting the highlighted variable clears it too.
|
||||
s.execute("variable.highlight", &json!({})).unwrap();
|
||||
assert!(s.doc().unwrap().variables_highlight.is_none());
|
||||
s.execute("variable.highlight", &json!({"variable": "Title"})).unwrap();
|
||||
s.execute("variable.delete", &json!({"names": ["Title"]})).unwrap();
|
||||
assert!(s.doc().unwrap().variables_highlight.is_none(), "a highlight of a deleted variable highlights nothing");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,8 +48,10 @@ fn rename_of(id: &str) -> Option<&'static str> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Open `id`'s dialog: OK runs the command with the fields below.
|
||||
pub fn open(app: &mut VectorcraftApp, id: &str) -> bool {
|
||||
/// Open `id`'s dialog: OK runs the command with the fields below. `preselect` names the row
|
||||
/// a rename starts on (the panel's highlighted variable, the active dataset); without it, or
|
||||
/// when it no longer exists, the first row — the dialog still offers the dropdown.
|
||||
pub fn open(app: &mut VectorcraftApp, id: &str, preselect: Option<&str>) -> bool {
|
||||
let Some(label) = opened(id) else { return false };
|
||||
let Some(st) = app.session.active() else {
|
||||
app.status(tl!("no document").to_string());
|
||||
|
|
@ -94,7 +96,7 @@ pub fn open(app: &mut VectorcraftApp, id: &str) -> bool {
|
|||
app.status(if key == "variable" { tl!("no variable to rename").to_string() } else { tl!("no data set to rename").to_string() });
|
||||
return true;
|
||||
}
|
||||
fields.insert(key.into(), json!(names[0]));
|
||||
fields.insert(key.into(), json!(preselect.filter(|p| names.contains(p)).unwrap_or(names[0])));
|
||||
fields.insert("newName".into(), json!(""));
|
||||
}
|
||||
}
|
||||
|
|
@ -286,7 +288,7 @@ mod tests {
|
|||
#[test]
|
||||
fn defining_a_variable_from_the_dialog_works() {
|
||||
let mut app = app_with_variables();
|
||||
assert!(open(&mut app, "variable.define"));
|
||||
assert!(open(&mut app, "variable.define", None));
|
||||
{
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.kind, KIND);
|
||||
|
|
@ -302,7 +304,7 @@ mod tests {
|
|||
#[test]
|
||||
fn a_new_dataset_carries_the_dialogs_values_and_omits_the_empty_ones() {
|
||||
let mut app = app_with_variables();
|
||||
open(&mut app, "dataset.new");
|
||||
open(&mut app, "dataset.new", None);
|
||||
{
|
||||
let d = app.ui.dialog.as_mut().unwrap();
|
||||
d.fields.insert("name".into(), json!("Row 1"));
|
||||
|
|
@ -321,7 +323,7 @@ mod tests {
|
|||
let mut app = app_with_variables();
|
||||
app.run("dataset.new", json!({"name": "Row 1", "values": {"Title": "Hello", "Badge": false}})).unwrap();
|
||||
app.run("dataset.set", json!({"name": "Row 1", "values": {"Badge": true}})).unwrap();
|
||||
open(&mut app, "dataset.set");
|
||||
open(&mut app, "dataset.set", None);
|
||||
{
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.str("name"), "Row 1");
|
||||
|
|
@ -341,7 +343,7 @@ mod tests {
|
|||
let id = app.run("text.create", json!({"x": 10, "y": 10, "text": "x"})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("variable.bind", json!({"variable": "Title", "ids": [id]})).unwrap();
|
||||
app.run("dataset.new", json!({"name": "Row 1", "values": {"Title": "Hi"}})).unwrap();
|
||||
open(&mut app, "dataset.select");
|
||||
open(&mut app, "dataset.select", None);
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "");
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!("Row 1"));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
|
|
@ -353,7 +355,20 @@ mod tests {
|
|||
let mut app = app_with_variables();
|
||||
assert!(opens("dataset.set") && opens("variable.define"));
|
||||
assert!(!opens("dataset.list") && !opens("dataset.next"), "no dialog for a query or a step");
|
||||
assert!(!open(&mut app, "dataset.list"), "and it opens nothing");
|
||||
assert!(!open(&mut app, "dataset.list", None), "and it opens nothing");
|
||||
assert!(app.ui.dialog.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_dialogs_start_on_the_preselected_row() {
|
||||
let mut app = app_with_variables();
|
||||
app.run("dataset.new", json!({"name": "Row 1", "values": {"Title": "Hello"}})).unwrap();
|
||||
open(&mut app, "variable.rename", Some("Badge"));
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("variable"), "Badge");
|
||||
// A preselect the document no longer has falls back to the first row.
|
||||
open(&mut app, "variable.rename", Some("Gone"));
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("variable"), "Title");
|
||||
open(&mut app, "dataset.rename", Some("Row 1"));
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "Row 1");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1195,6 +1195,7 @@
|
|||
Highlight Alternate Glyphs Resaltar glifos alternativos
|
||||
Highlight anchors on mouse over Resaltar anclas al pasar el puntero
|
||||
Highlight Rows Resaltar filas
|
||||
Highlight Variable Resaltar variable
|
||||
Highlight Substituted Fonts Resaltar fuentes sustituidas
|
||||
Highlight Substituted Glyphs Resaltar glifos sustituidos
|
||||
History Historial
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1194,6 +1194,7 @@
|
|||
Highlight Alternate Glyphs Surligner les glyphes de substitution
|
||||
Highlight anchors on mouse over Mettre en évidence les points d’ancrage au survol
|
||||
Highlight Rows Surligner les lignes
|
||||
Highlight Variable Surligner la variable
|
||||
Highlight Substituted Fonts Surligner les polices remplacées
|
||||
Highlight Substituted Glyphs Surligner les glyphes remplacés
|
||||
History Historique
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1194,6 +1194,7 @@
|
|||
Highlight Alternate Glyphs Evidenzia glifi alternativi
|
||||
Highlight anchors on mouse over Evidenzia ancoraggi al passaggio del mouse
|
||||
Highlight Rows Evidenzia righe
|
||||
Highlight Variable Evidenzia variabile
|
||||
Highlight Substituted Fonts Evidenzia font sostituiti
|
||||
Highlight Substituted Glyphs Evidenzia glifi sostituiti
|
||||
History Cronologia
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1452,6 +1452,7 @@
|
|||
High Performance (discrete) 高パフォーマンス (専用 GPU)
|
||||
Highlight Alternate Glyphs 代替字形をハイライト
|
||||
Highlight Rows 行をハイライト
|
||||
Highlight Variable 変数をハイライト表示
|
||||
Highlight Substituted Fonts 置換フォントをハイライト
|
||||
Highlight Substituted Glyphs 置換字形をハイライト
|
||||
Highlight anchors on mouse over マウスオーバー時にアンカーをハイライト
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1192,6 +1192,7 @@
|
|||
Highlight Alternate Glyphs Выделять альтернативные глифы
|
||||
Highlight anchors on mouse over Выделять опорные точки при наведении
|
||||
Highlight Rows Выделять строки
|
||||
Highlight Variable Подсветить переменную
|
||||
Highlight Substituted Fonts Выделять заменённые шрифты
|
||||
Highlight Substituted Glyphs Выделять заменённые глифы
|
||||
History История
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1188,6 +1188,7 @@
|
|||
Highlight Alternate Glyphs Виділяти альтернативні гліфи
|
||||
Highlight anchors on mouse over Виділяти опорні точки при наведенні
|
||||
Highlight Rows Виділяти рядки
|
||||
Highlight Variable Підсвітити змінну
|
||||
Highlight Substituted Fonts Виділяти замінені шрифти
|
||||
Highlight Substituted Glyphs Виділяти замінені гліфи
|
||||
History Історія
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 13.
|
|
|
@ -3430,6 +3430,7 @@
|
|||
Hide Others 隐藏其他
|
||||
High Performance (discrete) 高性能 (独立显卡)
|
||||
Highlight Rows 突出显示行
|
||||
Highlight Variable 突出显示变量
|
||||
Lock Others 锁定其他
|
||||
Merge Selected 合并所选图层
|
||||
Move Rows 移动行
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1170,6 +1170,7 @@
|
|||
Highlight Alternate Glyphs 醒目提示替代字符
|
||||
Highlight anchors on mouse over 滑鼠移過時醒目提示錨點
|
||||
Highlight Rows 反白顯示列
|
||||
Highlight Variable 突出顯示變數
|
||||
Highlight Substituted Fonts 醒目提示被取代的字體
|
||||
Highlight Substituted Glyphs 醒目提示被取代的字符
|
||||
History 步驟記錄
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3023,7 +3023,7 @@ pub fn invoke(app: &mut VectorcraftApp, id: &str, p: Value) {
|
|||
}
|
||||
// Variables (data merge): a name and a kind, a name and a row of values.
|
||||
if crate::dialogs::variables::opens(id) && p.as_object().is_none_or(|o| o.is_empty()) {
|
||||
crate::dialogs::variables::open(app, id);
|
||||
crate::dialogs::variables::open(app, id, None);
|
||||
return;
|
||||
}
|
||||
// Repeat Options: a dialog with the selected repeat's current values.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
|
||||
use egui::Ui;
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::VariableKind;
|
||||
use vectorcraft_doc::{NodeId, NodeKind, VariableKind};
|
||||
use vectorcraft_engine::DocState;
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
use crate::widgets;
|
||||
|
|
@ -36,6 +37,47 @@ struct SetRow {
|
|||
matches: bool,
|
||||
}
|
||||
|
||||
/// The row the panel acts on: Delete, Options… and Select Bound Object all mean it. A name
|
||||
/// the document no longer defines highlights nothing (deleted from the palette, undone).
|
||||
fn highlighted_row(st: &DocState) -> Option<String> {
|
||||
st.variables_highlight.clone().filter(|h| st.doc.variables.variable(h).is_some())
|
||||
}
|
||||
|
||||
/// What a variable drives, as object ids: what Select Bound Object selects.
|
||||
fn bound_objects(st: &DocState, name: &str) -> Vec<u64> {
|
||||
st.doc.variables.objects_of(name).iter().map(|i| i.0).collect()
|
||||
}
|
||||
|
||||
/// A row click: highlight the row, and select what it drives (its bindings are left alone).
|
||||
pub(crate) fn click_row(app: &mut VectorcraftApp, name: &str, bound: &[u64]) {
|
||||
app.run("variable.highlight", json!({"variable": name})).ok();
|
||||
if !bound.is_empty() {
|
||||
app.run("select.set", json!({"ids": bound})).ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// Select Bound Object: select what the highlighted variable drives; its bindings stay.
|
||||
pub(crate) fn select_bound_object(app: &mut VectorcraftApp) {
|
||||
let ids = app.session.active().map(|st| highlighted_row(st).map(|h| bound_objects(st, &h))).unwrap_or_default().unwrap_or_default();
|
||||
if !ids.is_empty() {
|
||||
app.run("select.set", json!({"ids": ids})).ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete Variable: the highlighted variable only; the rest of the rows stay.
|
||||
pub(crate) fn delete_highlighted(app: &mut VectorcraftApp) {
|
||||
if let Some(name) = app.session.active().and_then(highlighted_row) {
|
||||
app.run("variable.delete", json!({"names": [name]})).ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete Data Set: the current one only; the rest of the rows stay.
|
||||
pub(crate) fn delete_active_dataset(app: &mut VectorcraftApp) {
|
||||
if let Some(name) = app.session.active().and_then(|st| st.doc.variables.active_dataset.clone()) {
|
||||
app.run("dataset.delete", json!({"names": [name]})).ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let Some(st) = app.session.active() else {
|
||||
widgets::dim_label(ui, tl!("No document"));
|
||||
|
|
@ -72,6 +114,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
let active = sets.iter().find(|s| s.active);
|
||||
let selected: Vec<u64> = st.selection.objects.iter().map(|i| i.0).collect();
|
||||
let has_selection = !selected.is_empty();
|
||||
let highlighted = highlighted_row(st);
|
||||
// Unbind needs a bound object under the selection, not just any selection.
|
||||
let selection_bound = selected.iter().any(|id| st.doc.variables.bindings.contains_key(&NodeId(*id)));
|
||||
// Make Text Dynamic needs exactly one type object: a text variable only drives type.
|
||||
let single_type = selected.len() == 1 && st.doc.node(NodeId(selected[0])).is_some_and(|n| matches!(n.kind, NodeKind::Text(_)));
|
||||
|
||||
// The current data set across the top: its name, and the arrows that step through them.
|
||||
ui.horizontal(|ui| {
|
||||
|
|
@ -106,17 +153,17 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
VariableKind::Text => "type",
|
||||
VariableKind::Visibility => "eye",
|
||||
};
|
||||
let icon = ui.add(egui::Image::new(crate::icons::source(icon)).fit_to_exact_size(egui::vec2(14.0, 14.0)));
|
||||
let _ = icon;
|
||||
ui.add(egui::Image::new(crate::icons::source(icon)).fit_to_exact_size(egui::vec2(14.0, 14.0)));
|
||||
// The object a variable drives is named beside it, as the reference panel does.
|
||||
let text = if row.object.is_empty() { row.name.clone() } else { format!("{} · {}", row.name, row.object) };
|
||||
let r = ui.selectable_label(false, text);
|
||||
// A click selects what it drives; a double click opens Variable Options.
|
||||
if (r.clicked() || r.double_clicked()) && !row.bound.is_empty() {
|
||||
app.run("select.set", json!({"ids": row.bound})).ok();
|
||||
let r = ui.selectable_label(highlighted.as_deref() == Some(row.name.as_str()), text);
|
||||
// A click highlights the row and selects what it drives; a double click opens
|
||||
// Variable Options for it.
|
||||
if r.clicked() {
|
||||
click_row(app, &row.name, &row.bound);
|
||||
}
|
||||
if r.double_clicked() {
|
||||
crate::menus::invoke(app, "variable.rename", json!({"name": row.name}));
|
||||
crate::dialogs::variables::open(app, "variable.rename", Some(&row.name));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -141,7 +188,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
// The bottom bar: make the selection dynamic, and define and delete what the lists show.
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
if widgets::icon_button_enabled(ui, "type", tl!("Make Text Dynamic"), false, has_selection, 24.0).clicked() {
|
||||
if widgets::icon_button_enabled(ui, "type", tl!("Make Text Dynamic"), false, single_type, 24.0).clicked() {
|
||||
app.run("variable.makeTextDynamic", json!({})).ok();
|
||||
}
|
||||
if widgets::icon_button_enabled(ui, "eye", tl!("Make Visibility Dynamic"), false, has_selection, 24.0).clicked() {
|
||||
|
|
@ -150,14 +197,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
if widgets::icon_button(ui, "plus", tl!("New Variable…"), false, 24.0).clicked() {
|
||||
crate::menus::invoke(app, "variable.define", json!({}));
|
||||
}
|
||||
let bound_to_selection = !selected.is_empty();
|
||||
if widgets::icon_button_enabled(ui, "link-2-off", tl!("Unbind Variable"), false, bound_to_selection, 24.0).clicked() {
|
||||
if widgets::icon_button_enabled(ui, "link-2-off", tl!("Unbind Variable"), false, selection_bound, 24.0).clicked() {
|
||||
app.run("variable.unbind", json!({"ids": selected})).ok();
|
||||
}
|
||||
if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Variable"), false, !vars.is_empty(), 24.0).clicked() {
|
||||
// Everything the document defines goes, in one undo step, as the panel's bin does.
|
||||
let names: Vec<String> = vars.iter().map(|v| v.name.clone()).collect();
|
||||
app.run("variable.delete", json!({"names": names})).ok();
|
||||
// The bin deletes the highlighted variable only, as the reference panel's does.
|
||||
if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Variable"), false, highlighted.is_some(), 24.0).clicked() {
|
||||
delete_highlighted(app);
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
|
|
@ -181,15 +226,16 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
let sets: Vec<String> = st.doc.variables.datasets.iter().map(|d| d.name.clone()).collect();
|
||||
let active = st.doc.variables.active_dataset.clone();
|
||||
let bound: Vec<u64> = st.doc.variables.bindings.keys().map(|i| i.0).collect();
|
||||
let selected: Vec<u64> = st.selection.objects.iter().map(|i| i.0).collect();
|
||||
let highlighted = highlighted_row(st);
|
||||
let highlighted_bound: Vec<u64> = highlighted.as_ref().map(|h| bound_objects(st, h)).unwrap_or_default();
|
||||
|
||||
if widgets::menu_item(ui, tl!("New Variable…"), true, false) {
|
||||
crate::menus::invoke(app, "variable.define", json!({}));
|
||||
}
|
||||
// Variable Options renames the variable the panel has highlighted, else the first.
|
||||
// Variable Options renames the highlighted variable, else the first.
|
||||
if widgets::menu_item(ui, tl!("Variable Options…"), !vars.is_empty(), false) {
|
||||
let name = vars.first().cloned().unwrap_or_default();
|
||||
crate::menus::invoke(app, "variable.rename", json!({"name": name}));
|
||||
let name = highlighted.clone().or_else(|| vars.first().cloned()).unwrap_or_default();
|
||||
crate::dialogs::variables::open(app, "variable.rename", Some(&name));
|
||||
}
|
||||
ui.separator();
|
||||
if widgets::menu_item(ui, tl!("Capture Data Set"), true, false) {
|
||||
|
|
@ -205,25 +251,23 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
crate::menus::invoke(app, "dataset.set", json!({}));
|
||||
}
|
||||
if widgets::menu_item(ui, tl!("Rename Data Set…"), !sets.is_empty(), false) {
|
||||
let name = sets.first().cloned().unwrap_or_default();
|
||||
crate::menus::invoke(app, "dataset.rename", json!({"name": name}));
|
||||
let name = active.clone().or_else(|| sets.first().cloned()).unwrap_or_default();
|
||||
crate::dialogs::variables::open(app, "dataset.rename", Some(&name));
|
||||
}
|
||||
if widgets::menu_item(ui, tl!("Delete Data Set"), !sets.is_empty(), false) {
|
||||
app.run("dataset.delete", json!({"names": sets})).ok();
|
||||
// Only the current data set goes: the rest of the rows stay.
|
||||
if widgets::menu_item(ui, tl!("Delete Data Set"), active.is_some(), false) {
|
||||
delete_active_dataset(app);
|
||||
}
|
||||
ui.separator();
|
||||
if widgets::menu_item(ui, tl!("Select Bound Object"), !selected.is_empty(), false) {
|
||||
// The objects the selection is bound to, when it is bound at all.
|
||||
let ids: Vec<u64> = selected.clone();
|
||||
app.run("variable.unbind", json!({"ids": ids})).ok();
|
||||
// What the highlighted variable drives, selected — its bindings are left alone.
|
||||
if widgets::menu_item(ui, tl!("Select Bound Object"), !highlighted_bound.is_empty(), false) {
|
||||
select_bound_object(app);
|
||||
}
|
||||
if widgets::menu_item(ui, tl!("Select All Bound Objects"), !bound.is_empty(), false) {
|
||||
app.run("select.set", json!({"ids": bound})).ok();
|
||||
}
|
||||
}
|
||||
|
||||
use vectorcraft_doc::NodeId;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -285,6 +329,60 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// What the panel acts on is the highlight, and only while it names a variable: a stale one
|
||||
/// (deleted elsewhere, undone) highlights nothing, so Delete and Options… stay disabled.
|
||||
#[test]
|
||||
fn a_highlight_of_a_deleted_variable_highlights_nothing() {
|
||||
let mut app = app_with_data();
|
||||
app.session.execute("variable.highlight", &json!({"variable": "Name"})).unwrap();
|
||||
assert_eq!(highlighted_row(app.session.doc().unwrap()).as_deref(), Some("Name"));
|
||||
assert_eq!(bound_objects(app.session.doc().unwrap(), "Name").len(), 1);
|
||||
app.session.execute("variable.delete", &json!({"names": ["Name"]})).unwrap();
|
||||
assert!(highlighted_row(app.session.doc().unwrap()).is_none());
|
||||
}
|
||||
|
||||
/// The reviewer's regression case, through the calls the panel and its menu make: highlight
|
||||
/// a variable, Select Bound Object, then delete one variable and the current dataset. The
|
||||
/// selection lands on what the row drives with the binding intact, and the others remain.
|
||||
#[test]
|
||||
fn selecting_and_deleting_act_on_the_highlighted_row_only() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 300, "height": 200})).unwrap();
|
||||
let a = app.session.execute("text.create", &json!({"x": 10, "y": 40, "text": "Alice"})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = app.session.execute("text.create", &json!({"x": 10, "y": 80, "text": "Bob"})).unwrap()["id"].as_u64().unwrap();
|
||||
for (name, id) in [("Name", a), ("Other", b)] {
|
||||
app.session.execute("variable.define", &json!({"name": name, "kind": "text"})).unwrap();
|
||||
app.session.execute("variable.bind", &json!({"variable": name, "ids": [id]})).unwrap();
|
||||
}
|
||||
app.session.execute("dataset.capture", &json!({})).unwrap();
|
||||
app.session.execute("dataset.capture", &json!({})).unwrap();
|
||||
app.session.execute("dataset.select", &json!({"name": "Data Set 1"})).unwrap();
|
||||
|
||||
// A row click highlights; Select Bound Object selects what it drives, binding intact.
|
||||
click_row(&mut app, "Name", &[a]);
|
||||
assert_eq!(app.session.doc().unwrap().selection.objects, vec![NodeId(a)]);
|
||||
assert!(!app.session.doc().unwrap().doc.variables.objects_of("Name").is_empty());
|
||||
select_bound_object(&mut app);
|
||||
assert_eq!(app.session.doc().unwrap().selection.objects, vec![NodeId(a)]);
|
||||
assert!(!app.session.doc().unwrap().doc.variables.objects_of("Name").is_empty());
|
||||
|
||||
// Delete Variable deletes the highlighted one; Delete Data Set the current one.
|
||||
delete_highlighted(&mut app);
|
||||
delete_active_dataset(&mut app);
|
||||
let vars = app.run("variable.list", json!({})).unwrap();
|
||||
assert_eq!(vars["variables"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(vars["variables"][0]["name"], "Other");
|
||||
let sets = app.run("dataset.list", json!({})).unwrap();
|
||||
assert_eq!(sets["datasets"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(sets["datasets"][0]["name"], "Data Set 2");
|
||||
|
||||
// The panel and its menu still draw with the highlight gone stale.
|
||||
let text = crate::tests_labels::painted_text(&mut app, show);
|
||||
assert!(text.contains("Other") && text.contains("Data Set 2"), "{text}");
|
||||
let text = crate::tests_labels::painted_text(&mut app, menu);
|
||||
assert!(text.contains("Select Bound Object"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_panel_is_registered_with_its_own_icon() {
|
||||
let row = crate::state::ICON_PANELS.iter().find(|(id, ..)| *id == ID).expect("in the icon column");
|
||||
|
|
|
|||
Loading…
Reference in New Issue