diff --git a/corpus/styles/candidate/.snowbound/themes.json b/corpus/styles/candidate/.snowbound/themes.json index 549945dd36f23fa7a726c139f4d6865ceaedd612..23fc2f6d5ab4d043bdab59d4ce4a64e2ccf6a2b1 100644 --- a/corpus/styles/candidate/.snowbound/themes.json +++ b/corpus/styles/candidate/.snowbound/themes.json @@ -3,30 +3,30 @@ "assignments": [ { "scope": { - "section": "125e8f01c6ee404b96cc675d00ceb659" + "section": "f0eb3316ed995d4f9a403ead450892a1" }, "theme": "onenote", "assigned": 134000000000000000 }, { "scope": { - "section": "af4f8ea23c5f7e41838ac7cbea473590" + "section": "13d8c0f86a617e4cb35733d2e514408f" }, "theme": "manuscript", "assigned": 134000000000000000 }, { "scope": { - "section": "18af2996b565454c971191d887028fc8" + "section": "aae44a30901cff4aaca7fadf0c8bf659" }, "theme": "editorial", "assigned": 134000000000000000 }, { "scope": { - "section": "f0ca724b3a9d3d4b82bc6f28bd77966c" + "section": "709fb4685c18f945a24f9aa162fc78e4" }, - "theme": "modern", + "theme": "modern-2", "assigned": 134000000000000000 } ] diff --git a/corpus/styles/candidate/Editorial.one b/corpus/styles/candidate/Editorial.one index 1f5b13635c9519f0b1d6470a2a80781333fa1743..24ed6e93c7f9f84e53328a7b75e492de54dd8aba 100644 Binary files a/corpus/styles/candidate/Editorial.one and b/corpus/styles/candidate/Editorial.one differ diff --git a/corpus/styles/candidate/Manuscript.one b/corpus/styles/candidate/Manuscript.one index d595551c7d0126f3a0f5e118d6bf55b45c038576..2debba02524ead50186d43e9b237e716e3766b6a 100644 Binary files a/corpus/styles/candidate/Manuscript.one and b/corpus/styles/candidate/Manuscript.one differ diff --git a/corpus/styles/candidate/Modern.one b/corpus/styles/candidate/Modern.one index 7228d5e88fbbda251c692ea98fe89da570505f84..4a38d76d1405c3de7884eeaee32346e0659d567f 100644 Binary files a/corpus/styles/candidate/Modern.one and b/corpus/styles/candidate/Modern.one differ diff --git a/corpus/styles/candidate/OneNote.one b/corpus/styles/candidate/OneNote.one index 6915847a3b5e8c59004dcd085c9d9705da76d783..91a55e1519e618291e533fea933417773a9e4751 100644 Binary files a/corpus/styles/candidate/OneNote.one and b/corpus/styles/candidate/OneNote.one differ diff --git a/corpus/styles/candidate/Open Notebook.onetoc2 b/corpus/styles/candidate/Open Notebook.onetoc2 index d4ceb28b8e6899338a0ccf41cc757423a03dd3bd..a0d64387026996a2b08255213ee699d343223d6b 100644 Binary files a/corpus/styles/candidate/Open Notebook.onetoc2 and b/corpus/styles/candidate/Open Notebook.onetoc2 differ diff --git a/corpus/styles/cold/commands.jsonl b/corpus/styles/cold/commands.jsonl index 0843332e2b3ce3020bb737357ca84f70ba069ead..746eea2f97bf65d2e89558bee279d263bc14fc74 100644 --- a/corpus/styles/cold/commands.jsonl +++ b/corpus/styles/cold/commands.jsonl @@ -1,4 +1,4 @@ {"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} -{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\author.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-222E5D78", "exit": 0, "stdout": "", "stderr": "", "error": null} -{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-222E5D78 -ExpectedPages 4 -UseCurrentCache -KeepOpen", "exit": 0, "stdout": "Read 4 sections and 4 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\author.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-F5FCF7CA", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-F5FCF7CA -ExpectedPages 4 -UseCurrentCache -KeepOpen", "exit": 0, "stdout": "Read 4 sections and 4 pages.\r\n", "stderr": "", "error": null} {"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/styles/cold/machine.json b/corpus/styles/cold/machine.json index 5d1ccba7f887bfaf23686dfb5c2dae77c791d0be..d9ad4f5236be591af98c703770701c5faaa9c69f 100644 --- a/corpus/styles/cold/machine.json +++ b/corpus/styles/cold/machine.json @@ -1 +1 @@ -{"name": "m6-222e5d78", "hostname": "ONE-M6-222E5D78"} +{"name": "m6-f5fcf7ca", "hostname": "ONE-M6-F5FCF7CA"} diff --git a/corpus/styles/cold/notebook/Editorial.one b/corpus/styles/cold/notebook/Editorial.one index 42462a2e46e8b31d90bfd6e8fc0254a167a8add7..42f857451ba8abf0e81045c2d3b628320494a51f 100644 Binary files a/corpus/styles/cold/notebook/Editorial.one and b/corpus/styles/cold/notebook/Editorial.one differ diff --git a/corpus/styles/cold/notebook/Manuscript.one b/corpus/styles/cold/notebook/Manuscript.one index 8f3ce5fb57b2b2ff62ed6076782a98fac5baf849..6d1f3cd16fe92799c184c6678aa689fc3a8f62d7 100644 Binary files a/corpus/styles/cold/notebook/Manuscript.one and b/corpus/styles/cold/notebook/Manuscript.one differ diff --git a/corpus/styles/cold/notebook/Modern.one b/corpus/styles/cold/notebook/Modern.one index 403d594c04d6df0f50a8d65dc8c5b0178a5c40de..e3fe52ba72f439b9e62363cfe795e4e01414aff9 100644 Binary files a/corpus/styles/cold/notebook/Modern.one and b/corpus/styles/cold/notebook/Modern.one differ diff --git a/corpus/styles/cold/notebook/OneNote.one b/corpus/styles/cold/notebook/OneNote.one index eabc7f8002f2b284c95f9b55622de02338c54e86..f0d94cc4502a0c7ce214f7d3c5ff2016dc6244e1 100644 Binary files a/corpus/styles/cold/notebook/OneNote.one and b/corpus/styles/cold/notebook/OneNote.one differ diff --git a/corpus/styles/cold/notebook/Open Notebook.onetoc2 b/corpus/styles/cold/notebook/Open Notebook.onetoc2 index 6a5357126ab396e70a19ac2ae665da6f24f35860..173f9614ad2806f275337099312ee2669cf591ad 100644 Binary files a/corpus/styles/cold/notebook/Open Notebook.onetoc2 and b/corpus/styles/cold/notebook/Open Notebook.onetoc2 differ diff --git a/corpus/styles/cold/read/environment.json b/corpus/styles/cold/read/environment.json index e93a5211ce6a1aba021b77c79ee65472d25d7ba3..bcacd47aaab9e1d7dc2ea05d8c2181a0d19883cf 100644 --- a/corpus/styles/cold/read/environment.json +++ b/corpus/styles/cold/read/environment.json @@ -1,7 +1,7 @@ { "powershell": "5.1.14409.1005", "schema": "xs2010", - "hostname": "ONE-M6-222E5D78", + "hostname": "ONE-M6-F5FCF7CA", "cold": false, "onenote": "14.0.4763.1000" } diff --git a/corpus/styles/cold/read/hierarchy.xml b/corpus/styles/cold/read/hierarchy.xml index 21cad963c0a70f32b9f24f30a03e43419f1311f5..2e84ba8ccaf5c8bc608567fac40c07bd120cc051 100644 --- a/corpus/styles/cold/read/hierarchy.xml +++ b/corpus/styles/cold/read/hierarchy.xml @@ -1,2 +1,2 @@  - + diff --git a/corpus/styles/cold/read/page-000.navigation.json b/corpus/styles/cold/read/page-000.navigation.json index 556fccc0ba9aafbacaa8b574185841ec2226b9ac..eb3009ecdd53b4c3f537147cba67b7f35fb93b77 100644 --- a/corpus/styles/cold/read/page-000.navigation.json +++ b/corpus/styles/cold/read/page-000.navigation.json @@ -6,7 +6,7 @@ "h": 600, "error": null, "win": { - "title": "Heading 1 in Manuscript - Microsoft OneNote", + "title": "Heading 1 in OneNote - Microsoft OneNote", "class": "Framework::CFrame", "dialog": false } diff --git a/corpus/styles/cold/read/page-000.png b/corpus/styles/cold/read/page-000.png index cf74c1e244c403efb231e2bd111d7986ab78ec70..f0054128fc722539634e17b44dce82dca4a2b9e9 100644 Binary files a/corpus/styles/cold/read/page-000.png and b/corpus/styles/cold/read/page-000.png differ diff --git a/corpus/styles/cold/read/page-000.xml b/corpus/styles/cold/read/page-000.xml index 0a3a9b4653d5d2510d1851ed2749ae3eecf056d9..c23fa4941ab433dbb201a60b7632820b5dafeca8 100644 --- a/corpus/styles/cold/read/page-000.xml +++ b/corpus/styles/cold/read/page-000.xml @@ -1,7 +1,8 @@  -Heading 2 in Manuscript]]>Heading 3 in Manuscript]]>Heading 4 in Manuscript]]>Heading 6 in Manuscript]]>Quote in Manuscript]]> +Heading 1 in OneNote]]>Heading 2 in OneNote]]>Heading 3 in OneNote]]>Heading 4 in OneNote]]>Heading 6 in OneNote]]>Quote in OneNote]]> diff --git a/corpus/styles/cold/read/page-001.navigation.json b/corpus/styles/cold/read/page-001.navigation.json index eb3009ecdd53b4c3f537147cba67b7f35fb93b77..fbef07913da8ee5d5dac2283fcacb7e449ba65d5 100644 --- a/corpus/styles/cold/read/page-001.navigation.json +++ b/corpus/styles/cold/read/page-001.navigation.json @@ -6,7 +6,7 @@ "h": 600, "error": null, "win": { - "title": "Heading 1 in OneNote - Microsoft OneNote", + "title": "Heading 1 in Modern - Microsoft OneNote", "class": "Framework::CFrame", "dialog": false } diff --git a/corpus/styles/cold/read/page-001.png b/corpus/styles/cold/read/page-001.png index a5eb0b44677bb5f2dc2c2b6d278330c5775261da..52782aac431eb65ef6fa40043f7bd444cc753297 100644 Binary files a/corpus/styles/cold/read/page-001.png and b/corpus/styles/cold/read/page-001.png differ diff --git a/corpus/styles/cold/read/page-001.xml b/corpus/styles/cold/read/page-001.xml index ebc52a01470deea7c86e2d90066b47c2c3ebbfec..59154708f7de31d84f9bfc0d91e18e68ab1e4c4d 100644 --- a/corpus/styles/cold/read/page-001.xml +++ b/corpus/styles/cold/read/page-001.xml @@ -1,8 +1,9 @@  -Heading 1 in OneNote]]>Heading 2 in OneNote]]>Heading 3 in OneNote]]>Heading 4 in OneNote]]>Heading 6 in OneNote]]>Quote in OneNote]]> +Heading 1 in Modern]]>Heading 2 in Modern]]>Heading 3 in Modern]]>Heading 4 in Modern]]>Heading 5 in Modern]]>Heading 6 in Modern]]>Quote in Modern]]> diff --git a/corpus/styles/cold/read/page-002.png b/corpus/styles/cold/read/page-002.png index 235a6a47216251b5bd1ef8b008a07c93d72345c0..0f5ff46430e34aa7971d9584457be80632a4bdf8 100644 Binary files a/corpus/styles/cold/read/page-002.png and b/corpus/styles/cold/read/page-002.png differ diff --git a/corpus/styles/cold/read/page-002.xml b/corpus/styles/cold/read/page-002.xml index 58207e109ab7de0708bea486bea44f6486295835..7ed0b85e7bff9253474475955ecf43d03ea4501e 100644 --- a/corpus/styles/cold/read/page-002.xml +++ b/corpus/styles/cold/read/page-002.xml @@ -1,8 +1,8 @@  -Heading 1 in Editorial]]>Heading 2 in Editorial]]>Heading 3 in Editorial]]>Heading 4 in Editorial]]>Heading 6 in Editorial]]>Quote in Editorial]]> +Heading 1 in Editorial]]>Heading 2 in Editorial]]>Heading 3 in Editorial]]>Heading 4 in Editorial]]>Heading 6 in Editorial]]>Quote in Editorial]]> diff --git a/corpus/styles/cold/read/page-003.navigation.json b/corpus/styles/cold/read/page-003.navigation.json index fbef07913da8ee5d5dac2283fcacb7e449ba65d5..556fccc0ba9aafbacaa8b574185841ec2226b9ac 100644 --- a/corpus/styles/cold/read/page-003.navigation.json +++ b/corpus/styles/cold/read/page-003.navigation.json @@ -6,7 +6,7 @@ "h": 600, "error": null, "win": { - "title": "Heading 1 in Modern - Microsoft OneNote", + "title": "Heading 1 in Manuscript - Microsoft OneNote", "class": "Framework::CFrame", "dialog": false } diff --git a/corpus/styles/cold/read/page-003.png b/corpus/styles/cold/read/page-003.png index 6cab06bc59975eb692f514c892f4cc47c1807d3c..ed25a5304fec32984cfccceffdeecfb3d0bb35a6 100644 Binary files a/corpus/styles/cold/read/page-003.png and b/corpus/styles/cold/read/page-003.png differ diff --git a/corpus/styles/cold/read/page-003.xml b/corpus/styles/cold/read/page-003.xml index 60f71dd65b7be922721730dfa3f9c45bc3e4a52a..af3bcc55a623d5a9f6c81dbf9abf271be2f75db6 100644 --- a/corpus/styles/cold/read/page-003.xml +++ b/corpus/styles/cold/read/page-003.xml @@ -1,10 +1,7 @@  -Heading 1 in Modern]]>Heading 2 in Modern]]>Heading 3 in Modern]]>Heading 4 in Modern]]>Heading 5 in Modern]]>Heading 6 in Modern]]>Page Title in Modern]]>Quote in Modern]]> +Heading 2 in Manuscript]]>Heading 3 in Manuscript]]>Heading 4 in Manuscript]]>Heading 6 in Manuscript]]>Quote in Manuscript]]> diff --git a/corpus/styles/cold/run.json b/corpus/styles/cold/run.json index 8c605f3ac1747131b7482d6df45fe748c6729c44..ef5eb72df7edda326be0f35ce40f2e7ce099e1bc 100644 --- a/corpus/styles/cold/run.json +++ b/corpus/styles/cold/run.json @@ -1,7 +1,7 @@ { - "notebook": "/private/tmp/snowbound-styles/candidate", + "notebook": "/private/var/folders/90/4f4rngds01j55ggdj_vglk_c0000gn/T/styles-export", "expected_pages": 4, - "author": "/Users/clo/dev/one/tools/native/styles.ps1", + "author": "/Users/clo/dev/one-styles-review/tools/native/styles.ps1", "author_timeout_seconds": 600, "inspect": false, "collect_notebook": true, diff --git a/corpus/styles/cold/source.json b/corpus/styles/cold/source.json index 3788e618b7f47dc909b3ea59855c8f75ef059db2..4dbea11f7f6a2a93629a4acd850414502f94cecf 100644 --- a/corpus/styles/cold/source.json +++ b/corpus/styles/cold/source.json @@ -1,38 +1,38 @@ [ { "path": ".snowbound/themes.json", - "bytes": 669, - "sha256": "0ede06c023cbdfa06fe307225baeb461f5a70e6ce368ae69dec9d405ef5822ac", - "mtime_ns": 1790821816933321351 + "bytes": 671, + "sha256": "d6b191076a9f833cb3e79a38121ed9d1fe29a405a79138969fb88b0b43066594", + "mtime_ns": 1790988700942364730 }, { "path": "Editorial.one", "bytes": 12096, - "sha256": "0f65b622cdbeee22b146a6259074aeb4fd2b398d2bf982c7d712db31d190b6d1", - "mtime_ns": 1790821816932162894 + "sha256": "79f3d171ad78db53de276119c81f2fe4b6e298a6554494f407cd74436d43ecec", + "mtime_ns": 1790988700940784899 }, { "path": "Manuscript.one", "bytes": 12120, - "sha256": "109d595430cf434fabcab11bfe233a34b5182a4b48f8cc5146885ee22936f636", - "mtime_ns": 1790821816931895020 + "sha256": "f4d25c78ed8537f486afa7e4e1853f9816895dc5d794c598248de649fac851ef", + "mtime_ns": 1790988700940457857 }, { "path": "Modern.one", "bytes": 12000, - "sha256": "9ea5680d90a9b44bd036d4d50f5bcaae5b8f8ac505af4623e0ac3d0488b09ce2", - "mtime_ns": 1790821816932649894 + "sha256": "a91513231304e0a27e6896cf374cff98ddc4528a479622f0cbfb7577da151986", + "mtime_ns": 1790988700941597439 }, { "path": "OneNote.one", "bytes": 12072, - "sha256": "5b019d8246db5a2728de5502d30b2a1bfe7a0a5d376c1a5d15ce98ac551ed210", - "mtime_ns": 1790821816932870935 + "sha256": "1f84914801b00eb77a201c148146afc5e2544c207528710d70937833750ab300", + "mtime_ns": 1790988700941853856 }, { "path": "Open Notebook.onetoc2", "bytes": 2036, - "sha256": "9f613cf0b5db95b8809fe7d325cae9c37e8b9ae57fa3fff3b7d669649f1b416c", - "mtime_ns": 1790821816932392186 + "sha256": "a89533b7656fd9f5db22dadec54550a520b1b77980aad8398a7f3126f82b9d00", + "mtime_ns": 1790988700941091815 } ] diff --git a/crates/mobile/src/library.rs b/crates/mobile/src/library.rs index 993caaf77378bbc7540ffd8499c1eb0202b9fe7d..d3f48c587cba03bd6462f4214d453532112cf41c 100644 --- a/crates/mobile/src/library.rs +++ b/crates/mobile/src/library.rs @@ -1567,8 +1567,17 @@ pub unsafe extern "C" fn sb_notebook_create( time: *const c_char, error: *mut *mut c_char, ) -> bool { + use notebook::sidecar::themes::{Assignment, DEFAULT, Scope, Themes}; let created = dated(&string(author), &string(date), &string(time)).and_then(|page| { - Notebook::create(string(path), string(cache), Notebook::NEW_COLOR, &page)?; + let notebook = Notebook::create(string(path), string(cache), Notebook::NEW_COLOR, &page)?; + notebook.save_themes(Themes { + assignments: vec![Assignment { + scope: Scope::Notebook, + theme: Some(DEFAULT.into()), + assigned: filetime(), + }], + ..Default::default() + })?; Ok(()) }); created.map_err(|cause| failed(cause, error)).is_ok() diff --git a/crates/notebook/src/sidecar/themes.rs b/crates/notebook/src/sidecar/themes.rs index 610c782049d5cfe59613f0033782733e24a3b564..5d93db7125914171107fd00a915b6c78a8005e5d 100644 --- a/crates/notebook/src/sidecar/themes.rs +++ b/crates/notebook/src/sidecar/themes.rs @@ -112,8 +112,12 @@ impl Themes { all } + /// The theme `id` names, retired built-ins too, which pages may still wear. pub fn theme(&self, id: &str) -> Option { - self.all().into_iter().find(|theme| theme.id == id) + shipped() + .into_iter() + .chain(self.themes.iter().filter(|theme| !theme.deleted).cloned()) + .find(|theme| theme.id == id) } /// The theme `scope` names itself, if one it can find. @@ -224,8 +228,9 @@ pub(crate) fn write(storage: &dyn Storage, change: Themes) -> Result { Err(io::Error::from(io::ErrorKind::ResourceBusy).into()) } -fn is_built_in(id: &str) -> bool { - built_in().iter().any(|theme| theme.id == id) +/// Whether `id` names a theme Snowbound ships, or once offered. +pub fn is_built_in(id: &str) -> bool { + shipped().iter().any(|theme| theme.id == id) } /// The accent of a section coloured `section` (a COLORREF; none for OneNote's None), as a @@ -350,10 +355,22 @@ const ITALIC: [bool; 2] = [false, true]; const BOTH: [bool; 2] = [true, true]; const NONE: [f32; 2] = [0.0; 2]; -/// The themes Snowbound ships. Once shipped a built-in never changes: two versions -/// disagreeing would restyle a page back and forth as each opens it. A changed look ships -/// under a new id. +/// The theme new notebooks take unless the user chooses another. +pub const DEFAULT: &str = "modern-2"; + +/// Built-ins a page wears still but no longer offered: a changed look ships under a new id. +const RETIRED: [&str; 1] = ["modern"]; + +/// The themes Snowbound offers, OneNote 2010's first. pub fn built_in() -> Vec { + let mut offered = shipped(); + offered.retain(|theme| !RETIRED.contains(&theme.id.as_str())); + offered +} + +/// Every theme Snowbound ever shipped. Once shipped a built-in's look never changes: two +/// versions disagreeing would restyle a page back and forth as each opens it. +fn shipped() -> Vec { let theme = |id: &str, name: &str, styles: [ThemeStyle; 11]| Theme { id: id.into(), name: name.into(), @@ -366,6 +383,11 @@ pub fn built_in() -> Vec { deleted: false, }; let blue = Some("#366092"); + let tinted = |style: ThemeStyle| ThemeStyle { + color: Some(color_hex(accent(None))), + accent: true, + ..style + }; vec![ // OneNote 2010's own (lab, 2026-09-30). theme( @@ -419,9 +441,27 @@ pub fn built_in() -> Vec { style("Calibri", 11.5, PLAIN, Some("#262626"), [0.0, 3.0]), ], ), + // OneNote's page title in Arial; its accent the section's. theme( - "modern", + "modern-2", "Modern", + [ + tinted(style("Arial", 16.0, BOLD, None, [10.0, 2.0])), + style("Arial", 13.0, BOLD, Some("#1B2631"), [8.0, 2.0]), + style("Arial", 11.0, BOLD, Some("#1B2631"), [6.0, 0.0]), + style("Arial", 10.5, BOTH, Some("#1B2631"), [6.0, 0.0]), + tinted(style("Arial", 10.5, BOLD, None, [6.0, 0.0])), + tinted(style("Arial", 10.5, ITALIC, None, [6.0, 0.0])), + style("Arial", 17.0, PLAIN, None, NONE), + style("Arial", 8.0, PLAIN, Some("#7B868C"), NONE), + style("Arial", 10.5, ITALIC, Some("#5F6B73"), [0.0, 3.0]), + style("Courier New", 10.0, PLAIN, Some("#1B2631"), NONE), + style("Arial", 10.5, PLAIN, Some("#2E2E2E"), [0.0, 3.0]), + ], + ), + theme( + "modern", + "Modern (original)", [ style("Arial", 16.0, BOLD, Some("#0E6E6E"), [10.0, 2.0]), style("Arial", 13.0, BOLD, Some("#1B2631"), [8.0, 2.0]), @@ -544,9 +584,32 @@ mod tests { assert_eq!(json["accent"], true); } + /// Modern's title is OneNote's in Arial. The Modern first shipped, its title bold, is + /// no longer offered, but pages given it keep it. + #[test] + fn modern_keeps_onenote_s_page_title_and_the_first_modern_stays() { + let [onenote, modern] = ["onenote", "modern-2"] + .map(|id| built_in().into_iter().find(|theme| theme.id == id).unwrap()); + let title = |theme: &Theme| definition("PageTitle", &theme.styles["PageTitle"], None); + let mut expected = title(&onenote); + expected.format.font = Some("Arial".into()); + assert_eq!(title(&modern), expected); + assert!(built_in().iter().all(|theme| theme.id != "modern")); + let themes = Themes { + assignments: vec![Assignment { + scope: Scope::Notebook, + theme: Some("modern".into()), + assigned: 1, + }], + ..Themes::default() + }; + assert!(themes.effective(None, None).unwrap().styles["PageTitle"].bold); + assert!(themes.all().iter().all(|theme| theme.id != "modern")); + } + #[test] fn every_built_in_theme_names_every_gallery_style() { - for theme in built_in() { + for theme in shipped() { let sheet = theme.sheet(None); for (name, _) in STYLES { assert!(sheet.contains_key(name), "{} {name}", theme.id); diff --git a/crates/snowbound/src/main.rs b/crates/snowbound/src/main.rs index 34ea45cde11a961956ad3bb00ece27b7be883b7e..1a7143234bb15f86f428031cea8c93d37a3818df 100644 --- a/crates/snowbound/src/main.rs +++ b/crates/snowbound/src/main.rs @@ -868,6 +868,8 @@ struct State { password: Option, /// Options' Passwords. passwords: settings::Passwords, + /// The theme new notebooks take, by id. + notebook_theme: Option, /// What the pointer is dragging: a page's tab, a section tab or a sidebar row. drag: Option, /// Pages whose template strip was dismissed this run. @@ -1248,6 +1250,7 @@ impl State { locked, password: None, passwords: stored.passwords, + notebook_theme: stored.notebook_theme.0, live_options: stored.live, drag: None, templates: templates::View::Strip, diff --git a/crates/snowbound/src/manage.rs b/crates/snowbound/src/manage.rs index 4c5d8964e20de98fd66cc741dde01a31ef8d8374..790e4595f29cdd3cb0c5944d99f0276cc622702d 100644 --- a/crates/snowbound/src/manage.rs +++ b/crates/snowbound/src/manage.rs @@ -432,11 +432,22 @@ impl State { ) -> Result<(), Box> { let page = self.dated_page(None)?; let (cache, notify) = (self.cache.clone(), notify(self.proxy.clone())); + let theme = self.notebook_theme.clone(); self.load(move || { let location = notebook::fs::absolute(&root)? .to_string_lossy() .into_owned(); let notebook = Notebook::create(&location, &cache, Notebook::NEW_COLOR, &page)?; + if theme.is_some() { + notebook.save_themes(notebook::sidecar::themes::Themes { + assignments: vec![notebook::sidecar::themes::Assignment { + scope: notebook::sidecar::themes::Scope::Notebook, + theme, + assigned: crate::filetime(), + }], + ..Default::default() + })?; + } let library = Arc::new(Library::created(&location, notebook, &cache)); let path = library .first_section() diff --git a/crates/snowbound/src/options.rs b/crates/snowbound/src/options.rs index 9b68e084f7ea983dd90fe5f49f3c48d821564f78..21b721ec48b64cd25f429bf474a5de621a32dce0 100644 --- a/crates/snowbound/src/options.rs +++ b/crates/snowbound/src/options.rs @@ -109,6 +109,14 @@ const SECTIONS: &[Section] = &[ }, ], }, + Group { + heading: "Default style theme", + rows: &[Row { + label: "New notebooks:", + keywords: "default for new notebooks styles headings modern", + control: Control::Field(notebook_theme), + }], + }, Group { heading: "Proofing", rows: &[Row { @@ -273,6 +281,7 @@ pub struct Options { picture: bool, /// Live Share's relay; empty uses Snowbound's. relay: String, + notebook_theme: Option, pub(crate) keyboard: crate::keys::Keyboard, } @@ -312,6 +321,10 @@ fn font_colors() -> Id { id().child("font-colors") } +fn notebook_themes() -> Id { + id().child("notebook-themes") +} + /// Whether `word`, lowercase, starts a word of `text`: "pen" finds "Pen" and "pen-like", /// not "Open". fn starts_word(text: &str, word: &str) -> bool { @@ -372,6 +385,7 @@ impl State { presence: self.live_options.presence, picture: self.live_options.picture, relay: self.live_options.relay.clone().unwrap_or_default(), + notebook_theme: self.notebook_theme.clone(), keyboard: crate::keys::Keyboard::new(), }); self.ui.open_popup(id()); @@ -679,6 +693,7 @@ impl State { lock_after: options.lock_idle.then_some(options.lock_minutes), lock_on_leave: options.lock_on_leave, }; + self.notebook_theme = options.notebook_theme; self.updates.set_automatic(options.automatic_updates); let relay = options.relay.trim(); self.live_options = crate::settings::Live { @@ -790,6 +805,39 @@ fn appearance(state: &mut State, options: &mut Options) { } } +fn notebook_theme(state: &mut State, options: &mut Options) { + let ui = &mut state.ui; + let combo = ui.id("combo"); + let themes = notebook::sidecar::themes::built_in(); + let choices: Vec<(Option<&str>, &str)> = std::iter::once((None, "No Theme")) + .chain( + themes + .iter() + .map(|theme| (Some(theme.id.as_str()), theme.name.as_str())), + ) + .collect(); + let chosen = options.notebook_theme.as_deref(); + let current = choices + .iter() + .find(|(id, _)| *id == chosen) + .map_or("", |(_, name)| name); + let title = "Default for new notebooks"; + ui::shell::combo(ui, "combo", title, current, 140.0, notebook_themes(), true); + let items: Vec = choices + .iter() + .map(|(id, name)| Item { + text: name, + checked: Some(*id == chosen), + current: *id == chosen, + ..Item::default() + }) + .collect(); + let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default()); + if let Some(index) = ui::popup::menu(ui, notebook_themes(), anchor, &items, None) { + options.notebook_theme = choices[index].0.map(Into::into); + } +} + fn lock_after(state: &mut State, options: &mut Options) { let ui = &mut state.ui; let combo = ui.id("combo"); diff --git a/crates/snowbound/src/settings.rs b/crates/snowbound/src/settings.rs index de22af5a31355bc0ddbde5b337c4c170644d6390..b9326922040d493e3a1d6193971dd3f9c1f5f12e 100644 --- a/crates/snowbound/src/settings.rs +++ b/crates/snowbound/src/settings.rs @@ -52,6 +52,8 @@ pub struct Settings { pub recent: Vec, pub passwords: Passwords, pub live: Live, + /// Options' "Default for new notebooks": the built-in theme new notebooks take. + pub notebook_theme: NotebookTheme, } /// Options' Live Share. @@ -76,6 +78,17 @@ impl Default for Live { } } +/// A built-in theme by id, Modern until chosen otherwise; none leaves notebooks OneNote's. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct NotebookTheme(pub Option); + +impl Default for NotebookTheme { + fn default() -> Self { + Self(Some(notebook::sidecar::themes::DEFAULT.into())) + } +} + /// Options' Passwords, as OneNote 2010's Advanced page keeps them. #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] #[serde(default)] @@ -217,6 +230,7 @@ impl crate::State { keys: crate::commands::Keymap::current().saved(), recent: self.trail.recent.clone(), passwords: self.passwords, + notebook_theme: NotebookTheme(self.notebook_theme.clone()), live: self.live_options.clone(), }; if let Err(error) = settings.save(path) { @@ -307,6 +321,7 @@ mod tests { picture: false, relay: Some("wss://relay.example.net".into()), }, + notebook_theme: NotebookTheme(None), }; settings.save(&path).unwrap(); assert_eq!(Settings::load(&path), settings); @@ -323,6 +338,9 @@ mod tests { let toolbar = Settings::load(&path).toolbar; assert_eq!(toolbar.highlight, None); assert_eq!(toolbar.font_color, Toolbar::default().font_color); + // New notebooks take Modern until another theme, or none, is picked. + let theme = Settings::load(&path).notebook_theme; + assert_eq!(theme.0.as_deref(), Some(notebook::sidecar::themes::DEFAULT)); notebook::fs::remove_dir_all(&directory).unwrap(); } } diff --git a/crates/snowbound/src/themes.rs b/crates/snowbound/src/themes.rs index ec6557246b06bd5e5b6202af376ab370570de245..5cbd818d42c4a6635b636eaea2f346777b27fa79 100644 --- a/crates/snowbound/src/themes.rs +++ b/crates/snowbound/src/themes.rs @@ -183,10 +183,16 @@ impl State { section: Option, ) { let themes = library.themes(); - let assigned = themes.assigned(&target).map(|theme| theme.id); - let all = themes.all(); + let assigned = themes.assigned(&target); + let mut all = themes.all(); + // A retired built-in the scope wears is listed while it does. + if let Some(assigned) = &assigned + && !all.contains(assigned) + { + all.push(assigned.clone()); + } let selected = assigned - .and_then(|id| all.iter().position(|theme| theme.id == id)) + .and_then(|assigned| all.iter().position(|theme| theme.id == assigned.id)) .unwrap_or(0); self.themes = Some(Dialog { scope, @@ -315,9 +321,9 @@ impl State { ..Spec::default() }, ); + let built = stored::is_built_in(&dialog.themes[dialog.selected].id); let duplicate = ui::button(ui, "duplicate", "Duplicate").clicked; - let delete = - dialog.selected >= built_in().len() && ui::button(ui, "delete", "Delete").clicked; + let delete = !built && ui::button(ui, "delete", "Delete").clicked; ui.close(); ui.close(); // The selected theme's styles. @@ -330,7 +336,6 @@ impl State { ..Spec::default() }, ); - let built = dialog.selected < built_in().len(); if built { ui.leaf( "locked", @@ -591,7 +596,7 @@ impl State { copy(dialog, name); } if let Some(change) = change { - if dialog.selected < built_in().len() { + if built { let name = format!("My {}", dialog.themes[dialog.selected].name); copy(dialog, name); } @@ -606,7 +611,7 @@ impl State { dialog.changed.push(edited.id.clone()); } } - if delete && dialog.selected >= built_in().len() { + if delete { let mut gone = dialog.themes.remove(dialog.selected); gone.deleted = true; dialog.deleted.push(gone); diff --git a/tools/test_styles.py b/tools/test_styles.py index d86bab10e65cfd6c4b79162838bb90335d6eb144..64ea381929e7678b3ef6c2d9ffccd79d49ad3024 100644 --- a/tools/test_styles.py +++ b/tools/test_styles.py @@ -35,7 +35,7 @@ class StylesTest(unittest.TestCase): 'OneNote': ('Calibri', 16.0, True, '#17365D'), 'Manuscript': ('Georgia', 20.0, False, '#3B2F2A'), 'Editorial': ('Georgia', 18.0, True, '#9A3B1F'), - 'Modern': ('Arial', 16.0, True, '#0E6E6E'), + 'Modern': ('Arial', 16.0, True, '#2E5CB8'), } seen = set() for path in sorted((FIXTURE / 'cold/read').glob('page-*.xml')): @@ -44,6 +44,9 @@ class StylesTest(unittest.TestCase): seen.add(theme) self.assertEqual(set(defined), GALLERY, theme) self.assertEqual(defined['h1'], [heading[theme]], theme) + if theme == 'Modern': + # OneNote's own page title, in Arial. + self.assertEqual(defined['PageTitle'], [('Arial', 17.0, False, 'automatic')]) self.assertEqual(seen, set(heading)) def test_onenote_s_own_styles_read_under_the_same_names(self):