| 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 | 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 | |
| 63 | /// before the title, then the identities OneNote finds it by. | |
| 64 | fn clipboard_link( | |
| 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 | }; | |
| 62 | /// A section, page or paragraph as Copy Link gives it: the section file's path, then the | |
| 63 | /// title and identities OneNote finds it by. | |
| 64 | fn clipboard_link(path: &str, section: [u8; 16], target: LinkTarget) -> String { | |
| 82 | 65 | let stored = internal_link(section, "", target); |
| 83 | 66 | let fragment = stored |
| 84 | 67 | .strip_prefix("onenote:#") |
| ... | ... | @@ -335,6 +318,9 @@ impl State { |
| 335 | 318 | }; |
| 336 | 319 | let corrections = items.len(); |
| 337 | 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 | 324 | let lists = [commands::Id::ToDoList, commands::Id::BulletedList] |
| 339 | 325 | .into_iter() |
| 340 | 326 | .filter(|id| { |
| ... | ... | @@ -348,7 +334,7 @@ impl State { |
| 348 | 334 | } else { |
| 349 | 335 | let mut items = vec![ |
| 350 | 336 | Item { |
| 351 | disabled: !context.selected, | |
| 337 | disabled: !context.selected || fixed, | |
| 352 | 338 | separated: corrections > 0, |
| 353 | 339 | ..drawn("Cut", art::CUT) |
| 354 | 340 | }, |
| ... | ... | @@ -356,12 +342,16 @@ impl State { |
| 356 | 342 | disabled: !context.selected, |
| 357 | 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 | 350 | match &context.link { |
| 362 | 351 | Some(_) => items.extend([ |
| 363 | 352 | Item { |
| 364 | 353 | separated: true, |
| 354 | disabled: unlinkable, | |
| 365 | 355 | ..drawn("Edit Link", art::LINK) |
| 366 | 356 | }, |
| 367 | 357 | drawn("Copy Link to Paragraph", art::COPY_LINK), |
| ... | ... | @@ -370,11 +360,15 @@ impl State { |
| 370 | 360 | ..drawn("Copy Link", art::COPY_LINK) |
| 371 | 361 | }, |
| 372 | 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 | 368 | None => items.extend([ |
| 376 | 369 | Item { |
| 377 | 370 | separated: true, |
| 371 | disabled: unlinkable, | |
| 378 | 372 | ..drawn("Link", art::LINK) |
| 379 | 373 | }, |
| 380 | 374 | drawn("Copy Link to Paragraph", art::COPY_LINK), |
| ... | ... | @@ -391,9 +385,13 @@ impl State { |
| 391 | 385 | items.extend([ |
| 392 | 386 | Item { |
| 393 | 387 | separated: true, |
| 388 | disabled: fixed, | |
| 394 | 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 | 397 | items |
| ... | ... | @@ -489,12 +487,30 @@ impl State { |
| 489 | 487 | .iter() |
| 490 | 488 | .find(|(listed, ..)| *listed == space) |
| 491 | 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 | 498 | Ok(clipboard_link( |
| 493 | 499 | &session.section.file().to_string_lossy(), |
| 494 | 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 | 512 | identity, |
| 497 | object, | |
| 513 | LinkTarget::Section, | |
| 498 | 514 | )) |
| 499 | 515 | } |
| 500 | 516 | |
| ... | ... | @@ -541,13 +557,27 @@ mod tests { |
| 541 | 557 | } |
| 542 | 558 | |
| 543 | 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 | 562 | #[test] |
| 546 | fn page_links_copy_as_onenote_copies_them() { | |
| 563 | fn links_copy_as_onenote_copies_them() { | |
| 547 | 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 | 579 | assert_eq!( |
| 550 | clipboard_link("C:\\one\\links.one", section, "Links", page, None), | |
| 580 | clipboard_link("C:\\one\\links.one", section, page), | |
| 551 | 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 | 116 | /// A section's colour, COLORREF; none is OneNote's None. |
| 117 | 117 | Color(Option<u32>), |
| 118 | 118 | Sync, |
| 119 | NewNotebook, | |
| 120 | 119 | /// Moves a notebook up the notebook list, or down. |
| 121 | 120 | Raise(bool), |
| 122 | 121 | Properties, |
| ... | ... | @@ -154,7 +153,6 @@ impl Action { |
| 154 | 153 | Action::Theme => art::STYLES, |
| 155 | 154 | Action::Colors => art::SECTION_COLOR, |
| 156 | 155 | Action::Sync => art::SYNC_NOW, |
| 157 | Action::NewNotebook => art::NEW_NOTEBOOK, | |
| 158 | 156 | Action::Raise(true) => art::MOVE_UP, |
| 159 | 157 | Action::Raise(false) => art::MOVE_DOWN, |
| 160 | 158 | Action::Properties => art::PROPERTIES, |
| ... | ... | @@ -335,9 +333,9 @@ impl State { |
| 335 | 333 | }; |
| 336 | 334 | let nowhere = || self.destinations(target).is_empty(); |
| 337 | 335 | // Last on a page, section or notebook, under a rule. |
| 338 | let styles = || { | |
| 336 | let styles = |library: &Library| { | |
| 339 | 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 | 340 | match target { |
| 343 | 341 | // OneNote's Move or Copy takes a page out of the bin; nothing else changes it. |
| ... | ... | @@ -369,7 +367,7 @@ impl State { |
| 369 | 367 | // Levels and versions are known once the section is open. |
| 370 | 368 | let Some(session) = self.session.as_ref().filter(|_| self.open(library, path)) |
| 371 | 369 | else { |
| 372 | actions.push(styles()); | |
| 370 | actions.push(styles(library)); | |
| 373 | 371 | return actions; |
| 374 | 372 | }; |
| 375 | 373 | let pages = &session.pages; |
| ... | ... | @@ -398,12 +396,13 @@ impl State { |
| 398 | 396 | session.page_versions(*space).is_empty(), |
| 399 | 397 | true, |
| 400 | 398 | ), |
| 401 | styles(), | |
| 399 | styles(library), | |
| 402 | 400 | ]); |
| 403 | 401 | actions |
| 404 | 402 | } |
| 405 | 403 | Target::Section { library, path } | Target::Group { library, path } => { |
| 406 | 404 | let section = matches!(target, Target::Section { .. }); |
| 405 | let cataloged = library.catalog().is_some(); | |
| 407 | 406 | let mut actions = vec![item(Action::Rename, "Rename", false, false)]; |
| 408 | 407 | if section { |
| 409 | 408 | actions.push(item(Action::SaveAs, "Save As", false, false)); |
| ... | ... | @@ -411,15 +410,21 @@ impl State { |
| 411 | 410 | actions.extend([ |
| 412 | 411 | item(Action::Delete, "Delete", false, false), |
| 413 | 412 | item(Action::Move, "Move", nowhere(), false), |
| 414 | item(Action::NewSection, "New Section", false, true), | |
| 415 | item(Action::NewGroup, "New Section Group", false, false), | |
| 413 | ]); | |
| 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 | 422 | if section { |
| 418 | 423 | actions.extend([ |
| 419 | 424 | item( |
| 420 | 425 | Action::Password, |
| 421 | 426 | "Password Protect This Section", |
| 422 | library.catalog().is_none(), | |
| 427 | !cataloged, | |
| 423 | 428 | true, |
| 424 | 429 | ), |
| 425 | 430 | item(Action::Colors, "Section Color", false, false), |
| ... | ... | @@ -429,7 +434,12 @@ impl State { |
| 429 | 434 | section_file(library, path).is_none(), |
| 430 | 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 | 445 | actions |
| ... | ... | @@ -467,12 +477,16 @@ impl State { |
| 467 | 477 | library.catalog().is_none() || library.joined.is_some(), |
| 468 | 478 | false, |
| 469 | 479 | ), |
| 470 | item(Action::NewSection, "New Section", false, true), | |
| 471 | item(Action::NewGroup, "New Section Group", false, false), | |
| 472 | 480 | item( |
| 473 | Action::NewNotebook, | |
| 474 | commands::command(commands::Id::NewNotebook).title, | |
| 475 | false, | |
| 481 | Action::NewSection, | |
| 482 | "New Section", | |
| 483 | library.catalog().is_none(), | |
| 484 | true, | |
| 485 | ), | |
| 486 | item( | |
| 487 | Action::NewGroup, | |
| 488 | "New Section Group", | |
| 489 | library.catalog().is_none(), | |
| 476 | 490 | false, |
| 477 | 491 | ), |
| 478 | 492 | item( |
| ... | ... | @@ -511,7 +525,7 @@ impl State { |
| 511 | 525 | library.catalog().is_none(), |
| 512 | 526 | false, |
| 513 | 527 | ), |
| 514 | styles(), | |
| 528 | styles(library), | |
| 515 | 529 | ] |
| 516 | 530 | } |
| 517 | 531 | Target::Closed(_) | Target::Server(_) => { |
| ... | ... | @@ -779,6 +793,14 @@ impl State { |
| 779 | 793 | library, |
| 780 | 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 | 804 | (Target::Section { library, path }, Action::Reveal) => { |
| 783 | 805 | if let Some(file) = section_file(&library, &path) { |
| 784 | 806 | platform::show_file(&file); |
| ... | ... | @@ -825,10 +847,6 @@ impl State { |
| 825 | 847 | } |
| 826 | 848 | None |
| 827 | 849 | } |
| 828 | (Target::Notebook(_), Action::NewNotebook) => { | |
| 829 | self.choose(Choice::Command(commands::Id::NewNotebook)); | |
| 830 | None | |
| 831 | } | |
| 832 | 850 | (Target::Notebook(library), Action::Raise(up)) => { |
| 833 | 851 | let at = (self.notebooks.iter()).position(|open| Arc::ptr_eq(open, &library)); |
| 834 | 852 | let to = at.and_then(|at| if up { at.checked_sub(1) } else { Some(at + 1) }); |