mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-12 05:12:39 +00:00
Merge branch 'package-dialog' into package-pdf-preset
# Conflicts: # ROADMAP.md # crates/ui-egui/src/package_dialog.rs
This commit is contained in:
commit
2ddb60fa43
3 files changed
+100
-13
No files matched your search
@@ -450,11 +450,13 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
root.write(&path, &super::export::pdf_report(&packed, &s.cache, ¶ms)?.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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user