Merge branch 'package-dialog' into package-pdf-preset

# Conflicts:
#	ROADMAP.md
#	crates/ui-egui/src/package_dialog.rs
This commit is contained in:
moppeeer committed 2026-10-24 13:37:54 +02:00
commit 2ddb60fa43
3 files changed
+100 -13

No files matched your search

+5 -2
View File
@@ -450,11 +450,13 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
root.write(&path, &super::export::pdf_report(&packed, &s.cache, &params)?.bytes)?;
files.push(path);
}
// The report: fonts (missing first), links, preflight, the user's instructions.
let mut r = format!("Package report: {display_title}\n\n");
// The printing instructions first, as the form gave them; then the report: fonts (missing
// first), links, preflight.
let mut r = String::new();
if let Some(t) = str_param(p, "instructions").filter(|t| !t.trim().is_empty()) {
r += &format!("Instructions\n{t}\n\n");
}
r += &format!("Package report: {display_title}\n\n");
r += "Fonts\n";
r += &font_lines.concat();
if !fallback_lines.is_empty() {
@@ -690,6 +692,7 @@ mod tests {
assert!(out.join("Links").join("logo.svg").exists());
let report = std::fs::read_to_string(out.join("Instructions.txt")).unwrap();
assert!(report.contains("Print on matte.") && report.contains("logo.svg"), "{report}");
assert!(report.find("Print on matte.") < report.find("Package report:"), "the instructions come first: {report}");
assert!(report.contains("Preflight:"), "{report}");
// The packaged document links to its Links folder.
let doc_file = files.iter().find(|f| f.ends_with(".designcraft")).unwrap();
+7 -1
View File
@@ -137,6 +137,10 @@ fn text_field(ui: &mut egui::Ui, d: &mut Dialog, key: &str, w: f32) {
}
}
/// The temporary-data key a dialog sets, each frame, while one of its multi-line fields has focus:
/// Enter then makes a new line there, and the footer doesn't take it for OK.
pub(crate) const MULTILINE_FOCUS: &str = "dialog_multiline_focus";
/// Extract once when opened. Cancel is read-only; confirmation adds exactly these colours.
pub(crate) fn open_color_theme(app: &mut DesignApp, params: Value) {
match app.run("color.extractTheme", params) {
@@ -2885,6 +2889,8 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
app.ui.style_preview = d.b("preview");
style_options_preview(app, &mut d);
}
// A multi-line field with focus (set by the dialog this frame) takes Enter as a new line.
let multiline = ui.ctx().data_mut(|m| m.remove_temp::<bool>(egui::Id::new(MULTILINE_FOCUS))).unwrap_or(false);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui
.add(
@@ -2892,7 +2898,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
.fill(crate::theme::Tokens::get(ui.ctx()).accent_strong),
)
.clicked()
|| (!popup_open && ui.input(|i| i.key_pressed(egui::Key::Enter)))
|| (!popup_open && !multiline && ui.input(|i| i.key_pressed(egui::Key::Enter)))
{
result = Some(true);
}
+88 -10
View File
@@ -45,13 +45,7 @@ pub fn open(app: &mut DesignApp) -> Result<(), String> {
.unwrap_or_default()
.iter()
.map(|f| {
let status = if f["missing"].as_bool().unwrap_or(false) {
"missing"
} else if f["styleMissing"].as_bool().unwrap_or(false) || f["matchStatus"] == "styleSubstitute" {
"substituted"
} else {
"ok"
};
let status = font_status(f);
let name = format!("{} {}", f["family"].as_str().unwrap_or(""), f["style"].as_str().unwrap_or("")).trim().to_string();
json!({"name": name, "source": f["source"].as_str().unwrap_or(""), "status": status})
})
@@ -84,14 +78,36 @@ pub fn open(app: &mut DesignApp) -> Result<(), String> {
// The packaged PDF's preset: the one last used for Export PDF, as InDesign suggests it.
let last = app.ui.pdf_export.preset.clone();
let preset = crate::dialogs::pdf_presets().find(|p| *p == last).unwrap_or("Desktop Printing");
let doc = app.session.active().map(|d| d.uid);
app.ui.dialog = Some(Dialog::new(
ID,
json!({"page": "summary", "fonts": fonts, "links": links, "spot": spot, "instructions": false, "idml": true, "pdf": false,
json!({"page": "summary", "doc": doc, "fonts": fonts, "links": links, "spot": spot, "instructions": false, "idml": true, "pdf": false,
"pdfPreset": preset, "problemsOnly": false}),
));
Ok(())
}
/// A `font.list` entry's status here: a missing font, or a style its family lacks, is missing
/// (as `file.package` counts them); a style it stands in for is substituted.
fn font_status(f: &Value) -> &'static str {
if f["missing"].as_bool().unwrap_or(false) || f["styleMissing"].as_bool().unwrap_or(false) {
"missing"
} else if f["matchStatus"] == "styleSubstitute" {
"substituted"
} else {
"ok"
}
}
/// The document the dialog was opened on is still the active one (automation can switch
/// documents while it is open, and the package must be of the one summarised).
fn same_document(app: &DesignApp, d: &Dialog) -> Result<(), String> {
match app.session.active() {
Some(doc) if d.fields.get("doc").and_then(Value::as_u64) == Some(doc.uid) => Ok(()),
_ => Err("the active document changed after the Package dialog opened".into()),
}
}
fn rows(d: &Dialog, key: &str) -> Vec<Value> {
d.fields.get(key).and_then(Value::as_array).cloned().unwrap_or_default()
}
@@ -295,9 +311,10 @@ fn pdf_param(d: &Dialog) -> Value {
/// Package…: the Printing Instructions first when asked for, else the folder.
pub fn confirm(app: &mut DesignApp, d: &Dialog) -> Result<Value, String> {
same_document(app, d)?;
let (idml, pdf) = (d.b("idml"), pdf_param(d));
if d.b("instructions") {
app.ui.dialog = Some(Dialog::new(INSTRUCTIONS, json!({"idml": idml, "pdf": pdf})));
app.ui.dialog = Some(Dialog::new(INSTRUCTIONS, json!({"idml": idml, "pdf": pdf, "doc": d.fields.get("doc")})));
return Ok(Value::Null);
}
package_to_folder(app, idml, pdf, None)
@@ -311,9 +328,14 @@ pub fn show_instructions(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
crate::rtl::label(ui, crate::i18n::tr(&lang, label));
let mut text = d.s(key);
let edit = if lines > 1 { egui::TextEdit::multiline(&mut text).desired_rows(lines) } else { egui::TextEdit::singleline(&mut text) };
if ui.add(edit.desired_width(320.0)).changed() {
let resp = ui.add(edit.desired_width(320.0));
if resp.changed() {
d.fields.insert(key.into(), json!(text));
}
// Enter there makes a new line: the footer mustn't take it for Continue.
if lines > 1 && resp.has_focus() {
ui.ctx().data_mut(|m| m.insert_temp(egui::Id::new(crate::dialogs::MULTILINE_FOCUS), true));
}
ui.end_row();
}
});
@@ -345,6 +367,7 @@ fn instructions_text(d: &Dialog) -> String {
/// Continue: the folder, with the instructions.
pub fn confirm_instructions(app: &mut DesignApp, d: &Dialog) -> Result<Value, String> {
same_document(app, d)?;
let text = instructions_text(d);
package_to_folder(app, d.b("idml"), d.fields.get("pdf").cloned().unwrap_or(json!(false)), Some(text).filter(|t| !t.is_empty()))
}
@@ -516,4 +539,59 @@ mod tests {
assert!(app.ui.dialog.is_none());
std::fs::remove_dir_all(dir).unwrap();
}
/// A style the font's family lacks counts as missing, as `file.package` counts it.
#[test]
fn a_missing_font_style_is_a_missing_font() {
assert_eq!(font_status(&json!({"missing": true})), "missing");
assert_eq!(font_status(&json!({"missing": false, "styleMissing": true})), "missing");
assert_eq!(font_status(&json!({"missing": false, "matchStatus": "styleSubstitute"})), "substituted");
assert_eq!(font_status(&json!({"missing": false})), "ok");
}
/// The package is of the document the dialog summarised: when another document became active
/// meanwhile, Package… and Continue refuse, and nothing is written.
#[test]
fn package_refuses_when_the_active_document_changed() {
let dir = temp_dir("switched");
let mut app = app_with_missing_link(&dir);
let out = dir.join("Packed Folder");
let picked = out.to_string_lossy().to_string();
app.services.pick_save = Some(Box::new(move |_| Some(picked.clone())));
open(&mut app).unwrap();
let d = app.ui.dialog.take().unwrap();
app.run("file.new", json!({"title": "Other"})).unwrap();
assert!(confirm(&mut app, &d).is_err());
let ins = Dialog::new(INSTRUCTIONS, json!({"idml": true, "pdf": false, "doc": d.fields["doc"]}));
assert!(confirm_instructions(&mut app, &ins).is_err());
assert!(!out.exists(), "nothing packaged");
std::fs::remove_dir_all(dir).unwrap();
}
/// Enter in Address or Instructions makes a new line there; in a one-line field it continues.
#[test]
fn enter_in_a_multiline_instruction_field_is_a_new_line() {
let dir = temp_dir("enter");
let mut app = app_with_missing_link(&dir);
let doc = app.session.active().unwrap().uid;
app.ui.dialog = Some(Dialog::new(INSTRUCTIONS, json!({"idml": true, "pdf": false, "doc": doc})));
let mut h = harness(app);
h.run_steps(3);
let field = |h: &Harness<'static, DesignApp>, n: usize| {
h.query_all_by(|x| x.role() == egui::accesskit::Role::TextInput || x.role() == egui::accesskit::Role::MultilineTextInput)
.nth(n)
.unwrap()
.rect()
.center()
};
// Address is the third field.
let address = field(&h, 2);
h.event(egui::Event::PointerButton { pos: address, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE });
h.event(egui::Event::PointerButton { pos: address, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE });
h.run_steps(2);
h.key_press(egui::Key::Enter);
h.run_steps(3);
assert_eq!(h.state().ui.dialog.as_ref().map(|d| d.id.as_str()), Some(INSTRUCTIONS), "still open");
std::fs::remove_dir_all(dir).unwrap();
}
}