| author | |
| committer | |
| log | c09590a9c59a6645ff9cbe96e0037000405a8bf7 |
| tree | 3b5fdf2780fa9664f2cb9faeea03c7f70eb846ee |
| parent | 0fe456a8909d9b9991afbe28c28f915d031ca44f |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
Clover: New Notebook isn't about the notebook right-clicked, so its menu drops
it. Sections gain OneNote 2010's Copy Link to Section, as the lab's OneNote
copies it. New Section, New Section Group and Customize Font Styles are
disabled where the notebook has no catalog or the section no identity, and the
page's Cut, Paste, Link, Remove Link and equation forms where the page takes
no edits.
Assisted-by: claude-opus-5.52 files changed, 98 insertions(+), 50 deletions(-)
crates/snowbound/src/link.rs+60-30| ... | @@ -59,26 +59,9 @@ fn system_url(address: &str) -> String { | ... | @@ -59,26 +59,9 @@ fn system_url(address: &str) -> String { |
| 59 | } | 59 | } |
| 60 | } | 60 | } |
| 61 | 61 | ||
| 62 | /// A page, or a paragraph on it, as Copy Link to Page gives it: the section file's path | 62 | /// A section, page or paragraph as Copy Link gives it: the section file's path, then the |
| 63 | /// before the title, then the identities OneNote finds it by. | 63 | /// title and identities OneNote finds it by. |
| 64 | fn clipboard_link( | 64 | fn clipboard_link(path: &str, section: [u8; 16], target: LinkTarget) -> String { |
| 65 | path: &str, | ||
| 66 | section: [u8; 16], | ||
| 67 | title: &str, | ||
| 68 | page: [u8; 16], | ||
| 69 | object: Option<ExGuid>, | ||
| 70 | ) -> String { | ||
| 71 | let target = match object { | ||
| 72 | Some(object) => LinkTarget::Object { | ||
| 73 | identity: page, | ||
| 74 | title, | ||
| 75 | object, | ||
| 76 | }, | ||
| 77 | None => LinkTarget::Page { | ||
| 78 | identity: page, | ||
| 79 | title, | ||
| 80 | }, | ||
| 81 | }; | ||
| 82 | let stored = internal_link(section, "", target); | 65 | let stored = internal_link(section, "", target); |
| 83 | let fragment = stored | 66 | let fragment = stored |
| 84 | .strip_prefix("onenote:#") | 67 | .strip_prefix("onenote:#") |
| ... | @@ -335,6 +318,9 @@ impl State { | ... | @@ -335,6 +318,9 @@ impl State { |
| 335 | }; | 318 | }; |
| 336 | let corrections = items.len(); | 319 | let corrections = items.len(); |
| 337 | let format = self.format_state(); | 320 | let format = self.format_state(); |
| 321 | let disabled = |id| !self.status(&commands::Choice::Command(id), &format).enabled; | ||
| 322 | // Paste stands for any edit to the text here. | ||
| 323 | let (fixed, unlinkable) = (disabled(commands::Id::Paste), disabled(commands::Id::Link)); | ||
| 338 | let lists = [commands::Id::ToDoList, commands::Id::BulletedList] | 324 | let lists = [commands::Id::ToDoList, commands::Id::BulletedList] |
| 339 | .into_iter() | 325 | .into_iter() |
| 340 | .filter(|id| { | 326 | .filter(|id| { |
| ... | @@ -348,7 +334,7 @@ impl State { | ... | @@ -348,7 +334,7 @@ impl State { |
| 348 | } else { | 334 | } else { |
| 349 | let mut items = vec![ | 335 | let mut items = vec![ |
| 350 | Item { | 336 | Item { |
| 351 | disabled: !context.selected, | 337 | disabled: !context.selected || fixed, |
| 352 | separated: corrections > 0, | 338 | separated: corrections > 0, |
| 353 | ..drawn("Cut", art::CUT) | 339 | ..drawn("Cut", art::CUT) |
| 354 | }, | 340 | }, |
| ... | @@ -356,12 +342,16 @@ impl State { | ... | @@ -356,12 +342,16 @@ impl State { |
| 356 | disabled: !context.selected, | 342 | disabled: !context.selected, |
| 357 | ..drawn("Copy", art::COPY) | 343 | ..drawn("Copy", art::COPY) |
| 358 | }, | 344 | }, |
| 359 | drawn("Paste", art::PASTE), | 345 | Item { |
| 346 | disabled: fixed, | ||
| 347 | ..drawn("Paste", art::PASTE) | ||
| 348 | }, | ||
| 360 | ]; | 349 | ]; |
| 361 | match &context.link { | 350 | match &context.link { |
| 362 | Some(_) => items.extend([ | 351 | Some(_) => items.extend([ |
| 363 | Item { | 352 | Item { |
| 364 | separated: true, | 353 | separated: true, |
| 354 | disabled: unlinkable, | ||
| 365 | ..drawn("Edit Link", art::LINK) | 355 | ..drawn("Edit Link", art::LINK) |
| 366 | }, | 356 | }, |
| 367 | drawn("Copy Link to Paragraph", art::COPY_LINK), | 357 | drawn("Copy Link to Paragraph", art::COPY_LINK), |
| ... | @@ -370,11 +360,15 @@ impl State { | ... | @@ -370,11 +360,15 @@ impl State { |
| 370 | ..drawn("Copy Link", art::COPY_LINK) | 360 | ..drawn("Copy Link", art::COPY_LINK) |
| 371 | }, | 361 | }, |
| 372 | drawn("Select Link", art::SELECT), | 362 | drawn("Select Link", art::SELECT), |
| 373 | drawn("Remove Link", art::REMOVE_LINK), | 363 | Item { |
| 364 | disabled: unlinkable, | ||
| 365 | ..drawn("Remove Link", art::REMOVE_LINK) | ||
| 366 | }, | ||
| 374 | ]), | 367 | ]), |
| 375 | None => items.extend([ | 368 | None => items.extend([ |
| 376 | Item { | 369 | Item { |
| 377 | separated: true, | 370 | separated: true, |
| 371 | disabled: unlinkable, | ||
| 378 | ..drawn("Link", art::LINK) | 372 | ..drawn("Link", art::LINK) |
| 379 | }, | 373 | }, |
| 380 | drawn("Copy Link to Paragraph", art::COPY_LINK), | 374 | drawn("Copy Link to Paragraph", art::COPY_LINK), |
| ... | @@ -391,9 +385,13 @@ impl State { | ... | @@ -391,9 +385,13 @@ impl State { |
| 391 | items.extend([ | 385 | items.extend([ |
| 392 | Item { | 386 | Item { |
| 393 | separated: true, | 387 | separated: true, |
| 388 | disabled: fixed, | ||
| 394 | ..drawn("Professional", art::EQUATION) | 389 | ..drawn("Professional", art::EQUATION) |
| 395 | }, | 390 | }, |
| 396 | drawn("Linear", art::EQUATION), | 391 | Item { |
| 392 | disabled: fixed, | ||
| 393 | ..drawn("Linear", art::EQUATION) | ||
| 394 | }, | ||
| 397 | ]); | 395 | ]); |
| 398 | } | 396 | } |
| 399 | items | 397 | items |
| ... | @@ -489,12 +487,30 @@ impl State { | ... | @@ -489,12 +487,30 @@ impl State { |
| 489 | .iter() | 487 | .iter() |
| 490 | .find(|(listed, ..)| *listed == space) | 488 | .find(|(listed, ..)| *listed == space) |
| 491 | .map_or(page.title.as_str(), |(_, title, _)| title.as_str()); | 489 | .map_or(page.title.as_str(), |(_, title, _)| title.as_str()); |
| 490 | let target = match object { | ||
| 491 | Some(object) => LinkTarget::Object { | ||
| 492 | identity, | ||
| 493 | title, | ||
| 494 | object, | ||
| 495 | }, | ||
| 496 | None => LinkTarget::Page { identity, title }, | ||
| 497 | }; | ||
| 492 | Ok(clipboard_link( | 498 | Ok(clipboard_link( |
| 493 | &session.section.file().to_string_lossy(), | 499 | &session.section.file().to_string_lossy(), |
| 494 | session.section.identity()?, | 500 | session.section.identity()?, |
| 495 | title, | 501 | target, |
| 502 | )) | ||
| 503 | } | ||
| 504 | |||
| 505 | /// Copy Link to Section: the link OneNote 2010 puts on the clipboard for the section at | ||
| 506 | /// catalog `path`; none for one without a file here. | ||
| 507 | pub(crate) fn section_link(&self, library: &crate::Library, path: &str) -> Option<String> { | ||
| 508 | let file = library.folder()?.join(path); | ||
| 509 | let identity = library.section_identity(path)?; | ||
| 510 | Some(clipboard_link( | ||
| 511 | &file.to_string_lossy(), | ||
| 496 | identity, | 512 | identity, |
| 497 | object, | 513 | LinkTarget::Section, |
| 498 | )) | 514 | )) |
| 499 | } | 515 | } |
| 500 | 516 | ||
| ... | @@ -541,13 +557,27 @@ mod tests { | ... | @@ -541,13 +557,27 @@ mod tests { |
| 541 | } | 557 | } |
| 542 | 558 | ||
| 543 | /// OneNote 2010's Copy Link to Page (`corpus/link-edit/native-typed`, README): the file's | 559 | /// OneNote 2010's Copy Link to Page (`corpus/link-edit/native-typed`, README): the file's |
| 544 | /// path, the title, and the section and page identities. | 560 | /// path, the title, and the section and page identities; and its Copy Link to Section, |
| 561 | /// as the lab's OneNote 2010 copied one, the path's spaces escaped. | ||
| 545 | #[test] | 562 | #[test] |
| 546 | fn page_links_copy_as_onenote_copies_them() { | 563 | fn links_copy_as_onenote_copies_them() { |
| 547 | let section = *b"\x25\xfc\x1e\x0f\x7c\xdf\xcb\x45\x87\xa4\xbd\xd0\x3f\x42\x05\x7e"; | 564 | let section = *b"\x25\xfc\x1e\x0f\x7c\xdf\xcb\x45\x87\xa4\xbd\xd0\x3f\x42\x05\x7e"; |
| 548 | let page = *b"\x18\xa3\xcc\xa9\x3e\xa4\x12\x49\xa1\xc7\xf1\x9f\x1e\xaf\x30\xca"; | 565 | let identity = *b"\x18\xa3\xcc\xa9\x3e\xa4\x12\x49\xa1\xc7\xf1\x9f\x1e\xaf\x30\xca"; |
| 566 | let page = LinkTarget::Page { | ||
| 567 | identity, | ||
| 568 | title: "Links", | ||
| 569 | }; | ||
| 570 | let general = *b"\x1b\x79\xcd\xe4\x17\x94\x4f\x4b\x99\x3a\x34\xfe\xb3\xb2\xa8\x4d"; | ||
| 571 | assert_eq!( | ||
| 572 | clipboard_link( | ||
| 573 | "C:\\Users\\clover\\Documents\\OneNote Notebooks\\Personal\\General.one", | ||
| 574 | general, | ||
| 575 | LinkTarget::Section | ||
| 576 | ), | ||
| 577 | "onenote:///C:\\Users\\clover\\Documents\\OneNote%20Notebooks\\Personal\\General.one#section-id={E4CD791B-9417-4B4F-993A-34FEB3B2A84D}&end" | ||
| 578 | ); | ||
| 549 | assert_eq!( | 579 | assert_eq!( |
| 550 | clipboard_link("C:\\one\\links.one", section, "Links", page, None), | 580 | clipboard_link("C:\\one\\links.one", section, page), |
| 551 | "onenote:///C:\\one\\links.one#Links&section-id={0F1EFC25-DF7C-45CB-87A4-BDD03F42057E}&page-id={A9CCA318-A43E-4912-A1C7-F19F1EAF30CA}&end" | 581 | "onenote:///C:\\one\\links.one#Links&section-id={0F1EFC25-DF7C-45CB-87A4-BDD03F42057E}&page-id={A9CCA318-A43E-4912-A1C7-F19F1EAF30CA}&end" |
| 552 | ); | 582 | ); |
| 553 | } | 583 | } |
crates/snowbound/src/menus.rs+38-20| ... | @@ -116,7 +116,6 @@ pub enum Action { | ... | @@ -116,7 +116,6 @@ pub enum Action { |
| 116 | /// A section's colour, COLORREF; none is OneNote's None. | 116 | /// A section's colour, COLORREF; none is OneNote's None. |
| 117 | Color(Option<u32>), | 117 | Color(Option<u32>), |
| 118 | Sync, | 118 | Sync, |
| 119 | NewNotebook, | ||
| 120 | /// Moves a notebook up the notebook list, or down. | 119 | /// Moves a notebook up the notebook list, or down. |
| 121 | Raise(bool), | 120 | Raise(bool), |
| 122 | Properties, | 121 | Properties, |
| ... | @@ -154,7 +153,6 @@ impl Action { | ... | @@ -154,7 +153,6 @@ impl Action { |
| 154 | Action::Theme => art::STYLES, | 153 | Action::Theme => art::STYLES, |
| 155 | Action::Colors => art::SECTION_COLOR, | 154 | Action::Colors => art::SECTION_COLOR, |
| 156 | Action::Sync => art::SYNC_NOW, | 155 | Action::Sync => art::SYNC_NOW, |
| 157 | Action::NewNotebook => art::NEW_NOTEBOOK, | ||
| 158 | Action::Raise(true) => art::MOVE_UP, | 156 | Action::Raise(true) => art::MOVE_UP, |
| 159 | Action::Raise(false) => art::MOVE_DOWN, | 157 | Action::Raise(false) => art::MOVE_DOWN, |
| 160 | Action::Properties => art::PROPERTIES, | 158 | Action::Properties => art::PROPERTIES, |
| ... | @@ -335,9 +333,9 @@ impl State { | ... | @@ -335,9 +333,9 @@ impl State { |
| 335 | }; | 333 | }; |
| 336 | let nowhere = || self.destinations(target).is_empty(); | 334 | let nowhere = || self.destinations(target).is_empty(); |
| 337 | // Last on a page, section or notebook, under a rule. | 335 | // Last on a page, section or notebook, under a rule. |
| 338 | let styles = || { | 336 | let styles = |library: &Library| { |
| 339 | let title = commands::command(commands::Id::Themes).title; | 337 | let title = commands::command(commands::Id::Themes).title; |
| 340 | item(Action::Theme, title, false, true) | 338 | item(Action::Theme, title, library.catalog().is_none(), true) |
| 341 | }; | 339 | }; |
| 342 | match target { | 340 | match target { |
| 343 | // OneNote's Move or Copy takes a page out of the bin; nothing else changes it. | 341 | // OneNote's Move or Copy takes a page out of the bin; nothing else changes it. |
| ... | @@ -369,7 +367,7 @@ impl State { | ... | @@ -369,7 +367,7 @@ impl State { |
| 369 | // Levels and versions are known once the section is open. | 367 | // Levels and versions are known once the section is open. |
| 370 | let Some(session) = self.session.as_ref().filter(|_| self.open(library, path)) | 368 | let Some(session) = self.session.as_ref().filter(|_| self.open(library, path)) |
| 371 | else { | 369 | else { |
| 372 | actions.push(styles()); | 370 | actions.push(styles(library)); |
| 373 | return actions; | 371 | return actions; |
| 374 | }; | 372 | }; |
| 375 | let pages = &session.pages; | 373 | let pages = &session.pages; |
| ... | @@ -398,12 +396,13 @@ impl State { | ... | @@ -398,12 +396,13 @@ impl State { |
| 398 | session.page_versions(*space).is_empty(), | 396 | session.page_versions(*space).is_empty(), |
| 399 | true, | 397 | true, |
| 400 | ), | 398 | ), |
| 401 | styles(), | 399 | styles(library), |
| 402 | ]); | 400 | ]); |
| 403 | actions | 401 | actions |
| 404 | } | 402 | } |
| 405 | Target::Section { library, path } | Target::Group { library, path } => { | 403 | Target::Section { library, path } | Target::Group { library, path } => { |
| 406 | let section = matches!(target, Target::Section { .. }); | 404 | let section = matches!(target, Target::Section { .. }); |
| 405 | let cataloged = library.catalog().is_some(); | ||
| 407 | let mut actions = vec![item(Action::Rename, "Rename", false, false)]; | 406 | let mut actions = vec![item(Action::Rename, "Rename", false, false)]; |
| 408 | if section { | 407 | if section { |
| 409 | actions.push(item(Action::SaveAs, "Save As", false, false)); | 408 | actions.push(item(Action::SaveAs, "Save As", false, false)); |
| ... | @@ -411,15 +410,21 @@ impl State { | ... | @@ -411,15 +410,21 @@ impl State { |
| 411 | actions.extend([ | 410 | actions.extend([ |
| 412 | item(Action::Delete, "Delete", false, false), | 411 | item(Action::Delete, "Delete", false, false), |
| 413 | item(Action::Move, "Move", nowhere(), false), | 412 | item(Action::Move, "Move", nowhere(), false), |
| 414 | item(Action::NewSection, "New Section", false, true), | 413 | ]); |
| 415 | item(Action::NewGroup, "New Section Group", false, false), | 414 | if section { |
| 415 | let link = self.section_link(library, path).is_none(); | ||
| 416 | actions.push(item(Action::CopyLink, "Copy Link to Section", link, true)); | ||
| 417 | } | ||
| 418 | actions.extend([ | ||
| 419 | item(Action::NewSection, "New Section", !cataloged, true), | ||
| 420 | item(Action::NewGroup, "New Section Group", !cataloged, false), | ||
| 416 | ]); | 421 | ]); |
| 417 | if section { | 422 | if section { |
| 418 | actions.extend([ | 423 | actions.extend([ |
| 419 | item( | 424 | item( |
| 420 | Action::Password, | 425 | Action::Password, |
| 421 | "Password Protect This Section", | 426 | "Password Protect This Section", |
| 422 | library.catalog().is_none(), | 427 | !cataloged, |
| 423 | true, | 428 | true, |
| 424 | ), | 429 | ), |
| 425 | item(Action::Colors, "Section Color", false, false), | 430 | item(Action::Colors, "Section Color", false, false), |
| ... | @@ -429,7 +434,12 @@ impl State { | ... | @@ -429,7 +434,12 @@ impl State { |
| 429 | section_file(library, path).is_none(), | 434 | section_file(library, path).is_none(), |
| 430 | false, | 435 | false, |
| 431 | ), | 436 | ), |
| 432 | styles(), | 437 | item( |
| 438 | Action::Theme, | ||
| 439 | commands::command(commands::Id::Themes).title, | ||
| 440 | library.section_identity(path).is_none(), | ||
| 441 | true, | ||
| 442 | ), | ||
| 433 | ]); | 443 | ]); |
| 434 | } | 444 | } |
| 435 | actions | 445 | actions |
| ... | @@ -467,12 +477,16 @@ impl State { | ... | @@ -467,12 +477,16 @@ impl State { |
| 467 | library.catalog().is_none() || library.joined.is_some(), | 477 | library.catalog().is_none() || library.joined.is_some(), |
| 468 | false, | 478 | false, |
| 469 | ), | 479 | ), |
| 470 | item(Action::NewSection, "New Section", false, true), | ||
| 471 | item(Action::NewGroup, "New Section Group", false, false), | ||
| 472 | item( | 480 | item( |
| 473 | Action::NewNotebook, | 481 | Action::NewSection, |
| 474 | commands::command(commands::Id::NewNotebook).title, | 482 | "New Section", |
| 475 | false, | 483 | library.catalog().is_none(), |
| 484 | true, | ||
| 485 | ), | ||
| 486 | item( | ||
| 487 | Action::NewGroup, | ||
| 488 | "New Section Group", | ||
| 489 | library.catalog().is_none(), | ||
| 476 | false, | 490 | false, |
| 477 | ), | 491 | ), |
| 478 | item( | 492 | item( |
| ... | @@ -511,7 +525,7 @@ impl State { | ... | @@ -511,7 +525,7 @@ impl State { |
| 511 | library.catalog().is_none(), | 525 | library.catalog().is_none(), |
| 512 | false, | 526 | false, |
| 513 | ), | 527 | ), |
| 514 | styles(), | 528 | styles(library), |
| 515 | ] | 529 | ] |
| 516 | } | 530 | } |
| 517 | Target::Closed(_) | Target::Server(_) => { | 531 | Target::Closed(_) | Target::Server(_) => { |
| ... | @@ -779,6 +793,14 @@ impl State { | ... | @@ -779,6 +793,14 @@ impl State { |
| 779 | library, | 793 | library, |
| 780 | Structure::Color { path, color }, | 794 | Structure::Color { path, color }, |
| 781 | )), | 795 | )), |
| 796 | (Target::Section { library, path }, Action::CopyLink) => { | ||
| 797 | if let Some(link) = self.section_link(&library, &path) | ||
| 798 | && let Err(error) = self.clipboard.set_text(link) | ||
| 799 | { | ||
| 800 | eprintln!("Copying the link failed: {error}"); | ||
| 801 | } | ||
| 802 | None | ||
| 803 | } | ||
| 782 | (Target::Section { library, path }, Action::Reveal) => { | 804 | (Target::Section { library, path }, Action::Reveal) => { |
| 783 | if let Some(file) = section_file(&library, &path) { | 805 | if let Some(file) = section_file(&library, &path) { |
| 784 | platform::show_file(&file); | 806 | platform::show_file(&file); |
| ... | @@ -825,10 +847,6 @@ impl State { | ... | @@ -825,10 +847,6 @@ impl State { |
| 825 | } | 847 | } |
| 826 | None | 848 | None |
| 827 | } | 849 | } |
| 828 | (Target::Notebook(_), Action::NewNotebook) => { | ||
| 829 | self.choose(Choice::Command(commands::Id::NewNotebook)); | ||
| 830 | None | ||
| 831 | } | ||
| 832 | (Target::Notebook(library), Action::Raise(up)) => { | 850 | (Target::Notebook(library), Action::Raise(up)) => { |
| 833 | let at = (self.notebooks.iter()).position(|open| Arc::ptr_eq(open, &library)); | 851 | let at = (self.notebooks.iter()).position(|open| Arc::ptr_eq(open, &library)); |
| 834 | let to = at.and_then(|at| if up { at.checked_sub(1) } else { Some(at + 1) }); | 852 | let to = at.and_then(|at| if up { at.checked_sub(1) } else { Some(at + 1) }); |