mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
T: Track Changes — marked insertions and deletions, accept/reject all
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
60a8fefe37
commit
a4e76ed620
+2
-1
@@ -82,6 +82,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Edit ▸ Menus (`window.hideMenuItem`): hide menu items; menus with hidden items end in Show All Menu Items.
|
||||
- View ▸ Separations Preview (`view.separations`): one process plate as ink density, or areas over an ink limit in red (a GCR preview of the rendered view; spot plates and the flattener preview aren't there).
|
||||
- Books (`book.new/open/add/remove/list/styleSource/paginate/syncStyles/exportPdf`, Book panel): `.dcbook` files, continuous page numbering, style sync from a style source, one merged PDF.
|
||||
- Track Changes (`changes.track/list/acceptAll/rejectAll`, Type ▸ Track Changes): typing is marked added (highlighted on screen), deletions are kept but hidden from layout and exports until accepted or rejected. Per-change accept/reject and authors aren't there yet.
|
||||
- Drag and drop text editing: drag selected text to move it (Alt copies) with its formatting (`text.release`).
|
||||
- Image Import Options: place any page of a multi-page PDF (`file.place {pdfPage}`; the app asks which page), shown on screen and embedded as that page in PDF export.
|
||||
- Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override.
|
||||
@@ -130,7 +131,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
|
||||
## How far from full parity (estimate, 2026-10-02, updated)
|
||||
|
||||
**Breadth: ~89% weighted** (P0 core 99%, P1 95%, P2 39%) over the 275 features of the InDesign catalogue, scored
|
||||
**Breadth: ~93% weighted** (P0 core 99%, P1 97%, P2 58%) over the 275 features of the InDesign catalogue, scored
|
||||
row by row in [docs/parity.md](docs/parity.md) (`cargo xtask parity` recomputes it). Many features scored done still
|
||||
lack some of InDesign's options or dialog details, so **overall parity including depth is about 72%**.
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ pub struct RunStyle {
|
||||
pub custom_tracking: bool,
|
||||
/// The first condition applied (its indicator colour underlines the text on screen).
|
||||
pub condition: Option<String>,
|
||||
/// Added while tracking changes (marked on screen).
|
||||
pub inserted: bool,
|
||||
}
|
||||
|
||||
/// An underline or strikethrough bar: its top edge `offset` below the baseline (negative =
|
||||
|
||||
@@ -134,6 +134,7 @@ impl StyleTable<'_> {
|
||||
.or_insert_with(|| !p.font_family.is_empty() && !designcraft_fonts::FontDb::global().has_family(&p.font_family)),
|
||||
custom_tracking: p.tracking.abs() > 1e-9 || matches!(p.kerning, designcraft_doc::Kerning::Manual(_)),
|
||||
condition: p.conditions.first().cloned(),
|
||||
inserted: p.change == designcraft_doc::ChangeMark::Inserted,
|
||||
};
|
||||
if let Some(i) = self.styles.iter().rposition(|s| *s == rs) {
|
||||
return i as u32;
|
||||
@@ -199,7 +200,8 @@ pub(crate) fn shape_para(
|
||||
};
|
||||
let props = styles.resolve_char(para_chars, fmt);
|
||||
let style = table.intern(&props);
|
||||
if !props.conditions.is_empty() && props.conditions.iter().all(|c| sub.hidden_conditions.contains(c)) {
|
||||
let deleted = props.change == designcraft_doc::ChangeMark::Deleted;
|
||||
if deleted || (!props.conditions.is_empty() && props.conditions.iter().all(|c| sub.hidden_conditions.contains(c))) {
|
||||
// Hidden conditional text: zero-width, unbreakable, undrawn place-holders keep every
|
||||
// byte addressable (caret, selection) without taking space.
|
||||
let face = db.face(&props.font_family, &props.font_style);
|
||||
|
||||
@@ -115,6 +115,16 @@ pub enum Leading {
|
||||
Points(f64),
|
||||
}
|
||||
|
||||
/// A tracked change on text.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ChangeMark {
|
||||
#[default]
|
||||
None,
|
||||
Inserted,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "kind", content = "value")]
|
||||
pub enum Kerning {
|
||||
@@ -464,6 +474,8 @@ attr_set! {
|
||||
otf_features: Vec<String> = Vec::new(),
|
||||
/// Conditional text: the conditions applied (Window › Type & Tables › Conditional Text).
|
||||
conditions: Vec<String> = Vec::new(),
|
||||
/// Track Changes: text added or deleted while tracking (deleted text isn't laid out).
|
||||
change: ChangeMark = ChangeMark::None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -325,6 +325,8 @@ pub struct DocSettings {
|
||||
pub blend_space: BlendSpace,
|
||||
/// Preferences › Appearance of Black: [Black] at 100% overprints (Overprint Preview, output).
|
||||
pub overprint_black: bool,
|
||||
/// Type › Track Changes: edits are recorded as inserted / deleted text.
|
||||
pub track_changes: bool,
|
||||
}
|
||||
|
||||
impl Default for DocSettings {
|
||||
@@ -354,6 +356,7 @@ impl Default for DocSettings {
|
||||
advanced_type: AdvancedType::default(),
|
||||
blend_space: BlendSpace::Cmyk,
|
||||
overprint_black: true,
|
||||
track_changes: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
//! Type › Track Changes: record edits as inserted / deleted text; accept or reject them.
|
||||
|
||||
use designcraft_doc::{ChangeMark, Story, StoryId};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, cmd, has_doc};
|
||||
use crate::{Result, Session};
|
||||
|
||||
/// Mark `r` deleted (text this session inserted while tracking is simply removed). Returns where
|
||||
/// the text after the range now starts.
|
||||
pub(crate) fn mark_deleted(st: &mut Story, r: std::ops::Range<usize>) -> usize {
|
||||
if r.is_empty() {
|
||||
return r.start;
|
||||
}
|
||||
// Inserted pieces inside the range go away; the rest is marked deleted.
|
||||
let mut ins: Vec<std::ops::Range<usize>> = st
|
||||
.runs()
|
||||
.filter(|(rr, f)| f.over.change == Some(ChangeMark::Inserted) && rr.start < r.end && rr.end > r.start)
|
||||
.map(|(rr, _)| rr.start.max(r.start)..rr.end.min(r.end))
|
||||
.collect();
|
||||
let mut end = r.end;
|
||||
ins.sort_by_key(|x| std::cmp::Reverse(x.start));
|
||||
for x in ins {
|
||||
st.delete(x.clone());
|
||||
end -= x.len();
|
||||
}
|
||||
if end > r.start {
|
||||
st.format_chars(r.start..end, |f| f.over.change = Some(ChangeMark::Deleted));
|
||||
}
|
||||
end
|
||||
}
|
||||
|
||||
/// Ranges of `mark` in `st`, last first.
|
||||
fn ranges(st: &Story, mark: ChangeMark) -> Vec<std::ops::Range<usize>> {
|
||||
let mut v: Vec<std::ops::Range<usize>> = st.runs().filter(|(_, f)| f.over.change == Some(mark)).map(|(r, _)| r).collect();
|
||||
v.sort_by_key(|r| std::cmp::Reverse(r.start));
|
||||
v
|
||||
}
|
||||
|
||||
/// Accept (or reject) every change in `st`.
|
||||
fn resolve(st: &mut Story, accept: bool) -> usize {
|
||||
let (remove, keep) = if accept { (ChangeMark::Deleted, ChangeMark::Inserted) } else { (ChangeMark::Inserted, ChangeMark::Deleted) };
|
||||
let removed = ranges(st, remove);
|
||||
let n = removed.len() + ranges(st, keep).len();
|
||||
for r in removed {
|
||||
st.delete(r);
|
||||
}
|
||||
let len = st.len();
|
||||
st.format_chars(0..len, |f| {
|
||||
if f.over.change.is_some() {
|
||||
f.over.change = None;
|
||||
}
|
||||
});
|
||||
n
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!("changes.track", "Track Changes", ["Type", "Track Changes"], None, "{on?: bool (default: toggle)}", has_doc, |s, p| {
|
||||
let cur = s.doc()?.doc.settings.track_changes;
|
||||
let on = p.get("on").and_then(Value::as_bool).unwrap_or(!cur);
|
||||
s.edit(|d, _| {
|
||||
d.settings.track_changes = on;
|
||||
Ok(json!({"on": on}))
|
||||
})
|
||||
}),
|
||||
cmd!(query "changes.list", "Changes", [], None, "{} → [{story, start, end, kind: inserted|deleted, text}]", has_doc, |s, _| {
|
||||
let d = &s.doc()?.doc;
|
||||
let mut out = Vec::new();
|
||||
for st in d.stories.values() {
|
||||
for (r, f) in st.runs() {
|
||||
let kind = match f.over.change {
|
||||
Some(ChangeMark::Inserted) => "inserted",
|
||||
Some(ChangeMark::Deleted) => "deleted",
|
||||
_ => continue,
|
||||
};
|
||||
out.push(json!({"story": st.id.0, "start": r.start, "end": r.end, "kind": kind, "text": st.text[r].to_string()}));
|
||||
}
|
||||
}
|
||||
Ok(Value::Array(out))
|
||||
}),
|
||||
cmd!(
|
||||
"changes.acceptAll",
|
||||
"Accept All Changes",
|
||||
["Type", "Track Changes"],
|
||||
None,
|
||||
"{story?} — deleted text goes, added text stays",
|
||||
has_doc,
|
||||
|s, p| all(s, p, true)
|
||||
),
|
||||
cmd!(
|
||||
"changes.rejectAll",
|
||||
"Reject All Changes",
|
||||
["Type", "Track Changes"],
|
||||
None,
|
||||
"{story?} — added text goes, deleted text comes back",
|
||||
has_doc,
|
||||
|s, p| all(s, p, false)
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn all(s: &mut Session, p: &Value, accept: bool) -> Result<Value> {
|
||||
let only = p.get("story").and_then(Value::as_u64).map(StoryId);
|
||||
s.edit(|d, sel| {
|
||||
let ids: Vec<StoryId> = d.stories.keys().copied().filter(|id| only.is_none_or(|o| o == *id)).collect();
|
||||
let mut n = 0;
|
||||
for id in ids {
|
||||
if let Some(st) = d.story_mut(id) {
|
||||
n += resolve(st, accept);
|
||||
}
|
||||
}
|
||||
// The caret may now be past the end.
|
||||
if let Some(t) = &mut sel.text
|
||||
&& let Some(st) = d.story(t.story)
|
||||
{
|
||||
t.anchor = t.anchor.min(st.len());
|
||||
t.focus = t.focus.min(st.len());
|
||||
}
|
||||
Ok(json!({"changes": n}))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn tracked_edits_accept_and_reject() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 200], "content": "text", "text": "The cat sat"})).unwrap();
|
||||
let sid = designcraft_doc::StoryId(r["story"].as_u64().unwrap());
|
||||
s.execute("changes.track", &json!({"on": true})).unwrap();
|
||||
// Replace "cat" with "dog", then type and erase a typo (gone without a trace).
|
||||
s.execute("text.select", &json!({"story": sid.0, "anchor": 4, "focus": 7})).unwrap();
|
||||
s.execute("text.insert", &json!({"text": "dog"})).unwrap();
|
||||
s.execute("text.insert", &json!({"text": "x"})).unwrap();
|
||||
s.execute("text.delete", &json!({})).unwrap();
|
||||
let text = |s: &Session| s.doc().unwrap().doc.stories[&sid].text.clone();
|
||||
assert_eq!(text(&s), "The catdog sat", "deleted text stays (marked)");
|
||||
let l = s.execute("changes.list", &json!({})).unwrap();
|
||||
assert_eq!(l.as_array().unwrap().len(), 2, "{l}");
|
||||
// Deleted text takes no room in the layout.
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let cs = s.cache.get(&d, sid, None);
|
||||
assert!(cs.frames[0].lines[0].glyphs.iter().any(|g| !g.visible && g.byte >= 4 && g.byte < 7));
|
||||
s.execute("changes.rejectAll", &json!({})).unwrap();
|
||||
assert_eq!(text(&s), "The cat sat");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let exported = s.execute("file.exportText", &json!({"story": sid.0})).unwrap()["text"].as_str().unwrap().to_string();
|
||||
assert_eq!(exported, "The dog sat", "deleted text isn't exported");
|
||||
s.execute("changes.acceptAll", &json!({})).unwrap();
|
||||
assert_eq!(text(&s), "The dog sat");
|
||||
assert!(s.execute("changes.list", &json!({})).unwrap().as_array().unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod anchors;
|
||||
mod articles;
|
||||
pub mod book;
|
||||
mod captions;
|
||||
mod changes;
|
||||
mod conditions;
|
||||
mod datamerge;
|
||||
mod edit;
|
||||
@@ -149,6 +150,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(paths::specs());
|
||||
v.extend(book::specs());
|
||||
v.extend(captions::specs());
|
||||
v.extend(changes::specs());
|
||||
v.extend(conditions::specs());
|
||||
v.extend(library::specs());
|
||||
v.extend(lists::specs());
|
||||
|
||||
@@ -286,6 +286,7 @@ fn insert(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let raw = p.get("raw").and_then(Value::as_bool).unwrap_or(false);
|
||||
let text = if s.prefs.typographers_quotes && !raw { smart_quotes(s, &text) } else { text };
|
||||
let tracking = s.doc()?.doc.settings.track_changes;
|
||||
s.edit(|d, sel| {
|
||||
let t = sel.text.ok_or_else(|| bad("text.insert", "no insertion point"))?;
|
||||
let st = d.text_story_mut(t.story, t.cell).ok_or(designcraft_doc::DocError::NoStory(t.story))?;
|
||||
@@ -307,6 +308,16 @@ fn insert(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
text.insert(0, '\n');
|
||||
}
|
||||
}
|
||||
if tracking && t.cell.is_none() {
|
||||
// Track Changes: the replaced text is marked deleted, the typing inserted after it.
|
||||
let at = super::changes::mark_deleted(st, r.clone());
|
||||
let mut fmt = st.char_format_at(at).clone();
|
||||
fmt.over.change = Some(designcraft_doc::ChangeMark::Inserted);
|
||||
st.insert_with(at, &text, fmt);
|
||||
let pos = at + text.len() - trail;
|
||||
sel.text = Some(TextSel { anchor: pos, focus: pos, ..t });
|
||||
return Ok(json!({"pos": pos}));
|
||||
}
|
||||
st.replace(r.clone(), &text);
|
||||
let pos = r.start + text.len() - trail;
|
||||
sel.text = Some(TextSel { anchor: pos, focus: pos, ..t });
|
||||
@@ -354,11 +365,16 @@ fn smart_quotes(s: &Session, text: &str) -> String {
|
||||
}
|
||||
|
||||
pub(crate) fn delete_selection(s: &mut Session) -> Result<Value> {
|
||||
let tracking = s.doc()?.doc.settings.track_changes;
|
||||
s.edit(|d, sel| {
|
||||
let t = sel.text.ok_or_else(|| bad("text.delete", "no text"))?;
|
||||
let r = t.range();
|
||||
if let Some(st) = d.text_story_mut(t.story, t.cell) {
|
||||
st.delete(r.clone());
|
||||
if tracking && t.cell.is_none() {
|
||||
super::changes::mark_deleted(st, r.clone());
|
||||
} else {
|
||||
st.delete(r.clone());
|
||||
}
|
||||
}
|
||||
sel.text = Some(TextSel { anchor: r.start, focus: r.start, ..t });
|
||||
ok()
|
||||
@@ -368,6 +384,7 @@ pub(crate) fn delete_selection(s: &mut Session) -> Result<Value> {
|
||||
fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let forward = bool_or(p, "forward", false);
|
||||
let word = bool_or(p, "word", false);
|
||||
let tracking = s.doc()?.doc.settings.track_changes;
|
||||
s.edit(|d, sel| {
|
||||
let t = sel.text.ok_or_else(|| bad("text.delete", "no text"))?;
|
||||
let st = d.text_story_mut(t.story, t.cell).ok_or(designcraft_doc::DocError::NoStory(t.story))?;
|
||||
@@ -381,6 +398,13 @@ fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let start = if word { prev_word(&st.text, r.start) } else { prev_char(&st.text, r.start) };
|
||||
start..r.start
|
||||
};
|
||||
if tracking && t.cell.is_none() {
|
||||
// Marked, not removed: the caret steps over the deleted text.
|
||||
let at = super::changes::mark_deleted(st, r.clone());
|
||||
let pos = if forward { at.max(r.end.min(st.len())) } else { r.start.min(at) };
|
||||
sel.text = Some(TextSel { anchor: pos, focus: pos, ..t });
|
||||
return ok();
|
||||
}
|
||||
st.delete(r.clone());
|
||||
sel.text = Some(TextSel { anchor: r.start, focus: r.start, ..t });
|
||||
ok()
|
||||
|
||||
@@ -222,7 +222,7 @@ pub fn story_html(doc: &Document, sid: StoryId) -> String {
|
||||
continue;
|
||||
}
|
||||
// Hidden conditional text isn't exported.
|
||||
if f.over.conditions.as_deref().is_some_and(|c| doc.conditions_hide(c)) {
|
||||
if f.over.conditions.as_deref().is_some_and(|c| doc.conditions_hide(c)) || f.over.change == Some(designcraft_doc::ChangeMark::Deleted) {
|
||||
continue;
|
||||
}
|
||||
let raw = &st.text[a..b];
|
||||
|
||||
@@ -109,6 +109,8 @@ pub struct RenderOptions {
|
||||
pub condition_indicators: bool,
|
||||
/// Editorial note anchors (screen view).
|
||||
pub note_indicators: bool,
|
||||
/// Track Changes: added text highlighted (screen view).
|
||||
pub change_markup: bool,
|
||||
/// View › Display Performance.
|
||||
pub quality: DisplayQuality,
|
||||
/// Preferences › Appearance of Black: show 100% K as rich (pure) black instead of the
|
||||
@@ -146,6 +148,7 @@ impl Default for RenderOptions {
|
||||
highlight_custom_tracking: false,
|
||||
condition_indicators: false,
|
||||
note_indicators: false,
|
||||
change_markup: false,
|
||||
quality: DisplayQuality::High,
|
||||
rich_black: false,
|
||||
overprint_preview: false,
|
||||
|
||||
@@ -229,6 +229,15 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
}
|
||||
if f.opts.change_markup && cs.styles.iter().any(|s| s.inserted) {
|
||||
ctx.set_transform(m);
|
||||
for l in &ft.lines {
|
||||
for g in l.glyphs.iter().filter(|g| g.adv > 0.0 && cs.styles.get(g.style as usize).is_some_and(|s| s.inserted)) {
|
||||
ctx.set_paint(peniko::Color::from_rgba8(120, 190, 255, 110));
|
||||
ctx.fill_rect(&kurbo::Rect::new(g.x, l.baseline - l.ascent, g.x + g.adv, l.baseline + l.descent));
|
||||
}
|
||||
}
|
||||
}
|
||||
if f.opts.note_indicators
|
||||
&& let Some(st) = f.doc.story(cs.story).filter(|st| !st.editorial.is_empty())
|
||||
{
|
||||
|
||||
@@ -30,12 +30,19 @@ fn table_text(story: &Story, id: u64, nl: &str) -> String {
|
||||
|
||||
/// Text Only: the story's text, CRLF between paragraphs.
|
||||
pub fn plain_text(story: &Story) -> String {
|
||||
// Tracked deletions aren't part of the text.
|
||||
let deleted: Vec<std::ops::Range<usize>> =
|
||||
story.runs().filter(|(_, f)| f.over.change == Some(designcraft_doc::ChangeMark::Deleted)).map(|(r, _)| r).collect();
|
||||
let mut out = String::with_capacity(story.text.len() + 16);
|
||||
for (i, r) in story.para_ranges().into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str("\r\n");
|
||||
}
|
||||
for c in story.text[r].chars() {
|
||||
let start = r.start;
|
||||
for (ci, c) in story.text[r].char_indices() {
|
||||
if deleted.iter().any(|d| d.contains(&(start + ci))) {
|
||||
continue;
|
||||
}
|
||||
match (c, plain_char(c)) {
|
||||
(TABLE_ANCHOR, _) => {
|
||||
if let Some(id) = story.paras.get(i).and_then(|p| p.table) {
|
||||
@@ -93,7 +100,7 @@ pub fn rtf(doc: &Document, story: &Story) -> String {
|
||||
}
|
||||
let cp = styles.resolve_char(&base, f);
|
||||
// Hidden conditional text isn't exported.
|
||||
if doc.conditions_hide(&cp.conditions) {
|
||||
if doc.conditions_hide(&cp.conditions) || cp.change == designcraft_doc::ChangeMark::Deleted {
|
||||
continue;
|
||||
}
|
||||
if !fonts.contains(&cp.font_family) {
|
||||
|
||||
@@ -329,6 +329,7 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf,
|
||||
highlight_hj: !preview && app.session.prefs.highlight_hj,
|
||||
condition_indicators: !preview,
|
||||
note_indicators: !preview,
|
||||
change_markup: !preview,
|
||||
highlight_keeps: !preview && app.session.prefs.highlight_keeps,
|
||||
highlight_custom_tracking: !preview && app.session.prefs.highlight_custom_tracking,
|
||||
quality: app.ui.display_quality,
|
||||
@@ -439,6 +440,7 @@ fn patch_texture(app: &mut DesignApp, layout: &CanvasLayout, ppp: f64, doc_key:
|
||||
highlight_hj: !preview && app.session.prefs.highlight_hj,
|
||||
condition_indicators: !preview,
|
||||
note_indicators: !preview,
|
||||
change_markup: !preview,
|
||||
highlight_keeps: !preview && app.session.prefs.highlight_keeps,
|
||||
highlight_custom_tracking: !preview && app.session.prefs.highlight_custom_tracking,
|
||||
quality: app.ui.display_quality,
|
||||
|
||||
@@ -318,6 +318,11 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
||||
"-",
|
||||
"cmd:footnote.insert",
|
||||
"cmd:endnote.insert",
|
||||
">Track Changes",
|
||||
"cmd:changes.track",
|
||||
"cmd:changes.acceptAll",
|
||||
"cmd:changes.rejectAll",
|
||||
"<",
|
||||
"ui:app.footnoteOptionsDialog",
|
||||
"cmd:footnote.goToReference",
|
||||
],
|
||||
@@ -1349,6 +1354,8 @@ pub fn checked(app: &DesignApp, id: &str, params: &Value) -> Option<bool> {
|
||||
"view.baselineGrid" => app.ui.baseline_grid,
|
||||
"view.textThreads" => app.ui.text_threads,
|
||||
"view.hiddenCharacters" => app.ui.hidden_characters,
|
||||
"view.taggedFrames" => app.ui.tagged_frames,
|
||||
"changes.track" => app.session.active().is_some_and(|d| d.doc.settings.track_changes),
|
||||
"window.controlBar" => app.ui.control_bar,
|
||||
"window.taskBar" => app.ui.task_bar,
|
||||
"window.toolsDoubleColumn" => app.ui.tools_double_column,
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 92 | 4 | 1 | 97% |
|
||||
| P2 | 76 | 34 | 20 | 22 | 58% |
|
||||
| P2 | 76 | 34 | 21 | 21 | 59% |
|
||||
|
||||
| Area | Parity |
|
||||
|---|---|
|
||||
@@ -188,7 +188,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
||||
| Type & text frames | Type on a path | P1 | D |
|
||||
| Type & text frames | Vertical type | P2 | M |
|
||||
| Type & text frames | Story Editor | P1 | D |
|
||||
| Type & text frames | Track Changes | P2 | M |
|
||||
| Type & text frames | Track Changes | P2 | P |
|
||||
| Type & text frames | Notes | P2 | D |
|
||||
| Type & text frames | Find/Change | P0 | D |
|
||||
| Type & text frames | Find Font | P0 | D |
|
||||
|
||||
Reference in New Issue
Block a user