From 9a4bf4b9f8c3626ec95e01a8b8ab6af358d4e189 Mon Sep 17 00:00:00 2001 From: Jeena Date: Sun, 22 Mar 2026 01:51:12 +0000 Subject: [PATCH] article-row: fix bold on initial load, add right-click menu Set unread bold state directly in bind() instead of relying on obj.notify("unread"), which was unreliable during list factory binding (GLib may defer or drop notifications during initial bind). Also add a right-click context menu on each article row with a single "Mark as Unread" item. The menu is a GtkPopover positioned at the cursor. Clicking it activates the new win.mark-article-unread action, which takes the article ID as a string parameter and reuses the existing mark-unread logic. Refactor do_mark_unread() to delegate to the new do_mark_article_unread() so the behaviour is consistent whether triggered from the toolbar button, keyboard shortcut, or right-click menu. --- src/article_row.rs | 77 +++++++++++++++++++++++++++++++++++++++++++--- src/window.rs | 16 ++++++++-- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/article_row.rs b/src/article_row.rs index 23c6c1b..bb239b2 100644 --- a/src/article_row.rs +++ b/src/article_row.rs @@ -44,6 +44,7 @@ mod imp { pub bindings: RefCell>, pub unread_handler: RefCell>, + pub context_menu: std::cell::OnceCell, } #[glib::object_subclass] @@ -61,20 +62,88 @@ mod imp { } } - impl ObjectImpl for ArticleRow {} + impl ObjectImpl for ArticleRow { + fn constructed(&self) { + self.parent_constructed(); + self.setup_context_menu(); + } + + fn dispose(&self) { + if let Some(popover) = self.context_menu.get() { + popover.unparent(); + } + } + } impl WidgetImpl for ArticleRow {} impl BoxImpl for ArticleRow {} impl ArticleRow { + fn setup_context_menu(&self) { + let button = gtk4::Button::with_label("Mark as Unread"); + button.set_has_frame(false); + + let popover = gtk4::Popover::new(); + popover.set_child(Some(&button)); + popover.set_parent(&*self.obj()); + self.context_menu.set(popover.clone()).ok(); + + // Close popover and activate action when button is clicked. + let imp_weak = self.downgrade(); + let popover_weak = popover.downgrade(); + button.connect_clicked(move |_| { + if let Some(popover) = popover_weak.upgrade() { + popover.popdown(); + } + let Some(imp) = imp_weak.upgrade() else { return }; + let handler = imp.unread_handler.borrow(); + let Some((obj, _)) = handler.as_ref() else { return }; + let article_id = obj.article().id.clone(); + drop(handler); + imp.obj() + .activate_action( + "win.mark-article-unread", + Some(&article_id.to_variant()), + ) + .ok(); + }); + + // Right-click gesture to show the popover at cursor position. + let gesture = gtk4::GestureClick::new(); + gesture.set_button(3); + let popover_weak2 = popover.downgrade(); + gesture.connect_pressed(move |gesture, _, x, y| { + gesture.set_state(gtk4::EventSequenceState::Claimed); + let Some(popover) = popover_weak2.upgrade() else { return }; + popover.set_pointing_to(Some(>k4::gdk::Rectangle::new( + x as i32, + y as i32, + 1, + 1, + ))); + popover.popup(); + }); + self.obj().add_controller(gesture); + } + pub fn bind(&self, obj: &ArticleObject) { let article = obj.article(); self.feed_title_label.set_text(&article.feed_title); self.excerpt_label.set_text(&article.excerpt); self.date_label.set_text(&relative_time(article.published)); + + // Set initial bold state directly (using Pango markup to avoid + // CSS specificity issues with the zoom font-size provider). + let escaped = glib::markup_escape_text(&article.title); + if article.unread { + self.title_label.remove_css_class("dim-label"); + self.title_label.set_markup(&format!("{escaped}")); + } else { + self.title_label.add_css_class("dim-label"); + self.title_label.set_markup(&escaped); + } drop(article); - // Register handler first, then trigger it for the initial state. - // Using Pango markup for bold avoids CSS specificity issues. + // Connect handler for future unread state changes. let title_label = self.title_label.clone(); let id = obj.connect_notify_local(Some("unread"), move |obj, _| { let article = obj.article(); @@ -88,7 +157,6 @@ mod imp { } }); *self.unread_handler.borrow_mut() = Some((obj.clone(), id)); - obj.notify("unread"); } pub fn unbind(&self) { @@ -99,7 +167,6 @@ mod imp { b.unbind(); } } - } } diff --git a/src/window.rs b/src/window.rs index 683e046..c3c0699 100644 --- a/src/window.rs +++ b/src/window.rs @@ -77,6 +77,11 @@ pub mod imp { klass.install_action("win.reload", None, |win, _, _| win.imp().do_reload()); klass.install_action("win.logout", None, |win, _, _| win.imp().do_logout()); klass.install_action("win.mark-unread", None, |win, _, _| win.imp().do_mark_unread()); + klass.install_action("win.mark-article-unread", Some(glib::VariantTy::STRING), |win, _, param| { + if let Some(id) = param.and_then(|p| p.get::()) { + win.imp().do_mark_article_unread(id); + } + }); klass.install_action("win.open-in-browser", None, |win, _, _| { win.imp().do_open_in_browser() }); @@ -750,8 +755,11 @@ pub mod imp { fn do_mark_unread(&self) { let id = self.current_article_id.borrow().clone(); let Some(id) = id else { return }; + self.do_mark_article_unread(id); + } - // Find the ArticleObject in the store and set unread=true + fn do_mark_article_unread(&self, id: String) { + // Find the ArticleObject in the store and set unread=true. if let Some(store) = self.article_store.borrow().as_ref() { for i in 0..store.n_items() { if let Some(obj) = store.item(i).and_downcast::() { @@ -762,7 +770,11 @@ pub mod imp { } } - *self.mark_unread_guard.borrow_mut() = true; + // If this is the currently displayed article, guard against it + // being immediately re-marked read when the selection fires. + if self.current_article_id.borrow().as_deref() == Some(&*id) { + *self.mark_unread_guard.borrow_mut() = true; + } let api = self.api.borrow().clone(); let wt = self.write_token.borrow().clone();