From c09590a9c59a6645ff9cbe96e0037000405a8bf7 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 2 Oct 2026 21:43:11 -0700 Subject: [PATCH] fix: context menus offer only what applies to what was clicked, disabled when it can't 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.5 --- crates/snowbound/src/link.rs | 90 +++++++++++++++++++++++------------ crates/snowbound/src/menus.rs | 58 ++++++++++++++-------- 2 files changed, 98 insertions(+), 50 deletions(-) diff --git a/crates/snowbound/src/link.rs b/crates/snowbound/src/link.rs index 83d750dfcad8478afe467aafb5177459ee556d5a..8dcf2462f800259e6e9d97670f40450b8a89804b 100644 --- a/crates/snowbound/src/link.rs +++ b/crates/snowbound/src/link.rs @@ -59,26 +59,9 @@ fn system_url(address: &str) -> String { } } -/// A page, or a paragraph on it, as Copy Link to Page gives it: the section file's path -/// before the title, then the identities OneNote finds it by. -fn clipboard_link( - path: &str, - section: [u8; 16], - title: &str, - page: [u8; 16], - object: Option, -) -> String { - let target = match object { - Some(object) => LinkTarget::Object { - identity: page, - title, - object, - }, - None => LinkTarget::Page { - identity: page, - title, - }, - }; +/// A section, page or paragraph as Copy Link gives it: the section file's path, then the +/// title and identities OneNote finds it by. +fn clipboard_link(path: &str, section: [u8; 16], target: LinkTarget) -> String { let stored = internal_link(section, "", target); let fragment = stored .strip_prefix("onenote:#") @@ -335,6 +318,9 @@ impl State { }; let corrections = items.len(); let format = self.format_state(); + let disabled = |id| !self.status(&commands::Choice::Command(id), &format).enabled; + // Paste stands for any edit to the text here. + let (fixed, unlinkable) = (disabled(commands::Id::Paste), disabled(commands::Id::Link)); let lists = [commands::Id::ToDoList, commands::Id::BulletedList] .into_iter() .filter(|id| { @@ -348,7 +334,7 @@ impl State { } else { let mut items = vec![ Item { - disabled: !context.selected, + disabled: !context.selected || fixed, separated: corrections > 0, ..drawn("Cut", art::CUT) }, @@ -356,12 +342,16 @@ impl State { disabled: !context.selected, ..drawn("Copy", art::COPY) }, - drawn("Paste", art::PASTE), + Item { + disabled: fixed, + ..drawn("Paste", art::PASTE) + }, ]; match &context.link { Some(_) => items.extend([ Item { separated: true, + disabled: unlinkable, ..drawn("Edit Link", art::LINK) }, drawn("Copy Link to Paragraph", art::COPY_LINK), @@ -370,11 +360,15 @@ impl State { ..drawn("Copy Link", art::COPY_LINK) }, drawn("Select Link", art::SELECT), - drawn("Remove Link", art::REMOVE_LINK), + Item { + disabled: unlinkable, + ..drawn("Remove Link", art::REMOVE_LINK) + }, ]), None => items.extend([ Item { separated: true, + disabled: unlinkable, ..drawn("Link", art::LINK) }, drawn("Copy Link to Paragraph", art::COPY_LINK), @@ -391,9 +385,13 @@ impl State { items.extend([ Item { separated: true, + disabled: fixed, ..drawn("Professional", art::EQUATION) }, - drawn("Linear", art::EQUATION), + Item { + disabled: fixed, + ..drawn("Linear", art::EQUATION) + }, ]); } items @@ -489,12 +487,30 @@ impl State { .iter() .find(|(listed, ..)| *listed == space) .map_or(page.title.as_str(), |(_, title, _)| title.as_str()); + let target = match object { + Some(object) => LinkTarget::Object { + identity, + title, + object, + }, + None => LinkTarget::Page { identity, title }, + }; Ok(clipboard_link( &session.section.file().to_string_lossy(), session.section.identity()?, - title, + target, + )) + } + + /// Copy Link to Section: the link OneNote 2010 puts on the clipboard for the section at + /// catalog `path`; none for one without a file here. + pub(crate) fn section_link(&self, library: &crate::Library, path: &str) -> Option { + let file = library.folder()?.join(path); + let identity = library.section_identity(path)?; + Some(clipboard_link( + &file.to_string_lossy(), identity, - object, + LinkTarget::Section, )) } @@ -541,13 +557,27 @@ mod tests { } /// OneNote 2010's Copy Link to Page (`corpus/link-edit/native-typed`, README): the file's - /// path, the title, and the section and page identities. + /// path, the title, and the section and page identities; and its Copy Link to Section, + /// as the lab's OneNote 2010 copied one, the path's spaces escaped. #[test] - fn page_links_copy_as_onenote_copies_them() { + fn links_copy_as_onenote_copies_them() { let section = *b"\x25\xfc\x1e\x0f\x7c\xdf\xcb\x45\x87\xa4\xbd\xd0\x3f\x42\x05\x7e"; - let page = *b"\x18\xa3\xcc\xa9\x3e\xa4\x12\x49\xa1\xc7\xf1\x9f\x1e\xaf\x30\xca"; + let identity = *b"\x18\xa3\xcc\xa9\x3e\xa4\x12\x49\xa1\xc7\xf1\x9f\x1e\xaf\x30\xca"; + let page = LinkTarget::Page { + identity, + title: "Links", + }; + let general = *b"\x1b\x79\xcd\xe4\x17\x94\x4f\x4b\x99\x3a\x34\xfe\xb3\xb2\xa8\x4d"; assert_eq!( - clipboard_link("C:\\one\\links.one", section, "Links", page, None), + clipboard_link( + "C:\\Users\\clover\\Documents\\OneNote Notebooks\\Personal\\General.one", + general, + LinkTarget::Section + ), + "onenote:///C:\\Users\\clover\\Documents\\OneNote%20Notebooks\\Personal\\General.one#section-id={E4CD791B-9417-4B4F-993A-34FEB3B2A84D}&end" + ); + assert_eq!( + clipboard_link("C:\\one\\links.one", section, page), "onenote:///C:\\one\\links.one#Links§ion-id={0F1EFC25-DF7C-45CB-87A4-BDD03F42057E}&page-id={A9CCA318-A43E-4912-A1C7-F19F1EAF30CA}&end" ); } diff --git a/crates/snowbound/src/menus.rs b/crates/snowbound/src/menus.rs index c1670c2cbae061f65fbc443775ad098df7483f65..e38306b17fa26778dc99f1977d42c2fb7f0983c3 100644 --- a/crates/snowbound/src/menus.rs +++ b/crates/snowbound/src/menus.rs @@ -116,7 +116,6 @@ pub enum Action { /// A section's colour, COLORREF; none is OneNote's None. Color(Option), Sync, - NewNotebook, /// Moves a notebook up the notebook list, or down. Raise(bool), Properties, @@ -154,7 +153,6 @@ impl Action { Action::Theme => art::STYLES, Action::Colors => art::SECTION_COLOR, Action::Sync => art::SYNC_NOW, - Action::NewNotebook => art::NEW_NOTEBOOK, Action::Raise(true) => art::MOVE_UP, Action::Raise(false) => art::MOVE_DOWN, Action::Properties => art::PROPERTIES, @@ -335,9 +333,9 @@ impl State { }; let nowhere = || self.destinations(target).is_empty(); // Last on a page, section or notebook, under a rule. - let styles = || { + let styles = |library: &Library| { let title = commands::command(commands::Id::Themes).title; - item(Action::Theme, title, false, true) + item(Action::Theme, title, library.catalog().is_none(), true) }; match target { // OneNote's Move or Copy takes a page out of the bin; nothing else changes it. @@ -369,7 +367,7 @@ impl State { // Levels and versions are known once the section is open. let Some(session) = self.session.as_ref().filter(|_| self.open(library, path)) else { - actions.push(styles()); + actions.push(styles(library)); return actions; }; let pages = &session.pages; @@ -398,12 +396,13 @@ impl State { session.page_versions(*space).is_empty(), true, ), - styles(), + styles(library), ]); actions } Target::Section { library, path } | Target::Group { library, path } => { let section = matches!(target, Target::Section { .. }); + let cataloged = library.catalog().is_some(); let mut actions = vec![item(Action::Rename, "Rename", false, false)]; if section { actions.push(item(Action::SaveAs, "Save As", false, false)); @@ -411,15 +410,21 @@ impl State { actions.extend([ item(Action::Delete, "Delete", false, false), item(Action::Move, "Move", nowhere(), false), - item(Action::NewSection, "New Section", false, true), - item(Action::NewGroup, "New Section Group", false, false), + ]); + if section { + let link = self.section_link(library, path).is_none(); + actions.push(item(Action::CopyLink, "Copy Link to Section", link, true)); + } + actions.extend([ + item(Action::NewSection, "New Section", !cataloged, true), + item(Action::NewGroup, "New Section Group", !cataloged, false), ]); if section { actions.extend([ item( Action::Password, "Password Protect This Section", - library.catalog().is_none(), + !cataloged, true, ), item(Action::Colors, "Section Color", false, false), @@ -429,7 +434,12 @@ impl State { section_file(library, path).is_none(), false, ), - styles(), + item( + Action::Theme, + commands::command(commands::Id::Themes).title, + library.section_identity(path).is_none(), + true, + ), ]); } actions @@ -467,12 +477,16 @@ impl State { library.catalog().is_none() || library.joined.is_some(), false, ), - item(Action::NewSection, "New Section", false, true), - item(Action::NewGroup, "New Section Group", false, false), item( - Action::NewNotebook, - commands::command(commands::Id::NewNotebook).title, - false, + Action::NewSection, + "New Section", + library.catalog().is_none(), + true, + ), + item( + Action::NewGroup, + "New Section Group", + library.catalog().is_none(), false, ), item( @@ -511,7 +525,7 @@ impl State { library.catalog().is_none(), false, ), - styles(), + styles(library), ] } Target::Closed(_) | Target::Server(_) => { @@ -779,6 +793,14 @@ impl State { library, Structure::Color { path, color }, )), + (Target::Section { library, path }, Action::CopyLink) => { + if let Some(link) = self.section_link(&library, &path) + && let Err(error) = self.clipboard.set_text(link) + { + eprintln!("Copying the link failed: {error}"); + } + None + } (Target::Section { library, path }, Action::Reveal) => { if let Some(file) = section_file(&library, &path) { platform::show_file(&file); @@ -825,10 +847,6 @@ impl State { } None } - (Target::Notebook(_), Action::NewNotebook) => { - self.choose(Choice::Command(commands::Id::NewNotebook)); - None - } (Target::Notebook(library), Action::Raise(up)) => { let at = (self.notebooks.iter()).position(|open| Arc::ptr_eq(open, &library)); let to = at.and_then(|at| if up { at.checked_sub(1) } else { Some(at + 1) }); -- 2.54.0