authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 21:37:14-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 01:50:33-07:00
loga4a0ccf055b14aa599b027e96a679054fea6b5a2
tree2b104b885f46f98cc0eb5991d1b8a67bdaa4e801
parent9361b752ce57b06d0f55af85dc5f515f794ea8f2
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: menu items, commands and buttons drop their trailing ellipses

Clover: no ellipses in action titles, on desktop, web or iOS. The command table, context menus, dialog buttons and iOS menus name actions without them; "…" stays only on progress text and truncated text. Assisted-by: claude-opus-5.5

31 files changed, 100 insertions(+), 102 deletions(-)

apps/ios/Snowbound/Arranging.swift+2-2
......@@ -342,7 +342,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate
342342 if indentable(rows, at) { add("Make Subpage") { [weak self] in self?.indent(id, by: 1) } }
343343 if rows[at].level > 1 { add("Promote Subpage") { [weak self] in self?.indent(id, by: -1) } }
344344 if !moveActions(item).isEmpty {
345 add("Move to Section…") { [weak self] in
345 add("Move to Section") { [weak self] in
346346 guard let self, let row = items.firstIndex(where: { $0.id == id }),
347347 let cell = tableView.cellForRow(at: IndexPath(row: row, section: 0))
348348 else { return }
......@@ -571,7 +571,7 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie
571571 if at < order.count - 1 { add("Move Down") { [weak self] in self?.shift(path, of: notebook, by: 1) } }
572572 }
573573 if !notebook.destinations(of: path).isEmpty {
574 add("Move…") { [weak self, weak cell] in
574 add("Move") { [weak self, weak cell] in
575575 guard let self, let cell else { return }
576576 sheet("Move", moveActions(path, of: notebook), from: cell, in: self)
577577 }
apps/ios/Snowbound/Lists.swift+7-7
......@@ -94,10 +94,10 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke
9494 })
9595 var actions: [UIMenuElement] = [
9696 UIMenu(title: "New Notebook", options: .displayInline, children: places),
97 UIAction(title: "Open from Server…", image: UIImage(systemName: "server.rack")) { [weak self] _ in
97 UIAction(title: "Open from Server", image: UIImage(systemName: "server.rack")) { [weak self] _ in
9898 self?.connect()
9999 },
100 UIAction(title: "Open Folder…", image: UIImage(systemName: "folder")) { [weak self] _ in
100 UIAction(title: "Open Folder", image: UIImage(systemName: "folder")) { [weak self] _ in
101101 self?.openFolder()
102102 },
103103 ]
......@@ -114,7 +114,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke
114114 let more = UIMenu(children: [
115115 UIDeferredMenuElement.uncached { [weak self] provide in
116116 var actions: [UIMenuElement] = [
117 UIAction(title: "Sync Status…", image: UIImage(systemName: "arrow.triangle.2.circlepath")) {
117 UIAction(title: "Sync Status", image: UIImage(systemName: "arrow.triangle.2.circlepath")) {
118118 [weak self] _ in self.map(SyncViewController.present)
119119 }
120120 ]
......@@ -125,7 +125,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke
125125 })
126126 }
127127 actions.append(
128 UIAction(title: "Personalize…", image: UIImage(systemName: "person.crop.circle")) { [weak self] _ in
128 UIAction(title: "Personalize", image: UIImage(systemName: "person.crop.circle")) { [weak self] _ in
129129 guard let self else { return }
130130 Author.ask(from: self) {}
131131 })
......@@ -385,7 +385,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke
385385 return [UIMenu(options: .displayInline, children: [details, now])]
386386 } ?? []
387387 let actions = [
388 UIAction(title: "New Section…", image: UIImage(systemName: "plus.rectangle.portrait")) {
388 UIAction(title: "New Section", image: UIImage(systemName: "plus.rectangle.portrait")) {
389389 [weak self] _ in self?.newSection(in: notebook)
390390 },
391391 UIAction(title: "Tags Summary", image: UIImage(systemName: "tag")) { [weak self] _ in
......@@ -403,13 +403,13 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke
403403 return UIMenu(children: status + actions + [close])
404404 }
405405 let file = [
406 UIAction(title: "Rename…", image: UIImage(systemName: "pencil")) { [weak self] _ in
406 UIAction(title: "Rename", image: UIImage(systemName: "pencil")) { [weak self] _ in
407407 self?.rename(folder, of: notebook)
408408 },
409409 UIAction(title: "Duplicate", image: UIImage(systemName: "plus.square.on.square")) { [weak self] _ in
410410 self?.duplicate(folder, of: notebook)
411411 },
412 UIAction(title: "Share…", image: UIImage(systemName: "square.and.arrow.up")) { [weak self] _ in
412 UIAction(title: "Share", image: UIImage(systemName: "square.and.arrow.up")) { [weak self] _ in
413413 self?.share(folder, from: notebook)
414414 },
415415 UIAction(title: "Show in Files", image: UIImage(systemName: "folder")) { _ in
apps/ios/Snowbound/PageViewController.swift+3-3
......@@ -202,7 +202,7 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate
202202 ])
203203 return (Prototype.reading ? [readingAction()] : []) + [
204204 insert,
205 UIAction(title: "Page Background…", image: UIImage(systemName: "paintpalette"), attributes: editable) {
205 UIAction(title: "Page Background", image: UIImage(systemName: "paintpalette"), attributes: editable) {
206206 [weak self] _ in self?.showPaper()
207207 },
208208 themeMenu(),
......@@ -211,10 +211,10 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate
211211 scene?.showTags(of: section.notebook, from: self)
212212 },
213213 UIMenu(options: .displayInline, children: [
214 UIAction(title: "Print…", image: UIImage(systemName: "printer")) { [weak self] _ in
214 UIAction(title: "Print", image: UIImage(systemName: "printer")) { [weak self] _ in
215215 self?.printPage(export: false)
216216 },
217 UIAction(title: "Export as PDF…", image: UIImage(systemName: "doc.richtext")) { [weak self] _ in
217 UIAction(title: "Export as PDF", image: UIImage(systemName: "doc.richtext")) { [weak self] _ in
218218 self?.printPage(export: true)
219219 },
220220 ]),
apps/ios/Snowbound/Prototypes.swift+2-2
......@@ -52,8 +52,8 @@ extension Prototype {
5252 open.title = "Open Existing Notebook"
5353 welcome.secondaryButton = open
5454 var elsewhere = [
55 UIAction(title: "Open Folder…", image: UIImage(systemName: "folder")) { _ in openFolder() },
56 UIAction(title: "Connect to Server…", image: UIImage(systemName: "server.rack")) { _ in connect() },
55 UIAction(title: "Open Folder", image: UIImage(systemName: "folder")) { _ in openFolder() },
56 UIAction(title: "Connect to Server", image: UIImage(systemName: "server.rack")) { _ in connect() },
5757 ]
5858 if !ICloud.signedIn {
5959 elsewhere.append(
arc/platforms.md+2-2
......@@ -52,7 +52,7 @@ keyboard, the toolbar and the macOS menu bar all run commands from it.
5252 frame with the window's new size instead of stretching the last one.
5353- A notebook on a mounted SMB share is opened through the embedded SMB client,
5454 signed in with the password the keychain keeps for that mount (see
55 [sync](sync.md) for why the mount itself isn't enough). Open Notebook from Server… reaches one
55 [sync](sync.md) for why the mount itself isn't enough). Open Notebook from Server reaches one
5656 by address instead, and keeps a password it asks to remember as the Finder does, an
5757 SMB internet password.
5858- The menu bar (`menubar.rs`) is laid out as OneNote for Mac's. Its items are
......@@ -147,7 +147,7 @@ keyboard, the toolbar and the macOS menu bar all run commands from it.
147147 other writers) and takes OneNote's coordination bytes with byte-range
148148 locks, so both apps can have a section open on one machine.
149149 ReadDirectoryChangesW reports changes below a notebook, including other
150 clients' on a share. Open Notebook from Server… still uses the embedded
150 clients' on a share. Open Notebook from Server still uses the embedded
151151 client, its passwords in the Credential Manager.
152152- The common file dialogs, task dialogs, the date and time picker, and the
153153 Spell Checking API (from Windows 8) are the system's; audio plays and
arc/sync.md+1-1
......@@ -192,7 +192,7 @@ client with the account the system keeps for that mount. It falls back to the
192192mount only when it can't sign in that way.
193193
194194A notebook can also be opened from its server's address, with no mount at all (File ▸
195Open Notebook from Server…), which is how Mac OS X 10.6 reaches a server that no longer
195Open Notebook from Server), which is how Mac OS X 10.6 reaches a server that no longer
196196speaks SMB1, the only version its Finder has. The notebook is kept by that address
197197(`smb://[domain;]user@server/share/folder`), never with a password; the password lives in
198198the keychain (the Secret Service on Linux) when the user asks to remember it, and
corpus/page-versions/README.md+1-1
......@@ -47,7 +47,7 @@ Versions is chosen. An open version is read-only under a yellow bar (RGB 255, 23
4747"This is an earlier version of the page. It will be deleted over time. Click here to restore
4848or delete this version." What the version changed since the one before it is banded green
4949(RGB 214, 255, 214). The bar's menu (`shots/02-version-menu.png`): Restore Version, Delete
50Version, Copy Page To…; Delete All Versions in Section, Section Group, Notebook; Disable
50Version, Copy Page To; Delete All Versions in Section, Section Group, Notebook; Disable
5151History for This Notebook; Hide Page Versions. Share also has Hide Authors, Recent Edits and
5252Find by Author (`shots/14`–`16`).
5353
corpus/section-color/README.md+1-1
......@@ -6,7 +6,7 @@ ends with Section Color, sixteen named colours and None (`section-color-menu.png
66appends a revision to the section's own file setting its metadata's `0x14001cbe`
77(COLORREF; `teal.one`), and None sets `0xFFFFFFFF` (`none.one`); the table of contents is
88left byte for byte. Notebook Properties (`notebook-properties.png`, from the notebook's
9Rename… or Properties…) offers the same sixteen without None (`notebook-colors.png`) and
9Rename or Properties) offers the same sixteen without None (`notebook-colors.png`) and
1010appends a revision to the root `Open Notebook.onetoc2` setting its root's `0x14001cbe`
1111(`before.onetoc2`, `after.onetoc2`). Its Display name is written to neither file: OneNote
1212keeps it in its local `OneNoteOfflineCache.onecache`, so the hierarchy's `nickname`
crates/snowbound/linux/README.md+1-1
......@@ -11,7 +11,7 @@ cd snowbound-linux-x86_64
1111./bin/snowbound
1212```
1313
14Choose **Open Notebook…** in the notebook list and pick the notebook folder,
14Choose **Open Notebook** in the notebook list and pick the notebook folder,
1515the one holding the `.one` section files and `Open Notebook.onetoc2`.
1616Snowbound remembers the notebooks you open. `--notebook FOLDER` opens one
1717straight away.
crates/snowbound/src/background.rs+1-1
......@@ -161,7 +161,7 @@ pub fn menu(
161161 }
162162 match cells[index] {
163163 Cell::Color(None) | Cell::Art(None) | Cell::Rules(None) => label(ui, "None"),
164 Cell::ShowAll => label(ui, "Show All…"),
164 Cell::ShowAll => label(ui, "Show All"),
165165 Cell::Color(Some(color)) => {
166166 ui.leaf(
167167 "swatch",
crates/snowbound/src/commands.rs+20-20
......@@ -260,12 +260,12 @@ const NONE: &[Chord] = &[];
260260
261261/// OneNote 2010's chords off macOS; there, AppKit's and OneNote for Mac's.
262262pub const COMMANDS: &[Command] = &[
263 row!(Id::Settings, "Settings…", &[cmd(',')], &[cmd(',')]),
264 row!(Id::NewNotebook, "New Notebook…", NONE, NONE),
265 row!(Id::OpenNotebook, "Open Notebook…", &[cmd('o')], &[cmd('o')]),
266 row!(Id::OpenFromServer, "Open Notebook from Server…", NONE, NONE),
267 row!(Id::OpenShared, "Open Shared Notebook…", NONE, NONE),
268 row!(Id::LiveShare, "Live Share…", NONE, NONE),
263 row!(Id::Settings, "Settings", &[cmd(',')], &[cmd(',')]),
264 row!(Id::NewNotebook, "New Notebook", NONE, NONE),
265 row!(Id::OpenNotebook, "Open Notebook", &[cmd('o')], &[cmd('o')]),
266 row!(Id::OpenFromServer, "Open Notebook from Server", NONE, NONE),
267 row!(Id::OpenShared, "Open Shared Notebook", NONE, NONE),
268 row!(Id::LiveShare, "Live Share", NONE, NONE),
269269 row!(Id::CloseNotebook, "Close This Notebook", NONE, NONE),
270270 row!(Id::NewSection, "New Section", &[cmd('t')], &[cmd('t')]),
271271 row!(Id::NewSectionGroup, "New Section Group", NONE, NONE),
......@@ -280,7 +280,7 @@ pub const COMMANDS: &[Command] = &[
280280 row!(Id::CopyPageLink, "Copy Link to Page", NONE, NONE),
281281 row!(
282282 Id::PasswordProtect,
283 "Password Protect This Section…",
283 "Password Protect This Section",
284284 NONE,
285285 NONE
286286 ),
......@@ -302,7 +302,7 @@ pub const COMMANDS: &[Command] = &[
302302 ),
303303 row!(
304304 Id::SaveAs,
305 "Save As…",
305 "Save As",
306306 &[cmd('s').shift()],
307307 &[cmd('s').shift()]
308308 ),
......@@ -318,11 +318,11 @@ pub const COMMANDS: &[Command] = &[
318318 NONE
319319 ),
320320 row!(Id::NextUnread, "Next Unread", NONE, NONE),
321 row!(Id::ExportPdf, "Export as PDF…", NONE, NONE),
321 row!(Id::ExportPdf, "Export as PDF", NONE, NONE),
322322 // Go to takes OneNote's Ctrl+P, and Pause its Ctrl+Alt+P.
323323 row!(
324324 Id::Print,
325 "Print…",
325 "Print",
326326 &[cmd('p').option().shift()],
327327 &[cmd('p').option().shift()]
328328 ),
......@@ -351,10 +351,10 @@ pub const COMMANDS: &[Command] = &[
351351 &[cmd('f').option()],
352352 &[key('o').option()],
353353 ),
354 row!(Id::GoTo, "Go to Page or Section…", &[cmd('p')], &[cmd('p')]),
354 row!(Id::GoTo, "Go to Page or Section", &[cmd('p')], &[cmd('p')]),
355355 row!(
356356 Id::CommandPalette,
357 "Command Palette…",
357 "Command Palette",
358358 &[cmd('p').shift()],
359359 &[cmd('p').shift()]
360360 ),
......@@ -393,7 +393,7 @@ pub const COMMANDS: &[Command] = &[
393393 row!(Id::MarkdownShortcuts, "Markdown Shortcuts", NONE, NONE),
394394 row!(
395395 Id::Spelling,
396 "Spelling…",
396 "Spelling",
397397 &[named(NamedKey::F7)],
398398 &[named(NamedKey::F7)]
399399 ),
......@@ -406,10 +406,10 @@ pub const COMMANDS: &[Command] = &[
406406 ),
407407 row!(Id::PageColor, "Page Color", NONE, NONE),
408408 row!(Id::Table, "Table", NONE, NONE),
409 row!(Id::Picture, "Picture…", NONE, NONE),
409 row!(Id::Picture, "Picture", NONE, NONE),
410410 row!(Id::ScreenClipping, "Screen Clipping", NONE, NONE),
411 row!(Id::Attachment, "Attach File…", NONE, NONE),
412 row!(Id::Link, "Link…", &[cmd('k')], &[cmd('k')]),
411 row!(Id::Attachment, "Attach File", NONE, NONE),
412 row!(Id::Link, "Link", &[cmd('k')], &[cmd('k')]),
413413 row!(Id::InsertSpace, "Insert Space", NONE, NONE),
414414 row!(
415415 Id::Equation,
......@@ -465,7 +465,7 @@ pub const COMMANDS: &[Command] = &[
465465 NONE,
466466 NONE
467467 ),
468 row!(Id::Transport(Transport::SeekTo), "Seek To…", NONE, NONE),
468 row!(Id::Transport(Transport::SeekTo), "Seek To", NONE, NONE),
469469 row!(
470470 Id::Transport(Transport::SeePlayback),
471471 "See Playback",
......@@ -596,8 +596,8 @@ pub const COMMANDS: &[Command] = &[
596596 row!(Id::Style(8), "Quote", NONE, NONE),
597597 row!(Id::Style(9), "Code", NONE, NONE),
598598 row!(Id::Style(10), "Normal", NONE, NONE),
599 row!(Id::Themes, "Customize Theme…", NONE, NONE),
600 row!(Id::CustomizeTags, "Customize Tags…", NONE, NONE),
599 row!(Id::Themes, "Customize Theme", NONE, NONE),
600 row!(Id::CustomizeTags, "Customize Tags", NONE, NONE),
601601 row!(
602602 Id::RemoveTags,
603603 "Remove Tag",
......@@ -608,7 +608,7 @@ pub const COMMANDS: &[Command] = &[
608608 row!(Id::ToDoList, "Make To-Do List", NONE, NONE),
609609 row!(Id::BulletedList, "Make Bulleted List", NONE, NONE),
610610 row!(Id::Help, "Snowbound Help", NONE, NONE),
611 row!(Id::CheckForUpdates, "Check for Updates…", NONE, NONE),
611 row!(Id::CheckForUpdates, "Check for Updates", NONE, NONE),
612612];
613613
614614/// The most tags a list holds: MS-ONE's action types 0 to 99 number them.
crates/snowbound/src/history.rs+1-1
......@@ -105,7 +105,7 @@ pub fn menu(
105105 drawn("Delete Version", art::DELETE),
106106 ui::popup::Item {
107107 disabled: sections.is_empty(),
108 ..drawn("Copy Page To…", art::COPY)
108 ..drawn("Copy Page To", art::COPY)
109109 },
110110 ui::popup::Item {
111111 separated: true,
crates/snowbound/src/keys.rs+1-1
......@@ -78,7 +78,7 @@ fn label(chord: Chord) -> String {
7878fn title(id: Command) -> String {
7979 match id {
8080 Command::Tag(place) => format!("Tag {}", place + 1),
81 id => crate::title(id).to_owned(),
81 id => commands::command(id).title.to_owned(),
8282 }
8383}
8484
crates/snowbound/src/link.rs+8-8
......@@ -303,7 +303,7 @@ impl State {
303303 item("Ignore"),
304304 Item {
305305 separated: true,
306 ..drawn("Spelling…", art::SPELLING)
306 ..drawn("Spelling", art::SPELLING)
307307 },
308308 ],
309309 Some(correction) => {
......@@ -326,7 +326,7 @@ impl State {
326326 drawn("Add to Dictionary", art::ADD_TO_DICTIONARY),
327327 Item {
328328 separated: true,
329 ..drawn("Spelling…", art::SPELLING)
329 ..drawn("Spelling", art::SPELLING)
330330 },
331331 ]);
332332 items
......@@ -344,7 +344,7 @@ impl State {
344344 .collect::<Vec<_>>();
345345 items.extend(if context.attachment.is_some() {
346346 // OneNote 2010's commands for the file itself; its clipboard holds text alone.
347 vec![drawn("Open", art::OPEN), drawn("Save As…", art::SAVE)]
347 vec![drawn("Open", art::OPEN), drawn("Save As", art::SAVE)]
348348 } else {
349349 let mut items = vec![
350350 Item {
......@@ -362,7 +362,7 @@ impl State {
362362 Some(_) => items.extend([
363363 Item {
364364 separated: true,
365 ..drawn("Edit Link…", art::LINK)
365 ..drawn("Edit Link", art::LINK)
366366 },
367367 drawn("Copy Link to Paragraph", art::COPY_LINK),
368368 Item {
......@@ -375,7 +375,7 @@ impl State {
375375 None => items.extend([
376376 Item {
377377 separated: true,
378 ..drawn("Link…", art::LINK)
378 ..drawn("Link", art::LINK)
379379 },
380380 drawn("Copy Link to Paragraph", art::COPY_LINK),
381381 ]),
......@@ -414,7 +414,7 @@ impl State {
414414 self.view.correct(&correction, &chosen_text)?
415415 }
416416 "Delete Repeated Word" => self.view.correct(&correction, "")?,
417 "Spelling…" => {
417 "Spelling" => {
418418 self.open_spelling_pane();
419419 return Ok(());
420420 }
......@@ -441,7 +441,7 @@ impl State {
441441 }
442442 let response = match chosen_text.as_str() {
443443 "Open" => return self.open_attachment(&context.attachment.unwrap()),
444 "Save As…" => return self.save_attachment(&context.attachment.unwrap()),
444 "Save As" => return self.save_attachment(&context.attachment.unwrap()),
445445 "Cut" => self.view.copy(true)?,
446446 "Copy" => self.view.copy(false)?,
447447 "Paste" => {
......@@ -449,7 +449,7 @@ impl State {
449449 .push(Command::Page(canvas::interaction::Request::Paste));
450450 return Ok(());
451451 }
452 "Edit Link…" | "Link…" => {
452 "Edit Link" | "Link" => {
453453 self.open_link_dialog();
454454 return Ok(());
455455 }
crates/snowbound/src/main.rs+15-13
......@@ -2413,7 +2413,7 @@ impl State {
24132413 } else if ui::shell::split_button(
24142414 ui,
24152415 part,
2416 title(id),
2416 commands::command(id).title,
24172417 icon,
24182418 bar,
24192419 None,
......@@ -2496,7 +2496,7 @@ impl State {
24962496 } else if ui::shell::split_button(
24972497 ui,
24982498 part,
2499 title(id),
2499 commands::command(id).title,
25002500 icon,
25012501 None,
25022502 status.checked,
......@@ -2798,7 +2798,7 @@ impl State {
27982798 if ui::shell::split_button(
27992799 ui,
28002800 "pen",
2801 title(Cmd::Pen),
2801 commands::command(Cmd::Pen).title,
28022802 art::PEN,
28032803 bar,
28042804 status.checked,
......@@ -4724,7 +4724,7 @@ fn conflict_bar(ui: &mut Ui, bar: Bar, sections: &[&str], steps: [bool; 2]) -> O
47244724 ..Default::default()
47254725 },
47264726 ui::popup::Item {
4727 text: "Copy Page To…",
4727 text: "Copy Page To",
47284728 icon: Some(art::COPY),
47294729 disabled: sections.is_empty(),
47304730 ..Default::default()
......@@ -5349,7 +5349,9 @@ fn dropdown(
53495349 Head::Split(id) if status_of(id).enabled => {
53505350 let icon = artwork(id).unwrap_or_default();
53515351 let on = status_of(id).checked;
5352 if ui::shell::split_button(ui, part, title(id), icon, None, on, menu).clicked {
5352 if ui::shell::split_button(ui, part, commands::command(id).title, icon, None, on, menu)
5353 .clicked
5354 {
53535355 choice = Some(Choice::Command(id));
53545356 }
53555357 tip(ui, id);
......@@ -5368,7 +5370,7 @@ fn dropdown(
53685370 Head::Split(id) => {
53695371 let icon = artwork(id).unwrap_or_default();
53705372 let anchor = ui::shell::menu_button(ui, part, icon, None, menu);
5371 name(ui, button, title(id));
5373 name(ui, button, commands::command(id).title);
53725374 anchor
53735375 }
53745376 Head::Tag(_, tag) => {
......@@ -5422,7 +5424,7 @@ fn dropdown(
54225424 let options: Vec<_> = entries
54235425 .iter()
54245426 .map(|entry| match *entry {
5425 Entry::Open(_, id) if runs(id) => format!("{} Options", title(id)),
5427 Entry::Open(_, id) if runs(id) => format!("{} Options", commands::command(id).title),
54265428 _ => String::new(),
54275429 })
54285430 .collect();
......@@ -5723,12 +5725,12 @@ fn tag_tip(ui: &mut Ui, place: usize, tag: &canvas::editor::NoteTag) {
57235725
57245726/// A tooltip naming command `id` and its chord on the box built last.
57255727fn tip(ui: &mut Ui, id: commands::Id) {
5726 ui::popup::tooltip(ui, title(id), &commands::shortcut(id), None);
5727}
5728
5729/// Command `id`'s title without the menu's ellipsis, naming the command, not a dialog it opens.
5730fn title(id: commands::Id) -> &'static str {
5731 commands::command(id).title.trim_end_matches('…')
5728 ui::popup::tooltip(
5729 ui,
5730 commands::command(id).title,
5731 &commands::shortcut(id),
5732 None,
5733 );
57325734}
57335735
57345736/// Whether each toolbar group stands on a face of its own.
crates/snowbound/src/menubar.rs+1-1
......@@ -102,7 +102,7 @@ const MENUS: &[Item] = &[
102102 C(Id::Search),
103103 C(Id::SearchResults),
104104 S,
105 Item::System("Start Dictation…", "startDictation:", None),
105 Item::System("Start Dictation", "startDictation:", None),
106106 Item::System(
107107 "Emoji & Symbols",
108108 "orderFrontCharacterPalette:",
crates/snowbound/src/menus.rs+9-9
......@@ -342,7 +342,7 @@ impl State {
342342 item(Action::Copy, "Copy To", nowhere(), false),
343343 ],
344344 Target::Section { path, .. } if crate::recycle::binned(path) => vec![
345 item(Action::SaveAs, "Save As…", false, false),
345 item(Action::SaveAs, "Save As", false, false),
346346 item(Action::Move, "Restore To", nowhere(), false),
347347 item(Action::EmptyRecycleBin, "Empty Recycle Bin", false, true),
348348 item(Action::Colors, "Section Color", false, true),
......@@ -360,7 +360,7 @@ impl State {
360360 item(Action::NewPage, "New Page", false, true),
361361 // OneNote offers this beside New Page, which is a plain + here.
362362 item(Action::NewSubpage, "New Subpage", false, false),
363 item(Action::Theme, "Theme…", false, true),
363 item(Action::Theme, "Theme", false, true),
364364 ];
365365 // Levels and versions are known once the section is open.
366366 let Some(session) = self.session.as_ref().filter(|_| self.open(library, path))
......@@ -400,7 +400,7 @@ impl State {
400400 let section = matches!(target, Target::Section { .. });
401401 let mut actions = vec![item(Action::Rename, "Rename", false, false)];
402402 if section {
403 actions.push(item(Action::SaveAs, "Save As…", false, false));
403 actions.push(item(Action::SaveAs, "Save As", false, false));
404404 }
405405 actions.extend([
406406 item(Action::Delete, "Delete", false, false),
......@@ -412,7 +412,7 @@ impl State {
412412 actions.extend([
413413 item(
414414 Action::Password,
415 "Password Protect This Section…",
415 "Password Protect This Section",
416416 library.catalog().is_none(),
417417 true,
418418 ),
......@@ -423,7 +423,7 @@ impl State {
423423 section_file(library, path).is_none(),
424424 false,
425425 ),
426 item(Action::Theme, "Theme…", false, false),
426 item(Action::Theme, "Theme", false, false),
427427 ]);
428428 }
429429 actions
......@@ -433,10 +433,10 @@ impl State {
433433 let last = self.notebooks.len().saturating_sub(1);
434434 let unread = self.unread_notebook(library);
435435 vec![
436 item(Action::Rename, "Rename…", false, false),
436 item(Action::Rename, "Rename", false, false),
437437 item(
438438 Action::SaveAs,
439 "Save As…",
439 "Save As",
440440 library.catalog().is_none(),
441441 false,
442442 ),
......@@ -493,7 +493,7 @@ impl State {
493493 library.folder().is_none(),
494494 true,
495495 ),
496 item(Action::Theme, "Theme…", false, false),
496 item(Action::Theme, "Theme", false, false),
497497 item(
498498 Action::RecycleBin,
499499 "Notebook Recycle Bin",
......@@ -502,7 +502,7 @@ impl State {
502502 ),
503503 item(
504504 Action::Properties,
505 "Properties…",
505 "Properties",
506506 library.catalog().is_none(),
507507 false,
508508 ),
crates/snowbound/src/options.rs+1-1
......@@ -992,7 +992,7 @@ fn installed(state: &mut State, _: &mut Options) {
992992 let ui = &mut state.ui;
993993 if crate::desktop::uninstallable() {
994994 path(ui, &crate::desktop::binary().unwrap_or_default());
995 if ui::button(ui, "uninstall", "Uninstall…").clicked {
995 if ui::button(ui, "uninstall", "Uninstall").clicked {
996996 crate::desktop::uninstall(&state.proxy);
997997 }
998998 } else if crate::desktop::installable() {
crates/snowbound/src/print.rs+1-5
......@@ -257,11 +257,7 @@ impl State {
257257 });
258258 crate::buttons(ui);
259259 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
260 let action = if dialog.export {
261 "Export…"
262 } else {
263 "Print…"
264 };
260 let action = if dialog.export { "Export" } else { "Print" };
265261 let go = ui::button(ui, "go", action).clicked || entered;
266262 ui.close();
267263 ui.close();
crates/snowbound/src/protection.rs+5-5
......@@ -442,9 +442,9 @@ impl State {
442442 };
443443 text(ui, "status", &status, false);
444444 buttons = if protected {
445 &["Change Password…", "Remove Password…", "Lock All", "Close"]
445 &["Change Password", "Remove Password", "Lock All", "Close"]
446446 } else {
447 &["Set Password…", "Lock All", "Close"]
447 &["Set Password", "Lock All", "Close"]
448448 };
449449 }
450450 Dialog::Set([value, confirm], refused) => {
......@@ -527,9 +527,9 @@ impl State {
527527 self.ui.close_popup(id());
528528 return self.lock_all();
529529 }
530 (Some("Set Password…"), _) => Some(Dialog::Set(Default::default(), None)),
531 (Some("Change Password…"), _) => Some(Dialog::Change(Default::default(), None)),
532 (Some("Remove Password…"), _) => Some(Dialog::Remove(Zeroizing::default(), false)),
530 (Some("Set Password"), _) => Some(Dialog::Set(Default::default(), None)),
531 (Some("Change Password"), _) => Some(Dialog::Change(Default::default(), None)),
532 (Some("Remove Password"), _) => Some(Dialog::Remove(Zeroizing::default(), false)),
533533 (Some(_), Dialog::Unlock(value, _)) => match library.unlock(&path, &value) {
534534 Ok(()) => {
535535 self.commands
crates/snowbound/src/recording.rs+1-1
......@@ -495,7 +495,7 @@ impl State {
495495 let title =
496496 crate::commands::command(crate::commands::Id::Transport(Transport::SeekTo))
497497 .title;
498 node.set_label(title.trim_end_matches('…'));
498 node.set_label(title);
499499 node.set_value(clock_text);
500500 }
501501 if clicked {
crates/snowbound/src/save_as.rs+1-1
......@@ -135,7 +135,7 @@ impl State {
135135 }
136136 crate::buttons(ui);
137137 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
138 let go = ui::button(ui, "go", "Save As…").clicked || entered;
138 let go = ui::button(ui, "go", "Save As").clicked || entered;
139139 ui.close();
140140 ui.close();
141141 if !go && !cancel {
crates/snowbound/src/sidebar.rs+3-3
......@@ -279,7 +279,7 @@ fn sidebar(
279279 (
280280 "shared",
281281 Leading::Icon(art::LINK),
282 "Open Shared…",
282 "Open Shared",
283283 Action::OpenShared,
284284 ),
285285 (
......@@ -1267,13 +1267,13 @@ impl crate::State {
12671267 (
12681268 "shared",
12691269 Leading::Icon(art::LINK),
1270 "Open Shared Notebook…",
1270 "Open Shared Notebook",
12711271 crate::Command::OpenShared,
12721272 ),
12731273 (
12741274 "server",
12751275 Leading::Icon(art::SERVER),
1276 "Open Notebook from Server…",
1276 "Open Notebook from Server",
12771277 crate::Command::OpenFromServer(None),
12781278 ),
12791279 ]
crates/snowbound/src/symbol.rs+1-1
......@@ -101,7 +101,7 @@ pub fn gallery(ui: &mut Ui, id: Id, anchor: Anchor, symbols: &[char]) -> Option<
101101 "more",
102102 Spec {
103103 size: [fill(), fill()],
104 text: Some("More Symbols…"),
104 text: Some("More Symbols"),
105105 icon: Some(art::SYMBOL),
106106 gap: 6.0,
107107 ..Spec::default()
crates/snowbound/src/sync.rs+1-1
......@@ -739,7 +739,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {
739739 &format!("Using the system’s connection. Snowbound couldn’t sign in: {notice}"),
740740 theme.text_dim,
741741 );
742 picked.sign_in = ui::button(ui, "sign-in", "Sign In…").clicked;
742 picked.sign_in = ui::button(ui, "sign-in", "Sign In").clicked;
743743 }
744744 ui.close();
745745 }
crates/snowbound/src/tags.rs+3-3
......@@ -515,11 +515,11 @@ impl State {
515515 let new = button(
516516 ui,
517517 "new",
518 "New Tag…",
518 "New Tag",
519519 list.tags.len() < commands::TAGS && list.editor.is_none(),
520520 );
521521 modify = modify.or_else(|| {
522 button(ui, "modify", "Modify Tag…", list.selected.is_some())
522 button(ui, "modify", "Modify Tag", list.selected.is_some())
523523 .then_some(list.selected)
524524 .flatten()
525525 });
......@@ -1035,7 +1035,7 @@ fn gallery(ui: &mut Ui, id: Id, anchor: Anchor, snowbound: bool) -> Option<Pick>
10351035 ..Spec::default()
10361036 },
10371037 );
1038 if ui::button(ui, "picture", "Custom Image…").clicked {
1038 if ui::button(ui, "picture", "Custom Image").clicked {
10391039 picked = Some(Pick::Picture);
10401040 }
10411041 }
crates/snowbound/src/themes.rs+1-1
......@@ -807,7 +807,7 @@ pub(crate) fn gallery(
807807 "customize",
808808 Spec {
809809 size: [fill(), fill()],
810 text: Some("Customize…"),
810 text: Some("Customize"),
811811 ..Spec::default()
812812 },
813813 );
crates/snowbound/src/unpack.rs+1-1
......@@ -154,7 +154,7 @@ impl State {
154154 ..Spec::default()
155155 },
156156 );
157 let browse = ui::button(ui, "browse", "Browse…").clicked;
157 let browse = ui::button(ui, "browse", "Browse").clicked;
158158 let root = dialog.folder.join(dialog.name.trim());
159159 let full = format!("Full path: {}", root.display());
160160 ui.leaf(
crates/snowbound/tests/replay.rs+2-2
......@@ -452,7 +452,7 @@ fn a_version_copied_into_its_section_is_listed_and_found() {
452452 let notebook =
453453 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/page-versions/candidate-restore");
454454 let mut steps = version_menu();
455 // Copy Page To…, then the section itself.
455 // Copy Page To, then the section itself.
456456 steps.extend(["key Down", "key Down", "key Down", "key Enter", "settle"].map(String::from));
457457 steps.extend(["key Down", "key Enter", "accessibility copied"].map(String::from));
458458 steps.extend(search("Second author", "found"));
......@@ -674,7 +674,7 @@ fn open_shared_notebook_is_offered_with_notebooks_open() {
674674 let [sidebar, dialog] = replay_with(&scratch, Some(&notebook), &steps, &variables)
675675 .try_into()
676676 .unwrap();
677 assert!(sidebar.contains(r#""Open Shared…""#), "{sidebar}");
677 assert!(sidebar.contains(r#""Open Shared""#), "{sidebar}");
678678 assert!(
679679 dialog.contains(r#"Dialog "Open Shared Notebook""#),
680680 "{dialog}"
tools/RELEASE.md+2-2
......@@ -230,7 +230,7 @@ frames from the symbol table.
230230
231231`update.rs` runs one thread. With automatic checks on (Options, General) and a
232232published build, it checks shortly after launch and then daily, skipping while
233Work Offline is on; Check for Updates… (the app menu on macOS, the command
233Work Offline is on; Check for Updates (the app menu on macOS, the command
234234palette elsewhere) checks at once and reports what it found. A check reads
235235`latest.json`, then the named build's `build.json` and signature, then
236236`history.json` and the `build.json` of each build since its own, and
......@@ -241,7 +241,7 @@ the app unpacked into `.Snowbound.app.update` next to the bundle on macOS, the
241241executable into `.snowbound.update` next to it on Linux (`.snowbound.exe.update` on
242242Windows). The sync status icon gets a dot, and its popup says what the update
243243changes, unfolding to the titles, and offers Build Folder and Restart to
244Update; Check for Updates… lists them in its dialog.
244Update; Check for Updates lists them in its dialog.
245245
246246Restart to Update quits the app and starts the old executable with
247247`--finish-update`, which waits for the app to exit, renames the old bundle (or
tools/canvas/README.md+1-1
......@@ -50,7 +50,7 @@ snowbound title bar · toolbar · section frame · page tabs · the page as a
5050
5151`--notebook FOLDER` opens a notebook laid out as OneNote lays one out. The title bar shows the application's icon and the notebook's name; below it two rows of toolbar buttons follow the Home ribbon's Basic Text group, the first nine tags and zoom (only zoom acts yet). Readable top-level sections are tabs in the notebook's order, each leaning over the next at 45° with a faint highlight along its top, and the open one on top, casting a soft shadow and framing the page and the page tabs on the right in its colour, which shades slightly down the window; a tab that opens rises to meet the frame as its outline fades in. The frame's top corners and the page's corners are rounded concentric with the window's. The open page's tab is the page's colour and joins it with rounded inside corners; subpages are indented. The interface follows the system's light or dark appearance, and in the dark one the page's paper darkens and text left at OneNote's automatic colour turns light (except on a highlight), resolved when painting so nothing re-lays out; explicit colours, highlights, pictures and ink keep theirs, except that the background art of OneNote 2010's page templates, recognised by the picture's hash, paints from bundled vector recreations drawn for the paper. Sections take their stored colour's hue at a saturation and lightness chosen for each appearance. The section's colour is also the interface accent, easing to the next section's when another opens. The search box, New Page and the page-tab toggle sit above the page tabs. Command-F searches the page titles; Enter opens the first match and Escape clears it. A page that also changed on another computer shows Keep mine / Keep theirs above it. `--section FILE TITLE` opens one section the same way at that page. Section files must be regular files; discovery rejects symbolic links.
5252
53Launched without a file, the app reopens the notebooks open when it last closed, at the one shown last, or shows "No notebooks open" with New Notebook and Open Existing. Settings (the open notebooks, the sidebar, recent fonts and the user name edits are stored under, which Settings… (โŒ˜,, Ctrl+, on Linux, or Options in the sidebar) changes and which starts as the account's full name) are JSON in `~/Library/Application Support/Snowbound/settings.json` or `$XDG_CONFIG_HOME/snowbound`; `--settings FILE` uses another file, `--notebook FOLDER` adds a notebook to them, and `--screenshot` never writes them. Open Existing takes a notebook folder, its `Open Notebook.onetoc2` or a section file, which opens in the notebook its folders make up. A notebook on a mounted SMB share opens through the embedded SMB client, signed in with the password the system keychain holds, and through the mount only when that fails. The notebook button left of the section tabs opens OneNote 2010's navigation bar: the open notebooks with their sections and section groups, the open section marked; dragging a row reorders a folder or, onto a group, moves into it, and right-clicking a row, a section tab or a page tab offers OneNote's commands (Rename, Delete to the notebook's recycle bin, New Section, New Section Group, Close This Notebook; Delete, New Page, Make Subpage, Promote Subpage). Page tabs drag to reorder. The `+` above the page tabs adds a page at the end of the section, titled with its date and time as OneNote titles one, and puts the caret in its title; until its body is typed in, drawn on or clicked, templates offer our recreations of OneNote 2010's art (Ivy first), its page colours and Informal Meeting Notes' content, written as ops (`corpus/notebook-management`).
53Launched without a file, the app reopens the notebooks open when it last closed, at the one shown last, or shows "No notebooks open" with New Notebook and Open Existing. Settings (the open notebooks, the sidebar, recent fonts and the user name edits are stored under, which Settings (โŒ˜,, Ctrl+, on Linux, or Options in the sidebar) changes and which starts as the account's full name) are JSON in `~/Library/Application Support/Snowbound/settings.json` or `$XDG_CONFIG_HOME/snowbound`; `--settings FILE` uses another file, `--notebook FOLDER` adds a notebook to them, and `--screenshot` never writes them. Open Existing takes a notebook folder, its `Open Notebook.onetoc2` or a section file, which opens in the notebook its folders make up. A notebook on a mounted SMB share opens through the embedded SMB client, signed in with the password the system keychain holds, and through the mount only when that fails. The notebook button left of the section tabs opens OneNote 2010's navigation bar: the open notebooks with their sections and section groups, the open section marked; dragging a row reorders a folder or, onto a group, moves into it, and right-clicking a row, a section tab or a page tab offers OneNote's commands (Rename, Delete to the notebook's recycle bin, New Section, New Section Group, Close This Notebook; Delete, New Page, Make Subpage, Promote Subpage). Page tabs drag to reorder. The `+` above the page tabs adds a page at the end of the section, titled with its date and time as OneNote titles one, and puts the caret in its title; until its body is typed in, drawn on or clicked, templates offer our recreations of OneNote 2010's art (Ivy first), its page colours and Informal Meeting Notes' content, written as ops (`corpus/notebook-management`).
5454
5555As in OneNote 2010, Notebook Recycle Bin (File menu, a notebook's context menu, or the palette) shows the notebook's recycle bin as its own row of tabs, read-only under a bar: a page's context menu restores it to a section, keeping its identity, copies it out, or deletes it for good, a section's restores it to a folder, and Empty Recycle Bin deletes what it holds (`corpus/recycle-bin-view`). Pages another author changed since they were viewed are bold in the page list, and so are their section's tab and row and the groups and notebook holding them, until left or marked read (Mark as Read, Ctrl+Q off macOS; Mark Notebook as Read; Next Unread; Show Unread Changes in This Notebook); what was read is kept in the cache folder's `read.json`, as OneNote keeps it in its cache. Save As (โ‡งโŒ˜S) writes the page or section as a OneNote section, the notebook as a OneNote package, or any of them as a PDF; opening a `.onepkg` asks, as Unpack Notebook does, for its name, colour and folder (`corpus/notebook-package`).
5656