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) });