mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Pictures: Change Picture replaces the selected picture (#115)
The Picture Format ▸ Change Picture button ran the insert-picture dialog, so it added a second picture instead of replacing the selected one. A new app.changePictureDialog picks a file and runs picture.change, which keeps the picture's position, size and formatting; the canvas right-click menu offers it on pictures too. On the web, where the file arrives later through the inbox, the pending change is remembered for that pick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0ec0cc5e81
commit
2ff4285be6
@@ -563,6 +563,14 @@ fn context_menu(app: &mut SlideApp, resp: &egui::Response) {
|
||||
crate::ribbon::menu_item(app, ui, crate::icons::Icon::Group, "Group", "arrange.group", json!({}));
|
||||
crate::ribbon::menu_item(app, ui, crate::icons::Icon::Ungroup, "Ungroup", "arrange.ungroup", json!({}));
|
||||
});
|
||||
let picture = app
|
||||
.session
|
||||
.active()
|
||||
.is_some_and(|d| d.selected_shapes().iter().any(|s| matches!(s.kind, deckcraft_model::ShapeKind::Picture { .. })));
|
||||
if picture && ui.button("Change Picture…").clicked() {
|
||||
let _ = app.run("app.changePictureDialog", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Link…").clicked() {
|
||||
app.dialog = Some(crate::dialogs::Dialog::new("hyperlink"));
|
||||
ui.close();
|
||||
|
||||
@@ -156,6 +156,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
|
||||
("app.preferences", "Preferences…", Some("Cmd+,"), "{}"),
|
||||
("app.openDialog", "Open…", None, "{purpose?}"),
|
||||
("app.insertPictureDialog", "Picture from File…", None, "{}"),
|
||||
("app.changePictureDialog", "Change Picture…", None, "{}"),
|
||||
("app.insertAudioDialog", "Audio from File…", None, "{}"),
|
||||
("app.insertVideoDialog", "Video from File…", None, "{}"),
|
||||
("app.saveAsDialog", "Save As…", None, "{}"),
|
||||
@@ -209,6 +210,8 @@ pub struct SlideApp {
|
||||
pub(crate) notes_buf: (u64, usize, String),
|
||||
/// Thumbnail pane drag (slide index, current drop index).
|
||||
pub(crate) thumb_drag: Option<(usize, usize)>,
|
||||
/// The web picker's next picture replaces the selected one (`app.changePictureDialog`).
|
||||
change_picture: bool,
|
||||
}
|
||||
|
||||
impl SlideApp {
|
||||
@@ -244,6 +247,7 @@ impl SlideApp {
|
||||
quit_confirmed: false,
|
||||
notes_buf: (0, usize::MAX, String::new()),
|
||||
thumb_drag: None,
|
||||
change_picture: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,6 +352,7 @@ impl SlideApp {
|
||||
"app.preferences" => self.dialog = Some(dialogs::Dialog::new("preferences")),
|
||||
"app.openDialog" => self.pick_and_open(),
|
||||
"app.insertPictureDialog" => self.pick_and_insert("picture", "insert.picture"),
|
||||
"app.changePictureDialog" => self.pick_and_insert("picture", "picture.change"),
|
||||
"app.insertAudioDialog" => self.pick_and_insert("audio", "insert.audio"),
|
||||
"app.insertVideoDialog" => self.pick_and_insert("video", "insert.video"),
|
||||
"app.saveAsDialog" => self.save_as_dialog("deckcraft"),
|
||||
@@ -406,6 +411,7 @@ impl SlideApp {
|
||||
}
|
||||
}
|
||||
} else if let Some(f) = self.services.open_async.as_mut() {
|
||||
self.change_picture = cmd == "picture.change";
|
||||
f(purpose);
|
||||
}
|
||||
}
|
||||
@@ -491,6 +497,13 @@ impl SlideApp {
|
||||
let Some(inbox) = self.services.inbox.clone() else { return };
|
||||
let files: Vec<(String, Vec<u8>)> = inbox.lock().map(|mut v| std::mem::take(&mut *v)).unwrap_or_default();
|
||||
for (name, bytes) in files {
|
||||
if std::mem::take(&mut self.change_picture) && deckcraft_engine::cmd::insert::content_type(&name, &bytes).starts_with("image/") {
|
||||
let p = json!({"name": name, "data": deckcraft_engine::cmd::base64_encode(&bytes)});
|
||||
if let Err(e) = self.session.execute("picture.change", &p) {
|
||||
self.set_status(e.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
self.open_or_insert_bytes(&name, &bytes);
|
||||
}
|
||||
}
|
||||
@@ -889,6 +902,54 @@ mod tests {
|
||||
sh.text.as_ref().map(|t| t.paragraphs.iter().map(|p| p.text()).collect::<Vec<_>>().join("\n")).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// #115: Change Picture replaced nothing — it inserted a second picture.
|
||||
#[test]
|
||||
fn change_picture_replaces_the_selected_picture() {
|
||||
let svg = |c: &str| {
|
||||
format!("<svg xmlns='http://www.w3.org/2000/svg' width='4' height='4'><rect width='4' height='4' fill='{c}'/></svg>").into_bytes()
|
||||
};
|
||||
let media = |app: &SlideApp| {
|
||||
let d = app.session.active().unwrap();
|
||||
let pics: Vec<_> = d.doc.slides[0]
|
||||
.shapes
|
||||
.iter()
|
||||
.filter_map(|s| if let deckcraft_model::ShapeKind::Picture { fill } = &s.kind { Some(fill.media) } else { None })
|
||||
.collect();
|
||||
assert_eq!(pics.len(), 1, "one picture, replaced in place");
|
||||
pics[0]
|
||||
};
|
||||
let insert = |app: &mut SlideApp| {
|
||||
let id =
|
||||
app.session.execute("insert.picture", &json!({"name": "a.svg", "data": deckcraft_engine::cmd::base64_encode(&svg("red"))})).unwrap()
|
||||
["id"]
|
||||
.clone();
|
||||
app.session.execute("edit.select", &json!({"ids": [id]})).unwrap();
|
||||
media(app)
|
||||
};
|
||||
|
||||
// Desktop: the file dialog returns a path.
|
||||
let blue = svg("blue");
|
||||
let services = Services {
|
||||
pick_open: Some(Box::new(|_: &str| Some("b.svg".into()))),
|
||||
read: Some(Box::new(move |_: &str| Ok(blue.clone()))),
|
||||
..Default::default()
|
||||
};
|
||||
let mut app = SlideApp::new(Session::with_new(), services);
|
||||
let before = insert(&mut app);
|
||||
app.run("app.changePictureDialog", json!({})).unwrap();
|
||||
assert_ne!(media(&app), before);
|
||||
|
||||
// Web: the browser picker delivers the file through the inbox later.
|
||||
let inbox = Inbox::default();
|
||||
let services = Services { open_async: Some(Box::new(|_: &str| {})), inbox: Some(inbox.clone()), ..Default::default() };
|
||||
let mut app = SlideApp::new(Session::with_new(), services);
|
||||
let before = insert(&mut app);
|
||||
app.run("app.changePictureDialog", json!({})).unwrap();
|
||||
inbox.lock().unwrap().push(("b.svg".into(), svg("blue")));
|
||||
app.drain_inbox();
|
||||
assert_ne!(media(&app), before);
|
||||
}
|
||||
|
||||
/// #17: Home created a new blank presentation instead of going to the start screen.
|
||||
#[test]
|
||||
fn home_shows_the_start_screen_over_the_open_presentation() {
|
||||
|
||||
@@ -2070,7 +2070,7 @@ fn picture_format(app: &mut SlideApp, ui: &mut Ui) {
|
||||
}
|
||||
});
|
||||
rows(ui, |ui| {
|
||||
small(app, ui, Icon::ChangePicture, "Change Picture", "app.insertPictureDialog", json!({}));
|
||||
small(app, ui, Icon::ChangePicture, "Change Picture", "app.changePictureDialog", json!({}));
|
||||
small(app, ui, Icon::ResetPicture, "Reset Picture", "picture.reset", json!({}));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user