From a7cad22c13a0f7e05708c2279ce88185e3afd7de Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 19:28:22 +0000 Subject: [PATCH] #1284: right-clicking a Graph Editor keyframe opens its key menu The Graph Editor had no menu for keyframes. Right-click now opens the same key menu as the Timeline, for the commands that apply from the graph: Edit Value, Copy and Paste, selecting equal or neighbouring keys, Hold, Rove Across Time (spatial properties only), Easy Ease, interpolation and Delete. The items run the existing keys.* commands, so undo and the automation ids are unchanged. A right-click on an unselected key selects it first, as a click does. Not covered by an automated test; checked by compiling and by reading the menu against the Timeline's. Claude-Session: https://claude.ai/code/session_0136kZtXrYo5VUWEFxWduwUD --- crates/ui-egui/src/panels/graph.rs | 44 +++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/graph.rs b/crates/ui-egui/src/panels/graph.rs index 2dbb98268..41e2c5263 100644 --- a/crates/ui-egui/src/panels/graph.rs +++ b/crates/ui-egui/src/panels/graph.rs @@ -453,7 +453,7 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte } } else if kresp.clicked() && mods.shift { actions.push(("keys.select".into(), json!({"keys": [kjson.clone()], "toggle": true}))); - } else if (kresp.clicked() || kresp.drag_started()) && !k.sel { + } else if (kresp.clicked() || kresp.drag_started() || kresp.secondary_clicked()) && !k.sel { actions.push(("keys.select".into(), json!({"keys": [kjson.clone()], "add": mods.shift}))); } // A drag moves every selected key, by how far the pointer went from where the button @@ -485,6 +485,48 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte ctx.data_mut(|dd| dd.insert_temp(drag_id, false)); actions.push(("__endMerge".into(), json!({}))); } + // Right-click: the Timeline's key menu, for the commands that apply from the Graph Editor. + kresp.context_menu(|ui| { + let editable_value = matches!(key.value, Value::Scalar(_) | Value::Vec2(_) | Value::Vec3(_)); + for (lbl, cmd, params) in [ + ("Edit Value…", "keys.set", kjson.clone()), + ("-", "", json!({})), + ("Copy", "keys.copy", json!({})), + ("Paste", "keys.paste", json!({})), + ("-", "", json!({})), + ("Select Equal Keyframes", "keys.selectEqual", json!({})), + ("Select Previous Keyframes", "keys.selectPrevious", json!({})), + ("Select Following Keyframes", "keys.selectFollowing", json!({})), + ("-", "", json!({})), + ("Toggle Hold Keyframe", "keys.toggleHold", json!({})), + ("Rove Across Time", "keys.interpolation", json!({"roving": !key.roving})), + ("-", "", json!({})), + ("Easy Ease", "keys.easyEase", json!({})), + ("Easy Ease In", "keys.easyEaseIn", json!({})), + ("Easy Ease Out", "keys.easyEaseOut", json!({})), + ("-", "", json!({})), + ("Linear", "keys.interpolation", json!({"interpolation": "linear"})), + ("Bezier", "keys.interpolation", json!({"interpolation": "bezier"})), + ("Auto Bezier", "keys.interpolation", json!({"interpolation": "autoBezier"})), + ("-", "", json!({})), + ("Delete", "keys.delete", json!({})), + ] { + if lbl == "-" { + ui.separator(); + continue; + } + if lbl == "Rove Across Time" && !c.prop.spatial { + continue; + } + if lbl == "Edit Value…" && !editable_value { + continue; + } + if ui.button(lbl).clicked() { + actions.push((cmd.into(), params)); + ui.close(); + } + } + }); } }