mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 10:27:44 -06:00
Style Inspector: styles.inspector pane, styles.inspect query and per-level clearing (#236)
styles.inspector shows/hides the Style Inspector pane and returns the paragraph style, character style and the direct formatting that differs from each; styles.inspect is the pure query; styles.inspectorClear resets one level (paragraphStyle, paragraphFormatting, characterStyle, characterFormatting) as a single undo step. The pane opens from the Styles pane, with a clear button per level. Parity floor raised to 87%. Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Dmitrii Khlapov
Claude Opus 5.5
Brandon Thomas
parent
8bc4e6c999
commit
e71c2b6c5b
+2
-2
@@ -8,8 +8,8 @@ and agent control. This file tracks where we are honestly. Generated numbers com
|
||||
|
||||
| Measure | Value |
|
||||
|---|---|
|
||||
| Commands (every action, scriptable by CLI/MCP/control channel) | **427** |
|
||||
| Feature catalog coverage (Word ribbon/menu features with a command) | **382 / 430 (89%)** |
|
||||
| Commands (every action, scriptable by CLI/MCP/control channel) | **430** |
|
||||
| Feature catalog coverage (Word ribbon/menu features with a command) | **383 / 430 (89%)** |
|
||||
| **Estimated real feature parity** (depth and fidelity, not just a command) | **~62%** |
|
||||
| **Distance to alpha** | **~85% of the way; ≈20–25 h of Claude wall-clock work** |
|
||||
| **Distance to 100% parity** | **≈120–150 h of Claude Opus 5.5 wall-clock work** (with parallel agents) |
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
//! Style Inspector (Home › Styles): the paragraph style and the character style at the caret,
|
||||
//! the direct formatting layered on each, and a way to clear each level.
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::{Map, Value, json};
|
||||
use wordcraft_doc::props::{CharProps, ParaProps};
|
||||
|
||||
use super::sel_result;
|
||||
use crate::{CmdError, CmdResult, CommandSpec, Session, p};
|
||||
|
||||
/// The levels `styles.inspectorClear` can reset.
|
||||
pub const LEVELS: [&str; 4] = ["paragraphStyle", "paragraphFormatting", "characterStyle", "characterFormatting"];
|
||||
|
||||
/// Direct character properties that aren't formatting (links, revisions, proofing, direction).
|
||||
const CHAR_SKIP: [&str; 7] = ["style", "link", "ins", "del", "lang", "noProof", "rtl"];
|
||||
/// Direct paragraph properties the inspector doesn't list (lists have their own UI).
|
||||
const PARA_SKIP: [&str; 2] = ["style", "numbering"];
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("styles.inspector", "Style Inspector", "Home › Styles", |s, v| {
|
||||
s.view.style_inspector = p::bool(v, "value").unwrap_or(!s.view.style_inspector);
|
||||
let mut out = inspect(s);
|
||||
if let Value::Object(m) = &mut out {
|
||||
m.insert("pane".into(), json!(s.view.style_inspector));
|
||||
}
|
||||
Ok(out)
|
||||
})
|
||||
.params(r#"{"value"?: bool (show/hide the pane)}"#)
|
||||
.pure(),
|
||||
CommandSpec::new("styles.inspect", "Inspect Styles at Selection", "Home › Styles › Style Inspector", |s, _| Ok(inspect(s))).pure(),
|
||||
CommandSpec::new("styles.inspectorClear", "Clear Formatting Level", "Home › Styles › Style Inspector", clear)
|
||||
.params(r#"{"level": "paragraphStyle|paragraphFormatting|characterStyle|characterFormatting"}"#),
|
||||
]
|
||||
}
|
||||
|
||||
/// The paragraph props at the caret (the selection start for a selection).
|
||||
fn para_props(s: &Session) -> ParaProps {
|
||||
let (a, _) = s.sel.ordered();
|
||||
s.doc.para_at(&a).map(|p| p.props.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The direct character props at the caret (the first selected character for a selection).
|
||||
fn char_props(s: &Session) -> CharProps {
|
||||
let (a, b) = s.sel.ordered();
|
||||
if a == b {
|
||||
return s.typing_props();
|
||||
}
|
||||
match s.doc.para_at(&a) {
|
||||
Some(p) if a.off < p.len() => p.props_of_char(a.off).clone(),
|
||||
_ => s.typing_props(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Each set field of `direct` (minus `skip`) whose value changes the result of `resolve`, given
|
||||
/// a props value holding only that field. Returns `[{"prop", "value"}]` in field order.
|
||||
fn differences<T: Serialize + DeserializeOwned>(direct: &T, skip: &[&str], base: &Value, resolve: &dyn Fn(T) -> Value) -> Vec<Value> {
|
||||
let Ok(Value::Object(fields)) = serde_json::to_value(direct) else { return Vec::new() };
|
||||
let mut out = Vec::new();
|
||||
for (k, v) in fields {
|
||||
if skip.contains(&k.as_str()) || v.is_null() {
|
||||
continue;
|
||||
}
|
||||
let mut one = Map::new();
|
||||
one.insert(k.clone(), v.clone());
|
||||
let Ok(single) = serde_json::from_value::<T>(Value::Object(one)) else { continue };
|
||||
if resolve(single) != *base {
|
||||
out.push(json!({"prop": k, "value": v}));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn style_name(s: &Session, id: &str) -> String {
|
||||
s.doc.styles.get(id).map(|st| st.name.clone()).unwrap_or_else(|| id.to_string())
|
||||
}
|
||||
|
||||
/// What the Style Inspector shows, as JSON.
|
||||
pub fn inspect(s: &Session) -> Value {
|
||||
let pp = para_props(s);
|
||||
let pstyle = pp.style.clone().unwrap_or_else(|| "Normal".into());
|
||||
let styles = &s.doc.styles;
|
||||
let para_base = ParaProps { style: Some(pstyle.clone()), ..Default::default() };
|
||||
let pbase = serde_json::to_value(styles.resolve_para(¶_base)).unwrap_or(Value::Null);
|
||||
let pdiff = differences(&pp, &PARA_SKIP, &pbase, &|mut one: ParaProps| {
|
||||
one.style = Some(pstyle.clone());
|
||||
serde_json::to_value(styles.resolve_para(&one)).unwrap_or(Value::Null)
|
||||
});
|
||||
|
||||
let cp = char_props(s);
|
||||
let cstyle = cp.style.clone();
|
||||
let char_base = CharProps { style: cstyle.clone(), ..Default::default() };
|
||||
let cbase = serde_json::to_value(styles.resolve_char(Some(&pstyle), &char_base)).unwrap_or(Value::Null);
|
||||
let cdiff = differences(&cp, &CHAR_SKIP, &cbase, &|mut one: CharProps| {
|
||||
one.style = cstyle.clone();
|
||||
serde_json::to_value(styles.resolve_char(Some(&pstyle), &one)).unwrap_or(Value::Null)
|
||||
});
|
||||
|
||||
json!({
|
||||
"paragraph": {
|
||||
"style": pstyle,
|
||||
"styleName": style_name(s, &pstyle),
|
||||
"direct": pdiff,
|
||||
},
|
||||
"character": {
|
||||
"style": cstyle,
|
||||
"styleName": cstyle.as_deref().map(|c| style_name(s, c)),
|
||||
"direct": cdiff,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn clear(s: &mut Session, v: &Value) -> CmdResult {
|
||||
let level = p::req_str(v, "level")?;
|
||||
let (a, b) = s.sel.ordered();
|
||||
match level {
|
||||
"paragraphStyle" => {
|
||||
// Reset to Normal, keeping direct formatting.
|
||||
s.doc.format_paragraphs(&a, &b, &|p| p.style = None)?;
|
||||
}
|
||||
"paragraphFormatting" => {
|
||||
s.doc.format_paragraphs(&a, &b, &|p| {
|
||||
*p = ParaProps { style: p.style.clone(), numbering: p.numbering, ..Default::default() };
|
||||
})?;
|
||||
}
|
||||
"characterStyle" => return super::format::apply(s, &|c| c.style = None),
|
||||
"characterFormatting" => {
|
||||
return super::format::apply(s, &|c| {
|
||||
*c = CharProps {
|
||||
style: c.style.clone(),
|
||||
link: c.link.clone(),
|
||||
ins: c.ins,
|
||||
del: c.del,
|
||||
lang: c.lang.clone(),
|
||||
no_proof: c.no_proof,
|
||||
rtl: c.rtl,
|
||||
..Default::default()
|
||||
};
|
||||
});
|
||||
}
|
||||
other => return Err(CmdError::Params(format!("unknown level `{other}`; expected one of {}", LEVELS.join(", ")))),
|
||||
}
|
||||
sel_result(s)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod equation;
|
||||
pub mod file;
|
||||
pub mod format;
|
||||
pub mod insert;
|
||||
pub mod inspector;
|
||||
pub mod mailings;
|
||||
pub mod objects;
|
||||
pub mod page;
|
||||
@@ -37,6 +38,7 @@ pub fn registry() -> Registry {
|
||||
v.extend(paste::specs());
|
||||
v.extend(format::specs());
|
||||
v.extend(para::specs());
|
||||
v.extend(inspector::specs());
|
||||
v.extend(view::specs());
|
||||
v.extend(insert::specs());
|
||||
v.extend(equation::specs());
|
||||
|
||||
@@ -48,6 +48,9 @@ pub struct ViewState {
|
||||
pub table_gridlines: bool,
|
||||
pub nav_pane: bool,
|
||||
pub styles_pane: bool,
|
||||
/// Style Inspector pane (Home › Styles).
|
||||
#[serde(default)]
|
||||
pub style_inspector: bool,
|
||||
pub comments_pane: bool,
|
||||
pub multi_page: bool,
|
||||
/// Zoom to fit: "pageWidth", "onePage", "multiplePages", or empty.
|
||||
@@ -73,6 +76,7 @@ impl Default for ViewState {
|
||||
table_gridlines: false,
|
||||
nav_pane: false,
|
||||
styles_pane: false,
|
||||
style_inspector: false,
|
||||
comments_pane: false,
|
||||
multi_page: false,
|
||||
fit: String::new(),
|
||||
|
||||
@@ -1779,6 +1779,81 @@ fn timestamps_come_from_the_clock() {
|
||||
assert!(date >= cmd::iso_from_unix_secs(before) && date <= cmd::iso_from_unix_secs(cmd::now_unix()), "{date}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_inspector_reports_and_clears_levels() {
|
||||
let mut s = s();
|
||||
run(&mut s, "text.insert", json!({"text": "Chapter one"}));
|
||||
run(&mut s, "para.heading1", json!({}));
|
||||
run(&mut s, "select.all", json!({}));
|
||||
run(&mut s, "format.bold", json!({"value": true}));
|
||||
run(&mut s, "para.alignCenter", json!({}));
|
||||
|
||||
let r = run(&mut s, "styles.inspect", json!({}));
|
||||
assert_eq!(r["paragraph"]["style"], "Heading1");
|
||||
assert_eq!(r["paragraph"]["styleName"], "Heading 1");
|
||||
let pdirect = r["paragraph"]["direct"].as_array().unwrap();
|
||||
assert!(pdirect.iter().any(|d| d["prop"] == "align" && d["value"] == "center"), "{pdirect:?}");
|
||||
assert!(r["character"]["style"].is_null());
|
||||
let cdirect = r["character"]["direct"].as_array().unwrap();
|
||||
assert_eq!(cdirect, &vec![json!({"prop": "bold", "value": true})]);
|
||||
|
||||
// Formatting that matches the style isn't a difference (Heading 1 is 20 pt).
|
||||
run(&mut s, "format.size", json!({"size": 20}));
|
||||
let r = run(&mut s, "styles.inspect", json!({}));
|
||||
assert_eq!(r["character"]["direct"].as_array().unwrap().len(), 1, "{r}");
|
||||
|
||||
// Character style level.
|
||||
run(&mut s, "format.charStyle", json!({"style": "Emphasis"}));
|
||||
let r = run(&mut s, "styles.inspect", json!({}));
|
||||
assert_eq!(r["character"]["style"], "Emphasis");
|
||||
assert_eq!(r["character"]["styleName"], "Emphasis");
|
||||
run(&mut s, "styles.inspectorClear", json!({"level": "characterStyle"}));
|
||||
assert!(run(&mut s, "styles.inspect", json!({}))["character"]["style"].is_null());
|
||||
|
||||
// Clearing character formatting removes the bold and keeps the paragraph levels.
|
||||
run(&mut s, "styles.inspectorClear", json!({"level": "characterFormatting"}));
|
||||
let r = run(&mut s, "styles.inspect", json!({}));
|
||||
assert!(r["character"]["direct"].as_array().unwrap().is_empty(), "{r}");
|
||||
assert!(!run(&mut s, "format.state", json!({}))["bold"].as_bool().unwrap());
|
||||
assert_eq!(r["paragraph"]["style"], "Heading1");
|
||||
assert!(!r["paragraph"]["direct"].as_array().unwrap().is_empty());
|
||||
|
||||
run(&mut s, "styles.inspectorClear", json!({"level": "paragraphFormatting"}));
|
||||
let r = run(&mut s, "styles.inspect", json!({}));
|
||||
assert!(r["paragraph"]["direct"].as_array().unwrap().is_empty(), "{r}");
|
||||
assert_eq!(r["paragraph"]["style"], "Heading1");
|
||||
|
||||
run(&mut s, "styles.inspectorClear", json!({"level": "paragraphStyle"}));
|
||||
assert_eq!(run(&mut s, "styles.inspect", json!({}))["paragraph"]["style"], "Normal");
|
||||
|
||||
// Each clear is one undo step.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(run(&mut s, "styles.inspect", json!({}))["paragraph"]["style"], "Heading1");
|
||||
assert_eq!(text(&s), "Chapter one");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_inspector_pane_and_hostile_params() {
|
||||
let mut s = Session::new(crate::sample::sample_document());
|
||||
let r = run(&mut s, "styles.inspector", json!({}));
|
||||
assert_eq!(r["pane"], true);
|
||||
assert!(s.view.style_inspector);
|
||||
assert_eq!(r["paragraph"]["styleName"], "Title");
|
||||
let r = run(&mut s, "styles.inspector", json!({"value": false}));
|
||||
assert_eq!(r["pane"], false);
|
||||
let before = text(&s);
|
||||
for junk in [json!(null), json!({}), json!({"level": 5}), json!({"level": "everything"}), json!({"level": ""})] {
|
||||
assert!(s.run("styles.inspectorClear", &junk).is_err(), "{junk}");
|
||||
}
|
||||
assert_eq!(text(&s), before);
|
||||
assert!(s.run("styles.inspector", &json!({"value": "yes"})).is_ok());
|
||||
// A selection reaching far outside the document.
|
||||
s.sel.focus = Pos::body(9999, 99999);
|
||||
s.sel.anchor = Pos::body(0, 0);
|
||||
assert!(s.run("styles.inspect", &json!({})).is_ok());
|
||||
assert!(s.run("styles.inspectorClear", &json!({"level": "characterFormatting"})).is_ok());
|
||||
}
|
||||
|
||||
/// Home › Editing › Find › Advanced Find: every match, in the body or a selection, with Reading Highlight.
|
||||
#[test]
|
||||
fn advanced_find_lists_and_highlights_matches() {
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
Character Scale Escala del carácter
|
||||
Character Shading Sombreado de carácter
|
||||
Character Spacing Espaciado de caracteres
|
||||
Characters Caracteres
|
||||
Characters (no spaces) Caracteres (sin espacios)
|
||||
Characters (with spaces) Caracteres (con espacios)
|
||||
Chart Gráfico
|
||||
@@ -122,7 +123,12 @@
|
||||
Citation Style Estilo de cita
|
||||
Citations & Bibliography Citas y bibliografía
|
||||
Clear Borrar
|
||||
Clear All Borrar todo
|
||||
Clear All Formatting Borrar todo el formato
|
||||
Clear Character Formatting Borrar formato de carácter
|
||||
Clear Character Style Borrar estilo de carácter
|
||||
Clear Formatting Level Borrar nivel de formato
|
||||
Clear Paragraph Formatting Borrar formato de párrafo
|
||||
Clipboard Portapapeles
|
||||
Close Cerrar
|
||||
Close Header and Footer Cerrar encabezado y pie de página
|
||||
@@ -189,6 +195,7 @@
|
||||
Dictate Dictar
|
||||
Different First Page Primera página diferente
|
||||
Different Odd & Even Pages Páginas pares e impares diferentes
|
||||
Direct formatting Formato directo
|
||||
Discord Discord
|
||||
Discord community Comunidad de Discord
|
||||
Display Mostrar
|
||||
@@ -346,6 +353,7 @@
|
||||
Insert\nFootnote Insertar nota\nal pie
|
||||
Inside Borders Bordes interiores
|
||||
Inspect Document Inspeccionar documento
|
||||
Inspect Styles at Selection Inspeccionar estilos de la selección
|
||||
Interface language: Idioma de la interfaz:
|
||||
Italic Cursiva
|
||||
Join the ArtCraft community on Discord Únete a la comunidad de ArtCraft en Discord
|
||||
@@ -570,6 +578,7 @@
|
||||
Research Investigación
|
||||
Researcher Investigador
|
||||
Reset Picture Restablecer imagen
|
||||
Reset to Normal Restablecer a Normal
|
||||
Resolve Resolver
|
||||
Resolved Resuelto
|
||||
Restart Each Page Reiniciar en cada página
|
||||
@@ -684,6 +693,7 @@
|
||||
Studio cover Portada de Studio
|
||||
Studio handbook (sample) Manual de Studio (ejemplo)
|
||||
Style Estilo
|
||||
Style Inspector Inspector de estilos
|
||||
Style Set Conjunto de estilos
|
||||
Style based on: Estilo basado en:
|
||||
Style: APA Estilo: APA
|
||||
@@ -973,3 +983,30 @@
|
||||
Whole Table Toda la tabla
|
||||
Fill color: Color de relleno:
|
||||
{count} items highlighted {count} elementos resaltados
|
||||
{n} pt {n} pt
|
||||
Not {name} Sin {name}
|
||||
Level {n} Nivel {n}
|
||||
Body Text Texto independiente
|
||||
{n} lines {n} líneas
|
||||
At least {n} Mínimo {n}
|
||||
Exactly {n} Exacto {n}
|
||||
Centered Centrado
|
||||
Character spacing Espaciado de caracteres
|
||||
Don't add space between paragraphs of the same style No agregar espacio entre párrafos del mismo estilo
|
||||
Don't hyphenate No separar con guiones
|
||||
Double strikethrough Doble tachado
|
||||
Drop cap Letra capital
|
||||
First line Primera línea
|
||||
Font color Color de fuente
|
||||
Highlight Resaltado
|
||||
Justified Justificado
|
||||
Left indent Sangría izquierda
|
||||
Line spacing Interlineado
|
||||
Outline level Nivel de esquema
|
||||
Raised/lowered Elevado/bajado
|
||||
Right indent Sangría derecha
|
||||
Scale Escala
|
||||
Space after Espacio posterior
|
||||
Space before Espacio anterior
|
||||
Suppress line numbers Suprimir números de línea
|
||||
Underline color Color de subrayado
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -931,6 +931,16 @@
|
||||
Dark ダーク
|
||||
Use system setting システム設定を使用
|
||||
A file dialog is already open. ファイル ダイアログが既に開いています。
|
||||
Style Inspector スタイル インスペクター
|
||||
Inspect Styles at Selection 選択箇所のスタイルを調べる
|
||||
Clear Formatting Level 書式のレベルをクリア
|
||||
Characters 文字
|
||||
Direct formatting 直接の書式
|
||||
Reset to Normal 標準に戻す
|
||||
Clear Paragraph Formatting 段落の書式をクリア
|
||||
Clear Character Style 文字スタイルをクリア
|
||||
Clear Character Formatting 文字の書式をクリア
|
||||
Clear All すべてクリア
|
||||
Paste Special 形式を選択して貼り付け
|
||||
Paste Special… 形式を選択して貼り付け…
|
||||
Paste as: 貼り付ける形式:
|
||||
@@ -956,3 +966,30 @@
|
||||
Whole Table 表全体
|
||||
Fill color: 塗りつぶしの色:
|
||||
{count} items highlighted {count} 件を強調表示しました
|
||||
{n} pt {n} pt
|
||||
Not {name} {name} なし
|
||||
Level {n} レベル {n}
|
||||
Body Text 本文
|
||||
{n} lines {n} 行
|
||||
At least {n} 最小値 {n}
|
||||
Exactly {n} 固定値 {n}
|
||||
Centered 中央揃え
|
||||
Character spacing 文字間隔
|
||||
Don't add space between paragraphs of the same style 同じスタイルの段落間に間隔を追加しない
|
||||
Don't hyphenate ハイフネーションなし
|
||||
Double strikethrough 二重取り消し線
|
||||
Drop cap ドロップ キャップ
|
||||
First line 最初の行
|
||||
Font color フォントの色
|
||||
Highlight 蛍光ペン
|
||||
Justified 両端揃え
|
||||
Left indent 左インデント
|
||||
Line spacing 行間
|
||||
Outline level アウトライン レベル
|
||||
Raised/lowered 上げ/下げ
|
||||
Right indent 右インデント
|
||||
Scale 倍率
|
||||
Space after 段落後の間隔
|
||||
Space before 段落前の間隔
|
||||
Suppress line numbers 行番号を表示しない
|
||||
Underline color 下線の色
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 411 and column 13.
|
@@ -943,6 +943,16 @@
|
||||
Dark Escuro
|
||||
Use system setting Usar a configuração do sistema
|
||||
A file dialog is already open. Já há uma caixa de diálogo de arquivo aberta.
|
||||
Style Inspector Inspetor de estilos
|
||||
Inspect Styles at Selection Inspecionar estilos na seleção
|
||||
Clear Formatting Level Limpar nível de formatação
|
||||
Characters Caracteres
|
||||
Direct formatting Formatação direta
|
||||
Reset to Normal Redefinir para Normal
|
||||
Clear Paragraph Formatting Limpar formatação do parágrafo
|
||||
Clear Character Style Limpar estilo de caractere
|
||||
Clear Character Formatting Limpar formatação de caractere
|
||||
Clear All Limpar tudo
|
||||
Paste Special Colar especial
|
||||
Paste Special… Colar especial…
|
||||
Paste as: Colar como:
|
||||
@@ -968,3 +978,30 @@
|
||||
Whole Table Tabela inteira
|
||||
Fill color: Cor de preenchimento:
|
||||
{count} items highlighted {count} itens realçados
|
||||
{n} pt {n} pt
|
||||
Not {name} Sem {name}
|
||||
Level {n} Nível {n}
|
||||
Body Text Corpo do texto
|
||||
{n} lines {n} linhas
|
||||
At least {n} Pelo menos {n}
|
||||
Exactly {n} Exatamente {n}
|
||||
Centered Centralizado
|
||||
Character spacing Espaçamento entre caracteres
|
||||
Don't add space between paragraphs of the same style Não adicionar espaço entre parágrafos do mesmo estilo
|
||||
Don't hyphenate Não hifenizar
|
||||
Double strikethrough Tachado duplo
|
||||
Drop cap Capitular
|
||||
First line Primeira linha
|
||||
Font color Cor da fonte
|
||||
Highlight Realce
|
||||
Justified Justificado
|
||||
Left indent Recuo à esquerda
|
||||
Line spacing Espaçamento entre linhas
|
||||
Outline level Nível de tópicos
|
||||
Raised/lowered Elevado/rebaixado
|
||||
Right indent Recuo à direita
|
||||
Scale Escala
|
||||
Space after Espaço depois
|
||||
Space before Espaço antes
|
||||
Suppress line numbers Suprimir números de linha
|
||||
Underline color Cor do sublinhado
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -108,6 +108,7 @@
|
||||
Character Scale Масштаб символів
|
||||
Character Shading Заливка символів
|
||||
Character Spacing Інтервал між символами
|
||||
Characters Символи
|
||||
Characters (no spaces) Символи (без пробілів)
|
||||
Characters (with spaces) Символи (з пробілами)
|
||||
Chart Діаграма
|
||||
@@ -119,7 +120,12 @@
|
||||
Citation Style Стиль цитування
|
||||
Citations & Bibliography Цитати й бібліографія
|
||||
Clear Очистити
|
||||
Clear All Очистити все
|
||||
Clear All Formatting Очистити все форматування
|
||||
Clear Character Formatting Очистити форматування символів
|
||||
Clear Character Style Очистити стиль символів
|
||||
Clear Formatting Level Очистити рівень форматування
|
||||
Clear Paragraph Formatting Очистити форматування абзацу
|
||||
Clipboard Буфер обміну
|
||||
Close Закрити
|
||||
Close Header and Footer Закрити колонтитули
|
||||
@@ -190,6 +196,7 @@
|
||||
Dictate Диктувати
|
||||
Different First Page Інша перша сторінка
|
||||
Different Odd & Even Pages Різні парні й непарні сторінки
|
||||
Direct formatting Пряме форматування
|
||||
Discord Discord
|
||||
Discord community Спільнота Discord
|
||||
Display Відображення
|
||||
@@ -348,6 +355,7 @@
|
||||
Insert\nFootnote Вставити\nвиноску
|
||||
Inside Borders Внутрішні межі
|
||||
Inspect Document Перевірити документ
|
||||
Inspect Styles at Selection Переглянути стилі у виділенні
|
||||
Interface language: Мова інтерфейсу:
|
||||
Italic Курсив
|
||||
Join the ArtCraft community on Discord Приєднатися до спільноти ArtCraft у Discord
|
||||
@@ -571,6 +579,7 @@
|
||||
Research Дослідження
|
||||
Researcher Дослідник
|
||||
Reset Picture Скинути зображення
|
||||
Reset to Normal Скинути до звичайного
|
||||
Resolve Вирішити
|
||||
Resolved Вирішено
|
||||
Restart at 1 Почати з 1
|
||||
@@ -686,6 +695,7 @@
|
||||
Studio handbook (sample) Посібник студії (приклад)
|
||||
Style Стиль
|
||||
Style based on: Стиль на основі:
|
||||
Style Inspector Інспектор стилів
|
||||
Style Set Набір стилів
|
||||
Style: APA Стиль: APA
|
||||
Styles Стилі
|
||||
@@ -968,3 +978,30 @@
|
||||
Whole Table Уся таблиця
|
||||
Fill color: Колір заливки:
|
||||
{count} items highlighted Виділено елементів: {count}
|
||||
{n} pt {n} пт
|
||||
Not {name} Без: {name}
|
||||
Level {n} Рівень {n}
|
||||
Body Text Основний текст
|
||||
{n} lines Рядків: {n}
|
||||
At least {n} Щонайменше {n}
|
||||
Exactly {n} Точно {n}
|
||||
Centered По центру
|
||||
Character spacing Інтервал між символами
|
||||
Don't add space between paragraphs of the same style Не додавати інтервал між абзацами одного стилю
|
||||
Don't hyphenate Без перенесення слів
|
||||
Double strikethrough Подвійне закреслення
|
||||
Drop cap Буквиця
|
||||
First line Перший рядок
|
||||
Font color Колір шрифту
|
||||
Highlight Виділення кольором
|
||||
Justified За шириною
|
||||
Left indent Відступ ліворуч
|
||||
Line spacing Міжрядковий інтервал
|
||||
Outline level Рівень плану
|
||||
Raised/lowered Піднято/опущено
|
||||
Right indent Відступ праворуч
|
||||
Scale Масштаб
|
||||
Space after Інтервал після
|
||||
Space before Інтервал перед
|
||||
Suppress line numbers Без номерів рядків
|
||||
Underline color Колір підкреслення
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 31.
|
@@ -942,6 +942,16 @@
|
||||
Dark 深色
|
||||
Use system setting 使用系统设置
|
||||
A file dialog is already open. 已有一个文件对话框处于打开状态。
|
||||
Style Inspector 样式检查器
|
||||
Inspect Styles at Selection 检查所选内容的样式
|
||||
Clear Formatting Level 清除格式级别
|
||||
Characters 字符
|
||||
Direct formatting 直接格式
|
||||
Reset to Normal 重置为正文
|
||||
Clear Paragraph Formatting 清除段落格式
|
||||
Clear Character Style 清除字符样式
|
||||
Clear Character Formatting 清除字符格式
|
||||
Clear All 全部清除
|
||||
Paste Special 选择性粘贴
|
||||
Paste Special… 选择性粘贴…
|
||||
Paste as: 粘贴为:
|
||||
@@ -967,3 +977,30 @@
|
||||
Whole Table 整个表格
|
||||
Fill color: 填充颜色:
|
||||
{count} items highlighted 已突出显示 {count} 项
|
||||
{n} pt {n} 磅
|
||||
Not {name} 非{name}
|
||||
Level {n} {n} 级
|
||||
Body Text 正文文本
|
||||
{n} lines {n} 行
|
||||
At least {n} 最小值 {n}
|
||||
Exactly {n} 固定值 {n}
|
||||
Centered 居中
|
||||
Character spacing 字符间距
|
||||
Don't add space between paragraphs of the same style 相同样式的段落间不添加间距
|
||||
Don't hyphenate 不断字
|
||||
Double strikethrough 双删除线
|
||||
Drop cap 首字下沉
|
||||
First line 首行
|
||||
Font color 字体颜色
|
||||
Highlight 突出显示
|
||||
Justified 两端对齐
|
||||
Left indent 左缩进
|
||||
Line spacing 行距
|
||||
Outline level 大纲级别
|
||||
Raised/lowered 提升/降低
|
||||
Right indent 右缩进
|
||||
Scale 缩放
|
||||
Space after 段后间距
|
||||
Space before 段前间距
|
||||
Suppress line numbers 取消行号
|
||||
Underline color 下划线颜色
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -932,6 +932,16 @@
|
||||
Dark 深色
|
||||
Use system setting 使用系統設定
|
||||
A file dialog is already open. 已有一個檔案對話方塊開啟中。
|
||||
Style Inspector 樣式檢查器
|
||||
Inspect Styles at Selection 檢查選取範圍的樣式
|
||||
Clear Formatting Level 清除格式層級
|
||||
Characters 字元
|
||||
Direct formatting 直接格式設定
|
||||
Reset to Normal 重設為內文
|
||||
Clear Paragraph Formatting 清除段落格式設定
|
||||
Clear Character Style 清除字元樣式
|
||||
Clear Character Formatting 清除字元格式設定
|
||||
Clear All 全部清除
|
||||
Paste Special 選擇性貼上
|
||||
Paste Special… 選擇性貼上…
|
||||
Paste as: 貼上為:
|
||||
@@ -957,3 +967,30 @@
|
||||
Whole Table 整個表格
|
||||
Fill color: 填滿色彩:
|
||||
{count} items highlighted 已醒目提示 {count} 個項目
|
||||
{n} pt {n} 點
|
||||
Not {name} 非{name}
|
||||
Level {n} 第 {n} 階
|
||||
Body Text 本文
|
||||
{n} lines {n} 行
|
||||
At least {n} 最小行高 {n}
|
||||
Exactly {n} 固定行高 {n}
|
||||
Centered 置中
|
||||
Character spacing 字元間距
|
||||
Don't add space between paragraphs of the same style 相同樣式的段落之間不新增間距
|
||||
Don't hyphenate 不斷字
|
||||
Double strikethrough 雙刪除線
|
||||
Drop cap 首字放大
|
||||
First line 第一行
|
||||
Font color 字型色彩
|
||||
Highlight 醒目提示
|
||||
Justified 左右對齊
|
||||
Left indent 左縮排
|
||||
Line spacing 行距
|
||||
Outline level 大綱階層
|
||||
Raised/lowered 上移/下移
|
||||
Right indent 右縮排
|
||||
Scale 縮放
|
||||
Space after 段落後間距
|
||||
Space before 段落前間距
|
||||
Suppress line numbers 隱藏行號
|
||||
Underline color 底線色彩
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 412 and column 13.
|
+230
-1
@@ -1,4 +1,4 @@
|
||||
//! Side panes: Navigation (headings, pages, search results), Styles, Comments.
|
||||
//! Side panes: Navigation (headings, pages, search results), Styles, Style Inspector, Comments.
|
||||
|
||||
use egui::{Stroke, Ui, vec2};
|
||||
use serde_json::{Value, json};
|
||||
@@ -23,6 +23,13 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
|
||||
.frame(egui::Frame::NONE.fill(t.panel).inner_margin(10).stroke(Stroke::new(1.0, t.border)))
|
||||
.show(ui, |ui| styles(app, ui));
|
||||
}
|
||||
if app.session.view.style_inspector {
|
||||
egui::Panel::right("style_inspector")
|
||||
.default_size(240.0)
|
||||
.resizable(true)
|
||||
.frame(egui::Frame::NONE.fill(t.panel).inner_margin(10).stroke(Stroke::new(1.0, t.border)))
|
||||
.show(ui, |ui| inspector(app, ui));
|
||||
}
|
||||
if app.session.view.comments_pane {
|
||||
egui::Panel::right("comments_pane")
|
||||
.default_size(290.0)
|
||||
@@ -160,6 +167,9 @@ fn styles(app: &mut WordApp, ui: &mut Ui) {
|
||||
let _ = app.run("format.clear", json!({}));
|
||||
}
|
||||
});
|
||||
if ui.selectable_label(app.session.view.style_inspector, tl!("Style Inspector")).clicked() {
|
||||
let _ = app.run("styles.inspector", json!({}));
|
||||
}
|
||||
ui.separator();
|
||||
let mut list: Vec<(String, String, bool)> = app
|
||||
.session
|
||||
@@ -264,3 +274,222 @@ fn comments(app: &mut WordApp, ui: &mut Ui) {
|
||||
});
|
||||
let _ = vec2(0.0, 0.0);
|
||||
}
|
||||
|
||||
/// Style Inspector: the paragraph and character levels at the caret, each with its style and the
|
||||
/// direct formatting on top, and a button to clear each level.
|
||||
fn inspector(app: &mut WordApp, ui: &mut Ui) {
|
||||
if header(ui, "Style Inspector") {
|
||||
let _ = app.run("styles.inspector", json!({"value": false}));
|
||||
return;
|
||||
}
|
||||
// Read-only, so call it directly: running the command every frame would pay for the command
|
||||
// machinery and consume per-command state (a pending `join_next_undo`) just to draw the pane.
|
||||
let r = wordcraft_engine::cmd::inspector::inspect(&app.session);
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let none = tl!("None").to_string();
|
||||
let para = &r["paragraph"];
|
||||
let chr = &r["character"];
|
||||
let pstyle = para["styleName"].as_str().unwrap_or("Normal").to_string();
|
||||
let cstyle = chr["styleName"].as_str().map(str::to_string);
|
||||
let pdirect = direct_list(¶["direct"]);
|
||||
let cdirect = direct_list(&chr["direct"]);
|
||||
let mut clear: Option<&str> = None;
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
section(ui, &t, "Paragraph", |ui| {
|
||||
if level_row(ui, &t, "Style", &pstyle, false, "Reset to Normal", pstyle == "Normal" && para["style"] == "Normal") {
|
||||
clear = Some("paragraphStyle");
|
||||
}
|
||||
let (txt, empty) = if pdirect.is_empty() { (none.clone(), true) } else { (pdirect.join(", "), false) };
|
||||
if level_row(ui, &t, "Direct formatting", &txt, empty, "Clear Paragraph Formatting", empty) {
|
||||
clear = Some("paragraphFormatting");
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
section(ui, &t, "Characters", |ui| {
|
||||
let (txt, empty) = match &cstyle {
|
||||
Some(n) => (n.clone(), false),
|
||||
None => (none.clone(), true),
|
||||
};
|
||||
if level_row(ui, &t, "Style", &txt, empty, "Clear Character Style", empty) {
|
||||
clear = Some("characterStyle");
|
||||
}
|
||||
let (txt, empty) = if cdirect.is_empty() { (none.clone(), true) } else { (cdirect.join(", "), false) };
|
||||
if level_row(ui, &t, "Direct formatting", &txt, empty, "Clear Character Formatting", empty) {
|
||||
clear = Some("characterFormatting");
|
||||
}
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button(tl!("Clear All")).on_hover_text(tl!("Clear All Formatting")).clicked() {
|
||||
let _ = app.run("format.clear", json!({}));
|
||||
app.canvas.want_focus = true;
|
||||
}
|
||||
if ui.button(tl!("Styles Pane")).clicked() {
|
||||
let _ = app.run("view.stylesPane", json!({"value": true}));
|
||||
}
|
||||
});
|
||||
});
|
||||
if let Some(level) = clear {
|
||||
let _ = app.run("styles.inspectorClear", json!({"level": level}));
|
||||
app.canvas.want_focus = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// A titled, bordered group in the inspector.
|
||||
fn section(ui: &mut Ui, t: &Tokens, title: &str, body: impl FnOnce(&mut Ui)) {
|
||||
ui.label(egui::RichText::new(tl!(title)).font(semibold(12.5)).color(t.text));
|
||||
ui.add_space(2.0);
|
||||
egui::Frame::NONE.fill(t.input).stroke(Stroke::new(1.0, t.border)).corner_radius(4).inner_margin(8).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
body(ui);
|
||||
});
|
||||
}
|
||||
|
||||
/// One level: a caption, its value, and a clear button (disabled when there's nothing to clear).
|
||||
/// Returns true when the button was clicked.
|
||||
fn level_row(ui: &mut Ui, t: &Tokens, caption: &str, value: &str, dim: bool, tip: &str, disabled: bool) -> bool {
|
||||
ui.label(egui::RichText::new(tl!(caption)).font(regular(11.0)).color(t.text_dim));
|
||||
let mut clicked = false;
|
||||
ui.horizontal(|ui| {
|
||||
let w = (ui.available_width() - 26.0).max(40.0);
|
||||
ui.allocate_ui(vec2(w, 18.0), |ui| {
|
||||
ui.set_width(w);
|
||||
let text = egui::RichText::new(value).font(regular(12.5)).color(if dim { t.text_disabled } else { t.text });
|
||||
ui.add(egui::Label::new(text).wrap());
|
||||
});
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(22.0, 20.0), egui::Sense::click());
|
||||
let enabled = !disabled;
|
||||
if enabled && resp.hovered() {
|
||||
ui.painter().rect_filled(r, 3.0, t.hover);
|
||||
}
|
||||
let c = if enabled { t.icon } else { t.text_disabled };
|
||||
crate::icons::paint(
|
||||
ui.painter(),
|
||||
egui::Rect::from_center_size(r.center(), vec2(16.0, 16.0)),
|
||||
"clear",
|
||||
c,
|
||||
if enabled { t.accent } else { c },
|
||||
);
|
||||
let resp = resp.on_hover_text(tl!(tip));
|
||||
clicked = enabled && resp.clicked();
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
clicked
|
||||
}
|
||||
|
||||
/// Readable descriptions of the inspector's `direct` entries (`[{"prop", "value"}]`).
|
||||
fn direct_list(v: &Value) -> Vec<String> {
|
||||
v.as_array().map(|a| a.iter().take(64).map(|d| describe(d["prop"].as_str().unwrap_or(""), &d["value"])).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// "Bold", "Not Italic", "Font: Arial", "Size: 14 pt"…
|
||||
fn describe(prop: &str, v: &Value) -> String {
|
||||
let label = match prop {
|
||||
"font" => "Font",
|
||||
"size" => "Size",
|
||||
"bold" => "Bold",
|
||||
"italic" => "Italic",
|
||||
"underline" => "Underline",
|
||||
"underlineColor" => "Underline color",
|
||||
"strike" => "Strikethrough",
|
||||
"doubleStrike" => "Double strikethrough",
|
||||
"color" => "Font color",
|
||||
"highlight" => "Highlight",
|
||||
"shading" => "Shading",
|
||||
"vertAlign" => "Position",
|
||||
"caps" => "All caps",
|
||||
"smallCaps" => "Small caps",
|
||||
"hidden" => "Hidden",
|
||||
"spacing" => "Character spacing",
|
||||
"scale" => "Scale",
|
||||
"position" => "Raised/lowered",
|
||||
"outline" => "Outline",
|
||||
"shadow" => "Shadow",
|
||||
"emboss" => "Emboss",
|
||||
"engrave" => "Engrave",
|
||||
"align" => "Alignment",
|
||||
"indentLeft" => "Left indent",
|
||||
"indentRight" => "Right indent",
|
||||
"indentFirst" => "First line",
|
||||
"spaceBefore" => "Space before",
|
||||
"spaceAfter" => "Space after",
|
||||
"lineSpacing" => "Line spacing",
|
||||
"contextualSpacing" => "Don't add space between paragraphs of the same style",
|
||||
"keepNext" => "Keep with next",
|
||||
"keepLines" => "Keep lines together",
|
||||
"pageBreakBefore" => "Page break before",
|
||||
"widowControl" => "Widow/Orphan control",
|
||||
"outlineLevel" => "Outline level",
|
||||
"tabs" => "Tabs",
|
||||
"borders" => "Borders",
|
||||
"suppressHyphens" => "Don't hyphenate",
|
||||
"suppressLineNumbers" => "Suppress line numbers",
|
||||
"bidi" => "Right-to-left",
|
||||
"dropCap" => "Drop cap",
|
||||
other => other,
|
||||
};
|
||||
let label = tl!(label);
|
||||
let pt = |x: f64| crate::i18n::fmt(tl!("{n} pt"), &[("n", &trim_num(x))]);
|
||||
let hex = |c: &Value| -> Option<String> {
|
||||
let a = c.as_array()?;
|
||||
let ch = |i: usize| a.get(i).and_then(Value::as_u64).unwrap_or(0).min(255);
|
||||
Some(format!("#{:02X}{:02X}{:02X}", ch(0), ch(1), ch(2)))
|
||||
};
|
||||
let value = match (prop, v) {
|
||||
(_, Value::Bool(true)) => return label.to_string(),
|
||||
(_, Value::Bool(false)) => return crate::i18n::fmt(tl!("Not {name}"), &[("name", label)]),
|
||||
("scale", Value::Number(n)) => format!("{}%", trim_num(n.as_f64().unwrap_or(100.0))),
|
||||
("outlineLevel", Value::Number(n)) => match n.as_u64().unwrap_or(9) {
|
||||
l @ 0..=8 => crate::i18n::fmt(tl!("Level {n}"), &[("n", &(l + 1).to_string())]),
|
||||
_ => tl!("Body Text").to_string(),
|
||||
},
|
||||
("dropCap", Value::Number(n)) => crate::i18n::fmt(tl!("{n} lines"), &[("n", &n.to_string())]),
|
||||
(_, Value::Number(n)) => pt(n.as_f64().unwrap_or(0.0)),
|
||||
("align", Value::String(s)) => tl!(match s.as_str() {
|
||||
"center" => "Centered",
|
||||
"right" => "Right",
|
||||
"justify" => "Justified",
|
||||
"distribute" => "Distributed",
|
||||
_ => "Left",
|
||||
})
|
||||
.to_string(),
|
||||
("color", Value::String(_)) => tl!("Automatic").to_string(),
|
||||
("color", Value::Object(o)) => o.get("Rgb").and_then(hex).unwrap_or_default(),
|
||||
(_, Value::Array(_)) if prop != "tabs" => hex(v).unwrap_or_default(),
|
||||
("lineSpacing", Value::Object(o)) => {
|
||||
let x = o.get("value").and_then(Value::as_f64).unwrap_or(1.0);
|
||||
match o.get("rule").and_then(Value::as_str) {
|
||||
Some("atLeast") => crate::i18n::fmt(tl!("At least {n}"), &[("n", &pt(x))]),
|
||||
Some("exactly") => crate::i18n::fmt(tl!("Exactly {n}"), &[("n", &pt(x))]),
|
||||
_ => crate::i18n::fmt(tl!("{n} lines"), &[("n", &trim_num(x))]),
|
||||
}
|
||||
}
|
||||
(_, Value::String(s)) => tl!(&camel_words(s)).to_string(),
|
||||
_ => return label.to_string(),
|
||||
};
|
||||
format!("{label}: {value}")
|
||||
}
|
||||
|
||||
/// `1.50` → `1.5`, `12.0` → `12`.
|
||||
fn trim_num(x: f64) -> String {
|
||||
let s = format!("{:.2}", if x.is_finite() { x } else { 0.0 });
|
||||
s.trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
}
|
||||
|
||||
/// `dotDash` → `Dot dash` (enum values from the document model).
|
||||
fn camel_words(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len() + 4);
|
||||
for (i, c) in s.chars().take(64).enumerate() {
|
||||
if i == 0 {
|
||||
out.extend(c.to_uppercase());
|
||||
} else if c.is_uppercase() {
|
||||
out.push(' ');
|
||||
out.extend(c.to_lowercase());
|
||||
} else {
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
+2
-3
@@ -2,13 +2,13 @@
|
||||
|
||||
Generated by `cargo xtask parity` (`wordcraft-cli parity --markdown`): the word-processor feature catalog (`crates/engine/src/catalog.rs`) compared with the live command registry.
|
||||
|
||||
**382 of 430 catalog features have commands (88.8%).**
|
||||
**383 of 430 catalog features have commands (89.1%).**
|
||||
|
||||
| Tab | Live | Total |
|
||||
|---|---|---|
|
||||
| File | 20 | 20 |
|
||||
| Quick Access | 4 | 4 |
|
||||
| Home | 94 | 101 |
|
||||
| Home | 95 | 101 |
|
||||
| Insert | 42 | 53 |
|
||||
| Draw | 0 | 11 |
|
||||
| Design | 10 | 10 |
|
||||
@@ -34,7 +34,6 @@ Generated by `cargo xtask parity` (`wordcraft-cli parity --markdown`): the word-
|
||||
- Font › Phonetic Guide (`format.phonetic`)
|
||||
- Font › Enclose Characters (`format.enclose`)
|
||||
- Paragraph › Asian Typography (`para.asianTypography`)
|
||||
- Styles › Style Inspector (`styles.inspector`)
|
||||
- Styles › Manage Styles (`styles.manage`)
|
||||
- Voice › Dictate (`tools.dictate`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user