File › Options: Word's panes with working settings (#485) (#492)

File › Options is now a pane list (General, Display, Proofing, Save,
Language, Accessibility, Advanced, Agents), one function per pane in
crates/ui-egui/src/options.rs. Every setting shown is wired:

- General: theme, interface size (#475), mini toolbar on selection, dark page, community
  button, user name and initials (comments use the initials).
- Display: tab characters, spaces, paragraph marks and hidden text shown
  with Show/Hide ¶ off; rulers; print the page colour; update fields
  before printing.
- Proofing: ignore words in capitals / with numbers / web and file
  addresses (proof::Options, passed through layout), check spelling and
  mark grammar as you type, custom dictionary add/remove (kept in ui.json).
- Save: AutoSave. Language: interface language. Accessibility: Read Aloud
  speed and citations. Advanced: typing replaces selection, Insert key
  overtype, overtype (text.overtype), recent documents count, measurement
  units (dialogs, Layout ribbon indents and rulers).

file.options reads and sets the engine-side settings headlessly and only
opens the page when given no setting. ui.json loads leniently: a bad value
falls back to its default instead of dropping every setting.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Valentin Klein 2026-10-12 01:17:50 +02:00 committed by GitHub
parent 88a45329b2
commit 2e5ecb916b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 1671 additions and 233 deletions

View File

@ -131,6 +131,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| Date | What landed |
|---|---|
| 2026-10-11 | File › Options in Word's panes (#485): General, Display, Proofing, Save, Language, Accessibility, Advanced, Agents; initials, individual formatting marks, spelling ignore options, custom dictionary, overtype, typing replaces selection, measurement units, recent-documents count, print options; `file.options` reads and sets them headlessly |
| 2026-10-11 | Right-click Wrap Text submenu on pictures, shapes and text boxes: wrapping styles, Move with Text / Fix Position on Page (#513) |
| 2026-10-11 | Paragraph dialog options (#512): mirror indents (`w:mirrorIndents`, laid out on even pages), same-style spacing, suppress line numbers, don't hyphenate |
| 2026-10-11 | Screen readers can read the document canvas (#488): paragraphs, headings, lists, tables, links, pictures and the caret exposed through AccessKit |

View File

@ -91,7 +91,7 @@ fn load_prefs(app: &mut WordApp) {
}
if let Some(p) = prefs_path()
&& let Ok(bytes) = std::fs::read(&p)
&& let Ok(ui) = serde_json::from_slice::<UiState>(&bytes)
&& let Some(ui) = UiState::from_json(&bytes)
{
app.apply_prefs(ui);
}
@ -103,7 +103,7 @@ fn saved_window() -> Option<WindowGeometry> {
return None;
}
let bytes = std::fs::read(prefs_path()?).ok()?;
serde_json::from_slice::<UiState>(&bytes).ok()?.window?.sanitized()
UiState::from_json(&bytes)?.window?.sanitized()
}
fn save_prefs(app: &WordApp) {

View File

@ -12,7 +12,7 @@ pub const MAX_AUTHOR_CHARS: usize = 255;
/// Sets the user name that new tracked changes and comments are recorded under. The name is
/// trimmed, stripped of control characters (they can't go into the saved XML) and cut to
/// [`MAX_AUTHOR_CHARS`]; a blank name is refused so revisions always carry an author.
fn set_author(s: &mut Session, v: &Value) -> CmdResult {
pub(crate) fn set_author(s: &mut Session, v: &Value) -> CmdResult {
let name: String = p::req_str(v, "name")?.trim().chars().filter(|c| !c.is_control()).take(MAX_AUTHOR_CHARS).collect();
let name = name.trim_end();
if name.is_empty() {
@ -69,11 +69,6 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("file.properties", "Properties", "File › Info", properties)
.params(r#"{"title"?, "subject"?, "author"?, "keywords"?, "comments"?, "category"?, "custom"?: {name: string | null (removes)}}"#),
CommandSpec::new("file.info", "Info", "File", info).pure(),
CommandSpec::new("file.options", "Options", "File", |s, _| {
s.ui_requests.push(json!({"open": "options"}));
sel_result(s)
})
.pure(),
CommandSpec::new("file.setAuthor", "User Name", "File › Options › General", set_author)
.params(r#"{"name": string (author of new tracked changes and comments)}"#)
.pure(),

View File

@ -24,6 +24,7 @@ pub mod lists;
pub mod mailings;
pub mod moves;
pub mod objects;
pub mod options;
pub mod page;
pub mod para;
pub mod paste;
@ -68,6 +69,7 @@ pub fn registry() -> Registry {
v.extend(table_draw::specs());
v.extend(review::specs());
v.extend(file::specs());
v.extend(options::specs());
v.extend(design::specs());
v.extend(references::specs());
v.extend(fields::specs());

View File

@ -0,0 +1,188 @@
//! File › Options (#485): the settings agents can read and change without the window — user
//! name and initials, typing (replace selection, overtype), measurement units, formatting marks
//! and proofing. The Options page in the app sets them through the same command.
use serde_json::{Value, json};
use crate::{CmdError, CmdResult, CommandSpec, Prefs, Session, p};
/// Options panes, in the order the page lists them (the `pane` param of `file.options`).
pub const PANES: [&str; 8] = ["general", "display", "proofing", "save", "language", "accessibility", "advanced", "agents"];
/// Longest initials kept, in characters.
pub const MAX_INITIALS_CHARS: usize = 9;
/// Longest word the custom dictionary takes, in characters.
pub const MAX_WORD_CHARS: usize = 64;
/// Most words one call may add or remove.
const MAX_WORDS_PER_CALL: usize = 1000;
pub fn specs() -> Vec<CommandSpec> {
vec![
CommandSpec::new("file.options", "Options", "File", options)
.params(
r#"{"pane"?: "general|display|proofing|save|language|accessibility|advanced|agents", "author"?: string, "initials"?: string, "units"?: "inches|centimeters|millimeters|points|picas", "typingReplacesSelection"?: bool, "insertKeyOvertype"?: bool, "overtype"?: bool, "marks"?: {"tabs"?: bool, "spaces"?: bool, "paragraphs"?: bool, "hidden"?: bool}, "checkSpelling"?: bool, "markGrammar"?: bool, "ignoreUppercase"?: bool, "ignoreNumbers"?: bool, "ignoreInternet"?: bool, "addWords"?: [string], "removeWords"?: [string]} — with any setting it changes them and opens nothing; without, it opens File › Options (at `pane`). Returns the settings."#,
)
.pure(),
CommandSpec::new("text.overtype", "Overtype", "File › Options › Advanced", |s, v| {
s.prefs.overtype = p::bool(v, "value").unwrap_or(!s.prefs.overtype);
Ok(json!({"value": s.prefs.overtype}))
})
.params(r#"{"value"?: bool (default: toggle)}"#)
.pure(),
]
}
/// The on/off settings by their `file.options` name.
fn flags() -> [(&'static str, fn(&mut Prefs) -> &mut bool); 7] {
[
("typingReplacesSelection", |p| &mut p.typing_replaces_selection),
("insertKeyOvertype", |p| &mut p.insert_key_overtype),
("overtype", |p| &mut p.overtype),
("markGrammar", |p| &mut p.mark_grammar),
("ignoreUppercase", |p| &mut p.ignore_uppercase),
("ignoreNumbers", |p| &mut p.ignore_numbers),
("ignoreInternet", |p| &mut p.ignore_internet),
]
}
fn mark_flags() -> [(&'static str, fn(&mut wordcraft_layout::display::FormattingMarks) -> &mut bool); 4] {
[("tabs", |m| &mut m.tabs), ("spaces", |m| &mut m.spaces), ("paragraphs", |m| &mut m.paragraphs), ("hidden", |m| &mut m.hidden)]
}
fn bool_param(v: &Value, k: &str) -> Result<Option<bool>, CmdError> {
match v.get(k) {
None | Some(Value::Null) => Ok(None),
Some(Value::Bool(b)) => Ok(Some(*b)),
Some(_) => Err(CmdError::Params(format!("`{k}` must be true or false"))),
}
}
/// Words for the custom dictionary: trimmed, single words, at most [`MAX_WORD_CHARS`] long.
fn words_param(v: &Value, k: &str) -> Result<Vec<String>, CmdError> {
let Some(x) = v.get(k).filter(|x| !x.is_null()) else { return Ok(Vec::new()) };
let bad = || CmdError::Params(format!("`{k}` must be a list of words"));
let list = x.as_array().ok_or_else(bad)?;
if list.len() > MAX_WORDS_PER_CALL {
return Err(CmdError::Params(format!("`{k}` takes at most {MAX_WORDS_PER_CALL} words")));
}
let mut out = Vec::new();
for w in list {
let w = w.as_str().ok_or_else(bad)?.trim();
if w.is_empty() || w.chars().count() > MAX_WORD_CHARS || w.chars().any(|c| c.is_whitespace() || c.is_control()) {
return Err(CmdError::Params(format!("`{k}`: {w:?} isn't a single word of at most {MAX_WORD_CHARS} characters")));
}
out.push(w.to_string());
}
Ok(out)
}
fn options(s: &mut Session, v: &Value) -> CmdResult {
let pane = match p::str(v, "pane") {
Some(name) if PANES.contains(&name) => name,
Some(name) => return Err(CmdError::Params(format!("unknown pane `{name}`; one of {}", PANES.join(", ")))),
None => "general",
};
// Read every setting first, so a bad one changes nothing.
let author = match v.get("author") {
None | Some(Value::Null) => None,
Some(Value::String(n)) => Some(n.clone()),
Some(_) => return Err(CmdError::Params("`author` must be a string".into())),
};
let initials = match v.get("initials") {
None | Some(Value::Null) => None,
Some(Value::String(i)) => Some(i.trim().chars().filter(|c| !c.is_control()).take(MAX_INITIALS_CHARS).collect::<String>()),
Some(_) => return Err(CmdError::Params("`initials` must be a string".into())),
};
let units = match v.get("units") {
None | Some(Value::Null) => None,
Some(u) => Some(
serde_json::from_value::<wordcraft_geom::Unit>(u.clone())
.map_err(|_| CmdError::Params("`units` is one of inches, centimeters, millimeters, points, picas".into()))?,
),
};
let mut flag_values = Vec::new();
for (k, f) in flags() {
if let Some(b) = bool_param(v, k)? {
flag_values.push((f, b));
}
}
let mut mark_values = Vec::new();
match v.get("marks") {
None | Some(Value::Null) => {}
Some(m @ Value::Object(_)) => {
for (k, f) in mark_flags() {
if let Some(b) = bool_param(m, k)? {
mark_values.push((f, b));
}
}
}
Some(_) => return Err(CmdError::Params("`marks` must be an object of true/false".into())),
}
let check_spelling = bool_param(v, "checkSpelling")?;
let add = words_param(v, "addWords")?;
let remove = words_param(v, "removeWords")?;
let setting = author.is_some()
|| initials.is_some()
|| units.is_some()
|| !flag_values.is_empty()
|| !mark_values.is_empty()
|| check_spelling.is_some()
|| !add.is_empty()
|| !remove.is_empty();
if let Some(name) = author {
super::file::set_author(s, &json!({"name": name}))?;
}
if let Some(i) = initials {
s.initials = i;
}
if let Some(u) = units {
s.prefs.units = u;
}
for (f, b) in flag_values {
*f(&mut s.prefs) = b;
}
for (f, b) in mark_values {
*f(&mut s.prefs.marks) = b;
}
if let Some(b) = check_spelling {
s.view.proofing = b;
}
for w in &add {
wordcraft_proof::add_word(w);
}
for w in &remove {
wordcraft_proof::remove_word(w);
}
if setting {
// Marks, hidden text and proofing change what is laid out.
s.relayout();
} else {
s.ui_requests.push(json!({"open": "options", "pane": pane}));
}
let mut r = current(s);
if let Some(o) = r.as_object_mut() {
o.insert("opened".into(), json!(!setting));
}
Ok(r)
}
/// The settings `file.options` reads and sets.
pub fn current(s: &Session) -> Value {
let pr = &s.prefs;
json!({
"author": s.author,
"initials": s.user_initials(),
"units": pr.units,
"typingReplacesSelection": pr.typing_replaces_selection,
"insertKeyOvertype": pr.insert_key_overtype,
"overtype": pr.overtype,
"marks": pr.marks,
"checkSpelling": s.view.proofing,
"markGrammar": pr.mark_grammar,
"ignoreUppercase": pr.ignore_uppercase,
"ignoreNumbers": pr.ignore_numbers,
"ignoreInternet": pr.ignore_internet,
"dictionary": wordcraft_proof::user_dictionary(),
})
}

View File

@ -120,7 +120,7 @@ fn new_comment(s: &mut Session, v: &Value) -> CmdResult {
};
let part = s.doc.add_part(PartKind::Comment, vec![para_block(Paragraph::with_text(&text, Default::default()))]);
let id = s.doc.comments.keys().next_back().map(|k| k + 1).unwrap_or(0);
let initials: String = s.author.split_whitespace().filter_map(|w| w.chars().next()).collect();
let initials = s.user_initials();
s.doc.comments.insert(id, Comment { author: s.author.clone(), initials, date: now_iso(), parent: None, resolved: false, part });
let props = Default::default();
s.doc.insert_object(&b, InlineObject::CommentEnd { id }, &props)?;
@ -137,7 +137,7 @@ fn reply(s: &mut Session, v: &Value) -> CmdResult {
let text = p::req_str(v, "text")?;
let part = s.doc.add_part(PartKind::Comment, vec![para_block(Paragraph::with_text(text, Default::default()))]);
let id = s.doc.comments.keys().next_back().map(|k| k + 1).unwrap_or(0);
let initials: String = s.author.split_whitespace().filter_map(|w| w.chars().next()).collect();
let initials = s.user_initials();
s.doc.comments.insert(id, Comment { author: s.author.clone(), initials, date: now_iso(), parent: Some(parent), resolved: false, part });
Ok(json!({"id": id}))
}
@ -658,7 +658,7 @@ fn word_count(s: &mut Session, v: &Value) -> CmdResult {
fn para_issues(s: &Session, story: StoryRef, path: &wordcraft_doc::Path) -> Vec<(Pos, Pos, wordcraft_proof::Issue)> {
let Some(p) = s.doc.para(story, path) else { return Vec::new() };
let text = wordcraft_layout::para::proof_text(p);
let mut v: Vec<wordcraft_proof::Issue> = wordcraft_proof::check_spelling(&text);
let mut v: Vec<wordcraft_proof::Issue> = wordcraft_proof::check_spelling_with(&text, &s.prefs.spelling());
v.extend(wordcraft_proof::check_grammar(&text));
v.sort_by_key(|i| i.start);
v.into_iter()

View File

@ -1,7 +1,7 @@
//! Typing and deleting.
use serde_json::Value;
use wordcraft_doc::para::{COLUMN_BREAK, NB_HYPHEN, NBSP, PAGE_BREAK, SOFT_HYPHEN};
use wordcraft_doc::para::{COLUMN_BREAK, NB_HYPHEN, NBSP, OBJ, PAGE_BREAK, SOFT_HYPHEN};
use wordcraft_doc::props::{Border, BorderStyle, NumRef};
use wordcraft_doc::{Block, ListKind, Pos};
@ -60,9 +60,35 @@ fn autoformat(s: &mut Session, text: &str) -> Option<String> {
}
}
/// File › Options › Advanced before typed text goes in: with "Typing replaces selected text"
/// off the text goes in front of the selection and leaves it; in overtype the text replaces as
/// many characters after the caret as it has, up to the paragraph's end or an object (picture,
/// field), which stay.
fn typing_options(s: &mut Session, text: &str) {
if !s.sel.is_collapsed() {
if !s.prefs.typing_replaces_selection {
let (start, _) = s.sel.ordered();
s.sel = Selection::caret(start);
}
return;
}
if !s.prefs.overtype || text.contains(['\r', '\n', '\u{2029}', '\u{000B}']) {
return;
}
let f = s.sel.focus.clone();
let Some(rest) = s.doc.para_at(&f).and_then(|p| p.text.get(f.off..)) else { return };
let n = text.chars().count();
let len: usize = rest.chars().take(n).take_while(|c| *c != OBJ).map(char::len_utf8).sum();
if len > 0 {
// The caret (focus) stays where typing happens; the characters after it are selected.
s.sel = Selection { anchor: Pos { off: f.off + len, ..f.clone() }, focus: f };
}
}
fn insert(s: &mut Session, v: &Value) -> CmdResult {
let text = p::req_str(v, "text")?;
let raw = p::bool(v, "raw").unwrap_or(false);
typing_options(s, text);
let t = if raw { None } else { autoformat(s, text) };
type_text(s, t.as_deref().unwrap_or(text))?;
let trigger = text.chars().count() == 1 && text.chars().all(|c| c == ' ' || ",.;:!?".contains(c));

View File

@ -27,6 +27,7 @@ pub use ribbon::RibbonLayout;
pub use session::{ColumnBlock, EditSnapshot, FindState, MathEdit, Prefs, Selection, Session, ViewState};
pub use wordcraft_doc as doc;
pub use wordcraft_layout as layout;
pub use wordcraft_proof as proof;
pub use wordcraft_render as render;
#[derive(Debug, thiserror::Error, Clone, PartialEq)]

View File

@ -225,6 +225,8 @@ pub struct Session {
pub registry: Arc<Registry>,
/// Author name for comments and tracked changes.
pub author: String,
/// Initials for comments (File › Options › General); empty = the name's initials.
pub initials: String,
/// Format painter: copied formatting waiting to be applied (and whether it stays on).
pub painter: Option<(CharProps, wordcraft_doc::props::ParaProps, bool)>,
/// Last message for the status bar / agents.
@ -308,6 +310,7 @@ pub struct MathEdit {
}
/// Editing preferences that persist between runs (the front end saves and restores them).
/// File › Options sets most of them (`file.options`).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Prefs {
@ -318,6 +321,21 @@ pub struct Prefs {
pub markup: wordcraft_layout::display::MarkupOptions,
/// Language › Detect language automatically (per user, as in Word).
pub detect_language: bool,
/// Typing over a selection replaces it; off, the typed text goes in front of it.
pub typing_replaces_selection: bool,
/// The Insert key switches overtype on and off.
pub insert_key_overtype: bool,
/// Overtype: typed characters replace the ones after the caret.
pub overtype: bool,
/// The unit rulers and dialogs show lengths in.
pub units: wordcraft_geom::Unit,
/// Formatting marks shown even while Show/Hide ¶ is off.
pub marks: wordcraft_layout::display::FormattingMarks,
/// Mark grammar errors as you type (with Check spelling as you type on).
pub mark_grammar: bool,
pub ignore_uppercase: bool,
pub ignore_numbers: bool,
pub ignore_internet: bool,
/// AutoCorrect Options: replacements, capitalization and AutoFormat As You Type (per user, as
/// in Word). A damaged saved value reads as the defaults.
#[serde(deserialize_with = "crate::cmd::tools::lenient_autocorrect")]
@ -326,7 +344,32 @@ pub struct Prefs {
impl Default for Prefs {
fn default() -> Self {
Prefs { count_notes: true, markup: Default::default(), detect_language: true, autocorrect: Default::default() }
Prefs {
count_notes: true,
markup: Default::default(),
typing_replaces_selection: true,
insert_key_overtype: false,
overtype: false,
units: Default::default(),
marks: Default::default(),
mark_grammar: true,
ignore_uppercase: true,
ignore_numbers: true,
ignore_internet: true,
detect_language: true,
autocorrect: Default::default(),
}
}
}
impl Prefs {
/// What the spelling check skips.
pub fn spelling(&self) -> wordcraft_proof::Options {
wordcraft_proof::Options {
ignore_uppercase: self.ignore_uppercase,
ignore_numbers: self.ignore_numbers,
ignore_internet: self.ignore_internet,
}
}
}
@ -352,6 +395,7 @@ impl Session {
page_hint: 0,
registry: Arc::new(crate::cmd::registry()),
author: "WordCraft User".into(),
initials: String::new(),
painter: None,
status: String::new(),
history: Vec::new(),
@ -452,6 +496,12 @@ impl Session {
self.find.highlights.as_ref().map(|(_, v)| v.as_slice()).unwrap_or(&[])
}
/// Initials for new comments: the ones set in File › Options, else the user name's.
pub fn user_initials(&self) -> String {
let set = self.initials.trim();
if set.is_empty() { self.author.split_whitespace().filter_map(|w| w.chars().next()).collect() } else { set.to_string() }
}
/// The current layout (recomputed when the document or view changed).
pub fn layout(&mut self) -> Arc<DocLayout> {
let ww = self.view.web_width;
@ -466,10 +516,11 @@ impl Session {
let opts = LayoutOptions {
view: self.view.mode,
web_width: ww,
show_hidden: self.view.marks,
show_hidden: self.view.marks || self.prefs.marks.hidden,
hide_deleted: !self.view.show_markup || self.prefs.markup.hides_deletions(),
proofing: self.view.proofing,
field_codes: self.view.field_codes,
proof: wordcraft_layout::ProofOptions { grammar: self.prefs.mark_grammar, spelling: self.prefs.spelling() },
};
let l = Arc::new(wordcraft_layout::layout(&self.doc, &mut self.cache, &opts));
self.layout = Some((self.rev, ww, self.view.mode, l.clone(), self.view.proofing));
@ -478,7 +529,7 @@ impl Session {
/// A layout for output (PDF, images, print): no proofing marks, print view.
pub fn export_layout(&self) -> Arc<DocLayout> {
let opts =
LayoutOptions { view: ViewMode::Print, web_width: 0.0, show_hidden: false, hide_deleted: false, proofing: false, field_codes: false };
LayoutOptions { view: ViewMode::Print, web_width: 0.0, show_hidden: false, hide_deleted: false, proofing: false, ..Default::default() };
Arc::new(wordcraft_layout::layout(&self.doc, &mut LayoutCache::new(), &opts))
}
@ -643,6 +694,7 @@ impl Session {
page_hint: _,
registry: _,
author: _,
initials: _,
painter: _,
status: _,
history,

View File

@ -540,6 +540,40 @@ fn shift_f5_goes_back_through_the_last_edits() {
assert!(Session::new(wordcraft_doc::Document::new()).run("edit.goBack", &json!({})).is_err(), "nothing edited yet");
}
/// File › Options › Advanced, "Typing replaces selected text" (#485): on (Word's default) the
/// typed text replaces the selection; off, it goes in front of the selection and keeps it.
#[test]
fn typing_over_a_selection_follows_the_replace_selection_option() {
let mut s = typed("One two");
run(&mut s, "select.text", json!({"text": "two"}));
typ(&mut s, "X");
assert_eq!(s.doc.plain_text(StoryRef::Body), "One X");
let mut s = typed("One two");
run(&mut s, "file.options", json!({"typingReplacesSelection": false}));
run(&mut s, "select.text", json!({"text": "two"}));
typ(&mut s, "XY");
assert_eq!(s.doc.plain_text(StoryRef::Body), "One XYtwo");
}
/// Overtype (#485): typed characters replace the ones after the caret, never past the end of the
/// paragraph; the Insert key toggles it only with "Use the Insert key to control overtype" on
/// (the UI checks that option; `text.overtype` is the switch).
#[test]
fn overtype_replaces_the_characters_after_the_caret() {
let mut s = typed("Abcdef\nXy");
run(&mut s, "caret.set", json!({"pos": Pos::body(0, 1)}));
run(&mut s, "text.overtype", json!({"value": true}));
typ(&mut s, "XY");
assert_eq!(s.doc.plain_text(StoryRef::Body), "AXYdef\nXy");
run(&mut s, "caret.set", json!({"pos": Pos::body(0, 5)}));
typ(&mut s, "123");
assert_eq!(s.doc.plain_text(StoryRef::Body), "AXYde123\nXy", "the paragraph mark isn't typed over");
run(&mut s, "text.overtype", json!({}));
run(&mut s, "caret.set", json!({"pos": Pos::body(0, 0)}));
typ(&mut s, "Z");
assert_eq!(s.doc.plain_text(StoryRef::Body), "ZAXYde123\nXy", "insert mode again");
}
#[test]
fn reject_all_takes_out_the_paragraph_breaks_a_tracked_paste_inserted() {
// Issue #417: Paste's paragraph marks weren't tracked, so Reject All left the split.

View File

@ -152,10 +152,40 @@ impl Draw {
}
}
/// Formatting marks shown even while Show/Hide ¶ is off (File › Options › Display, "Always show
/// these formatting marks"). Show/Hide ¶ on shows them all.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct FormattingMarks {
/// Tab characters (→).
pub tabs: bool,
/// Spaces (·).
pub spaces: bool,
/// Paragraph marks (¶), line breaks, page, column and section breaks.
pub paragraphs: bool,
/// Hidden text (shown with its dotted underline instead of left out).
pub hidden: bool,
}
impl FormattingMarks {
pub const ALL: FormattingMarks = FormattingMarks { tabs: true, spaces: true, paragraphs: true, hidden: true };
/// The marks on screen: all of them with Show/Hide ¶ on, else the chosen ones.
pub fn shown(self, show_all: bool) -> FormattingMarks {
if show_all { FormattingMarks::ALL } else { self }
}
pub fn any(self) -> bool {
self.tabs || self.spaces || self.paragraphs || self.hidden
}
}
#[derive(Clone, Debug)]
pub struct DisplayOptions {
/// Show formatting marks (¶).
/// Show formatting marks (¶): Show/Hide ¶, every mark.
pub marks: bool,
/// Marks shown while `marks` is off (File › Options › Display).
pub always: FormattingMarks,
/// Dim headers/footers (editing the body) or the body (editing a header/footer).
pub dim_header: bool,
pub dim_body: bool,
@ -173,6 +203,7 @@ impl Default for DisplayOptions {
fn default() -> Self {
DisplayOptions {
marks: false,
always: FormattingMarks::default(),
dim_header: true,
dim_body: false,
markup: true,
@ -975,7 +1006,8 @@ fn lines(
}
}
// Formatting marks.
if opts.marks {
let marks = opts.always.shown(opts.marks);
if marks.any() {
let msize = pl.styles.first().map(|s| s.size).unwrap_or(11.0);
for k in line.c0..line.c1 {
let Some(c) = pl.clusters.get(k) else { continue };
@ -984,14 +1016,14 @@ fn lines(
let size = pl.styles.get(c.style as usize).map(|s| s.size).unwrap_or(msize);
let arrow = if line.rtl { '←' } else { '→' };
match c.kind {
ClKind::Space => {
ClKind::Space if marks.spaces => {
out.push(Draw::Mark { x: (cx + nx) / 2.0 - size * 0.12, baseline: base - size * 0.08, size, ch: '·', color: None })
}
ClKind::Tab => {
ClKind::Tab if marks.tabs => {
out.push(Draw::Mark { x: cx + ((nx - cx) / 2.0 - size * 0.3).max(0.0), baseline: base, size, ch: arrow, color: None })
}
ClKind::LineBreak => out.push(Draw::Mark { x: cx + 1.0, baseline: base, size, ch: '↵', color: None }),
ClKind::PageBreak | ClKind::ColumnBreak => {
ClKind::LineBreak if marks.paragraphs => out.push(Draw::Mark { x: cx + 1.0, baseline: base, size, ch: '↵', color: None }),
ClKind::PageBreak | ClKind::ColumnBreak if marks.paragraphs => {
let label = if c.kind == ClKind::PageBreak { '⤓' } else { '⇥' };
out.push(Draw::Line {
x0: cx + 2.0,
@ -1008,7 +1040,7 @@ fn lines(
_ => {}
}
}
if line.end == LineEnd::Para {
if line.end == LineEnd::Para && marks.paragraphs {
// The mark follows the line's visual end, past every glyph, even where the
// logically last text runs against the paragraph's direction.
let ex = x + line.visual_end_x();

View File

@ -55,11 +55,27 @@ pub struct LayoutOptions {
pub show_hidden: bool,
/// Final text ("No Markup"): tracked deletions take no space and draw nothing.
pub hide_deleted: bool,
/// Check spelling and grammar (squiggles).
/// Check spelling as you type (squiggles).
pub proofing: bool,
/// Show field codes (`{ PAGE }`, shaded) instead of field results (View › Field Codes, Alt+F9).
/// A field whose own `code` flag is set shows the other one.
pub field_codes: bool,
/// What the spelling check skips, and whether grammar is marked too (File › Options › Proofing).
pub proof: ProofOptions,
}
/// File › Options › Proofing choices that change the squiggles.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ProofOptions {
/// Mark grammar errors as you type.
pub grammar: bool,
pub spelling: wordcraft_proof::Options,
}
impl Default for ProofOptions {
fn default() -> Self {
ProofOptions { grammar: true, spelling: wordcraft_proof::Options::default() }
}
}
/// Something placed on a page (page coordinates, points, y down).
@ -362,7 +378,8 @@ fn env_hash(doc: &Document, opts: &LayoutOptions) -> u64 {
opts.hide_deleted,
opts.proofing,
opts.field_codes,
wordcraft_proof::user_dictionary().len(),
opts.proof,
wordcraft_proof::dictionary_generation(),
hyphenation_key(doc),
))
}
@ -495,7 +512,7 @@ impl Ctx<'_> {
field_codes: false,
hide_deleted: false,
table: None,
proofing: false,
proofing: None,
exclusions: &[],
eq_number: 0,
grid: para::Grid::default(),
@ -563,7 +580,7 @@ impl Ctx<'_> {
field_codes: self.opts.field_codes,
hide_deleted: self.opts.hide_deleted,
table,
proofing: self.opts.proofing,
proofing: self.opts.proofing.then_some(self.opts.proof),
exclusions,
eq_number,
grid: self.grid,
@ -2554,7 +2571,7 @@ fn next_first_line(ctx: &mut Ctx, block: usize, width: f32) -> f32 {
field_codes: ctx.opts.field_codes,
hide_deleted: ctx.opts.hide_deleted,
table: None,
proofing: false,
proofing: None,
exclusions: &[],
eq_number: ctx.eq_count,
grid: ctx.grid,

View File

@ -316,7 +316,8 @@ pub struct ParaEnv<'a> {
pub hide_deleted: bool,
/// The table style's formatting for text in this paragraph's cell (`None` outside tables).
pub table: Option<&'a CellText>,
pub proofing: bool,
/// Check spelling (and grammar, as the options say) for squiggles; `None` = no squiggles.
pub proofing: Option<crate::ProofOptions>,
/// Areas text must flow around (floating objects), relative to the paragraph: x from the
/// column's left edge, y from the top of the first line.
pub exclusions: &'a [Exclusion],
@ -1258,7 +1259,7 @@ pub fn layout_para(p: &Paragraph, env: &ParaEnv) -> ParaLayout {
text_len: p.text.len(),
has_page_fields,
notes,
issues: if env.proofing { proof_issues(p) } else { Vec::new() },
issues: env.proofing.map(|o| proof_issues(p, &o)).unwrap_or_default(),
drop_cap,
hyph_after: Vec::new(),
bidi_levels: b.levels,
@ -2225,7 +2226,7 @@ impl ParaLayout {
}
/// Spelling and grammar issues in a paragraph (skipping "do not check" and hidden runs).
fn proof_issues(p: &Paragraph) -> Vec<(usize, usize, bool)> {
fn proof_issues(p: &Paragraph, opts: &crate::ProofOptions) -> Vec<(usize, usize, bool)> {
if p.text.trim().is_empty() || p.text.len() > 100_000 {
return Vec::new();
}
@ -2233,9 +2234,14 @@ fn proof_issues(p: &Paragraph) -> Vec<(usize, usize, bool)> {
let skip = |a: usize, b: usize| {
p.run_ranges().any(|(r, c)| r.start < b && a < r.end && (c.no_proof == Some(true) || c.hidden == Some(true) || c.link.is_some()))
};
let mut v: Vec<(usize, usize, bool)> =
wordcraft_proof::check_spelling(&text).into_iter().filter(|i| !skip(i.start, i.end)).map(|i| (i.start, i.end, false)).collect();
v.extend(wordcraft_proof::check_grammar(&text).into_iter().filter(|i| !skip(i.start, i.end)).map(|i| (i.start, i.end, true)));
let mut v: Vec<(usize, usize, bool)> = wordcraft_proof::check_spelling_with(&text, &opts.spelling)
.into_iter()
.filter(|i| !skip(i.start, i.end))
.map(|i| (i.start, i.end, false))
.collect();
if opts.grammar {
v.extend(wordcraft_proof::check_grammar(&text).into_iter().filter(|i| !skip(i.start, i.end)).map(|i| (i.start, i.end, true)));
}
v
}

View File

@ -167,7 +167,7 @@ pub fn measure_table_columns(doc: &wordcraft_doc::Document, t: &Table) -> Vec<(f
field_codes: false,
hide_deleted: false,
table: text.as_ref(),
proofing: false,
proofing: None,
exclusions: &[],
eq_number: 0,
grid: crate::para::Grid::default(),

View File

@ -867,7 +867,7 @@ fn web_view_is_one_page() {
let l = layout(
&d,
&mut c,
&LayoutOptions { view: ViewMode::Web, web_width: 800.0, show_hidden: false, hide_deleted: false, proofing: false, field_codes: false },
&LayoutOptions { view: ViewMode::Web, web_width: 800.0, show_hidden: false, hide_deleted: false, proofing: false, ..Default::default() },
);
assert_eq!(l.pages.len(), 1);
assert!(l.pages[0].h > 800.0);

View File

@ -15,8 +15,26 @@ pub mod hyphen;
pub mod patterns;
use std::collections::HashSet;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{OnceLock, RwLock};
/// What the spelling check skips (File › Options › Proofing). Word's defaults: all on.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Options {
/// Words in capitals (`NASA`, `WORDCRAFT`) are taken as correct.
pub ignore_uppercase: bool,
/// Words containing digits (`B2B`, `mp3`) are taken as correct.
pub ignore_numbers: bool,
/// Web and email addresses and file paths (`https://…`, `a@b.org`, `www.…`) are skipped.
pub ignore_internet: bool,
}
impl Default for Options {
fn default() -> Self {
Options { ignore_uppercase: true, ignore_numbers: true, ignore_internet: true }
}
}
/// A problem found in text: byte range, kind, message and suggestions.
#[derive(Clone, Debug, PartialEq)]
pub struct Issue {
@ -38,9 +56,28 @@ fn user_words() -> &'static RwLock<HashSet<String>> {
U.get_or_init(|| RwLock::new(HashSet::new()))
}
/// Bumped whenever the user dictionary changes, so cached proofing results can be dropped.
static DICTIONARY_GENERATION: AtomicU64 = AtomicU64::new(0);
/// Add a word to the user dictionary ("Add to Dictionary").
pub fn add_word(w: &str) {
user_words().write().unwrap_or_else(|e| e.into_inner()).insert(w.to_lowercase());
if user_words().write().unwrap_or_else(|e| e.into_inner()).insert(w.to_lowercase()) {
DICTIONARY_GENERATION.fetch_add(1, Ordering::Relaxed);
}
}
/// Take a word out of the user dictionary (File › Options › Proofing). Returns whether it was there.
pub fn remove_word(w: &str) -> bool {
let removed = user_words().write().unwrap_or_else(|e| e.into_inner()).remove(&w.to_lowercase());
if removed {
DICTIONARY_GENERATION.fetch_add(1, Ordering::Relaxed);
}
removed
}
/// A number that changes whenever a word is added to or removed from the user dictionary.
pub fn dictionary_generation() -> u64 {
DICTIONARY_GENERATION.load(Ordering::Relaxed)
}
/// Words in the user dictionary.
@ -124,15 +161,24 @@ fn known_core(w: &str) -> bool {
false
}
/// Is `word` spelled correctly? Numbers, single letters, ALL-CAPS acronyms, URLs and words with
/// digits are accepted.
/// Is `word` spelled correctly? Numbers, single letters, words in capitals and words with
/// digits are accepted (Word's default [`Options`]).
pub fn is_correct(word: &str) -> bool {
is_correct_with(word, &Options::default())
}
/// [`is_correct`] with the File › Options › Proofing choices.
pub fn is_correct_with(word: &str, opts: &Options) -> bool {
let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'');
let w = w.trim_matches('\'');
if w.chars().count() <= 1 || w.chars().any(|c| c.is_ascii_digit()) || !w.chars().any(char::is_alphabetic) {
if w.chars().count() <= 1 || !w.chars().any(char::is_alphabetic) {
return true;
}
if w.chars().all(|c| !c.is_lowercase()) && w.chars().count() <= 6 {
let digits = w.chars().any(|c| c.is_ascii_digit());
if digits && opts.ignore_numbers {
return true;
}
if opts.ignore_uppercase && w.chars().all(|c| !c.is_lowercase()) {
return true;
}
if !w.is_ascii() && !w.chars().any(|c| c.is_ascii_alphabetic()) {
@ -255,6 +301,11 @@ pub fn words(text: &str) -> Vec<(usize, usize)> {
/// Spelling issues in a paragraph of text (no suggestions; ask [`suggest`] on demand).
pub fn check_spelling(text: &str) -> Vec<Issue> {
check_spelling_with(text, &Options::default())
}
/// [`check_spelling`] with the File › Options › Proofing choices.
pub fn check_spelling_with(text: &str, opts: &Options) -> Vec<Issue> {
let mut v = Vec::new();
for (a, b) in words(text) {
let Some(w) = text.get(a..b) else { continue };
@ -262,10 +313,10 @@ pub fn check_spelling(text: &str) -> Vec<Issue> {
let ts = text.get(..a).and_then(|t| t.rfind(char::is_whitespace)).map(|i| i + 1).unwrap_or(0);
let te = text.get(b..).and_then(|t| t.find(char::is_whitespace)).map(|i| b + i).unwrap_or(text.len());
let token = text.get(ts..te).unwrap_or("");
if token.contains("://") || token.contains('@') || token.starts_with("www.") {
if opts.ignore_internet && (token.contains("://") || token.contains('@') || token.starts_with("www.")) {
continue;
}
if !is_correct(w) {
if !is_correct_with(w, opts) {
v.push(Issue { start: a, end: b, kind: IssueKind::Spelling, message: "Possible spelling mistake".into(), suggestions: Vec::new() });
}
}
@ -472,6 +523,18 @@ mod tests {
assert!(is_correct("zxqwordcrafty"));
}
/// File › Options › Proofing (#485): each "Ignore" choice turned off makes those words count.
#[test]
fn ignore_options_decide_what_is_checked() {
let t = "SENTENSE helo2 www.exampel.org";
let bad = |o: Options| check_spelling_with(t, &o).iter().map(|i| t[i.start..i.end].to_string()).collect::<Vec<_>>();
assert!(bad(Options::default()).is_empty(), "Word's defaults skip all three");
let all = Options { ignore_uppercase: false, ignore_numbers: false, ignore_internet: false };
assert_eq!(bad(all), ["SENTENSE", "helo2", "www", "exampel"]);
assert_eq!(bad(Options { ignore_uppercase: false, ..Options::default() }), ["SENTENSE"]);
assert!(is_correct_with("NASA", &all), "capitals that are a word stay correct");
}
#[test]
fn hyphenation() {
let l = hyphen::Limits::default();

View File

@ -110,7 +110,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
"open" => open_page(app, ui),
"info" => info_page(app, ui),
"print" | "export" => export_page(app, ui),
"options" => options_page(app, ui),
"options" => crate::options::page(app, ui),
_ => home_page(app, ui),
});
});
@ -240,7 +240,7 @@ fn open_list(app: &mut WordApp, ui: &mut Ui) {
if app.ui.recent.is_empty() {
ui.label(egui::RichText::new(tl!("Documents you open will show up here.")).color(t.text_dim));
}
for p in app.ui.recent.clone() {
for p in app.ui.recent.iter().take(app.ui.recent_shown()).cloned().collect::<Vec<_>>() {
let name = std::path::Path::new(&p).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or(p.clone());
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width().min(700.0), 40.0), Sense::click());
if resp.hovered() {
@ -365,129 +365,6 @@ fn export_page(app: &mut WordApp, ui: &mut Ui) {
}
}
fn options_page(app: &mut WordApp, ui: &mut Ui) {
heading(ui, "Options");
ui.label(egui::RichText::new(tl!("General")).font(semibold(15.0)));
language_picker(app, ui);
ui.horizontal(|ui| {
ui.label(tl!("User name:"));
let mut n = app.session.author.clone();
if ui.text_edit_singleline(&mut n).changed() {
app.session.author = n;
}
});
theme_picker(app, ui);
interface_size_picker(app, ui);
// The browser can't write to the user's files, so AutoSave can't be turned on there (#176).
let browser = app.autosave_block() == Some(crate::AutoSaveBlock::Browser);
let mut autosave = app.autosave && !browser;
let r = ui.add_enabled(!browser, egui::Checkbox::new(&mut autosave, tl!("AutoSave documents you have saved in WordCraft")));
if browser {
r.on_disabled_hover_text(crate::AutoSaveBlock::Browser.reason());
} else if r.changed() {
app.autosave = autosave;
app.session.autosave = autosave;
}
let mut dark_page = app.session.view.dark_mode;
if ui
.checkbox(&mut dark_page, tl!("Dark page (white text on black)"))
.on_hover_text(tl!("Show documents with their colours inverted, like View › Switch Modes. Saving, printing and PDFs are unchanged."))
.changed()
{
let _ = app.run("view.darkMode", json!({"value": dark_page}));
}
ui.checkbox(&mut app.ui.show_discord, tl!("Show the community button in the title bar"));
ui.add_space(10.0);
ui.label(egui::RichText::new(tl!("Display")).font(semibold(15.0)));
let mut marks = app.session.view.marks;
if ui.checkbox(&mut marks, tl!("Show all formatting marks")).changed() {
let _ = app.run("view.marks", json!({"value": marks}));
}
let mut ruler = app.session.view.ruler;
if ui.checkbox(&mut ruler, tl!("Show rulers")).changed() {
let _ = app.run("view.ruler", json!({"value": ruler}));
}
ui.add_space(10.0);
ui.label(egui::RichText::new(tl!("Customize")).font(semibold(15.0)));
ui.horizontal(|ui| {
ui.label(tl!("Keyboard shortcuts:"));
if ui.button(tl!("Customize…")).clicked() {
let _ = app.run("tools.customizeKeyboard", json!({}));
}
});
ui.horizontal(|ui| {
ui.label(tl!("Ribbon and Quick Access Toolbar:"));
if ui.button(tl!("Customize…")).clicked() {
let _ = app.run("tools.customizeRibbon", json!({}));
}
});
ui.add_space(10.0);
ui.label(egui::RichText::new(tl!("Agents")).font(semibold(15.0)));
ui.label(tl!("Every command is available to scripts and AI agents: run `wordcraft-cli mcp` for an MCP server, or start the app with `--control <port>` for the JSON control channel."));
}
/// File ▸ Options ▸ Interface theme: Light, Dark, or follow the system's appearance (#115).
fn theme_picker(app: &mut WordApp, ui: &mut Ui) {
use crate::theme::Appearance;
ui.horizontal(|ui| {
ui.label(tl!("Interface theme:"));
egui::ComboBox::from_id_salt("interface_theme").selected_text(tl!(app.ui.theme.label())).width(220.0).show_ui(ui, |ui| {
for a in Appearance::ALL {
if ui.selectable_label(app.ui.theme == a, tl!(a.label())).clicked() {
let _ = app.run("ui.theme", json!({"value": a.code()}));
}
}
});
});
}
/// File ▸ Options ▸ Interface size: 80–200 %, on top of the system's display scale (#475).
fn interface_size_picker(app: &mut WordApp, ui: &mut Ui) {
use crate::theme::InterfaceSize;
let current = app.ui.interface_size.percent();
ui.horizontal(|ui| {
ui.label(tl!("Interface size:"));
egui::ComboBox::from_id_salt("interface_size").selected_text(format!("{current}%")).width(220.0).show_ui(ui, |ui| {
for pct in InterfaceSize::PRESETS {
if ui.selectable_label(current == pct, format!("{pct}%")).clicked() {
let _ = app.run("ui.interfaceSize", json!({"percent": pct}));
}
}
});
});
ui.label(
egui::RichText::new(tl!(
"Makes the ribbon, menus and dialogs larger or smaller, on top of your system's display scaling. Document zoom is separate."
))
.small()
.weak(),
);
}
/// File ▸ Options ▸ Interface language: follow the system (the default) or pick one (#8).
fn language_picker(app: &mut WordApp, ui: &mut Ui) {
use crate::i18n::{AUTO, Lang};
// The language names are in their own scripts: fonts for them load from the next frame.
app.want_system_cjk = true;
let system = crate::i18n::system_lang();
let auto_label = crate::i18n::fmt(tl!("Automatic ({language})"), &[("language", system.name())]);
let current = if app.ui.language == AUTO { auto_label.clone() } else { Lang::from_pref(&app.ui.language).name().to_string() };
ui.horizontal(|ui| {
ui.label(tl!("Interface language:"));
egui::ComboBox::from_id_salt("interface_language").selected_text(current).width(220.0).show_ui(ui, |ui| {
if ui.selectable_label(app.ui.language == AUTO, auto_label.as_str()).clicked() {
let _ = app.run("ui.language", json!({"value": AUTO}));
}
for lang in Lang::all() {
if ui.selectable_label(app.ui.language == lang.code(), lang.name()).clicked() {
let _ = app.run("ui.language", json!({"value": lang.code()}));
}
}
});
});
ui.label(egui::RichText::new(tl!("Automatic follows your system's language. Menus and commands change; your documents don't.")).small().weak());
}
#[cfg(test)]
mod tests {
use egui_kittest::kittest::Queryable;

View File

@ -272,7 +272,7 @@ fn page_key(app: &WordApp, page: &Page, scale_px: f32, dim_body: bool) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
scale_px.to_bits().hash(&mut h);
let v = &app.session.view;
(v.marks, v.show_markup, v.dark_mode, wordcraft_render::DARK_PAPER, v.hide_ink, v.draw.replay).hash(&mut h);
(v.marks, app.session.prefs.marks, v.show_markup, v.dark_mode, wordcraft_render::DARK_PAPER, v.hide_ink, v.draw.replay).hash(&mut h);
app.session.prefs.markup.hash(&mut h);
dim_body.hash(&mut h);
format!("{:?}{:?}", app.session.doc.settings.page_color, app.session.doc.settings.watermark).hash(&mut h);
@ -465,6 +465,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
if !fresh && (rendered < 2 || !app.canvas.textures.contains_key(&i) && rendered < 4) {
let mut opts = screen_render_options();
opts.display.marks = app.session.view.marks;
opts.display.always = app.session.prefs.marks;
opts.display.placeholders = true;
opts.display.markup = app.session.view.show_markup;
// Ink Replay draws the ink itself, stroke by stroke, over pages without it.
@ -765,7 +766,9 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
if let (Some(h), Some(v)) = (hruler, vruler) {
rulers(app, ui, h, v, &rects, &layout, geo.scale);
}
if let Some(sel) = app.canvas.mini_anchor {
if let Some(sel) = app.canvas.mini_anchor
&& app.ui.mini_toolbar
{
crate::mini_toolbar::show(app, ui.ctx(), sel, area);
}
}
@ -1267,21 +1270,22 @@ fn rulers(app: &mut WordApp, ui: &mut Ui, h: Rect, v: Rect, rects: &[Rect], layo
hp.rect_filled(bar, 0.0, t.ruler_margin);
let text = Rect::from_min_max(pos2(x0 + page.body.x * scale, bar.min.y), pos2(x0 + page.body.right() * scale, bar.max.y));
hp.rect_filled(text, 0.0, t.ruler);
let unit = 72.0;
// Graduations in the File › Options unit.
let (unit, div, step) = ruler_scale(app.session.prefs.units);
let origin = page.body.x;
let mut k = -((origin / unit).ceil() as i32) * 8;
let mut k = -((origin / unit).ceil() as i32) * div;
loop {
let xpt = origin + k as f32 * unit / 8.0;
let xpt = origin + k as f32 * unit / div as f32;
if xpt > page.w {
break;
}
if xpt >= 0.0 {
let sx = x0 + xpt * scale;
let (len, label) = if k % 8 == 0 {
(0.0, Some(k / 8))
} else if k % 4 == 0 {
let (len, label) = if k % div == 0 {
(0.0, Some(k / div * step))
} else if k % (div / 2) == 0 {
(5.0, None)
} else if k % 2 == 0 {
} else if div >= 8 && k % (div / 4) == 0 {
(3.0, None)
} else {
(1.5, None)
@ -1292,7 +1296,7 @@ fn rulers(app: &mut WordApp, ui: &mut Ui, h: Rect, v: Rect, rects: &[Rect], layo
}
Some(_) => {}
None => {
if scale * unit / 8.0 > 4.0 || k % 2 == 0 {
if scale * unit / div as f32 > 4.0 || k % 2 == 0 {
hp.line_segment([pos2(sx, bar.center().y - len / 2.0), pos2(sx, bar.center().y + len / 2.0)], Stroke::new(1.0, t.ruler_tick));
}
}
@ -1410,15 +1414,21 @@ fn rulers(app: &mut WordApp, ui: &mut Ui, h: Rect, v: Rect, rects: &[Rect], layo
margin_handle(app, ui, edge, zone, |p| (p.y - y0) / scale, &sect);
}
let origin = page.body.y;
let mut k = -((origin / unit).ceil() as i32) * 8;
let mut k = -((origin / unit).ceil() as i32) * div;
loop {
let ypt = origin + k as f32 * unit / 8.0;
let ypt = origin + k as f32 * unit / div as f32;
if ypt > page.h.min(20_000.0) {
break;
}
if ypt >= 0.0 && k % 8 == 0 && k != 0 {
vp.text(pos2(vbar.center().x, y0 + ypt * scale), egui::Align2::CENTER_CENTER, (k / 8).abs().to_string(), regular(9.5), t.ruler_tick);
} else if ypt >= 0.0 && k % 4 == 0 {
if ypt >= 0.0 && k % div == 0 && k != 0 {
vp.text(
pos2(vbar.center().x, y0 + ypt * scale),
egui::Align2::CENTER_CENTER,
(k / div * step).abs().to_string(),
regular(9.5),
t.ruler_tick,
);
} else if ypt >= 0.0 && k % (div / 2) == 0 {
let sy = y0 + ypt * scale;
vp.line_segment([pos2(vbar.center().x - 2.5, sy), pos2(vbar.center().x + 2.5, sy)], Stroke::new(1.0, t.ruler_tick));
}
@ -1429,6 +1439,19 @@ fn rulers(app: &mut WordApp, ui: &mut Ui, h: Rect, v: Rect, rects: &[Rect], layo
}
}
/// A ruler's graduation for a unit: points per numbered step, divisions per step, and how much
/// each step counts (millimetre rulers number every centimetre as 10, 20…).
fn ruler_scale(u: wordcraft_geom::Unit) -> (f32, i32, i32) {
use wordcraft_geom::Unit;
match u {
Unit::Centimeters => (wordcraft_geom::PT_PER_CM, 4, 1),
Unit::Millimeters => (wordcraft_geom::PT_PER_CM, 4, 10),
Unit::Points => (72.0, 8, 72),
Unit::Picas => (72.0, 8, 6),
Unit::Inches => (72.0, 8, 1),
}
}
/// One edge of a page's text area on a ruler.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum MarginEdge {

View File

@ -160,7 +160,7 @@ pub fn title_bar(app: &mut WordApp, ui: &mut Ui) {
// Account / community.
let (r, resp) = ui.allocate_exact_size(vec2(control_h, control_h), Sense::click());
ui.painter().circle_filled(r.center(), control_h / 2.0 - 1.0, t.accent);
let initials: String = app.session.author.split_whitespace().filter_map(|w| w.chars().next()).take(2).collect();
let initials: String = app.session.user_initials().chars().take(2).collect();
ui.painter().text(r.center(), Align2::CENTER_CENTER, initials, semibold(10.5), egui::Color32::WHITE);
resp.on_hover_text(format!("{} — set your name in File › Options", app.session.author));
ui.add_space(8.0);
@ -295,6 +295,9 @@ pub fn status_bar(app: &mut WordApp, ui: &mut Ui) {
if app.session.doc.settings.track_changes {
st(ui, tl!("Track Changes: On"));
}
if app.session.prefs.overtype && st(ui, tl!("Overtype")).on_hover_text(tl!("Click to switch back to inserting")).clicked() {
let _ = app.run("text.overtype", json!({"value": false}));
}
if let Some((msg, at)) = &app.status_msg
&& crate::now_ms() - at < 6000.0
{

View File

@ -366,7 +366,7 @@ impl TableForm {
let s = &app.session;
let (tp, r, c) = s.sel.focus.path.cell()?;
let t = s.doc.table(s.sel.focus.story, &tp)?;
let k = wordcraft_geom::Unit::default().pt_per_unit();
let k = crate::options::unit().pt_per_unit();
let row = t.rows.get(r).map(|x| x.props.clone()).unwrap_or_default();
let cell = t.rows.get(r).and_then(|x| x.cells.get(c)).map(|x| x.props.clone()).unwrap_or_default();
let tw = wordcraft_engine::cmd::page::sect(s).text_width();
@ -402,7 +402,7 @@ impl TableForm {
/// `table.properties` parameters for what differs from `basis` (empty when nothing does).
pub fn changes(&self, basis: &TableForm) -> Value {
let k = wordcraft_geom::Unit::default().pt_per_unit();
let k = crate::options::unit().pt_per_unit();
let moved = |a: f32, b: f32| (a - b).abs() > 1e-4;
let mut v = serde_json::Map::new();
if self.width_on != basis.width_on || (self.width_on && moved(self.width, basis.width)) {
@ -827,9 +827,9 @@ impl Dialog {
Dialog::Paragraph {
rtl: rp.bidi,
align: format!("{:?}", rp.align.visual(rp.bidi)).to_lowercase(),
left: vl / 72.0,
right: vr / 72.0,
first: rp.indent_first / 72.0,
left: vl / crate::options::unit().pt_per_unit(),
right: vr / crate::options::unit().pt_per_unit(),
first: rp.indent_first / crate::options::unit().pt_per_unit(),
before: rp.space_before,
after: rp.space_after,
line,
@ -876,10 +876,10 @@ impl Dialog {
let size = app.session.doc.styles.resolve_char(Some("Normal"), &Default::default()).size;
let grid = (kind, sp.grid_lines_per_page().min(1000), sp.grid_chars_per_line(size).min(1000));
Dialog::PageSetup {
top: sp.margin_top / 72.0,
bottom: sp.margin_bottom / 72.0,
left: sp.margin_left / 72.0,
right: sp.margin_right / 72.0,
top: sp.margin_top / crate::options::unit().pt_per_unit(),
bottom: sp.margin_bottom / crate::options::unit().pt_per_unit(),
left: sp.margin_left / crate::options::unit().pt_per_unit(),
right: sp.margin_right / crate::options::unit().pt_per_unit(),
landscape: sp.landscape,
grid,
grid_was: grid,
@ -1358,12 +1358,27 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
let (l_label, r_label) = if opts[0] { ("Inside:", "Outside:") } else { ("Left:", "Right:") };
egui::Grid::new("ind").num_columns(4).show(ui, |ui| {
ui.label(tl!(l_label));
ui.add(egui::DragValue::new(left).speed(0.05).suffix("\"").max_decimals(2));
ui.add(
egui::DragValue::new(left)
.speed(3.6 / crate::options::unit().pt_per_unit())
.suffix(crate::options::unit().suffix())
.max_decimals(2),
);
ui.label(tl!(r_label));
ui.add(egui::DragValue::new(right).speed(0.05).suffix("\"").max_decimals(2));
ui.add(
egui::DragValue::new(right)
.speed(3.6 / crate::options::unit().pt_per_unit())
.suffix(crate::options::unit().suffix())
.max_decimals(2),
);
ui.end_row();
ui.label(tl!("First line:"));
ui.add(egui::DragValue::new(first).speed(0.05).suffix("\"").max_decimals(2));
ui.add(
egui::DragValue::new(first)
.speed(3.6 / crate::options::unit().pt_per_unit())
.suffix(crate::options::unit().suffix())
.max_decimals(2),
);
ui.end_row();
});
ui.checkbox(&mut opts[0], tl!("Mirror indents"));
@ -1572,14 +1587,34 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
ui.label(egui::RichText::new(tl!("Margins")).font(semibold(12.5)));
egui::Grid::new("ps").num_columns(4).show(ui, |ui| {
ui.label(tl!("Top:"));
ui.add(egui::DragValue::new(top).speed(0.05).suffix("\"").max_decimals(2));
ui.add(
egui::DragValue::new(top)
.speed(3.6 / crate::options::unit().pt_per_unit())
.suffix(crate::options::unit().suffix())
.max_decimals(2),
);
ui.label(tl!("Bottom:"));
ui.add(egui::DragValue::new(bottom).speed(0.05).suffix("\"").max_decimals(2));
ui.add(
egui::DragValue::new(bottom)
.speed(3.6 / crate::options::unit().pt_per_unit())
.suffix(crate::options::unit().suffix())
.max_decimals(2),
);
ui.end_row();
ui.label(tl!("Left:"));
ui.add(egui::DragValue::new(left).speed(0.05).suffix("\"").max_decimals(2));
ui.add(
egui::DragValue::new(left)
.speed(3.6 / crate::options::unit().pt_per_unit())
.suffix(crate::options::unit().suffix())
.max_decimals(2),
);
ui.label(tl!("Right:"));
ui.add(egui::DragValue::new(right).speed(0.05).suffix("\"").max_decimals(2));
ui.add(
egui::DragValue::new(right)
.speed(3.6 / crate::options::unit().pt_per_unit())
.suffix(crate::options::unit().suffix())
.max_decimals(2),
);
ui.end_row();
});
ui.label(egui::RichText::new(tl!("Orientation")).font(semibold(12.5)));
@ -1610,7 +1645,7 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
if ok {
let _ = app.run("layout.orientation", json!({"value": if *landscape { "landscape" } else { "portrait" }}));
let _ =
app.run("layout.margins", json!({"top": *top * 72.0, "bottom": *bottom * 72.0, "left": *left * 72.0, "right": *right * 72.0}));
app.run("layout.margins", json!({"top": *top * crate::options::unit().pt_per_unit(), "bottom": *bottom * crate::options::unit().pt_per_unit(), "left": *left * crate::options::unit().pt_per_unit(), "right": *right * crate::options::unit().pt_per_unit()}));
if grid != grid_was {
let kind = ["none", "lines", "linesAndChars", "snapToChars"].get(grid.0 as usize).copied().unwrap_or("none");
let mut v = json!({"type": kind});
@ -2419,7 +2454,7 @@ fn recipient_list(app: &mut WordApp, ui: &mut Ui, fields: &mut Vec<String>, rows
/// The Table Properties dialog's fields: Table, Row, Column and Cell sections.
fn table_properties(ui: &mut Ui, f: &mut TableForm) {
let unit = wordcraft_geom::Unit::default();
let unit = crate::options::unit();
fn len(v: &mut f32, lo: f32, unit: wordcraft_geom::Unit) -> egui::DragValue<'_> {
egui::DragValue::new(v).speed(0.01).range(lo..=22.0).suffix(unit.suffix()).max_decimals(2)
}
@ -2511,7 +2546,7 @@ fn apply_paragraph(
let _ = app.run("para.align", json!({"value": align}));
let (start, end) = if rtl { (right, left) } else { (left, right) };
let mut props = json!({
"indentLeft": start * 72.0, "indentRight": end * 72.0, "indentFirst": first * 72.0,
"indentLeft": start * crate::options::unit().pt_per_unit(), "indentRight": end * crate::options::unit().pt_per_unit(), "indentFirst": first * crate::options::unit().pt_per_unit(),
"spaceBefore": before, "spaceAfter": after, "lineSpacing": {"rule": "multiple", "value": line},
"keepNext": keep_next, "keepLines": keep_lines, "pageBreakBefore": page_break, "widowControl": widow,
});

View File

@ -115,7 +115,7 @@ pub struct ColumnsForm {
impl ColumnsForm {
fn k() -> f32 {
wordcraft_geom::Unit::default().pt_per_unit()
crate::options::unit().pt_per_unit()
}
pub fn read(app: &WordApp) -> ColumnsForm {
@ -346,7 +346,7 @@ fn columns_preview(ui: &mut Ui, f: &ColumnsForm, t: &Tokens) {
fn columns_ui(app: &mut WordApp, ui: &mut Ui, f: &mut ColumnsForm) -> bool {
let t = Tokens::get(ui.ctx());
let unit = wordcraft_geom::Unit::default();
let unit = crate::options::unit();
ui.set_width(420.0);
ui.label(egui::RichText::new(tl!("Presets")).font(semibold(12.5)));
let current = f.current_preset();

View File

@ -52,7 +52,7 @@ pub struct LevelForm {
impl LevelForm {
fn read(i: usize, v: &Value) -> LevelForm {
let k = wordcraft_geom::Unit::default().pt_per_unit();
let k = crate::options::unit().pt_per_unit();
let s = |key: &str| v.get(key).and_then(Value::as_str).unwrap_or("").to_string();
let f = |key: &str| v.get(key).and_then(Value::as_f64).map(|x| x as f32 / k).filter(|x| x.is_finite());
let restart_after = v.get("restartAfter").and_then(Value::as_u64);
@ -78,7 +78,7 @@ impl LevelForm {
/// The level as `list.define` takes it.
pub fn params(&self, i: usize) -> Value {
let k = wordcraft_geom::Unit::default().pt_per_unit();
let k = crate::options::unit().pt_per_unit();
json!({
"format": self.format,
"text": self.text,
@ -162,7 +162,7 @@ impl ListForm {
/// Define New Multilevel List. Returns true to close.
pub fn define_list(app: &mut WordApp, ui: &mut Ui, f: &mut ListForm) -> bool {
let unit = wordcraft_geom::Unit::default();
let unit = crate::options::unit();
fn len(v: &mut f32, unit: wordcraft_geom::Unit) -> egui::DragValue<'_> {
egui::DragValue::new(v).speed(0.01).range(-10.0..=22.0).suffix(unit.suffix()).max_decimals(2)
}
@ -310,7 +310,7 @@ fn preview(ui: &mut Ui, f: &ListForm) {
let p = ui.painter_at(rect);
p.rect_filled(rect, 2.0, egui::Color32::WHITE);
p.rect_stroke(rect, 2.0, egui::Stroke::new(1.0, t.border_strong), egui::StrokeKind::Inside);
let k = wordcraft_geom::Unit::default().pt_per_unit();
let k = crate::options::unit().pt_per_unit();
// Positions in points, scaled so 4.5 inches fit.
let scale = (rect.width() - 16.0) / 324.0;
let x_of = |v: f32| rect.left() + 8.0 + (v * k * scale).clamp(0.0, rect.width() - 40.0);

View File

@ -15,7 +15,7 @@ use crate::theme::{Tokens, semibold};
const SAME_POS: f32 = 0.5;
fn unit() -> wordcraft_geom::Unit {
wordcraft_geom::Unit::default()
crate::options::unit()
}
/// The Tabs dialog's fields. Positions in `stops` and `cleared` are points; `pos` and

View File

@ -183,6 +183,11 @@ fn untranslated_interface_text() {
let drawn = frame(&ctx, &mut app);
record(&app, &format!("File › {page}"), drawn);
}
for pane in wordcraft_engine::cmd::options::PANES {
let _ = app.run("ui.backstage", json!({"value": true, "page": "options", "pane": pane}));
let drawn = frame(&ctx, &mut app);
record(&app, &format!("File › Options › {pane}"), drawn);
}
let _ = app.run("ui.backstage", json!({"value": false}));
let dialogs = [
"font",

View File

@ -62,6 +62,7 @@
Arrange\nAll Organizar\ntodo
Arrow Flecha
Artistic Effects Efectos artísticos
As you type Mientras escribes
Asian Typography Tipografía asiática
Author Autor
AutoCorrect Options Opciones de autocorrección
@ -72,39 +73,72 @@
AutoSave saves every change to the file (needs a saved document) El autoguardado guarda cada cambio en el archivo (requiere un documento guardado)
AutoSave isn't available in the browser: it can't write to your files. Save downloads a copy. El autoguardado no está disponible en el navegador: no puede escribir en tus archivos. Guardar descarga una copia.
AutoSave only keeps .docx, .odt and .rtf files; {name} can't keep all formatting. Save as .docx to turn on AutoSave. El autoguardado solo admite archivos .docx, .odt y .rtf; {name} no puede conservar todo el formato. Guarda como .docx para activar el autoguardado.
AutoSave writes your changes to the file a moment after you stop typing. El autoguardado escribe tus cambios en el archivo poco después de que dejes de escribir.
Balloons Globos
Bullet Viñeta
By author Por autor
Centimeters Centímetros
Changed lines: Líneas cambiadas:
Click level to modify: Haz clic en el nivel que quieras modificar:
Check spelling as you type Revisar la ortografía mientras escribes
Click to switch back to inserting Haz clic para volver a insertar
Color only Solo color
Custom dictionary Diccionario personalizado
Define New Multilevel List Definir nueva lista multinivel
Define New Multilevel List… Definir nueva lista multinivel…
Deletions: Eliminaciones:
Enter formatting for number: Formato del número:
Follow number with: Después del número:
Formatting Formato
Formatting marks Marcas de formato
Hidden text Texto oculto
Ignore web and file addresses Omitir direcciones web y de archivo
Ignore words in capitals Omitir palabras en mayúsculas
Ignore words with numbers Omitir palabras con números
Inches Pulgadas
Formatting exceptions Excepciones de formato
Include level number from: Incluir número de nivel de:
Initials: Iniciales:
Ink Entrada de lápiz
Insertions and Deletions Inserciones y eliminaciones
Insertions: Inserciones:
Interface Interfaz
Interface language Idioma de la interfaz
Inside: Interior:
Left border Borde izquierdo
Legal style numbering Numeración de estilo legal
Link level to style: Vincular nivel con estilo:
List Definition Definición de lista
Mark grammar errors as you type Marcar errores gramaticales mientras escribes
Measurement units: Unidades de medida:
Millimeters Milímetros
New comments and tracked changes are recorded under this name and these initials. Los comentarios y cambios con seguimiento nuevos se registran con este nombre y estas iniciales.
New word Palabra nueva
No words yet. Add to Dictionary on a misspelled word puts it here. Aún no hay palabras. Añadir al diccionario sobre una palabra mal escrita la pone aquí.
Mirror indents Sangrías simétricas
Nothing Nada
Number alignment: Alineación del número:
Number format Formato de número
Number style for this level: Estilo de número para este nivel:
Outside border Borde exterior
Overtype Sobrescribir
Overtype (typing replaces the characters after the caret) Sobrescribir (lo que escribes reemplaza los caracteres tras el cursor)
Page display Visualización de página
Paragraph marks Marcas de párrafo
Personalize Personalizar
Picas Picas
Points Puntos
Print the page color Imprimir el color de página
Printing Impresión
Proofing language Idioma de revisión
Recent documents to show: Documentos recientes que mostrar:
Remove Quitar
Outside: Exterior:
Restart list after: Reiniciar lista después de:
Right border Borde derecho
Save the document to turn on AutoSave Guarda el documento para activar el autoguardado
Save the document to turn on AutoSave (a file you have only opened isn't rewritten until you save it) Guarda el documento para activar el autoguardado (un archivo que solo has abierto no se reescribe hasta que lo guardes)
Save documents Guardar documentos
AutoText Texto automático
Automatic Automático
Automatic ({language}) Automático ({language})
@ -130,6 +164,11 @@
Border Painter Pincel de bordes
Border\nPainter Pincel de\nbordes
Borders Bordes
Show the mini toolbar when text is selected Mostrar la minibarra de herramientas al seleccionar texto
Spaces Espacios
Spelling Ortografía
Spelling and grammar are checked in English (United States). La ortografía y la gramática se revisan en inglés (Estados Unidos).
Tab characters Caracteres de tabulación
Tab stop position: Posición de la tabulación:
Default tab stops: Tabulaciones predeterminadas:
Tab stops to be cleared: Tabulaciones que se borrarán:
@ -140,6 +179,10 @@
Set Fijar
Page Border Borde de página
Apply to: Aplicar a:
These marks show even when Show/Hide ¶ is off. Estas marcas se muestran aunque Mostrar u ocultar ¶ esté desactivado.
Typing replaces selected text Lo que escribes reemplaza el texto seleccionado
Update fields before printing Actualizar campos antes de imprimir
Use the Insert key to switch overtype on and off Usar la tecla Insert para activar y desactivar la sobrescritura
Whole document Todo el documento
This section Esta sección
Fill Relleno de color
@ -658,7 +701,6 @@
Reject All Changes Rechazar todos los cambios
Reject Change Rechazar cambio
Relaxed Relajado
Remove Quitar
Remove Background Quitar fondo
Remove Footer Quitar pie de página
Remove Header Quitar encabezado

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -49,6 +49,7 @@
Arrange\nAll Paiguta\nkõik
Arrow Nool
Artistic Effects Kunstilised efektid
As you type Kirjutamise ajal
Author Autor
AutoCorrect Options Automaatparanduse valikud
AutoFit Automaatne sobitus
@ -56,6 +57,7 @@
AutoFit Window Sobita aknaga
AutoSave Automaatsalvestus
AutoSave saves every change to the file (needs a saved document) Automaatsalvestus salvestab iga muudatuse faili (dokument peab olema salvestatud)
AutoSave writes your changes to the file a moment after you stop typing. Automaatsalvestus kirjutab muudatused faili veidi pärast seda, kui lõpetad kirjutamise.
AutoText Automaattekst
Automatic Automaatne
Automatic ({language}) Automaatne ({language})
@ -96,6 +98,7 @@
Cell Size Lahtri suurus
Cell\nMargins Lahtri\nveerised
Center Keskele
Centimeters Sentimeetrid
Change Muuda
Change Case Muuda tähesuurust
Change List Level Muuda loendi taset
@ -114,6 +117,7 @@
Check Compatibility Kontrolli ühilduvust
Check Spelling as You Type Kontrolli õigekirja kirjutamise ajal
Check for Errors Kontrolli vigu
Check spelling as you type Kontrolli õigekirja kirjutamise ajal
Check\nAccessibility Kontrolli\nligipääsetavust
Citation Style Viitamisstiil
Citations & Bibliography Viited ja allikaloend
@ -124,6 +128,7 @@
Clear Character Style Eemalda märgistiil
Clear Formatting Level Eemalda vormindustase
Clear Paragraph Formatting Eemalda lõiguvormindus
Click to switch back to inserting Klõpsa, et naasta lisamisrežiimi
Clipboard Lõikelaud
Close Sulge
Close Header and Footer Sulge päis ja jalus
@ -164,6 +169,7 @@
Custom Margins… Kohandatud veerised…
Custom Watermark Kohandatud vesimärk
Custom Watermark… Kohandatud vesimärk…
Custom dictionary Kohandatud sõnastik
Customize Quick Access Toolbar Kohanda kiirpääsuriba
Cut Lõika
Dark Mode Tume režiim
@ -276,6 +282,7 @@
Format Painter Vormingupintsel
Formatted Text Vormindatud tekst
Formatting at Selection Valiku vormindus
Formatting marks Vormindusmärgid
Formatting exceptions Vormindamise erandid
Formula Valem
General Üldine
@ -302,6 +309,7 @@
Heart Süda
Help Abi
Hidden Peidetud
Hidden text Peidetud tekst
Highlight Merge Fields Tõsta ühendamisväljad esile
Highlight\nMerge Fields Tõsta ühendamis-\nväljad esile
Highlighter Marker
@ -311,10 +319,14 @@
Hyphenation Poolitus
Icons Ikoonid
Ignore All Eira kõiki
Ignore web and file addresses Eira veebi- ja failiaadresse
Ignore words in capitals Eira suurtähtedega sõnu
Ignore words with numbers Eira numbritega sõnu
Illustrations Illustratsioonid
Immersive Süvenemine
Immersive Reader Süvaluger
Immersive\nReader Süva-\nluger
Inches Tollid
Increase Font Size Suurenda fondi suurust
Increase Indent Suurenda taanet
Indent Taane
@ -322,6 +334,7 @@
Indents Taanded
Index Register
Info Teave
Initials: Initsiaalid:
Ink to\nMath Käsikiri\nvalemiks
Ink to\nShape Käsikiri\nkujundiks
Ink\nReplay Esita\nkäsikiri
@ -351,6 +364,8 @@
Inside: Sisemine:
Inspect Document Kontrolli dokumenti
Inspect Styles at Selection Kontrolli valiku stiile
Interface Kasutajaliides
Interface language Kasutajaliidese keel
Interface language: Kasutajaliidese keel:
Italic Kaldkiri
Join the ArtCraft community on Discord Liitu ArtCrafti kogukonnaga Discordis
@ -401,16 +416,19 @@
Margins Veerised
Mark Citation Märgi õigusallikas
Mark Entry Märgi registrikirje
Mark grammar errors as you type Märgi grammatikavead kirjutamise ajal
Mark\nCitation Märgi\nõigusallikas
Mark\nEntry Märgi\nregistrikirje
Markdown (*.md) Markdown (*.md)
Match Fields Vastenda väljad
Match case Erista suur- ja väiketähti
Matrix Maatriks
Measurement units: Mõõtühikud:
Merge Ühenda
Merge Cells Ühenda lahtrid
Merge Formatting Ühenda vormindus
Merged PRs Ühendatud PR-id
Millimeters Millimeetrid
Mirrored Peegeldatud
Moderate Mõõdukas
Modify Style Muuda stiili
@ -429,7 +447,9 @@
New Paragraph Uus lõik
New Style… Uus stiil…
New Window Uus aken
New comments and tracked changes are recorded under this name and these initials. Uued kommentaarid ja jälitatud muudatused salvestatakse selle nime ja nende initsiaalidega.
New from Template Uus mallist
New word Uus sõna
New\nComment Uus\nkommentaar
New\nWindow Uus\naken
Next Järgmine
@ -446,6 +466,7 @@
No contributor data was built into this copy. Selles versioonis pole kaasautorite andmeid.
No model credits were built into this copy. Selles versioonis pole mudelite autoriteavet.
No templates match your search. Otsingule vastavaid malle pole.
No words yet. Add to Dictionary on a misspelled word puts it here. Sõnu veel pole. Valik „Lisa sõnastikku“ valesti kirjutatud sõnal lisab selle siia.
Nonbreaking Hyphen Poolitamatu sidekriips
Nonbreaking Space Poolitamatu tühik
None Puudub
@ -474,6 +495,8 @@
Outside Borders Välisäärised
Outside: Välimine:
Oval Ovaal
Overtype Ülekirjutus
Overtype (typing replaces the characters after the caret) Ülekirjutus (kirjutatu asendab kursori järel olevad märgid)
PDF document (*.pdf) PDF-dokument (*.pdf)
Page Leht
Page Background Lehe taust
@ -490,6 +513,7 @@
Page Width Lehe laius
Page X of Y Leht X / Y
Page break before Lehepiir enne
Page display Lehe kuvamine
Page image (*.png) Lehe pilt (*.png)
Page width Lehe laius
Page {page} Leht {page}
@ -504,6 +528,7 @@
Paragraph Settings Lõigu sätted
Paragraph Spacing Lõiguvahe
Paragraph Up Lõigu võrra üles
Paragraph marks Lõigumärgid
Paragraphs Lõigud
Paste Kleebi
Paste (⌘V) Kleebi (⌘V)
@ -515,11 +540,14 @@
Paste: Merge Formatting Kleebi: ühenda vormindus
Pen Pliiats
Pencil Grafiitpliiats
Personalize Isikupärastamine
Picas Piikad
Picture Border Pildi ääris
Picture Format Pildi vorming
Picture Styles Pildistiilid
Pictures Pildid
Plain text (*.txt) Lihttekst (*.txt)
Points Punktid
Portrait Püstpaigutus
Position Asukoht
Preview Results Tulemuste eelvaade
@ -531,9 +559,12 @@
Previous Section Eelmine jaotis
Print Prindi
Print Layout Prindipaigutus
Print the page color Prindi lehe värv
Print\nLayout Prindi-\npaigutus
Printing Printimine
Proofing Keelekontroll
Proofing Issues Keelekontrolli probleemid
Proofing language Keelekontrolli keel
Properties Atribuudid
Protect Kaitse
Protect Document Kaitse dokumenti
@ -548,6 +579,7 @@
Reads the clipboard's web content and keeps its formatting. Loeb lõikelaualt veebisisu ja säilitab vorminduse.
Reading Highlight Otsingutulemuste esiletõst
Recent Hiljutised
Recent documents to show: Kuvatavad hiljutised dokumendid:
Record Macro Salvesta makro
Recover Unsaved Documents Taasta salvestamata dokumendid
Rectangle Ristkülik
@ -558,6 +590,7 @@
Reject All Changes Lükka kõik muudatused tagasi
Reject Change Lükka muudatus tagasi
Relaxed Hõre
Remove Eemalda
Remove Background Eemalda taust
Remove Footer Eemalda jalus
Remove Header Eemalda päis
@ -608,6 +641,7 @@
Save As Salvesta nimega
Save a copy in another format. Printing goes through a PDF you can print from any viewer. Salvesta koopia teises vormingus. Printimiseks luuakse PDF, mille saad printida PDF-vaaturist.
Save as Template Salvesta mallina
Save documents Dokumentide salvestamine
Saved Salvestatud
Search Commands Otsi käske
Search commands and help Otsi käske ja abi
@ -656,6 +690,7 @@
Show all formatting marks Kuva kõik vormindusmärgid
Show rulers Kuva joonlauad
Show the community button in the title bar Kuva tiitliribal kogukonna nupp
Show the mini toolbar when text is selected Kuva teksti valimisel minitööriistariba
Show/Hide ¶ Kuva/peida ¶
Show\nComments Kuva\nkommentaarid
Shrink Font 1 Point Vähenda fonti 1 punkti võrra
@ -672,12 +707,15 @@
Sort Sordi
Source code: github.com/storytold/wordcraft Lähtekood: github.com/storytold/wordcraft
Space before/after: Vahe enne/pärast:
Spaces Tühikud
Spacing Vahe
Spacing (pt): Vahe (pt):
Speech Kõne
Spelling Õigekiri
Spelling & Grammar Õigekiri ja grammatika
Spelling &\nGrammar Õigekiri ja\ngrammatika
Spelling Suggestions Õigekirjasoovitused
Spelling and grammar are checked in English (United States). Õigekirja ja grammatikat kontrollitakse inglise keeles (Ameerika Ühendriigid).
Split Tükelda
Split Cells Tükelda lahtrid
Split Table Tükelda tabel
@ -706,6 +744,7 @@
Symbols Sümbolid
Synonyms Sünonüümid
Tab Tabeldus
Tab characters Tabeldusmärgid
Table Tabel
Table Column Width Tabeli veerulaius
Table Design Tabeli kujundus
@ -739,6 +778,7 @@
Theme List Kujunduste loend
Themes Kujundused
Thesaurus Sünonüümisõnastik
These marks show even when Show/Hide ¶ is off. Need märgid on nähtavad ka siis, kui Kuva/peida ¶ on välja lülitatud.
Thick Paks
Three Kolm
Tight Tihe
@ -755,6 +795,7 @@
Two Kaks
Type Text Sisesta tekst
Type a command, e.g. "insert table" Sisesta käsk, nt "lisa tabel"
Typing replaces selected text Kirjutatu asendab valitud teksti
UPPERCASE SUURTÄHED
Underline Allajoon
Underline Words Only Jooni alla ainult sõnad
@ -767,8 +808,10 @@
Update Style to Match Selection Uuenda stiil valiku järgi
Update Table Uuenda tabelit
Update Table of Figures Uuenda illustratsioonide loendit
Update fields before printing Värskenda välju enne printimist
Update to Match Uuenda vastavaks
Update to Match Selection Uuenda valiku järgi
Use the Insert key to switch overtype on and off Kasuta Insert-klahvi ülekirjutuse sisse- ja väljalülitamiseks
User Name Kasutajanimi
User name: Kasutajanimi:
Version History Versiooniajalugu
@ -1454,7 +1497,6 @@
Not used by any command Ükski käsk ei kasuta seda
Now used by {command} Praegu kasutab: {command}
Press a key combination Vajuta klahvikombinatsiooni
Remove Eemalda
Remove every custom shortcut and restore the built-in keys? Kas eemaldada kõik kohandatud kiirklahvid ja taastada sisseehitatud klahvid?
Reset All… Lähtesta kõik…
(Custom) (Kohandatud)

Can't render this file because it has a wrong number of fields in line 12.

View File

@ -48,12 +48,14 @@
Arrange\nAll 整列
Arrow 矢印
Artistic Effects アート効果
As you type 入力中
Asian Typography アジア文字の組版
Author 作成者
AutoCorrect Options オートコレクトのオプション
AutoFit 自動調整
AutoFit Contents 文字列の幅に合わせる
AutoFit Window ウィンドウ幅に合わせる
AutoSave writes your changes to the file a moment after you stop typing. 自動保存は、入力を止めて少したつと変更をファイルに書き込みます。
Automatic 自動
Automatic ({language}) 自動({language})
Automatic follows your system's language. Menus and commands change; your documents don't. 「自動」はシステムの言語に従います。メニューとコマンドの表示が変わり、文書の内容は変わりません。
@ -64,13 +66,17 @@
AutoSave failed: {error}. Save the document to turn AutoSave back on. 自動保存できませんでした: {error}。文書を保存し直すと、自動保存が再開します。
Balloons 吹き出し
Bottom Margin 下余白
Centimeters センチメートル
Check spelling as you type 入力中にスペル チェックを行う
Click a border between two cells to merge them. Esc stops. 2 つのセルの間の境界線をクリックすると結合します。Esc で終了します。
Bullet 行頭文字
By author 校閲者別
Changed lines: 変更された行:
Click level to modify: 変更するレベルをクリック:
Click to switch back to inserting クリックすると挿入モードに戻ります
Color only 色のみ
Compress punctuation at the start of a line 行頭の約物を詰める
Custom dictionary ユーザー辞書
Dark page (white text on black) ダーク ページ (黒地に白い文字)
Drag a rectangle to draw a table, or a line across cells to split them. Esc stops. 四角形をドラッグして表を描くか、セルを横切る線を引いて分割します。Esc で終了します。
Draw Table 表を描画
@ -81,11 +87,20 @@
Enter formatting for number: 番号の書式:
Follow number with: 番号に続く文字:
Formatting 書式
Formatting marks 編集記号
Hidden text 隠し文字
Ignore web and file addresses Web アドレスとファイル パスを無視する
Ignore words in capitals すべて大文字の単語を無視する
Ignore words with numbers 数字を含む単語を無視する
Inches インチ
Formatting exceptions 書式の例外
Include level number from: 含めるレベルの番号:
Initials: イニシャル:
Ink インク
Insertions and Deletions 挿入と削除
Insertions: 挿入された箇所:
Interface インターフェイス
Interface language 表示言語
Inside: 内側:
Left border 左罫線
Left Margin 左余白
@ -94,14 +109,32 @@
Line breaking 行の分割
Link level to style: レベルに対応付けるスタイル:
List Definition リストの定義
Mark grammar errors as you type 入力中に文法の誤りに印を付ける
Measurement units: 単位:
Millimeters ミリメートル
Mirror indents 左右対称のインデント
More templates → その他のテンプレート →
New comments and tracked changes are recorded under this name and these initials. 新しいコメントと変更履歴は、この名前とイニシャルで記録されます。
New word 新しい単語
No templates match your search. 検索に一致するテンプレートはありません。
No words yet. Add to Dictionary on a misspelled word puts it here. まだ単語はありません。スペルミスの単語で [辞書に追加] を選ぶと、ここに追加されます。
Nothing なし
Number alignment: 番号の配置:
Number format 番号書式
Number style for this level: このレベルの番号の種類:
Outside border 外側の罫線
Overtype 上書き
Overtype (typing replaces the characters after the caret) 上書き (入力した文字がカーソルの後ろの文字を置き換えます)
Page display ページの表示
Paragraph marks 段落記号
Personalize 個人設定
Picas パイカ
Points ポイント
Print the page color ページの色を印刷する
Printing 印刷
Proofing language 文章校正の言語
Recent documents to show: 表示する最近使ったドキュメントの数:
Remove 削除
Outside: 外側:
Reopen 再度開く
Restart list after: リストを振り直すレベル:
@ -112,9 +145,9 @@
Distribute spacing 間隔を均等に配置
Offset (pt): オフセット (pt):
Phonetic Guide ルビ
Remove 削除
Right (vertical) 右 (縦書き)
Ruby text: ルビ文字:
Save documents ドキュメントの保存
Search for templates テンプレートを検索
Show all revisions inline すべての変更履歴を本文中に表示
Show documents with their colours inverted, like View › Switch Modes. Saving, printing and PDFs are unchanged. 表示 › モードの切り替えと同様に、文書の色を反転して表示します。保存、印刷、PDF は変わりません。
@ -142,6 +175,11 @@
Border Painter 罫線の書式設定
Border\nPainter 罫線の書式設定
Borders 罫線
Show the mini toolbar when text is selected テキストを選択したときにミニ ツール バーを表示する
Spaces スペース
Spelling スペル チェック
Spelling and grammar are checked in English (United States). スペルと文法は英語 (米国) でチェックされます。
Tab characters タブ
Tab stop position: タブ位置:
Default tab stops: 既定のタブ間隔:
Tab stops to be cleared: 削除するタブ:
@ -152,6 +190,10 @@
Set 設定
Page Border ページ罫線の設定
Apply to: 適用先:
These marks show even when Show/Hide ¶ is off. これらの記号は、[編集記号の表示/非表示 ¶] がオフでも表示されます。
Typing replaces selected text 入力時に選択文字列を置き換える
Update fields before printing 印刷前にフィールドを更新する
Use the Insert key to switch overtype on and off Insert キーで上書きのオンとオフを切り替える
Whole document 文書全体
This section このセクション
Fill 塗りつぶし

Can't render this file because it has a wrong number of fields in line 3.

View File

@ -58,12 +58,14 @@
Arrange\nAll Organizar\ntudo
Arrow Seta
Artistic Effects Efeitos artísticos
As you type Enquanto você digita
Asian Typography Tipografia asiática
Author Autor
AutoCorrect Options Opções de autocorreção
AutoFit Autoajuste
AutoFit Contents Autoajustar ao conteúdo
AutoFit Window Autoajustar à janela
AutoSave writes your changes to the file a moment after you stop typing. O salvamento automático grava suas alterações no arquivo pouco depois que você para de digitar.
Automatic Automático
Automatic ({language}) Automático ({language})
Automatic follows your system's language. Menus and commands change; your documents don't. Automático segue o idioma do seu sistema. Menus e comandos mudam; seus documentos, não.
@ -76,34 +78,66 @@
Balloons Balões
Bullet Marcador
By author Por autor
Centimeters Centímetros
Changed lines: Linhas alteradas:
Check spelling as you type Verificar a ortografia enquanto você digita
Click level to modify: Clique no nível a modificar:
Click to switch back to inserting Clique para voltar a inserir
Color only Somente cor
Custom dictionary Dicionário personalizado
Define New Multilevel List Definir nova lista de vários níveis
Define New Multilevel List… Definir nova lista de vários níveis…
Deletions: Exclusões:
Enter formatting for number: Formatação do número:
Follow number with: Após o número:
Formatting Formatação
Formatting marks Marcas de formatação
Hidden text Texto oculto
Ignore web and file addresses Ignorar endereços da Web e de arquivos
Ignore words in capitals Ignorar palavras em maiúsculas
Ignore words with numbers Ignorar palavras com números
Inches Polegadas
Formatting exceptions Exceções de formatação
Include level number from: Incluir número do nível:
Initials: Iniciais:
Ink Tinta
Insertions and Deletions Inserções e exclusões
Insertions: Inserções:
Interface Interface
Interface language Idioma da interface
Inside: Interno:
Left border Borda esquerda
Legal style numbering Numeração no estilo legal
Link level to style: Vincular nível ao estilo:
List Definition Definição da lista
Mark grammar errors as you type Marcar erros gramaticais enquanto você digita
Measurement units: Unidades de medida:
Millimeters Milímetros
New comments and tracked changes are recorded under this name and these initials. Novos comentários e alterações controladas são registrados com este nome e estas iniciais.
New word Nova palavra
No words yet. Add to Dictionary on a misspelled word puts it here. Ainda não há palavras. Adicionar ao dicionário em uma palavra com erro a coloca aqui.
Mirror indents Recuos espelhados
Nothing Nada
Number alignment: Alinhamento do número:
Number format Formato do número
Number style for this level: Estilo de número deste nível:
Outside border Borda externa
Overtype Sobrescrever
Overtype (typing replaces the characters after the caret) Sobrescrever (o que você digita substitui os caracteres após o cursor)
Page display Exibição da página
Paragraph marks Marcas de parágrafo
Personalize Personalizar
Picas Paicas
Points Pontos
Print the page color Imprimir a cor da página
Printing Impressão
Proofing language Idioma de revisão
Recent documents to show: Documentos recentes a mostrar:
Remove Remover
Outside: Externo:
Restart list after: Reiniciar lista após:
Right border Borda direita
Save documents Salvar documentos
Save the document to turn on AutoSave Salve o documento para ativar o salvamento automático
Save the document to turn on AutoSave (a file you have only opened isn't rewritten until you save it) Salve o documento para ativar o salvamento automático (um arquivo que você apenas abriu não é regravado até você salvá-lo)
AutoText Autotexto
@ -126,6 +160,11 @@
Border Painter Pincel de bordas
Border\nPainter Pincel\nde bordas
Borders Bordas
Show the mini toolbar when text is selected Mostrar a minibarra de ferramentas ao selecionar texto
Spaces Espaços
Spelling Ortografia
Spelling and grammar are checked in English (United States). A ortografia e a gramática são verificadas em inglês (Estados Unidos).
Tab characters Caracteres de tabulação
Tab stop position: Posição da tabulação:
Default tab stops: Tabulações padrão:
Tab stops to be cleared: Tabulações a remover:
@ -136,6 +175,10 @@
Set Definir
Page Border Borda da página
Apply to: Aplicar a:
These marks show even when Show/Hide ¶ is off. Estas marcas aparecem mesmo com Mostrar/ocultar ¶ desativado.
Typing replaces selected text O que você digita substitui o texto selecionado
Update fields before printing Atualizar campos antes de imprimir
Use the Insert key to switch overtype on and off Usar a tecla Insert para ativar e desativar a sobrescrita
Whole document Documento inteiro
This section Esta seção
Fill Cor de fundo
@ -274,7 +317,6 @@
Distribute spacing Distribuir espaçamento
Offset (pt): Deslocamento (pt):
Phonetic Guide Guia fonético
Remove Remover
Right (vertical) Direita (vertical)
Ruby text: Texto ruby:
Search for templates Pesquisar modelos

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -2206,3 +2206,47 @@ preview Title Пример
Turkish Турецкий
Ukrainian Украинский
{language} is now the document's default language. {language} теперь язык документа по умолчанию.
# File > Options panes (#485).
As you type По мере ввода
AutoSave writes your changes to the file a moment after you stop typing. Автосохранение записывает изменения в файл вскоре после того, как вы перестанете печатать.
Centimeters Сантиметры
Check spelling as you type Проверять орфографию при вводе
Click to switch back to inserting Нажмите, чтобы вернуться к вставке
Custom dictionary Пользовательский словарь
Formatting marks Знаки форматирования
Hidden text Скрытый текст
Ignore web and file addresses Пропускать веб-адреса и пути к файлам
Ignore words in capitals Пропускать слова из прописных букв
Ignore words with numbers Пропускать слова с цифрами
Inches Дюймы
Initials: Инициалы:
Interface Интерфейс
Interface language Язык интерфейса
Mark grammar errors as you type Отмечать грамматические ошибки при вводе
Measurement units: Единицы измерения:
Millimeters Миллиметры
New comments and tracked changes are recorded under this name and these initials. Новые примечания и исправления записываются под этим именем и инициалами.
New word Новое слово
No words yet. Add to Dictionary on a misspelled word puts it here. Слов пока нет. Команда «Добавить в словарь» для слова с ошибкой помещает его сюда.
Overtype Замена
Overtype (typing replaces the characters after the caret) Замена (вводимый текст заменяет символы после курсора)
Page display Отображение страницы
Paragraph marks Знаки абзаца
Personalize Персонализация
Picas Пики
Points Пункты
Print the page color Печатать цвет страницы
Printing Печать
Proofing language Язык проверки правописания
Recent documents to show: Число недавних документов:
Save documents Сохранение документов
Show the mini toolbar when text is selected Показывать мини-панель при выделении текста
Spaces Пробелы
Spelling Орфография
Spelling and grammar are checked in English (United States). Орфография и грамматика проверяются для английского языка (США).
Tab characters Знаки табуляции
These marks show even when Show/Hide ¶ is off. Эти знаки видны, даже когда «Показать все знаки» выключено.
Typing replaces selected text Ввод заменяет выделенный текст
Update fields before printing Обновлять поля перед печатью
Use the Insert key to switch overtype on and off Клавиша Insert включает и выключает режим замены

Can't render this file because it contains an unexpected character in line 7 and column 25.

View File

@ -65,12 +65,14 @@
Arrow Strelica
Artistic Effects Umetnički efekti
As Recommended Kao što je preporučeno
As you type Tokom kucanja
Asian Typography Azijska tipografija
Author Autor
AutoCorrect Options Opcije automatske ispravke
AutoFit Automatsko uklapanje
AutoFit Contents Automatski uklopi prema sadržaju
AutoFit Window Automatski uklopi prema prozoru
AutoSave writes your changes to the file a moment after you stop typing. Automatsko čuvanje upisuje izmene u datoteku čim prestanete da kucate.
Automatic Automatski
Automatic ({language}) Automatski ({language})
Automatic follows your system's language. Menus and commands change; your documents don't. Automatski prati jezik sistema. Meniji i naredbe se menjaju; dokumenti ne.
@ -81,6 +83,8 @@
AutoSave failed: {error}. Save the document to turn AutoSave back on. Automatsko čuvanje neuspelo: {error}. Sačuvajte dokument da biste ga ponovo uključili.
Balloons Oblačići
Bottom Margin Donja margina
Centimeters Centimetri
Check spelling as you type Proveravaj pravopis tokom kucanja
Click a border between two cells to merge them. Esc stops. Kliknite ivicu između dve ćelije da ih spojite. Esc završava.
Based on: {style} Zasnovan na: {style}
Built-in style Ugrađeni stil
@ -92,12 +96,14 @@
Character style Stil znakova
Click an item to paste it. Kliknite na stavku da biste je nalepili.
Click level to modify: Kliknite nivo koji menjate:
Click to switch back to inserting Kliknite da biste se vratili na umetanje
Clipboard Items Stavke ostave
Clipboard Pane Okno ostave
Color only Samo boja
Color: {color} Boja: {color}
Compress punctuation at the start of a line Sažmi interpunkciju na početku reda
Confirm password: Potvrdite lozinku:
Custom dictionary Prilagođeni rečnik
Dark page (white text on black) Tamna stranica (beo tekst na crnom)
Drag a rectangle to draw a table, or a line across cells to split them. Esc stops. Prevucite pravougaonik da nacrtate tabelu ili liniju preko ćelija da ih podelite. Esc završava.
Draw Table Nacrtaj tabelu
@ -108,23 +114,50 @@
Enter formatting for number: Oblik broja:
Follow number with: Posle broja:
Formatting Oblikovanje
Formatting marks Oznake oblikovanja
Hidden text Skriveni tekst
Ignore web and file addresses Zanemari veb-adrese i putanje datoteka
Ignore words in capitals Zanemari reči pisane velikim slovima
Ignore words with numbers Zanemari reči sa brojevima
Inches Inči
Formatting exceptions Izuzeci formatiranja
Include level number from: Uključi broj nivoa:
Initials: Inicijali:
Ink Mastilo
Insertions and Deletions Umetanja i brisanja
Insertions: Umetanja:
Interface Interfejs
Interface language Jezik interfejsa
Inside: Unutra:
Left border Leva ivica
Left Margin Leva margina
Legal style numbering Pravno numerisanje
Link level to style: Poveži nivo sa stilom:
List Definition Definicija liste
Mark grammar errors as you type Označavaj gramatičke greške tokom kucanja
Measurement units: Jedinice mere:
Millimeters Milimetri
New comments and tracked changes are recorded under this name and these initials. Novi komentari i praćene izmene beleže se pod ovim imenom i inicijalima.
New word Nova reč
No words yet. Add to Dictionary on a misspelled word puts it here. Još nema reči. Dodaj u rečnik na pogrešno napisanoj reči stavlja je ovde.
Mirror indents Preslikaj uvlačenja
Nothing Ništa
Number alignment: Poravnanje broja:
Number format Format broja
Number style for this level: Stil broja za ovaj nivo:
Outside border Spoljna ivica
Overtype Prepisivanje
Overtype (typing replaces the characters after the caret) Prepisivanje (otkucano zamenjuje znakove posle kursora)
Page display Prikaz strane
Paragraph marks Oznake pasusa
Personalize Personalizacija
Picas Pike
Points Tačke
Print the page color Štampaj boju strane
Printing Štampanje
Proofing language Jezik provere pisanja
Recent documents to show: Broj nedavnih dokumenata za prikaz:
Remove Ukloni
Outside: Spolja:
Reopen Ponovo otvori
Restart list after: Ponovo počni listu posle:
@ -163,6 +196,7 @@
Paste Clipboard Item Nalepi stavku iz ostave
Preview Pregled
Remove Password Ukloni lozinku
Save documents Čuvanje dokumenata
Shadow, glow and soft edges Senka, sjaj i meke ivice
Shape Effects Efekti oblika
Show all revisions inline Prikaži sve izmene u tekstu
@ -171,7 +205,6 @@
Distribute spacing Rasporedi razmak
Offset (pt): Pomak (pt):
Phonetic Guide Fonetski vodič
Remove Ukloni
Right (vertical) Desno (vertikalno)
Ruby text: Rubi tekst:
Show documents with their colours inverted, like View › Switch Modes. Saving, printing and PDFs are unchanged. Prikaži dokumente sa invertovanim bojama, kao kod Prikaz › Promeni režim. Čuvanje, štampanje i PDF nisu pogođeni.
@ -199,6 +232,11 @@
Border Painter Kopiranje ivica
Border\nPainter Kopiranje\nivica
Borders Ivice
Show the mini toolbar when text is selected Prikaži mini traku sa alatkama pri izboru teksta
Spaces Razmaci
Spelling Pravopis
Spelling and grammar are checked in English (United States). Pravopis i gramatika se proveravaju na engleskom (Sjedinjene Države).
Tab characters Znakovi tabulatora
Tab stop position: Položaj tabulatora:
Default tab stops: Podrazumevani tabulatori:
Tab stops to be cleared: Tabulatori za brisanje:
@ -209,6 +247,10 @@
Set Postavi
Page Border Ivica strane
Apply to: Primeni na:
These marks show even when Show/Hide ¶ is off. Ove oznake se prikazuju i kada je Prikaži/sakrij ¶ isključeno.
Typing replaces selected text Otkucano zamenjuje izabrani tekst
Update fields before printing Ažuriraj polja pre štampanja
Use the Insert key to switch overtype on and off Taster Insert uključuje i isključuje prepisivanje
Whole document Ceo dokument
This section Ovaj odeljak
Fill Ispuna

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -69,12 +69,14 @@
Arrow Стрелица
Artistic Effects Уметнички ефекти
As Recommended Као што је препоручено
As you type Током куцања
Asian Typography Азијска типографија
Author Аутор
AutoCorrect Options Опције аутоматске исправке
AutoFit Аутоматско уклапање
AutoFit Contents Аутоматски уклопи према садржају
AutoFit Window Аутоматски уклопи према прозору
AutoSave writes your changes to the file a moment after you stop typing. Аутоматско чување уписује измене у датотеку чим престанете да куцате.
Automatic Аутоматски
Automatic ({language}) Аутоматски ({language})
Automatic follows your system's language. Menus and commands change; your documents don't. Аутоматски прати језик система. Менији и наредбе се мењају; документи не.
@ -85,6 +87,8 @@
AutoSave failed: {error}. Save the document to turn AutoSave back on. Аутоматско чување неуспело: {error}. Сачувајте документ да бисте га поново укључили.
Balloons Облачићи
Bottom Margin Доња маргина
Centimeters Центиметри
Check spelling as you type Проверавај правопис током куцања
Click a border between two cells to merge them. Esc stops. Кликните ивицу између две ћелије да их спојите. Esc завршава.
Based on: {style} Заснован на: {style}
Built-in style Уграђени стил
@ -96,12 +100,14 @@
Character style Стил знакова
Click an item to paste it. Кликните на ставку да бисте је налепили.
Click level to modify: Кликните ниво који мењате:
Click to switch back to inserting Кликните да бисте се вратили на уметање
Clipboard Items Ставке оставе
Clipboard Pane Окно оставе
Color only Само боја
Color: {color} Боја: {color}
Compress punctuation at the start of a line Сажми интерпункцију на почетку реда
Confirm password: Потврдите лозинку:
Custom dictionary Прилагођени речник
Dark page (white text on black) Тамна страница (бео текст на црном)
Drag a rectangle to draw a table, or a line across cells to split them. Esc stops. Превуците правоугаоник да нацртате табелу или линију преко ћелија да их поделите. Esc завршава.
Draw Table Нацртај табелу
@ -112,23 +118,50 @@
Enter formatting for number: Облик броја:
Follow number with: После броја:
Formatting Обликовање
Formatting marks Ознаке обликовања
Hidden text Скривени текст
Ignore web and file addresses Занемари веб-адресе и путање датотека
Ignore words in capitals Занемари речи писане великим словима
Ignore words with numbers Занемари речи са бројевима
Inches Инчи
Formatting exceptions Изузеци форматирања
Include level number from: Укључи број нивоа:
Initials: Иницијали:
Ink Мастило
Insertions and Deletions Уметања и брисања
Insertions: Уметања:
Interface Интерфејс
Interface language Језик интерфејса
Inside: Унутра:
Left border Лева ивица
Left Margin Лева маргина
Legal style numbering Правно нумерисање
Link level to style: Повежи ниво са стилом:
List Definition Дефиниција листе
Mark grammar errors as you type Означавај граматичке грешке током куцања
Measurement units: Јединице мере:
Millimeters Милиметри
New comments and tracked changes are recorded under this name and these initials. Нови коментари и праћене измене бележе се под овим именом и иницијалима.
New word Нова реч
No words yet. Add to Dictionary on a misspelled word puts it here. Још нема речи. Додај у речник на погрешно написаној речи ставља је овде.
Mirror indents Пресликај увлачења
Nothing Ништа
Number alignment: Поравнање броја:
Number format Формат броја
Number style for this level: Стил броја за овај ниво:
Outside border Спољна ивица
Overtype Преписивање
Overtype (typing replaces the characters after the caret) Преписивање (откуцано замењује знакове после курсора)
Page display Приказ стране
Paragraph marks Ознаке пасуса
Personalize Персонализација
Picas Пике
Points Тачке
Print the page color Штампај боју стране
Printing Штампање
Proofing language Језик провере писања
Recent documents to show: Број недавних докумената за приказ:
Remove Уклони
Outside: Споља:
Reopen Поново отвори
Restart list after: Поново почни листу после:
@ -167,6 +200,7 @@
Paste Clipboard Item Налепи ставку из оставе
Preview Преглед
Remove Password Уклони лозинку
Save documents Чување докумената
Shadow, glow and soft edges Сенка, сјај и меке ивице
Shape Effects Ефекти облика
Show all revisions inline Прикажи све измене у тексту
@ -175,7 +209,6 @@
Distribute spacing Распореди размак
Offset (pt): Помак (пт):
Phonetic Guide Фонетски водич
Remove Уклони
Right (vertical) Десно (вертикално)
Ruby text: Руби текст:
Show documents with their colours inverted, like View › Switch Modes. Saving, printing and PDFs are unchanged. Прикажи документе са инвертованим бојама, као код Приказ › Промени режим. Чување, штампање и PDF нису погођени.
@ -203,6 +236,11 @@
Border Painter Копирање ивица
Border\nPainter Копирање\nивица
Borders Ивице
Show the mini toolbar when text is selected Прикажи мини траку са алаткама при избору текста
Spaces Размаци
Spelling Правопис
Spelling and grammar are checked in English (United States). Правопис и граматика се проверавају на енглеском (Сједињене Државе).
Tab characters Знакови табулатора
Tab stop position: Положај табулатора:
Default tab stops: Подразумевани табулатори:
Tab stops to be cleared: Табулатори за брисање:
@ -213,6 +251,10 @@
Set Постави
Page Border Ивица стране
Apply to: Примени на:
These marks show even when Show/Hide ¶ is off. Ове ознаке се приказују и када је Прикажи/сакриј ¶ искључено.
Typing replaces selected text Откуцано замењује изабрани текст
Update fields before printing Ажурирај поља пре штампања
Use the Insert key to switch overtype on and off Тастер Insert укључује и искључује преписивање
Whole document Цео документ
This section Овај одељак
Fill Испуна

Can't render this file because it contains an unexpected character in line 11 and column 25.

View File

@ -1828,3 +1828,47 @@
Turkish Türkçe
Ukrainian Ukraynaca
{language} is now the document's default language. {language} artık belgenin varsayılan dili.
# File > Options panes (#485).
As you type Yazarken
AutoSave writes your changes to the file a moment after you stop typing. Otomatik kaydetme, yazmayı bıraktıktan kısa süre sonra değişikliklerinizi dosyaya yazar.
Centimeters Santimetre
Check spelling as you type Yazarken yazımı denetle
Click to switch back to inserting Eklemeye geri dönmek için tıklayın
Custom dictionary Özel sözlük
Formatting marks Biçimlendirme işaretleri
Hidden text Gizli metin
Ignore web and file addresses Web ve dosya adreslerini yoksay
Ignore words in capitals Büyük harfli sözcükleri yoksay
Ignore words with numbers Rakam içeren sözcükleri yoksay
Inches İnç
Initials: Baş harfler:
Interface Arayüz
Interface language Arayüz dili
Mark grammar errors as you type Yazarken dil bilgisi hatalarını işaretle
Measurement units: Ölçü birimleri:
Millimeters Milimetre
New comments and tracked changes are recorded under this name and these initials. Yeni açıklamalar ve izlenen değişiklikler bu ad ve baş harflerle kaydedilir.
New word Yeni sözcük
No words yet. Add to Dictionary on a misspelled word puts it here. Henüz sözcük yok. Yanlış yazılmış bir sözcükte «Sözlüğe ekle» onu buraya koyar.
Overtype Üzerine yaz
Overtype (typing replaces the characters after the caret) Üzerine yazma (yazılan metin imleçten sonraki karakterlerin yerine geçer)
Page display Sayfa görünümü
Paragraph marks Paragraf işaretleri
Personalize Kişiselleştir
Picas Pika
Points Punto
Print the page color Sayfa rengini yazdır
Printing Yazdırma
Proofing language Yazım denetimi dili
Recent documents to show: Gösterilecek son belge sayısı:
Save documents Belgeleri kaydet
Show the mini toolbar when text is selected Metin seçildiğinde mini araç çubuğunu göster
Spaces Boşluklar
Spelling Yazım
Spelling and grammar are checked in English (United States). Yazım ve dil bilgisi İngilizce (Amerika Birleşik Devletleri) için denetlenir.
Tab characters Sekme karakterleri
These marks show even when Show/Hide ¶ is off. Bu işaretler, Göster/Gizle ¶ kapalıyken de görünür.
Typing replaces selected text Yazmak seçili metnin yerine geçer
Update fields before printing Yazdırmadan önce alanları güncelle
Use the Insert key to switch overtype on and off Üzerine yazmayı açıp kapatmak için Insert tuşunu kullan

Can't render this file because it has a wrong number of fields in line 18.

View File

@ -60,12 +60,14 @@
Arrange\nAll Упорядкувати\nвсі
Arrow Стрілка
Artistic Effects Художні ефекти
As you type Під час введення
Asian Typography Азійська типографіка
Author Автор
AutoCorrect Options Параметри автовиправлення
AutoFit Добір розміру
AutoFit Contents Добір розміру за вмістом
AutoFit Window Добір розміру за шириною вікна
AutoSave writes your changes to the file a moment after you stop typing. Автозбереження записує зміни у файл невдовзі після того, як ви припините вводити текст.
Automatic Автоматично
Automatic ({language}) Автоматично ({language})
Automatic follows your system's language. Menus and commands change; your documents don't. Автоматичний режим використовує мову вашої системи. Змінюються меню й команди, а вміст документів залишається незмінним.
@ -78,34 +80,66 @@
Balloons Виноски
Bullet Маркер
By author За автором
Centimeters Сантиметри
Changed lines: Змінені рядки:
Check spelling as you type Перевіряти правопис під час введення
Click level to modify: Виберіть рівень для змінення:
Click to switch back to inserting Клацніть, щоб повернутися до вставлення
Color only Лише колір
Custom dictionary Власний словник
Define New Multilevel List Визначити новий багаторівневий список
Define New Multilevel List… Визначити новий багаторівневий список…
Deletions: Видалення:
Enter formatting for number: Вигляд номера:
Follow number with: Після номера:
Formatting Форматування
Formatting marks Знаки форматування
Hidden text Прихований текст
Ignore web and file addresses Пропускати веб-адреси та шляхи до файлів
Ignore words in capitals Пропускати слова з великих літер
Ignore words with numbers Пропускати слова з цифрами
Inches Дюйми
Formatting exceptions Винятки форматування
Include level number from: Додати номер рівня:
Initials: Ініціали:
Ink Рукописні примітки
Insertions and Deletions Вставлення й видалення
Insertions: Вставлення:
Interface Інтерфейс
Interface language Мова інтерфейсу
Inside: Усередині:
Left border Ліва межа
Legal style numbering Юридична нумерація
Link level to style: Зв'язати рівень зі стилем:
List Definition Визначення списку
Mark grammar errors as you type Позначати граматичні помилки під час введення
Measurement units: Одиниці виміру:
Millimeters Міліметри
New comments and tracked changes are recorded under this name and these initials. Нові коментарі та відстежувані зміни записуються під цим іменем і цими ініціалами.
New word Нове слово
No words yet. Add to Dictionary on a misspelled word puts it here. Слів ще немає. «Додати до словника» для слова з помилкою додає його сюди.
Mirror indents Дзеркальні відступи
Nothing Нічого
Number alignment: Вирівнювання номера:
Number format Формат номера
Number style for this level: Стиль номера для цього рівня:
Outside border Зовнішня межа
Overtype Заміна
Overtype (typing replaces the characters after the caret) Заміна (введений текст замінює символи після курсора)
Page display Відображення сторінки
Paragraph marks Знаки абзацу
Personalize Персоналізація
Picas Піки
Points Пункти
Print the page color Друкувати колір сторінки
Printing Друк
Proofing language Мова перевірки
Recent documents to show: Кількість останніх документів:
Remove Вилучити
Outside: Зовні:
Restart list after: Почати список знову після:
Right border Права межа
Save documents Збереження документів
Save the document to turn on AutoSave Збережіть документ, щоб увімкнути автозбереження
Save the document to turn on AutoSave (a file you have only opened isn't rewritten until you save it) Збережіть документ, щоб увімкнути автозбереження (файл, який ви лише відкрили, не перезаписується, доки ви його не збережете)
AutoText Автоматичний текст
@ -128,6 +162,11 @@
Border Painter Пензель меж
Border\nPainter Пензель\nмеж
Borders Межі
Show the mini toolbar when text is selected Показувати міні-панель інструментів під час виділення тексту
Spaces Пробіли
Spelling Правопис
Spelling and grammar are checked in English (United States). Правопис і граматика перевіряються англійською (США).
Tab characters Символи табуляції
Tab stop position: Позиція табуляції:
Default tab stops: Типові позиції табуляції:
Tab stops to be cleared: Позиції, які буде видалено:
@ -138,6 +177,10 @@
Set Встановити
Page Border Межа сторінки
Apply to: Застосувати до:
These marks show even when Show/Hide ¶ is off. Ці знаки відображаються, навіть коли «Показати/приховати ¶» вимкнено.
Typing replaces selected text Введення замінює виділений текст
Update fields before printing Оновлювати поля перед друком
Use the Insert key to switch overtype on and off Клавіша Insert вмикає та вимикає режим заміни
Whole document Увесь документ
This section Цей розділ
Fill Колір заливки
@ -281,7 +324,6 @@
Distribute spacing Розподілити інтервали
Offset (pt): Зсув (пункти):
Phonetic Guide Фонетична підказка
Remove Прибрати
Right (vertical) Праворуч (вертикально)
Ruby text: Текст рубі:
Search for templates Пошук шаблонів

Can't render this file because it contains an unexpected character in line 6 and column 31.

View File

@ -60,12 +60,14 @@
Arrange\nAll 全部重排
Arrow 箭头
Artistic Effects 艺术效果
As you type 键入时
Asian Typography 亚洲版式
Author 作者
AutoCorrect Options 自动更正选项
AutoFit 自动调整
AutoFit Contents 根据内容自动调整
AutoFit Window 根据窗口自动调整
AutoSave writes your changes to the file a moment after you stop typing. 停止键入片刻后,自动保存会将更改写入文件。
Automatic 自动
Automatic ({language}) 自动({language})
Automatic follows your system's language. Menus and commands change; your documents don't. “自动”跟随系统语言。菜单和命令会改变,文档内容不会改变。
@ -76,13 +78,17 @@
AutoSave failed: {error}. Save the document to turn AutoSave back on. 自动保存失败:{error}。重新保存文档后将恢复自动保存。
Balloons 批注框
Bottom Margin 下边距
Centimeters 厘米
Check spelling as you type 键入时检查拼写
Click a border between two cells to merge them. Esc stops. 单击两个单元格之间的边框以合并它们。按 Esc 结束。
Bullet 项目符号
By author 按作者
Changed lines: 修订行:
Click level to modify: 单击要修改的级别:
Click to switch back to inserting 单击以切换回插入模式
Color only 仅颜色
Compress punctuation at the start of a line 压缩行首标点
Custom dictionary 自定义词典
Dark page (white text on black) 深色页面(黑底白字)
Drag a rectangle to draw a table, or a line across cells to split them. Esc stops. 拖出矩形以绘制表格,或拖出穿过单元格的线条以拆分单元格。按 Esc 结束。
Draw Table 绘制表格
@ -93,11 +99,20 @@
Enter formatting for number: 输入编号的格式:
Follow number with: 编号之后:
Formatting 格式
Formatting marks 格式标记
Hidden text 隐藏文字
Ignore web and file addresses 忽略网址和文件路径
Ignore words in capitals 忽略全部大写的单词
Ignore words with numbers 忽略包含数字的单词
Inches 英寸
Formatting exceptions 格式例外
Include level number from: 包含的级别编号来自:
Initials: 缩写:
Ink 墨迹
Insertions and Deletions 插入和删除
Insertions: 插入内容:
Interface 界面
Interface language 界面语言
Inside: 内侧:
Left border 左框线
Left Margin 左边距
@ -106,14 +121,32 @@
Line breaking 断行
Link level to style: 将级别链接到样式:
List Definition 列表定义
Mark grammar errors as you type 键入时标记语法错误
Measurement units: 度量单位:
Millimeters 毫米
Mirror indents 对称缩进
More templates → 更多模板 →
New comments and tracked changes are recorded under this name and these initials. 新批注和修订将以此名称和缩写记录。
New word 新单词
No templates match your search. 没有与搜索匹配的模板。
No words yet. Add to Dictionary on a misspelled word puts it here. 还没有单词。对拼写错误的单词选择“添加到词典”会将其放在这里。
Nothing 无
Number alignment: 编号对齐方式:
Number format 编号格式
Number style for this level: 此级别的编号样式:
Outside border 外侧框线
Overtype 改写
Overtype (typing replaces the characters after the caret) 改写(键入的内容替换插入点后的字符)
Page display 页面显示
Paragraph marks 段落标记
Personalize 个性化
Picas 派卡
Points 磅
Print the page color 打印页面颜色
Printing 打印
Proofing language 校对语言
Recent documents to show: 显示的最近文档数:
Remove 移除
Outside: 外侧:
Reopen 重新打开
Restart list after: 重新开始列表的级别:
@ -124,9 +157,9 @@
Distribute spacing 间距均匀分布
Offset (pt): 偏移量(磅):
Phonetic Guide 拼音指南
Remove 删除
Right (vertical) 右对齐(竖排)
Ruby text: 拼音文字:
Save documents 保存文档
Search for templates 搜索模板
Show all revisions inline 以嵌入方式显示所有修订
Show documents with their colours inverted, like View › Switch Modes. Saving, printing and PDFs are unchanged. 像“视图 › 切换模式”一样以反转的颜色显示文档。保存、打印和 PDF 不受影响。
@ -154,6 +187,11 @@
Border Painter 边框刷
Border\nPainter 边框刷
Borders 边框
Show the mini toolbar when text is selected 选择文本时显示浮动工具栏
Spaces 空格
Spelling 拼写
Spelling and grammar are checked in English (United States). 拼写和语法按英语(美国)检查。
Tab characters 制表符
Tab stop position: 制表位位置:
Default tab stops: 默认制表位:
Tab stops to be cleared: 要清除的制表位:
@ -164,6 +202,10 @@
Set 设置
Page Border 页面边框线
Apply to: 应用于:
These marks show even when Show/Hide ¶ is off. 即使关闭“显示/隐藏 ¶”,这些标记也会显示。
Typing replaces selected text 键入内容替换所选文字
Update fields before printing 打印前更新域
Use the Insert key to switch overtype on and off 用 Insert 键切换改写模式
Whole document 整篇文档
This section 本节
Fill 填充

Can't render this file because it contains an unexpected character in line 7 and column 25.

View File

@ -49,12 +49,14 @@
Arrange\nAll 全部排列
Arrow 箭號
Artistic Effects 美術效果
As you type 輸入時
Asian Typography 亞洲版式
Author 作者
AutoCorrect Options 自動校正選項
AutoFit 自動調整
AutoFit Contents 依內容自動調整
AutoFit Window 依視窗自動調整
AutoSave writes your changes to the file a moment after you stop typing. 停止輸入片刻後,自動儲存會將變更寫入檔案。
Automatic 自動
Automatic ({language}) 自動({language})
Automatic follows your system's language. Menus and commands change; your documents don't. 「自動」會跟隨系統語言。功能表與命令會改變,文件內容不會改變。
@ -65,13 +67,17 @@
AutoSave failed: {error}. Save the document to turn AutoSave back on. 自動儲存失敗:{error}。重新儲存文件後將恢復自動儲存。
Balloons 註解方塊
Bottom Margin 下邊界
Centimeters 公分
Check spelling as you type 輸入時檢查拼字
Click a border between two cells to merge them. Esc stops. 按一下兩個儲存格之間的框線以合併它們。按 Esc 結束。
Bullet 項目符號
By author 依作者
Changed lines: 修訂行:
Click level to modify: 按一下要修改的層級:
Click to switch back to inserting 按一下以切換回插入模式
Color only 僅色彩
Compress punctuation at the start of a line 壓縮行首標點
Custom dictionary 自訂字典
Dark page (white text on black) 深色頁面(黑底白字)
Drag a rectangle to draw a table, or a line across cells to split them. Esc stops. 拖出矩形以繪製表格,或拖出穿過儲存格的線條以分割儲存格。按 Esc 結束。
Draw Table 繪製表格
@ -82,11 +88,20 @@
Enter formatting for number: 輸入編號的格式:
Follow number with: 編號之後:
Formatting 格式設定
Formatting marks 格式化標記
Hidden text 隱藏文字
Ignore web and file addresses 忽略網址和檔案路徑
Ignore words in capitals 忽略全部大寫的單字
Ignore words with numbers 忽略包含數字的單字
Inches 英寸
Formatting exceptions 格式例外
Include level number from: 包含的層級編號來自:
Initials: 縮寫:
Ink 筆跡
Insertions and Deletions 插入和刪除
Insertions: 插入內容:
Interface 介面
Interface language 介面語言
Inside: 內側:
Left border 左框線
Left Margin 左邊界
@ -95,14 +110,32 @@
Line breaking 斷行
Link level to style: 將層級連結到樣式:
List Definition 清單定義
Mark grammar errors as you type 輸入時標示文法錯誤
Measurement units: 度量單位:
Millimeters 公釐
Mirror indents 對稱縮排
More templates → 更多範本 →
New comments and tracked changes are recorded under this name and these initials. 新的註解和追蹤修訂會以此名稱和縮寫記錄。
New word 新單字
No templates match your search. 沒有符合搜尋的範本。
No words yet. Add to Dictionary on a misspelled word puts it here. 還沒有單字。對拼字錯誤的單字選擇「新增至字典」會將它放在這裡。
Nothing 無
Number alignment: 編號對齊方式:
Number format 編號格式
Number style for this level: 此層級的編號樣式:
Outside border 外側框線
Overtype 取代
Overtype (typing replaces the characters after the caret) 取代(輸入的內容會取代插入點後的字元)
Page display 頁面顯示
Paragraph marks 段落標記
Personalize 個人化
Picas 派卡
Points 點
Print the page color 列印頁面色彩
Printing 列印
Proofing language 校訂語言
Recent documents to show: 顯示的最近文件數:
Remove 移除
Outside: 外側:
Reopen 重新開啟
Restart list after: 重新開始清單的層級:
@ -113,9 +146,9 @@
Distribute spacing 間距平均分散
Offset (pt): 位移(點):
Phonetic Guide 注音標示
Remove 移除
Right (vertical) 靠右(直書)
Ruby text: 注音文字:
Save documents 儲存文件
Search for templates 搜尋範本
Show all revisions inline 以內嵌方式顯示所有修訂
Show documents with their colours inverted, like View › Switch Modes. Saving, printing and PDFs are unchanged. 像「檢視 › 切換模式」一樣以反轉的色彩顯示文件。儲存、列印和 PDF 不受影響。
@ -143,6 +176,11 @@
Border Painter 框線複製
Border\nPainter 框線複製
Borders 框線
Show the mini toolbar when text is selected 選取文字時顯示迷你工具列
Spaces 空格
Spelling 拼字
Spelling and grammar are checked in English (United States). 拼字和文法依英文(美國)檢查。
Tab characters 定位字元
Tab stop position: 定位點位置:
Default tab stops: 預設定位點間距:
Tab stops to be cleared: 要清除的定位點:
@ -153,6 +191,10 @@
Set 設定
Page Border 頁面框線設定
Apply to: 套用至:
These marks show even when Show/Hide ¶ is off. 即使關閉「顯示/隱藏 ¶」,這些標記也會顯示。
Typing replaces selected text 輸入的內容取代選取的文字
Update fields before printing 列印前更新功能變數
Use the Insert key to switch overtype on and off 使用 Insert 鍵切換取代模式
Whole document 整份文件
This section 本節
Fill 填滿

Can't render this file because it has a wrong number of fields in line 4.

View File

@ -271,6 +271,13 @@ pub fn canvas_events(app: &mut WordApp, ctx: &egui::Context) {
if key == Key::Escape && crate::table_pen::stop(app) {
continue;
}
// File › Options › Advanced: the Insert key switches overtype only when asked to.
if key == Key::Insert && modifiers.is_none() {
if app.session.prefs.insert_key_overtype {
let _ = app.run("text.overtype", json!({}));
}
continue;
}
if crate::objects::key(app, key, modifiers) {
continue;
}

View File

@ -43,6 +43,7 @@ pub mod keys;
pub mod keytips;
pub mod mini_toolbar;
pub mod objects;
pub mod options;
pub mod panes;
pub mod paste_picture;
pub mod previews;
@ -151,8 +152,28 @@ pub struct UiState {
/// Entries that aren't pens are dropped on reading.
#[serde(deserialize_with = "wordcraft_engine::cmd::draw::lenient_pens")]
pub ink_pens: Vec<wordcraft_engine::cmd::draw::CustomPen>,
/// File › Options › General: initials for comments; empty = the user name's. The session
/// owns them while running, like `author`.
pub initials: String,
/// The File › Options pane last shown ([`wordcraft_engine::cmd::options::PANES`]).
pub options_pane: String,
/// File › Options › Advanced: how many recent documents File › Home and Open list (0–50).
pub recent_count: usize,
/// File › Options › General: show the mini toolbar over a selection.
pub mini_toolbar: bool,
/// File › Options › Display: print the page colour (off, as in Word).
pub print_backgrounds: bool,
/// File › Options › Display: update fields (dates, page numbers, SEQ…) before printing.
pub update_fields_before_print: bool,
/// The custom dictionary (File › Options › Proofing, Add to Dictionary), kept between runs.
pub dictionary: Vec<String>,
}
/// Most recent documents kept (File › Options › Advanced offers 0–50, as Word does).
pub const MAX_RECENT: usize = 50;
/// Most custom dictionary words read back from `ui.json`.
pub const MAX_DICTIONARY: usize = 10_000;
impl Default for UiState {
fn default() -> Self {
UiState {
@ -180,10 +201,32 @@ impl Default for UiState {
ribbon: wordcraft_engine::RibbonLayout::default(),
caption_labels: Vec::new(),
ink_pens: Vec::new(),
initials: String::new(),
options_pane: "general".into(),
recent_count: 12,
mini_toolbar: true,
print_backgrounds: false,
update_fields_before_print: false,
dictionary: Vec::new(),
}
}
}
impl UiState {
/// Read `ui.json` leniently: a setting with a value of the wrong type (or a newer or older
/// version's idea of it) falls back to its default instead of discarding every setting.
/// `None` only when the file isn't a JSON object.
pub fn from_json(bytes: &[u8]) -> Option<UiState> {
let v: Value = serde_json::from_slice(bytes).ok()?;
v.is_object().then(|| options::lenient(&v))
}
/// How many recent documents to list.
pub fn recent_shown(&self) -> usize {
self.recent_count.min(MAX_RECENT)
}
}
/// The application.
pub struct WordApp {
pub session: Session,
@ -323,6 +366,8 @@ impl WordApp {
pub fn prefs(&self) -> UiState {
let mut ui = self.ui.clone();
ui.author = self.session.author.clone();
ui.initials = self.session.initials.clone();
ui.dictionary = wordcraft_engine::proof::user_dictionary();
ui.editing = self.session.prefs.clone();
ui.read_aloud_rate = self.session.read_aloud.rate();
ui.read_aloud_skip_citations = self.session.read_aloud.skip_citations;
@ -346,6 +391,13 @@ impl WordApp {
if !author.trim().is_empty() {
self.session.author = author;
}
self.session.initials = std::mem::take(&mut self.ui.initials).chars().take(wordcraft_engine::cmd::options::MAX_INITIALS_CHARS).collect();
for w in std::mem::take(&mut self.ui.dictionary).iter().take(MAX_DICTIONARY) {
let w = w.trim();
if !w.is_empty() && w.chars().count() <= wordcraft_engine::cmd::options::MAX_WORD_CHARS {
wordcraft_engine::proof::add_word(w);
}
}
self.session.prefs = std::mem::take(&mut self.ui.editing);
self.session.read_aloud.set_rate(self.ui.read_aloud_rate);
self.session.read_aloud.skip_citations = self.ui.read_aloud_skip_citations;
@ -563,6 +615,7 @@ impl WordApp {
}
fn after_command(&mut self, id: &str) {
options::sync(self);
self.canvas.caret_visible_since = now_ms();
if !id.starts_with("view.") && !id.starts_with("document.") && !id.starts_with("format.state") {
self.canvas.scroll_to_caret = true;
@ -575,7 +628,7 @@ impl WordApp {
{
self.ui.recent.retain(|r| *r != p);
self.ui.recent.insert(0, p);
self.ui.recent.truncate(12);
self.ui.recent.truncate(MAX_RECENT);
}
if !self.session.status.is_empty() {
let s = std::mem::take(&mut self.session.status);
@ -599,6 +652,9 @@ impl WordApp {
"options" => {
self.ui.backstage = true;
self.ui.backstage_page = "options".into();
if let Some(pane) = req.get("pane").and_then(Value::as_str) {
self.ui.options_pane = pane.into();
}
}
"pasteSpecial" => self.dialog = Some(dialogs::Dialog::paste_special(self, req)),
other => self.dialog = dialogs::Dialog::open(other, self),
@ -633,6 +689,9 @@ impl WordApp {
if let Some(pg) = s("page") {
self.ui.backstage_page = pg.into();
}
if let Some(pane) = s("pane") {
self.ui.options_pane = pane.into();
}
json!({"backstage": self.ui.backstage})
}
"ui.dialog" => {
@ -745,7 +804,18 @@ impl WordApp {
if self.services.print.is_none() {
return Err("printing to the system print dialog isn't available here; export a PDF instead".into());
}
let result = wordcraft_engine::io::save_bytes("document.pdf", &self.session.doc)
// File › Options › Display: fields first, and the page colour only when asked for.
if self.ui.update_fields_before_print
&& let Err(e) = self.session.run("references.updateFields", &json!({}))
{
log::warn!("updating fields before printing: {e}");
}
let without_colour = (!self.ui.print_backgrounds && self.session.doc.settings.page_color.is_some()).then(|| {
let mut d = self.session.doc.clone();
d.settings.page_color = None;
d
});
let result = wordcraft_engine::io::save_bytes("document.pdf", without_colour.as_ref().unwrap_or(&self.session.doc))
.and_then(|bytes| self.services.print.as_ref().map_or(Ok(()), |print| print(&bytes)).map(|()| bytes.len()));
match result {
Ok(len) => {
@ -854,6 +924,7 @@ impl WordApp {
pub fn logic(&mut self, ctx: &egui::Context) {
let lang = i18n::Lang::from_pref(&self.ui.language);
i18n::set_current(lang);
options::sync(self);
// Chinese text wants the Chinese face before the Japanese one (one glyph style per line).
// An installed CJK font is read only once CJK text is shown (#241): never for an English
// interface that doesn't open the language list.

View File

@ -0,0 +1,521 @@
//! File › Options (#485): a list of panes on the left (General, Display, Proofing, Save,
//! Language, Accessibility, Advanced, Agents), the chosen pane's settings on the right. Only
//! settings WordCraft carries out are shown. Each pane is one function below, so a new setting
//! goes into its pane's function. Settings agents can change too go through `file.options`; the
//! rest live in [`crate::UiState`] (saved in `ui.json`).
use std::cell::Cell;
use egui::{RichText, Ui, vec2};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::{Map, Value, json};
use wordcraft_engine::cmd::options::{MAX_WORD_CHARS, PANES};
use wordcraft_geom::Unit;
use crate::WordApp;
use crate::theme::{Tokens, semibold};
thread_local! {
// The unit dialogs and the ribbon show lengths in: File › Options › Advanced, copied from the
// session each frame so dialog code without the app can read it.
static UNIT: Cell<Unit> = const { Cell::new(Unit::Inches) };
}
/// The unit lengths are shown in (File › Options › Advanced › Measurement units).
pub fn unit() -> Unit {
UNIT.get()
}
/// Copy the session's settings the UI reads without the app.
pub(crate) fn sync(app: &WordApp) {
UNIT.set(app.session.prefs.units);
}
/// The units offered, with their names.
pub const UNITS: [(Unit, &str); 5] = [
(Unit::Inches, "Inches"),
(Unit::Centimeters, "Centimeters"),
(Unit::Millimeters, "Millimeters"),
(Unit::Points, "Points"),
(Unit::Picas, "Picas"),
];
/// The pane names, in [`PANES`] order.
fn pane_label(id: &str) -> &'static str {
match id {
"display" => "Display",
"proofing" => "Proofing",
"save" => "Save",
"language" => "Language",
"accessibility" => "Accessibility",
"advanced" => "Advanced",
"agents" => "Agents",
_ => "General",
}
}
/// The Options page.
pub fn page(app: &mut WordApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
ui.label(RichText::new(tl!("Options")).font(semibold(26.0)));
ui.add_space(16.0);
let current = if PANES.contains(&app.ui.options_pane.as_str()) { app.ui.options_pane.clone() } else { "general".to_string() };
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
ui.set_width(170.0);
for id in PANES {
let label = tl!(pane_label(id));
let active = current == id;
let (r, resp) = ui.allocate_exact_size(vec2(170.0, 30.0), egui::Sense::click());
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, active, label));
if active {
ui.painter().rect_filled(r, 4.0, t.checked);
} else if resp.hovered() {
ui.painter().rect_filled(r, 4.0, t.hover);
}
let font = if active { semibold(14.0) } else { crate::theme::regular(14.0) };
ui.painter().text(egui::pos2(r.min.x + 12.0, r.center().y), egui::Align2::LEFT_CENTER, label, font, t.text);
if resp.clicked() {
app.ui.options_pane = id.to_string();
}
}
});
ui.add_space(12.0);
let (line, _) = ui.allocate_exact_size(vec2(1.0, 420.0), egui::Sense::hover());
ui.painter().rect_filled(line, 0.0, t.border);
ui.add_space(20.0);
ui.vertical(|ui| {
ui.set_max_width(640.0);
match current.as_str() {
"display" => display(app, ui),
"proofing" => proofing(app, ui),
"save" => save(app, ui),
"language" => language(app, ui),
"accessibility" => accessibility(app, ui),
"advanced" => advanced(app, ui),
"agents" => agents(ui),
_ => general(app, ui),
}
});
});
}
fn section(ui: &mut Ui, title: &str) {
ui.add_space(6.0);
ui.label(RichText::new(tl!(title)).font(semibold(15.0)));
ui.add_space(2.0);
}
fn note(ui: &mut Ui, text: &str) {
ui.label(RichText::new(tl!(text)).small().weak());
}
/// Change settings through `file.options` (the command agents use).
fn set(app: &mut WordApp, params: Value) {
if let Err(e) = app.run("file.options", params) {
app.status(e);
}
}
/// A checkbox for a `file.options` on/off setting.
fn flag(app: &mut WordApp, ui: &mut Ui, label: &str, key: &str, value: bool) {
let mut v = value;
if ui.checkbox(&mut v, tl!(label)).changed() {
set(app, json!({ key: v }));
}
}
fn general(app: &mut WordApp, ui: &mut Ui) {
section(ui, "Interface");
theme_picker(app, ui);
interface_size_picker(app, ui);
ui.checkbox(&mut app.ui.mini_toolbar, tl!("Show the mini toolbar when text is selected"));
let mut dark_page = app.session.view.dark_mode;
if ui
.checkbox(&mut dark_page, tl!("Dark page (white text on black)"))
.on_hover_text(tl!("Show documents with their colours inverted, like View › Switch Modes. Saving, printing and PDFs are unchanged."))
.changed()
{
let _ = app.run("view.darkMode", json!({"value": dark_page}));
}
ui.checkbox(&mut app.ui.show_discord, tl!("Show the community button in the title bar"));
ui.add_space(8.0);
section(ui, "Personalize");
egui::Grid::new("options_user").num_columns(2).spacing(vec2(8.0, 6.0)).show(ui, |ui| {
ui.label(tl!("User name:"));
let mut n = app.session.author.clone();
if ui.add(egui::TextEdit::singleline(&mut n).desired_width(260.0)).changed() {
app.session.author = n;
}
ui.end_row();
ui.label(tl!("Initials:"));
let mut i = app.session.initials.clone();
let hint = app.session.user_initials();
if ui
.add(
egui::TextEdit::singleline(&mut i).desired_width(80.0).char_limit(wordcraft_engine::cmd::options::MAX_INITIALS_CHARS).hint_text(hint),
)
.changed()
{
set(app, json!({"initials": i}));
}
ui.end_row();
});
note(ui, "New comments and tracked changes are recorded under this name and these initials.");
}
fn display(app: &mut WordApp, ui: &mut Ui) {
section(ui, "Formatting marks");
note(ui, "These marks show even when Show/Hide ¶ is off.");
let m = app.session.prefs.marks;
for (label, key, on) in [
("Tab characters", "tabs", m.tabs),
("Spaces", "spaces", m.spaces),
("Paragraph marks", "paragraphs", m.paragraphs),
("Hidden text", "hidden", m.hidden),
] {
let mut v = on;
if ui.checkbox(&mut v, tl!(label)).changed() {
set(app, json!({"marks": { key: v }}));
}
}
let mut marks = app.session.view.marks;
if ui.checkbox(&mut marks, tl!("Show all formatting marks")).changed() {
let _ = app.run("view.marks", json!({"value": marks}));
}
ui.add_space(8.0);
section(ui, "Page display");
let mut ruler = app.session.view.ruler;
if ui.checkbox(&mut ruler, tl!("Show rulers")).changed() {
let _ = app.run("view.ruler", json!({"value": ruler}));
}
ui.add_space(8.0);
section(ui, "Printing");
ui.checkbox(&mut app.ui.print_backgrounds, tl!("Print the page color"));
ui.checkbox(&mut app.ui.update_fields_before_print, tl!("Update fields before printing"));
}
fn proofing(app: &mut WordApp, ui: &mut Ui) {
// #476 (AutoCorrect Options…) goes here, at the top.
section(ui, "Spelling");
let pr = app.session.prefs.clone();
flag(app, ui, "Ignore words in capitals", "ignoreUppercase", pr.ignore_uppercase);
flag(app, ui, "Ignore words with numbers", "ignoreNumbers", pr.ignore_numbers);
flag(app, ui, "Ignore web and file addresses", "ignoreInternet", pr.ignore_internet);
ui.add_space(8.0);
section(ui, "As you type");
let spelling = app.session.view.proofing;
flag(app, ui, "Check spelling as you type", "checkSpelling", spelling);
ui.add_enabled_ui(app.session.view.proofing, |ui| flag(app, ui, "Mark grammar errors as you type", "markGrammar", pr.mark_grammar));
ui.add_space(8.0);
section(ui, "Custom dictionary");
custom_dictionary(app, ui);
}
/// The custom dictionary: the words Add to Dictionary kept, each with a Remove button, and a
/// field to add one.
fn custom_dictionary(app: &mut WordApp, ui: &mut Ui) {
let words = wordcraft_engine::proof::user_dictionary();
if words.is_empty() {
note(ui, "No words yet. Add to Dictionary on a misspelled word puts it here.");
}
egui::ScrollArea::vertical().id_salt("options_dictionary").max_height(180.0).show(ui, |ui| {
for w in &words {
ui.horizontal(|ui| {
ui.allocate_ui_with_layout(vec2(220.0, 20.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
ui.set_min_width(220.0);
ui.add(egui::Label::new(w.as_str()).truncate());
});
if ui.small_button(tl!("Remove")).clicked() {
set(app, json!({"removeWords": [w]}));
}
});
}
});
let id = egui::Id::new("options_dictionary_new");
let mut word = ui.data(|d| d.get_temp::<String>(id)).unwrap_or_default();
ui.horizontal(|ui| {
let r = ui.add(egui::TextEdit::singleline(&mut word).desired_width(220.0).char_limit(MAX_WORD_CHARS).hint_text(tl!("New word")));
let enter = r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
let w = word.trim().to_string();
let ok = !w.is_empty() && !w.chars().any(char::is_whitespace);
if (ui.add_enabled(ok, egui::Button::new(tl!("Add"))).clicked() || (enter && ok)) && app.run("file.options", json!({"addWords": [w]})).is_ok()
{
word.clear();
}
});
ui.data_mut(|d| d.insert_temp(id, word));
}
fn save(app: &mut WordApp, ui: &mut Ui) {
section(ui, "Save documents");
// The browser can't write to the user's files, so AutoSave can't be turned on there (#176).
let browser = app.autosave_block() == Some(crate::AutoSaveBlock::Browser);
let mut autosave = app.autosave && !browser;
let r = ui.add_enabled(!browser, egui::Checkbox::new(&mut autosave, tl!("AutoSave documents you have saved in WordCraft")));
if browser {
r.on_disabled_hover_text(crate::AutoSaveBlock::Browser.reason());
} else if r.changed() {
app.autosave = autosave;
app.session.autosave = autosave;
}
note(ui, "AutoSave writes your changes to the file a moment after you stop typing.");
}
fn language(app: &mut WordApp, ui: &mut Ui) {
section(ui, "Interface language");
language_picker(app, ui);
// #414 (detect the document's language) goes here.
ui.add_space(8.0);
section(ui, "Proofing language");
note(ui, "Spelling and grammar are checked in English (United States).");
}
fn accessibility(app: &mut WordApp, ui: &mut Ui) {
use wordcraft_engine::speech::{MAX_RATE, MIN_RATE};
section(ui, "Read Aloud");
let mut rate = app.session.read_aloud.rate();
ui.horizontal(|ui| {
ui.label(tl!("Speed"));
let r = ui.add(egui::Slider::new(&mut rate, MIN_RATE..=MAX_RATE).step_by(0.05).custom_formatter(|v, _| format!("{v:.2}×")));
if r.changed() {
let _ = app.run("readAloud.speed", json!({"value": rate}));
}
});
let mut skip = app.session.read_aloud.skip_citations;
if ui.checkbox(&mut skip, tl!("Skip citations & bibliography")).changed() {
let _ = app.run("readAloud.skipCitations", json!({"value": skip}));
}
}
fn advanced(app: &mut WordApp, ui: &mut Ui) {
section(ui, "Editing");
let pr = app.session.prefs.clone();
flag(app, ui, "Typing replaces selected text", "typingReplacesSelection", pr.typing_replaces_selection);
flag(app, ui, "Use the Insert key to switch overtype on and off", "insertKeyOvertype", pr.insert_key_overtype);
flag(app, ui, "Overtype (typing replaces the characters after the caret)", "overtype", pr.overtype);
ui.add_space(8.0);
section(ui, "Display");
egui::Grid::new("options_display").num_columns(2).spacing(vec2(8.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Recent documents to show:"));
let mut n = app.ui.recent_shown();
if ui.add(egui::DragValue::new(&mut n).range(0..=crate::MAX_RECENT).speed(0.2)).changed() {
app.ui.recent_count = n;
}
ui.end_row();
ui.label(tl!("Measurement units:"));
let name = UNITS.iter().find(|(u, _)| *u == pr.units).map_or("Inches", |(_, n)| n);
egui::ComboBox::from_id_salt("options_units").selected_text(tl!(name)).width(160.0).show_ui(ui, |ui| {
for (u, label) in UNITS {
if ui.selectable_label(pr.units == u, tl!(label)).clicked() {
set(app, json!({"units": u}));
}
}
});
ui.end_row();
});
ui.add_space(8.0);
section(ui, "Customize");
ui.horizontal(|ui| {
ui.label(tl!("Keyboard shortcuts:"));
if ui.button(tl!("Customize…")).clicked() {
let _ = app.run("tools.customizeKeyboard", json!({}));
}
});
ui.horizontal(|ui| {
ui.label(tl!("Ribbon and Quick Access Toolbar:"));
if ui.button(tl!("Customize…")).clicked() {
let _ = app.run("tools.customizeRibbon", json!({}));
}
});
}
fn agents(ui: &mut Ui) {
section(ui, "Agents");
ui.label(tl!("Every command is available to scripts and AI agents: run `wordcraft-cli mcp` for an MCP server, or start the app with `--control <port>` for the JSON control channel."));
}
/// Interface theme: Light, Dark, or follow the system's appearance (#115).
fn theme_picker(app: &mut WordApp, ui: &mut Ui) {
use crate::theme::Appearance;
ui.horizontal(|ui| {
ui.label(tl!("Interface theme:"));
egui::ComboBox::from_id_salt("interface_theme").selected_text(tl!(app.ui.theme.label())).width(220.0).show_ui(ui, |ui| {
for a in Appearance::ALL {
if ui.selectable_label(app.ui.theme == a, tl!(a.label())).clicked() {
let _ = app.run("ui.theme", json!({"value": a.code()}));
}
}
});
});
}
/// Interface size: 80–200 %, on top of the system's display scale (#475).
fn interface_size_picker(app: &mut WordApp, ui: &mut Ui) {
use crate::theme::InterfaceSize;
let current = app.ui.interface_size.percent();
ui.horizontal(|ui| {
ui.label(tl!("Interface size:"));
egui::ComboBox::from_id_salt("interface_size").selected_text(format!("{current}%")).width(220.0).show_ui(ui, |ui| {
for pct in InterfaceSize::PRESETS {
if ui.selectable_label(current == pct, format!("{pct}%")).clicked() {
let _ = app.run("ui.interfaceSize", json!({"percent": pct}));
}
}
});
});
ui.label(
RichText::new(tl!(
"Makes the ribbon, menus and dialogs larger or smaller, on top of your system's display scaling. Document zoom is separate."
))
.small()
.weak(),
);
}
/// Interface language: follow the system (the default) or pick one (#8).
fn language_picker(app: &mut WordApp, ui: &mut Ui) {
use crate::i18n::{AUTO, Lang};
// The language names are in their own scripts: fonts for them load from the next frame.
app.want_system_cjk = true;
let system = crate::i18n::system_lang();
let auto_label = crate::i18n::fmt(tl!("Automatic ({language})"), &[("language", system.name())]);
let current = if app.ui.language == AUTO { auto_label.clone() } else { Lang::from_pref(&app.ui.language).name().to_string() };
ui.horizontal(|ui| {
ui.label(tl!("Interface language:"));
egui::ComboBox::from_id_salt("interface_language").selected_text(current).width(220.0).show_ui(ui, |ui| {
if ui.selectable_label(app.ui.language == AUTO, auto_label.as_str()).clicked() {
let _ = app.run("ui.language", json!({"value": AUTO}));
}
for lang in Lang::all() {
if ui.selectable_label(app.ui.language == lang.code(), lang.name()).clicked() {
let _ = app.run("ui.language", json!({"value": lang.code()}));
}
}
});
});
ui.label(RichText::new(tl!("Automatic follows your system's language. Menus and commands change; your documents don't.")).small().weak());
}
/// Deserialize `input` over `T`'s defaults one setting at a time: a value of the wrong type is
/// dropped (its default stays) instead of failing the whole read. Objects nest (`editing`).
pub fn lenient<T: Serialize + DeserializeOwned + Default>(input: &Value) -> T {
let mut base = serde_json::to_value(T::default()).unwrap_or(Value::Null);
if base.is_object() {
overlay::<T>(&mut base, &mut Vec::new(), input, 0);
}
serde_json::from_value(base).unwrap_or_default()
}
/// The object at `path` inside `root`.
fn object_at<'a>(root: &'a mut Value, path: &[String]) -> Option<&'a mut Map<String, Value>> {
let mut v = root;
for k in path {
v = v.as_object_mut()?.get_mut(k)?;
}
v.as_object_mut()
}
fn overlay<T: DeserializeOwned>(root: &mut Value, path: &mut Vec<String>, input: &Value, depth: usize) {
let Some(map) = input.as_object() else { return };
for (k, v) in map {
let Some(parent) = object_at(root, path) else { return };
let old = parent.get(k).cloned();
if depth < 4 && v.is_object() && old.as_ref().is_some_and(Value::is_object) {
path.push(k.clone());
overlay::<T>(root, path, v, depth + 1);
path.pop();
continue;
}
parent.insert(k.clone(), v.clone());
if serde_json::from_value::<T>(root.clone()).is_err()
&& let Some(parent) = object_at(root, path)
{
match old {
Some(o) => parent.insert(k.clone(), o),
None => parent.remove(k),
};
}
}
}
#[cfg(test)]
mod tests {
use egui_kittest::kittest::Queryable;
use serde_json::json;
use wordcraft_geom::Unit;
use crate::theme::Appearance;
use crate::{Services, UiState, WordApp};
fn app() -> WordApp {
WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Services::default())
}
/// File › Options settings come back after a restart, and a `ui.json` with a wrong value in
/// one setting keeps every other setting (#485).
#[test]
fn options_survive_a_restart_and_junk_in_ui_json() {
let mut a = app();
a.run(
"file.options",
json!({"initials": "VK", "units": "centimeters", "typingReplacesSelection": false, "marks": {"tabs": true}, "ignoreNumbers": false, "addWords": ["zqxoptionsword"]}),
)
.unwrap();
a.ui.recent_count = 30;
a.ui.print_backgrounds = true;
let saved = serde_json::to_vec(&a.prefs()).unwrap();
let mut b = app();
b.apply_prefs(UiState::from_json(&saved).unwrap());
assert_eq!(b.session.prefs, a.session.prefs);
assert_eq!(b.session.prefs.units, Unit::Centimeters);
assert_eq!(b.session.initials, "VK");
assert_eq!((b.ui.recent_count, b.ui.print_backgrounds), (30, true));
assert!(wordcraft_engine::proof::user_dictionary().contains(&"zqxoptionsword".to_string()));
let junk = br#"{"theme": "dark", "recentCount": "lots", "printBackgrounds": 7, "optionsPane": ["x"], "dark": true,
"editing": {"units": "furlongs", "typingReplacesSelection": false, "marks": {"tabs": "yes", "spaces": true}}}"#;
let ui = UiState::from_json(junk).unwrap();
assert_eq!(ui.theme, Appearance::Dark);
assert_eq!((ui.recent_count, ui.print_backgrounds, ui.options_pane.as_str()), (12, false, "general"));
assert_eq!(ui.legacy_dark, Some(true));
assert_eq!(ui.editing.units, Unit::Inches);
assert!(!ui.editing.typing_replaces_selection);
assert!(ui.editing.marks.spaces && !ui.editing.marks.tabs);
assert!(UiState::from_json(b"[1, 2]").is_none() && UiState::from_json(b"not json").is_none());
}
/// `file.options` opens the page at a pane; the pane list switches panes, and their
/// checkboxes change the settings (#485).
#[test]
fn options_panes_open_and_change_settings() {
let mut a = app();
a.run("file.options", json!({"pane": "display"})).unwrap();
assert!(a.ui.backstage);
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
a,
);
h.run_steps(4);
h.get_by_label("Spaces").click();
h.run_steps(2);
assert!(h.state().session.prefs.marks.spaces, "Display › Spaces");
h.get_by_label("Proofing").click();
h.run_steps(4);
assert_eq!(h.state().ui.options_pane, "proofing");
h.get_by_label("Ignore words in capitals").click();
h.run_steps(2);
assert!(!h.state().session.prefs.ignore_uppercase);
h.get_by_label("Advanced").click();
h.run_steps(4);
h.get_by_label("Typing replaces selected text").click();
h.run_steps(2);
assert!(!h.state().session.prefs.typing_replaces_selection);
assert!(h.query_by_label("Measurement units:").is_some());
}
}

View File

@ -1038,12 +1038,15 @@ fn layout(app: &mut WordApp, ui: &mut Ui) {
ui.label("");
ui.label(egui::RichText::new(tl!("Spacing")).small().strong());
ui.end_row();
let mut l = rp.indent_left / 72.0;
// Indents in the File › Options unit.
let u = crate::options::unit();
let k = u.pt_per_unit();
let mut l = rp.indent_left / k;
let mut b = rp.space_before;
ui.horizontal(|ui| {
ui.label(egui::RichText::new(tl!("Left:")).small());
if ui.add(egui::DragValue::new(&mut l).speed(0.05).range(-11.0..=22.0).suffix("\"").max_decimals(2)).changed() {
let _ = app.run("para.indents", json!({"left": l * 72.0}));
if ui.add(egui::DragValue::new(&mut l).speed(3.6 / k).range(-792.0 / k..=1584.0 / k).suffix(u.suffix()).max_decimals(2)).changed() {
let _ = app.run("para.indents", json!({"left": l * k}));
}
});
ui.label("");
@ -1054,12 +1057,12 @@ fn layout(app: &mut WordApp, ui: &mut Ui) {
}
});
ui.end_row();
let mut r = rp.indent_right / 72.0;
let mut r = rp.indent_right / k;
let mut a = rp.space_after;
ui.horizontal(|ui| {
ui.label(egui::RichText::new(tl!("Right:")).small());
if ui.add(egui::DragValue::new(&mut r).speed(0.05).range(-11.0..=22.0).suffix("\"").max_decimals(2)).changed() {
let _ = app.run("para.indents", json!({"right": r * 72.0}));
if ui.add(egui::DragValue::new(&mut r).speed(3.6 / k).range(-792.0 / k..=1584.0 / k).suffix(u.suffix()).max_decimals(2)).changed() {
let _ = app.run("para.indents", json!({"right": r * k}));
}
});
ui.label("");
@ -1981,7 +1984,7 @@ fn cell_size_boxes(ui: &mut Ui, app: &mut WordApp) {
}
_ => None,
});
let unit = wordcraft_geom::Unit::default();
let unit = crate::options::unit();
let k = unit.pt_per_unit();
let h0 = row.height.or(laid.map(|x| x.h)).unwrap_or(0.0);
let w0 = laid.map(|x| x.w).or(stored_w).unwrap_or(0.0);

View File

@ -1,6 +1,6 @@
# Where WordCraft falls short of Word
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (#18: the document canvas is exposed to screen readers, #488; previously trivial: #24 narrowed: Customize Keyboard and Customize Ribbon landed; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (File › Options panes landed, #485; previously trivial: #24 narrowed: Customize Keyboard and Customize Ribbon landed; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
The work list. One entry per shortfall, ranked by how much it stops a Word user from switching
(**B** = blocks beta). Each says what's missing, the evidence, who it hurts, an Opus 5.5 agent
@ -17,7 +17,7 @@ first (`gh pr list`), and update this file and the parity doc when a gap closes.
| 4 | **Pagination differs from Word**: Aptos has no metric-matched substitute; legacy compatibility options ignored; no page-by-page comparison harness | B | [layout-parity.md](layout-parity.md); [typing-parity.md](typing-parity.md) Known gaps | Page counts and line breaks differ, which matters for forms, legal and academic work | 30–50 + owner (font) | [layout-parity.md](layout-parity.md) |
| 5 | **Content controls** landed (#381): `w:sdt` kept, shown and edited (Developer tab, Design Mode, Properties). Move tracking landed (#400): `w:moveFrom`/`w:moveTo` with their named ranges round-trip, a tracked cut and paste of a paragraph or sentence records a move, either end accepts/rejects both, green double strike/underline markup and "Moved from/to" balloons; still missing: drag-and-drop moves (no text drag yet) and move options in Track Changes Options. Formatting revisions landed (#41): `w:rPrChange`/`w:pPrChange`/`w:tblPrChange`/`w:trPrChange`/`w:tcPrChange`/`w:sectPrChange`/`w:numberingChange` round-trip, are recorded while tracking, accepted/rejected and shown in balloons; still missing: tracked style-definition changes and table/section changes made in WordCraft | B | Issues #41, #400; source grep | Legal and editorial review workflows; templates with form controls | 5–8 | [file-format-parity.md](file-format-parity.md) |
| 6 | **No native printing** on desktop: File › Print opens the PDF in the system viewer, which prints (#286); no print dialog of our own (printer, copies, ranges); web prints through the browser (#209) | B | `ui.print` → `apps/wordcraft/src/print.rs`; issue #15 | Everyone who prints | 5–8 | [hardware-parity.md](hardware-parity.md) |
| 7 | **Dialog depth**: 52 modal dialogs vs Word's ~100 (Options panes…) | B | `Dialog` enum in `crates/ui-egui/src/dialogs.rs` | Power users reach for dialogs the ribbon alone doesn't cover | 11–19 | [ui-parity.md](ui-parity.md) |
| 7 | **Dialog depth**: 52 modal dialogs vs Word's ~100 | B | `Dialog` enum in `crates/ui-egui/src/dialogs.rs` | Power users reach for dialogs the ribbon alone doesn't cover | 11–19 | [ui-parity.md](ui-parity.md) |
| 8 | **Proofing is English only**; no language per run, no dictionaries for other languages | | `crates/proof` has one dictionary; Word ships 120 proofing tools; issues #25, #40, #100 | Every non-English writer | 25–45 | [localization-parity.md](localization-parity.md) |
| 9 | **Password-protected files** can't be opened or saved | | #55; `docbin` detects and refuses RC4/XOR | Corporate and legal users | 6–10 | [file-format-parity.md](file-format-parity.md) |
| 10 | **Objects**: no group (#267 open), text in a freely rotated text box (free rotation and flips landed, #332; text direction, alignment and linked text boxes landed, #369; contour wrap and wrap side landed, #354), shape effects, WordArt 3-D effects and connector routing around shapes (WordArt effects and warps and glued connectors landed, #416), dragging members inside a Drawing Canvas (the canvas landed, #344) | | [parity-checklist.md](parity-checklist.md) (Shape Format 6/6, Layout › Group) | Newsletters, flyers, reports with diagrams | 20–30 | [target-app-parity.md](target-app-parity.md) |
@ -64,6 +64,7 @@ The same gaps grouped the way the parity documents are, for agents working in on
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | trivial | File › Options panes landed (#485); removed from the dialog-depth list |
| 2026-10-11 | trivial | #18: the document canvas describes its text, structure and caret to screen readers through AccessKit (#488); real screen-reader testing remains |
| 2026-10-11 | trivial | Compare / Combine Documents and AutoCorrect Options dialogs and the Restrict Editing pane landed (#412); removed from the dialog-depth list; duplicate #7 rows merged |
| 2026-10-11 | trivial | #10: WordArt text effects and Transform warps, connectors glued to shapes (#416) |

View File

@ -1,6 +1,6 @@
# Typing parity with Word
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (pasted paragraph breaks: tracked under Track Changes, kept as text in lists, #417, #422; previously minor: Move Paragraph Up/Down, Promote/Demote, Go Back and Alt+X keys, #343) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (editing options: typing replaces selection, overtype, #485; previously minor: Move Paragraph Up/Down, Promote/Demote, Go Back and Alt+X keys, #343) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
What Word does when you type, observed black-box (same keystrokes typed into Word and
WordCraft, results read back paragraph by paragraph) and pinned by tests in
@ -76,6 +76,13 @@ Applied when a word is finished by a space, punctuation **or Enter** (`enter_fin
and "don't capitalize after" exceptions extend the abbreviation list. A replacement of several
words takes a sentence capital on its first (`autocorrect_options_change_what_typing_does`).
## Editing options (File › Options › Advanced)
| Keys / setting | Result | Test |
|---|---|---|
| Typing over a selection | replaces it (Word's default); with "Typing replaces selected text" off, the text goes in front of the selection, which stays | `typing_over_a_selection_follows_the_replace_selection_option` |
| Overtype on, typing | each character replaces the one after the caret, never the paragraph mark or an object; Insert switches overtype only with the Insert-key option on (off by default, as in Word) | `overtype_replaces_the_characters_after_the_caret` |
## Layout
- No line break right after a slash (`and/or`, URLs) unless the word can't fit a line
@ -106,6 +113,7 @@ Applied when a word is finished by a space, punctuation **or Enter** (`enter_fin
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Editing options: typing replaces selection, overtype and the Insert key (#485) |
| 2026-10-11 | minor | Pasted paragraph breaks: tracked under Track Changes (#417), kept in a list by Keep Text Only (#422) |
| 2026-10-11 | minor | Undo rows: a selection ends the typing group (#419); Undo of Bold at the caret restores unbolded typing (#424) |
| 2026-10-11 | minor | Names of days capitalised; AutoCorrect Options (user entries, deleted built-ins, exceptions, every AutoCorrect/AutoFormat switch) change typing (#412) |

View File

@ -1,6 +1,6 @@
# UI parity with Microsoft Word
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (screen readers can read the document canvas, #488; previously trivial: Customize Ribbon and the Quick Access Toolbar, #379; previously: major, first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (File › Options panes, #485; previously trivial: Customize Ribbon and the Quick Access Toolbar, #379; previously: major, first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch.
Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our
@ -17,9 +17,9 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
| Customize Ribbon / Customize Keyboard | Customize Keyboard (#368): categories, a command's keys, press a combination to see which command uses it, Assign / Remove / Reset All; custom keys override built-ins, removed built-ins do nothing, saved per user; `tools.customizeKeyboard` for agents. Customize Ribbon (#379): show, hide and reorder tabs, custom tabs and custom groups (on any tab) holding any commands, rename, remove, Reset; right-click on the ribbon; saved per user; `tools.customizeRibbon` for agents. Built-in tabs can't be renamed; no import/export file | ✅ | 85% | 1–2 |
| Keyboard shortcuts | **125** commands bound in `CommandSpec::key`, 144 key combinations (measured on Linux/Windows); styles, spacing, symbols, fields, views, mail merge (#330); field keys Alt+F9, Shift+F9, Ctrl+Shift+F9, Ctrl+F11, Ctrl+Shift+F11, F11, Shift+F11 (#339); Alt+Shift+arrows (move paragraphs, promote/demote), Shift+F5 Go Back and Alt+X (#343). Missing: en dash on the numeric keypad | ~250 default shortcuts | ~70% | 2–4 |
| Modal dialogs | **52** (`Dialog` enum: Font, Enclose Characters, Paragraph, Tabs, Borders and Shading, Find, Go To, Insert Table, Table Properties, Page Setup, Link, Bookmark, Word Count, Zoom, Watermark, New Style, Modify Style, Manage Styles, Table Style, Commands, Icons, Compare and Combine, AutoCorrect Options, Paste Special, About, Save Changes, Password, Encrypt with Password, Define New Multilevel List, Track Changes Options; Columns, Symbol with Special Characters, Field (#321); Field code (#339); mail merge Recipients, Insert Merge Field, Find Recipient, Rules, Match Fields, Check for Errors; Caption with New Label and Numbering, Index, Mark Index Entry, Custom Table of Contents with Options and TOC styles, Source Manager, Create/Edit Source (#402); Line Numbers, Hyphenation with Manual Hyphenation, Language, Envelopes, Labels (#407)) plus panes and menus for the rest | ~100 (Options panes…) | ~45% | 11–19 |
| Context menus (right-click) | text, spelling suggestions, fields (Update Field, Edit Field…, Toggle Field Codes, #339); right-click selects a picture, shape or text box and offers Wrap Text (styles, Move with Text / Fix Position on Page, #513); tables partial | rich per context | 60% | 3–5 |
| Task panes (Navigation, Styles, Comments, Reviewing, Format Picture/Shape, Thesaurus, Accessibility, Clipboard, Selection) | Navigation, Styles, Comments, Thesaurus, Restrict Editing, Accessibility (grouped findings, jump-to, fixes) and Alt Text (#386), Read Aloud, Zotero; Style Inspector (#236); Clipboard pane (#264) open | all | 70% | 4–6 |
| Backstage (Home, New, Open, Info, Save As, Print, Share, Export, Options) | Home and New separate (#254), Info properties kept (#268) | | 68% | 4–6 |
| Context menus (right-click) | text, spelling suggestions, fields (Update Field, Edit Field…, Toggle Field Codes, #339); right-click selects a picture, shape or text box and offers Wrap Text (styles, Move with Text / Fix Position on Page, #513); tables partial | rich per context | 60% | 3–5 |
| Backstage (Home, New, Open, Info, Save As, Print, Share, Export, Options) | Home and New separate (#254), Info properties kept (#268), Options in Word's panes with working settings (#485) | | 68% | 4–6 |
| Rulers: indents, tabs, margins, table columns | indents draggable (one undo, #62); tabs; table column drag pending (#49) | ✅ | 70% | 2–3 |
| On-canvas objects: select, drag, resize handles, nudge | ✅ tested (#228) | ✅ | 75% | — |
| Rotation handle, crop handles on canvas, alignment guides, smart guides | rotation handle (Shift: 15° steps), turned frames resize along their axes (#332); crop handles and guides missing | ✅ | 25% | 3–5 |
@ -37,6 +37,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | trivial | File › Options as panes (General, Display, Proofing, Save, Language, Accessibility, Advanced, Agents), every setting wired; `file.options` headless (#485) |
| 2026-10-11 | trivial | Right-click on a picture, shape or text box selects it and offers Wrap Text: the seven wrapping styles (current one checked), Move with Text and Fix Position on Page (`arrange.moveWithText`) (#513) |
| 2026-10-11 | trivial | Paragraph dialog: Mirror indents (Inside/Outside labels), Don't add space between paragraphs of the same style, Formatting exceptions (Suppress line numbers, Don't hyphenate) (#512) |
| 2026-10-11 | trivial | Screen readers: the document canvas exposes paragraphs, headings, lists, tables, links, pictures and the caret through AccessKit (#488); 25% → 50% |