authorclover caruso <git@paperclover.net> 2026-10-15 02:46:10-07:00
committerclover caruso <git@paperclover.net> 2026-10-15 02:58:22-07:00
loge3d4066dc407255c595ad64f3ec4aaed85e64628
tree124b617d8f5fae8d69fef9aa985596a0734401d6
parentc355a943bcb3d015015c97abff850d15d654252f
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

fix(desktop): keep menu shortcuts from acting behind other windows

On macOS, commands wait while another of the app's windows, such as the About panel, has the keys. The page list compares titles the way the store derives them, so pages whose stored title differs from their title text no longer reread the list on every edit. Refs #129 Assisted-by: claude-opus-5.5

4 files changed, 24 insertions(+), 13 deletions(-)

crates/onestore/src/op/mod.rs+1-1
...@@ -27,7 +27,7 @@ pub(crate) mod tests;...@@ -27,7 +27,7 @@ pub(crate) mod tests;
27pub use lower::{lower, lower_page};27pub use lower::{lower, lower_page};
28/// `op` applied to `page`, the model of what the section holds: the page `Section::apply`28/// `op` applied to `page`, the model of what the section holds: the page `Section::apply`
29/// leaves, as reading it back shows it.29/// leaves, as reading it back shows it.
30pub use model::apply as predict;30pub use model::{apply as predict, title_line};
3131
32/// Property identifiers with their encoded values.32/// Property identifiers with their encoded values.
33pub(crate) type Values = Vec<(u32, Vec<u8>)>;33pub(crate) type Values = Vec<(u32, Vec<u8>)>;
crates/onestore/src/op/model.rs+8-5
...@@ -551,22 +551,25 @@ fn relevel(list: &mut [PageParagraph], index: usize, level: u32) {...@@ -551,22 +551,25 @@ fn relevel(list: &mut [PageParagraph], index: usize, level: u32) {
551/// The title a page's writers store after changing it: the title text's first line when551/// The title a page's writers store after changing it: the title text's first line when
552/// it has one, which only an edit of that text changes, else the first line of body text.552/// it has one, which only an edit of that text changes, else the first line of body text.
553fn retitle(page: &mut Page, edited: Option<ExGuid>) {553fn retitle(page: &mut Page, edited: Option<ExGuid>) {
554 let line = |text: &str| {
555 crate::edit::without_fields(text.trim_start().split('\r').next().unwrap_or_default())
556 };
557 match find_title_text(page) {554 match find_title_text(page) {
558 Some(title) => {555 Some(title) => {
559 let text = text_of(page, title);556 let text = text_of(page, title);
560 if line(&text).is_empty() {557 if title_line(&text).is_empty() {
561 automatic(page);558 automatic(page);
562 } else if edited == Some(title) {559 } else if edited == Some(title) {
563 page.title = line(&text);560 page.title = title_line(&text);
564 }561 }
565 }562 }
566 None => automatic(page),563 None => automatic(page),
567 }564 }
568}565}
569566
567/// The title stored for title text `text`: its first line, without leading space or fields.
568/// Empty leaves the page titled by its body.
569pub fn title_line(text: &str) -> String {
570 crate::edit::without_fields(text.trim_start().split('\r').next().unwrap_or_default())
571}
572
570fn automatic(page: &mut Page) {573fn automatic(page: &mut Page) {
571 let mut roots: Vec<&PageObject> = page574 let mut roots: Vec<&PageObject> = page
572 .objects575 .objects
crates/snowbound/src/macos.rs+8-4
...@@ -1456,16 +1456,20 @@ pub fn text_colors(window: &Window) -> [[f32; 4]; 3] {...@@ -1456,16 +1456,20 @@ pub fn text_colors(window: &Window) -> [[f32; 4]; 3] {
1456 }1456 }
1457}1457}
14581458
1459/// Whether the window takes commands: not while a native dialog is up over it.1459/// Whether the window takes commands: not while a native dialog is up over it or another
1460/// of the application's windows has the keys.
1460pub(crate) fn commands_allowed() -> bool {1461pub(crate) fn commands_allowed() -> bool {
1461 let app = NSApplication::sharedApplication(1462 let app = NSApplication::sharedApplication(
1462 MainThreadMarker::new().expect("Commands run on the main thread"),1463 MainThreadMarker::new().expect("Commands run on the main thread"),
1463 );1464 );
1464 WINDOW.with_borrow(|window| unsafe {1465 WINDOW.with_borrow(|window| unsafe {
1465 app.modalWindow().is_none()1466 app.modalWindow().is_none()
1466 && window1467 && window.as_ref().is_none_or(|window| {
1467 .as_ref()1468 window.attachedSheet().is_none()
1468 .is_none_or(|window| window.attachedSheet().is_none())1469 && app
1470 .keyWindow()
1471 .is_none_or(|key| std::ptr::eq(&*key, &**window))
1472 })
1469 })1473 })
1470}1474}
14711475
crates/snowbound/src/main.rs+7-3
...@@ -4304,10 +4304,14 @@ impl State {...@@ -4304,10 +4304,14 @@ impl State {
4304 },4304 },
4305 )?;4305 )?;
4306 // The page list shows the title as it is typed, even when the caret left it since.4306 // The page list shows the title as it is typed, even when the caret left it since.
4307 let title = (self.view.editor.outlines().iter())4307 let editor = &self.view.editor;
4308 let title = (editor.outlines().iter())
4308 .find(|outline| outline.title)4309 .find(|outline| outline.title)
4309 .map(canvas::editor::TextOutline::shown_text);4310 .and_then(|outline| outline.document().paragraphs().last())
4310 if title.is_some_and(|title| title != session.title()) {4311 .map(|paragraph| onestore::op::title_line(paragraph.text()));
4312 if editor.active_outline().title
4313 || title.is_some_and(|title| !title.is_empty() && title != session.title())
4314 {
4311 session.pages = session.section.pages()?;4315 session.pages = session.section.pages()?;
4312 }4316 }
4313 self.edited(vec![space]);4317 self.edited(vec![space]);