Text editing on the Program monitor: double-click and Ctrl/Cmd+A select all the text (#1102)

This commit is contained in:
MarekSpodBiedry 2026-10-11 19:10:53 +02:00
parent 0800a8b0aa
commit 78a4c73a1e
4 changed files with 48 additions and 1 deletions

View File

@ -175,6 +175,9 @@ pub fn targets_source(app: &FilmcraftApp, params: &Value) -> bool {
/// Execute a UI or engine command by id.
pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params: Value) -> Result<Value, String> {
if id == "edit.selectAll" && crate::panels::graphics::select_all_edited_text(app) {
return Ok(Value::Null);
}
if filmcraft_engine::source_monitor::source_command(id) && targets_source(app, &params) && params.get("target").is_none() {
let object = params.as_object_mut().ok_or("command parameters must be an object")?;
object.insert("target".into(), json!("source"));

View File

@ -835,6 +835,10 @@ pub fn monitor_overlay(app: &mut FilmcraftApp, ui: &mut egui::Ui, pic: Rect, fra
if resp.double_clicked() {
if let Some(v) = views.iter().find(|v| v.is_text() && v.hit(p)) {
start_edit_at(app, ui, v, p, &mut actions);
// double-click selects all the text so typing replaces it, as in Premiere
if let (Some(tp), Some(e)) = (text_of(&v.spec), app.ui.gfx_edit.as_mut()) {
(e.anchor, e.caret) = (0, tp.text.len());
}
}
} else if let Some(v) = views.iter().find(|v| v.hit(p)) {
if app.ui.gfx_edit.is_some() {
@ -964,6 +968,21 @@ fn start_edit_at(app: &mut FilmcraftApp, ui: &egui::Ui, v: &LayerView, p: Pos2,
actions.push(("graphics.selectLayer".into(), json!({"clip": v.clip.0, "layers": [v.layer]})));
}
/// Edit ▸ Select All while text is edited on the monitor selects all of that text, not the
/// timeline clips: on macOS the native menu takes Cmd+A before the text sees the key (#1102).
/// False when no text is being edited.
pub(crate) fn select_all_edited_text(app: &mut FilmcraftApp) -> bool {
let Some((clip, layer)) = app.ui.gfx_edit.as_ref().map(|e| (e.clip, e.layer)) else { return false };
let t = app.session.playhead();
let Some((_, it)) = app.session.active_sequence().and_then(|q| q.find_item(ClipId(clip))) else { return false };
let Some(ItemKind::Graphic { width, height, .. }) = app.session.project.item(it.item).map(|p| &p.kind) else { return false };
let Some(effect) = layer_indices(&it.effects).get(layer).and_then(|&i| it.effects.get(i)) else { return false };
let Some(len) = eval_layer(effect, it.source_time_at(t), (*width, *height)).as_ref().and_then(text_of).map(|tp| tp.text.len()) else { return false };
let Some(ed) = app.ui.gfx_edit.as_mut() else { return false };
(ed.anchor, ed.caret) = (0, len);
true
}
/// Keyboard handling while a text layer is being edited.
fn text_edit(app: &mut FilmcraftApp, ui: &mut egui::Ui, v: &LayerView, mut ed: GfxEdit, resp: &egui::Response, actions: &mut Vec<(String, Value)>) {
let _ = resp;

View File

@ -545,3 +545,28 @@ fn a_hidden_layer_has_no_box_and_cannot_be_clicked() {
d.frames(4);
assert_eq!(ids(&mut d).len(), 14);
}
/// A double-click on a text layer edits it with all its text selected, so typing replaces it; while
/// it is edited, Select All selects the text again, not the timeline clips, as in Premiere (#1102).
#[test]
fn double_click_and_select_all_select_the_text_being_edited() {
let mut d = Driver::new();
let (clip, pre) = d.point_text();
let (a, b) = (d.handle(&pre, 0), d.handle(&pre, 2));
d.ok("ui.click", json!({"x": (a.0 + b.0) / 2.0, "y": (a.1 + b.1) / 2.0, "count": 2}));
d.frames(4);
let edit = |d: &Driver| d.harness.state().ui.gfx_edit.as_ref().map(|e| (e.clip, e.layer, e.anchor, e.caret));
assert_eq!(edit(&d), Some((clip, 0, 0, "Title".len())), "the double-click selects the whole text");
// caret at the end, then Select All as the native macOS menu sends it (it takes Cmd+A first)
if let Some(e) = d.harness.state_mut().ui.gfx_edit.as_mut() {
(e.anchor, e.caret) = (5, 5);
}
let selection = d.harness.state().session.state.selection.clone();
let (tx, rx) = channel();
d.harness.state_mut().command_inbox = Some(rx);
tx.send("edit.selectAll".to_string()).unwrap();
d.frames(2);
assert_eq!(edit(&d), Some((clip, 0, 0, 5)), "Select All selects the text being edited");
assert_eq!(d.harness.state().session.state.selection, selection, "and leaves the timeline selection alone");
}

View File

@ -116,7 +116,7 @@ duration (a track is added if needed).
| Tool | On the Program monitor |
|---|---|
| Type (T) | Click empty picture: new point-text layer with a caret. Drag on empty picture: new paragraph-text layer with that box. Click a text layer: caret there. The box of the text being typed into is red. Type; ←/→ (⌥ word, ⌘ line), ↑/↓, Home/End, Shift to select, ⌘A, ⌘C/⌘X/⌘V, Return = new line, Backspace/Delete, Esc = stop editing. Drag inside the edited text to select. |
| Selection (V) | Click a layer to select it (box with handles and anchor point; only selected layers have a box, and a layer whose visibility is off has none and cannot be clicked); drag to move; drag the anchor point to move it alone; drag a handle to scale point text about its anchor, resize a paragraph-text box (see *Point text and paragraph text*), or change a shape's Size: the dragged side or corner follows the pointer while the opposite one and the anchor point stay, Scale is untouched, and Shift on a corner keeps the proportions (a path, which has no Size, stretches instead); double-click a text layer to edit it. |
| Selection (V) | Click a layer to select it (box with handles and anchor point; only selected layers have a box, and a layer whose visibility is off has none and cannot be clicked); drag to move; drag the anchor point to move it alone; drag a handle to scale point text about its anchor, resize a paragraph-text box (see *Point text and paragraph text*), or change a shape's Size: the dragged side or corner follows the pointer while the opposite one and the anchor point stay, Scale is untouched, and Shift on a corner keeps the proportions (a path, which has no Size, stretches instead); double-click a text layer to edit it with all its text selected, so typing replaces it; while text is edited, Select All (Cmd/Ctrl+A or Edit ▸ Select All) selects all of that text instead of the timeline clips. |
| Rectangle / Ellipse | Drag to draw a shape layer. |
| Pen (P) | Click to place points; click the first point (or Return) to close the path; Esc cancels. |