[fractal] context-menu-bin: Fix non-working actions



commit 5ac882eaa15ba0ae47d4581a78fda47242b3772e
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Wed Apr 13 18:21:22 2022 +0200

    context-menu-bin: Fix non-working actions
    
    Destroying the popover on close results in the action not being
    called.
    
    Instead do like the members list and keep a single popover for the
    whole list. To do that we need to pass a weak reference to the
    closest common parent of the list items, via the list view factory.

 data/resources/resources.gresource.xml       |   2 -
 data/resources/style.css                     |   5 +-
 data/resources/ui/content-item.ui            |  18 ---
 data/resources/ui/content-room-history.ui    |   5 -
 data/resources/ui/sidebar-item.ui            |  13 --
 data/resources/ui/sidebar-room-row.ui        |  53 --------
 data/resources/ui/sidebar.ui                 |  59 +++++++-
 po/POTFILES.in                               |   1 -
 src/components/context_menu_bin.rs           | 192 ++++++++++++++++++++-------
 src/session/content/room_history/item_row.rs | 128 ++++++++++++------
 src/session/content/room_history/mod.rs      |  20 ++-
 src/session/sidebar/mod.rs                   |  28 +++-
 src/session/sidebar/room_row.rs              |  22 ++-
 src/session/sidebar/row.rs                   |  39 ++++--
 14 files changed, 373 insertions(+), 212 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 8f23ec3f8..a364c6e5a 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -48,7 +48,6 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-invite.ui">ui/content-invite.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-invitee-item.ui">ui/content-invitee-item.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-invitee-row.ui">ui/content-invitee-row.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks" alias="content-item.ui">ui/content-item.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-markdown-popover.ui">ui/content-markdown-popover.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-member-item.ui">ui/content-member-item.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-member-page.ui">ui/content-member-page.ui</file>
@@ -91,7 +90,6 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-account-switcher.ui">ui/sidebar-account-switcher.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-category-row.ui">ui/sidebar-category-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-entry-row.ui">ui/sidebar-entry-row.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks" alias="sidebar-item.ui">ui/sidebar-item.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-room-row.ui">ui/sidebar-room-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-verification-row.ui">ui/sidebar-verification-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="sidebar.ui">ui/sidebar.ui</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index 64e3e1dda..ee2cb0867 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -240,7 +240,6 @@ login {
 
 .sidebar-list row > * {
   margin: 0 12px;
-  padding: 12px 6px;
   border-radius: 6px;
   transition-property: outline, outline-width, outline-offset, outline-color;
   transition-duration: 300ms;
@@ -249,6 +248,10 @@ login {
   outline-offset: 2px;
 }
 
+.sidebar-list sidebar-row > * > box {
+  padding: 12px 6px;
+}
+
 .sidebar-list row:focus-within > * {
   outline-color: alpha(@accent_color, 0.5);
   outline-width: 2px;
diff --git a/data/resources/ui/content-room-history.ui b/data/resources/ui/content-room-history.ui
index 9e06944b6..35e85cc8d 100644
--- a/data/resources/ui/content-room-history.ui
+++ b/data/resources/ui/content-room-history.ui
@@ -201,11 +201,6 @@
                                 <style>
                                   <class name="navigation-sidebar"/>
                                 </style>
-                                <property name="factory">
-                                  <object class="GtkBuilderListItemFactory">
-                                    <property name="resource">/org/gnome/Fractal/content-item.ui</property>
-                                  </object>
-                                </property>
                                 <property name="single-click-activate">True</property>
                                 <accessibility>
                                   <property name="label" translatable="yes">Room History</property>
diff --git a/data/resources/ui/sidebar-room-row.ui b/data/resources/ui/sidebar-room-row.ui
index d8ab375e8..a722ef3fa 100644
--- a/data/resources/ui/sidebar-room-row.ui
+++ b/data/resources/ui/sidebar-room-row.ui
@@ -1,58 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <menu id="room_row_menu">
-    <section>
-      <item>
-        <attribute name="label" translatable="yes">_Accept</attribute>
-        <attribute name="action">room-row.accept-invite</attribute>
-        <attribute name="hidden-when">action-disabled</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">_Reject</attribute>
-        <attribute name="action">room-row.reject-invite</attribute>
-        <attribute name="hidden-when">action-disabled</attribute>
-      </item>
-    </section>
-    <section>
-      <item>
-        <attribute name="label" translatable="yes">Mark as _Favorite</attribute>
-        <attribute name="action">room-row.set-favorite</attribute>
-        <attribute name="hidden-when">action-disabled</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">Unmark as _Favorite</attribute>
-        <attribute name="action">room-row.unset-favorite</attribute>
-        <attribute name="hidden-when">action-disabled</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">Mark as Low _Priority</attribute>
-        <attribute name="action">room-row.set-lowpriority</attribute>
-        <attribute name="hidden-when">action-disabled</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">Unmark as Low _Priority</attribute>
-        <attribute name="action">room-row.unset-lowpriority</attribute>
-        <attribute name="hidden-when">action-disabled</attribute>
-      </item>
-    </section>
-    <section>
-      <item>
-        <attribute name="label" translatable="yes">_Leave Room</attribute>
-        <attribute name="action">room-row.leave</attribute>
-        <attribute name="hidden-when">action-disabled</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">Re_join Room</attribute>
-        <attribute name="action">room-row.join</attribute>
-        <attribute name="hidden-when">action-disabled</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">_Forget Room</attribute>
-        <attribute name="action">room-row.forget</attribute>
-        <attribute name="hidden-when">action-disabled</attribute>
-      </item>
-    </section>
-  </menu>
   <template class="SidebarRoomRow" parent="ContextMenuBin">
     <child>
       <object class="GtkBox">
diff --git a/data/resources/ui/sidebar.ui b/data/resources/ui/sidebar.ui
index 4dabb491a..2fe1a64ef 100644
--- a/data/resources/ui/sidebar.ui
+++ b/data/resources/ui/sidebar.ui
@@ -22,6 +22,59 @@
       </item>
     </section>
   </menu>
+  <menu id="room_row_menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Accept</attribute>
+        <attribute name="action">room-row.accept-invite</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Reject</attribute>
+        <attribute name="action">room-row.reject-invite</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">Mark as _Favorite</attribute>
+        <attribute name="action">room-row.set-favorite</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Unmark as _Favorite</attribute>
+        <attribute name="action">room-row.unset-favorite</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Mark as Low _Priority</attribute>
+        <attribute name="action">room-row.set-lowpriority</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Unmark as Low _Priority</attribute>
+        <attribute name="action">room-row.unset-lowpriority</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Leave Room</attribute>
+        <attribute name="action">room-row.leave</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Re_join Room</attribute>
+        <attribute name="action">room-row.join</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Forget Room</attribute>
+        <attribute name="action">room-row.forget</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+      </item>
+    </section>
+  </menu>
   <template class="Sidebar" parent="AdwBin">
     <child>
       <object class="GtkBox">
@@ -98,11 +151,6 @@
                   <class name="sidebar-list"/>
                 </style>
                 <property name="single-click-activate">true</property>
-                <property name="factory">
-                  <object class="GtkBuilderListItemFactory">
-                    <property name="resource">/org/gnome/Fractal/sidebar-item.ui</property>
-                  </object>
-                </property>
                 <accessibility>
                   <property name="label" translatable="yes">Sidebar</property>
                   <property name="description" translatable="yes">Allows to navigate between rooms</property>
@@ -115,4 +163,3 @@
     </child>
   </template>
 </interface>
-
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 1f3367553..8f7a71ec4 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -36,7 +36,6 @@ data/resources/ui/member-menu.ui
 data/resources/ui/room-creation.ui
 data/resources/ui/session-verification.ui
 data/resources/ui/shortcuts.ui
-data/resources/ui/sidebar-room-row.ui
 data/resources/ui/sidebar.ui
 data/resources/ui/qr-code-scanner.ui
 
diff --git a/src/components/context_menu_bin.rs b/src/components/context_menu_bin.rs
index 758f61b27..16a2e57de 100644
--- a/src/components/context_menu_bin.rs
+++ b/src/components/context_menu_bin.rs
@@ -5,26 +5,43 @@ use log::debug;
 mod imp {
     use std::cell::RefCell;
 
-    use glib::subclass::InitializingObject;
+    use glib::{subclass::InitializingObject, SignalHandlerId};
 
     use super::*;
-    type FactoryFn = RefCell<Option<Box<dyn Fn(&super::ContextMenuBin, &gtk::PopoverMenu)>>>;
 
-    #[derive(Default, CompositeTemplate)]
+    #[repr(C)]
+    pub struct ContextMenuBinClass {
+        pub parent_class: glib::object::Class<adw::Bin>,
+        pub menu_opened: fn(&super::ContextMenuBin),
+    }
+
+    unsafe impl ClassStruct for ContextMenuBinClass {
+        type Type = ContextMenuBin;
+    }
+
+    pub(super) fn context_menu_bin_menu_opened(this: &super::ContextMenuBin) {
+        let klass = this.class();
+        (klass.as_ref().menu_opened)(this)
+    }
+
+    #[derive(Debug, Default, CompositeTemplate)]
     #[template(resource = "/org/gnome/Fractal/context-menu-bin.ui")]
     pub struct ContextMenuBin {
         #[template_child]
         pub click_gesture: TemplateChild<gtk::GestureClick>,
         #[template_child]
         pub long_press_gesture: TemplateChild<gtk::GestureLongPress>,
-        pub factory: FactoryFn,
+        pub popover: RefCell<Option<gtk::PopoverMenu>>,
+        pub signal_handler: RefCell<Option<SignalHandlerId>>,
     }
 
     #[glib::object_subclass]
     impl ObjectSubclass for ContextMenuBin {
         const NAME: &'static str = "ContextMenuBin";
+        const ABSTRACT: bool = true;
         type Type = super::ContextMenuBin;
         type ParentType = adw::Bin;
+        type Class = ContextMenuBinClass;
 
         fn class_init(klass: &mut Self::Class) {
             Self::bind_template(klass);
@@ -44,6 +61,12 @@ mod imp {
                 "context-menu.activate",
                 None,
             );
+
+            klass.install_action("context-menu.close", None, move |widget, _, _| {
+                if let Some(popover) = widget.popover() {
+                    popover.popdown();
+                }
+            });
         }
 
         fn instance_init(obj: &InitializingObject<Self>) {
@@ -52,6 +75,41 @@ mod imp {
     }
 
     impl ObjectImpl for ContextMenuBin {
+        fn properties() -> &'static [glib::ParamSpec] {
+            use once_cell::sync::Lazy;
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![glib::ParamSpecObject::new(
+                    "popover",
+                    "Popover",
+                    "The popover of the context menu",
+                    gtk::PopoverMenu::static_type(),
+                    glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                )]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "popover" => obj.set_popover(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "popover" => obj.popover().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
         fn constructed(&self, obj: &Self::Type) {
             self.long_press_gesture
                 .connect_pressed(clone!(@weak obj => move |gesture, x, y| {
@@ -72,6 +130,12 @@ mod imp {
             );
             self.parent_constructed(obj);
         }
+
+        fn dispose(&self, _obj: &Self::Type) {
+            if let Some(popover) = self.popover.take() {
+                popover.unparent()
+            }
+        }
     }
 
     impl WidgetImpl for ContextMenuBin {}
@@ -86,27 +150,12 @@ glib::wrapper! {
 }
 
 impl ContextMenuBin {
-    pub fn new() -> Self {
-        glib::Object::new(&[]).expect("Failed to create ContextMenuBin")
-    }
-
     fn open_menu_at(&self, x: i32, y: i32) {
-        debug!("Context menu was activated");
-        if let Some(factory) = &*self.imp().factory.borrow() {
-            let popover = gtk::PopoverMenu::builder()
-                .position(gtk::PositionType::Bottom)
-                .has_arrow(false)
-                .halign(gtk::Align::Start)
-                .build();
-
-            popover.set_parent(self);
-
-            popover.connect_closed(|popover| {
-                popover.unparent();
-            });
-
-            (factory)(self, &popover);
+        debug!("Open menu at ({x}, {y})");
+        self.menu_opened();
 
+        if let Some(popover) = self.popover() {
+            debug!("Context menu was activated");
             popover.set_pointing_to(Some(&gdk::Rectangle::new(x, y, 0, 0)));
             popover.popup();
         }
@@ -114,43 +163,90 @@ impl ContextMenuBin {
 }
 
 pub trait ContextMenuBinExt: 'static {
-    /// Set the closure used to create the content of the `gtk::PopoverMenu`
-    fn set_factory<F>(&self, factory: F)
-    where
-        F: Fn(&Self, &gtk::PopoverMenu) + 'static;
+    /// Get the `PopoverMenu` used in the context menu.
+    fn popover(&self) -> Option<gtk::PopoverMenu>;
+
+    /// Set the `PopoverMenu` used in the context menu.
+    fn set_popover(&self, popover: Option<gtk::PopoverMenu>);
 
-    fn remove_factory(&self);
+    /// Called when the menu was requested to open but before the menu is shown.
+    fn menu_opened(&self);
 }
 
 impl<O: IsA<ContextMenuBin>> ContextMenuBinExt for O {
-    fn set_factory<F>(&self, factory: F)
-    where
-        F: Fn(&O, &gtk::PopoverMenu) + 'static,
-    {
-        let f = move |obj: &ContextMenuBin, popover: &gtk::PopoverMenu| {
-            factory(obj.downcast_ref::<O>().unwrap(), popover);
-        };
-        self.upcast_ref().imp().factory.replace(Some(Box::new(f)));
+    fn popover(&self) -> Option<gtk::PopoverMenu> {
+        self.upcast_ref().imp().popover.borrow().clone()
     }
 
-    fn remove_factory(&self) {
-        self.upcast_ref().imp().factory.take();
+    fn set_popover(&self, popover: Option<gtk::PopoverMenu>) {
+        let obj = self.upcast_ref();
+
+        if obj.popover() == popover {
+            return;
+        }
+
+        let priv_ = obj.imp();
+
+        if let Some(popover) = &popover {
+            popover.unparent();
+            popover.set_parent(obj);
+            priv_
+                .signal_handler
+                .replace(Some(popover.connect_parent_notify(
+                    clone!(@weak obj => move |popover| {
+                        if popover.parent().as_ref() != Some(obj.upcast_ref()) {
+                            let priv_ = obj.imp();
+                            if let Some(popover) = priv_.popover.take() {
+                                if let Some(signal_handler) = priv_.signal_handler.take() {
+                                    popover.disconnect(signal_handler)
+                                }
+                            }
+                        }
+                    }),
+                )));
+        }
+
+        obj.imp().popover.replace(popover);
+        obj.notify("popover");
+    }
+
+    fn menu_opened(&self) {
+        imp::context_menu_bin_menu_opened(self.upcast_ref())
     }
 }
 
-pub trait ContextMenuBinImpl: BinImpl {}
+/// Public trait that must be implemented for everything that derives from
+/// `ContextMenuBin`.
+///
+/// Overriding a method from this Trait overrides also its behavior in
+/// `ContextMenuBinExt`.
+pub trait ContextMenuBinImpl: BinImpl {
+    /// Called when the menu was requested to open but before the menu is shown.
+    ///
+    /// This method should be used to set the popover dynamically.
+    fn menu_opened(&self, _obj: &Self::Type) {}
+}
 
-unsafe impl<T: ContextMenuBinImpl> IsSubclassable<T> for ContextMenuBin {
+unsafe impl<T> IsSubclassable<T> for ContextMenuBin
+where
+    T: ContextMenuBinImpl,
+    T::Type: IsA<ContextMenuBin>,
+{
     fn class_init(class: &mut glib::Class<Self>) {
-        <gtk::Widget as IsSubclassable<T>>::class_init(class);
-    }
-    fn instance_init(instance: &mut glib::subclass::InitializingObject<T>) {
-        <gtk::Widget as IsSubclassable<T>>::instance_init(instance);
+        Self::parent_class_init::<T>(class.upcast_ref_mut());
+
+        let klass = class.as_mut();
+
+        klass.menu_opened = menu_opened_trampoline::<T>;
     }
 }
 
-impl Default for ContextMenuBin {
-    fn default() -> Self {
-        Self::new()
-    }
+// Virtual method implementation trampolines.
+fn menu_opened_trampoline<T>(this: &ContextMenuBin)
+where
+    T: ObjectSubclass + ContextMenuBinImpl,
+    T::Type: IsA<ContextMenuBin>,
+{
+    let this = this.downcast_ref::<T::Type>().unwrap();
+    this.imp().menu_opened(this)
 }
diff --git a/src/session/content/room_history/item_row.rs b/src/session/content/room_history/item_row.rs
index 49ac0f3c3..6e4b3862f 100644
--- a/src/session/content/room_history/item_row.rs
+++ b/src/session/content/room_history/item_row.rs
@@ -6,7 +6,7 @@ use matrix_sdk::ruma::events::AnySyncRoomEvent;
 use crate::{
     components::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl, ReactionChooser},
     session::{
-        content::room_history::{message_row::MessageRow, DividerRow, StateRow},
+        content::room_history::{message_row::MessageRow, DividerRow, RoomHistory, StateRow},
         room::{
             Event, EventActions, TimelineDayDivider, TimelineItem, TimelineNewMessagesDivider,
             TimelineSpinner,
@@ -17,14 +17,16 @@ use crate::{
 mod imp {
     use std::cell::RefCell;
 
-    use glib::signal::SignalHandlerId;
+    use glib::{signal::SignalHandlerId, WeakRef};
+    use once_cell::unsync::OnceCell;
 
     use super::*;
 
     #[derive(Debug, Default)]
     pub struct ItemRow {
+        pub room_history: OnceCell<WeakRef<RoomHistory>>,
         pub item: RefCell<Option<TimelineItem>>,
-        pub menu_model: RefCell<Option<gio::MenuModel>>,
+        pub action_group: RefCell<Option<gio::SimpleActionGroup>>,
         pub notify_handler: RefCell<Option<SignalHandlerId>>,
         pub binding: RefCell<Option<glib::Binding>>,
         pub reaction_chooser: RefCell<Option<ReactionChooser>>,
@@ -42,13 +44,22 @@ mod imp {
         fn properties() -> &'static [glib::ParamSpec] {
             use once_cell::sync::Lazy;
             static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
-                vec![glib::ParamSpecObject::new(
-                    "item",
-                    "Item",
-                    "The timeline item represented by this row",
-                    TimelineItem::static_type(),
-                    glib::ParamFlags::READWRITE,
-                )]
+                vec![
+                    glib::ParamSpecObject::new(
+                        "item",
+                        "Item",
+                        "The timeline item represented by this row",
+                        TimelineItem::static_type(),
+                        glib::ParamFlags::READWRITE,
+                    ),
+                    glib::ParamSpecObject::new(
+                        "room-history",
+                        "room-history",
+                        "The ancestor room history of this row",
+                        RoomHistory::static_type(),
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+                    ),
+                ]
             });
 
             PROPERTIES.as_ref()
@@ -63,6 +74,10 @@ mod imp {
         ) {
             match pspec.name() {
                 "item" => obj.set_item(value.get().unwrap()),
+                "room-history" => self
+                    .room_history
+                    .set(value.get::<RoomHistory>().unwrap().downgrade())
+                    .unwrap(),
                 _ => unimplemented!(),
             }
         }
@@ -70,6 +85,7 @@ mod imp {
         fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
             match pspec.name() {
                 "item" => obj.item().to_value(),
+                "room-history" => obj.room_history().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -90,7 +106,40 @@ mod imp {
 
     impl WidgetImpl for ItemRow {}
     impl BinImpl for ItemRow {}
-    impl ContextMenuBinImpl for ItemRow {}
+
+    impl ContextMenuBinImpl for ItemRow {
+        fn menu_opened(&self, obj: &Self::Type) {
+            if let Some(event) = obj.item().and_then(|item| item.downcast::<Event>().ok()) {
+                let popover = obj.room_history().item_context_menu().clone();
+
+                if event.message_content().is_some() {
+                    let menu_model = Self::Type::event_message_menu_model();
+
+                    if popover.menu_model().as_ref() != Some(menu_model) {
+                        let action_group = obj.action_group().unwrap();
+                        popover.set_menu_model(Some(menu_model));
+
+                        let reaction_chooser = ReactionChooser::new();
+                        reaction_chooser.set_reactions(Some(event.reactions().to_owned()));
+                        popover.add_child(&reaction_chooser, "reaction-chooser");
+
+                        // Open emoji chooser
+                        let more_reactions = gio::SimpleAction::new("more-reactions", None);
+                        more_reactions.connect_activate(
+                            clone!(@weak obj, @weak popover => move |_, _| {
+                                obj.show_emoji_chooser(&popover);
+                            }),
+                        );
+                        action_group.add_action(&more_reactions);
+                    }
+                } else {
+                    popover.set_menu_model(Some(Self::Type::event_state_menu_model()));
+                }
+
+                obj.set_popover(Some(popover));
+            }
+        }
+    }
 }
 
 glib::wrapper! {
@@ -101,8 +150,24 @@ glib::wrapper! {
 // TODO:
 // - [ ] Don't show rows for items that don't have a visible UI
 impl ItemRow {
-    pub fn new() -> Self {
-        glib::Object::new(&[]).expect("Failed to create ItemRow")
+    pub fn new(room_history: &RoomHistory) -> Self {
+        glib::Object::new(&[("room-history", room_history)]).expect("Failed to create ItemRow")
+    }
+
+    pub fn room_history(&self) -> RoomHistory {
+        self.imp().room_history.get().unwrap().upgrade().unwrap()
+    }
+
+    pub fn action_group(&self) -> Option<gio::SimpleActionGroup> {
+        self.imp().action_group.borrow().clone()
+    }
+
+    fn set_action_group(&self, action_group: Option<gio::SimpleActionGroup>) {
+        if self.action_group() == action_group {
+            return;
+        }
+
+        self.imp().action_group.replace(action_group);
     }
 
     /// Get the row's [`TimelineItem`].
@@ -133,26 +198,7 @@ impl ItemRow {
 
         if let Some(ref item) = item {
             if let Some(event) = item.downcast_ref::<Event>() {
-                if event.message_content().is_some() {
-                    let action_group = self.set_event_actions(Some(event)).unwrap();
-                    self.set_factory(clone!(@weak event => move |obj, popover| {
-                            popover.set_menu_model(Some(Self::event_message_menu_model()));
-                            let reaction_chooser = ReactionChooser::new();
-                            reaction_chooser.set_reactions(Some(event.reactions().to_owned()));
-                            popover.add_child(&reaction_chooser, "reaction-chooser");
-
-                            // Open emoji chooser
-                            let more_reactions = gio::SimpleAction::new("more-reactions", None);
-                            more_reactions.connect_activate(clone!(@weak obj, @weak popover => move |_, _| {
-                                obj.show_emoji_chooser(&popover);
-                            }));
-                            action_group.add_action(&more_reactions);
-                        }));
-                } else {
-                    self.set_factory(|_, popover| {
-                        popover.set_menu_model(Some(Self::event_state_menu_model()));
-                    });
-                }
+                self.set_action_group(self.set_event_actions(Some(event)));
 
                 let notify_handler = event.connect_notify_local(
                     Some("event"),
@@ -164,7 +210,8 @@ impl ItemRow {
 
                 self.set_event_widget(event);
             } else if let Some(divider) = item.downcast_ref::<TimelineDayDivider>() {
-                self.remove_factory();
+                self.set_popover(None);
+                self.set_action_group(None);
                 self.set_event_actions(None);
 
                 let child = if let Some(child) =
@@ -188,6 +235,10 @@ impl ItemRow {
                     .filter(|widget| widget.is::<gtk::Spinner>())
                     .is_none()
             {
+                self.set_popover(None);
+                self.set_action_group(None);
+                self.set_event_actions(None);
+
                 let spinner = gtk::Spinner::builder()
                     .spinning(true)
                     .margin_top(12)
@@ -195,7 +246,8 @@ impl ItemRow {
                     .build();
                 self.set_child(Some(&spinner));
             } else if item.downcast_ref::<TimelineNewMessagesDivider>().is_some() {
-                self.remove_factory();
+                self.set_popover(None);
+                self.set_action_group(None);
                 self.set_event_actions(None);
 
                 let label = gettext("New Messages");
@@ -258,10 +310,4 @@ impl ItemRow {
     }
 }
 
-impl Default for ItemRow {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
 impl EventActions for ItemRow {}
diff --git a/src/session/content/room_history/mod.rs b/src/session/content/room_history/mod.rs
index 2b32b174b..d74d28b8c 100644
--- a/src/session/content/room_history/mod.rs
+++ b/src/session/content/room_history/mod.rs
@@ -37,7 +37,7 @@ use crate::{
     i18n::gettext_f,
     session::{
         content::{MarkdownPopover, RoomDetails},
-        room::{Event, Room, RoomType, Timeline, TimelineState},
+        room::{Event, Room, RoomType, Timeline, TimelineItem, TimelineState},
         user::UserExt,
     },
     spawn,
@@ -55,6 +55,7 @@ mod imp {
     use std::cell::{Cell, RefCell};
 
     use glib::{signal::SignalHandlerId, subclass::InitializingObject};
+    use once_cell::unsync::OnceCell;
 
     use super::*;
     use crate::{components::Toast, window::Window, Application};
@@ -70,6 +71,7 @@ mod imp {
         pub md_enabled: Cell<bool>,
         pub is_auto_scrolling: Cell<bool>,
         pub sticky: Cell<bool>,
+        pub item_context_menu: OnceCell<gtk::PopoverMenu>,
         #[template_child]
         pub headerbar: TemplateChild<adw::HeaderBar>,
         #[template_child]
@@ -270,6 +272,16 @@ mod imp {
         }
 
         fn constructed(&self, obj: &Self::Type) {
+            let factory = gtk::SignalListItemFactory::new();
+            factory.connect_setup(clone!(@weak obj => move |_, item| {
+                let row = ItemRow::new(&obj);
+                item.set_child(Some(&row));
+                ItemRow::this_expression("item").chain_property::<TimelineItem>("activatable").bind(item, 
"activatable", Some(&row));
+                item.bind_property("item", &row, "item").build();
+                item.set_selectable(false);
+            }));
+            self.listview.set_factory(Some(&factory));
+
             // Needed to use the natural height of GtkPictures
             self.listview
                 .set_vscroll_policy(gtk::ScrollablePolicy::Natural);
@@ -926,6 +938,12 @@ impl RoomHistory {
             Err(err) => log::debug!("Could not read file: {}", err),
         }
     }
+
+    pub fn item_context_menu(&self) -> &gtk::PopoverMenu {
+        self.imp()
+            .item_context_menu
+            .get_or_init(|| gtk::PopoverMenu::from_model(gio::MenuModel::NONE))
+    }
 }
 
 impl Default for RoomHistory {
diff --git a/src/session/sidebar/mod.rs b/src/session/sidebar/mod.rs
index c4e4d5596..199ac91f1 100644
--- a/src/session/sidebar/mod.rs
+++ b/src/session/sidebar/mod.rs
@@ -14,7 +14,12 @@ mod verification_row;
 
 use account_switcher::AccountSwitcher;
 use adw::{prelude::*, subclass::prelude::*};
-use gtk::{gio, glib, glib::closure, subclass::prelude::*, CompositeTemplate, SelectionModel};
+use gtk::{
+    gio, glib,
+    glib::{clone, closure},
+    subclass::prelude::*,
+    CompositeTemplate, SelectionModel,
+};
 
 pub use self::{
     category::Category,
@@ -44,7 +49,7 @@ mod imp {
     };
 
     use glib::subclass::InitializingObject;
-    use once_cell::sync::Lazy;
+    use once_cell::{sync::Lazy, unsync::OnceCell};
 
     use super::*;
 
@@ -63,6 +68,9 @@ mod imp {
         pub room_search_entry: TemplateChild<gtk::SearchEntry>,
         #[template_child]
         pub room_search: TemplateChild<gtk::SearchBar>,
+        #[template_child]
+        pub room_row_menu: TemplateChild<gio::MenuModel>,
+        pub room_row_popover: OnceCell<gtk::PopoverMenu>,
         pub user: RefCell<Option<User>>,
         /// The type of the source that activated drop mode.
         pub drop_source_type: Cell<Option<RoomType>>,
@@ -205,6 +213,15 @@ mod imp {
         fn constructed(&self, obj: &Self::Type) {
             self.parent_constructed(obj);
 
+            let factory = gtk::SignalListItemFactory::new();
+            factory.connect_setup(clone!(@weak obj => move |_, item| {
+                let row = Row::new(&obj);
+                item.set_child(Some(&row));
+                item.bind_property("item", &row, "list-row").build();
+                row.set_can_focus(false);
+            }));
+            self.listview.set_factory(Some(&factory));
+
             self.listview.connect_activate(move |listview, pos| {
                 let model: Option<Selection> = listview.model().and_then(|o| o.downcast().ok());
                 let row: Option<gtk::TreeListRow> = model
@@ -464,6 +481,13 @@ impl Sidebar {
             child = widget.next_sibling();
         }
     }
+
+    pub fn room_row_popover(&self) -> &gtk::PopoverMenu {
+        let priv_ = self.imp();
+        priv_
+            .room_row_popover
+            .get_or_init(|| gtk::PopoverMenu::from_model(Some(&*priv_.room_row_menu)))
+    }
 }
 
 impl Default for Sidebar {
diff --git a/src/session/sidebar/room_row.rs b/src/session/sidebar/room_row.rs
index 53de48e2d..144ae42fa 100644
--- a/src/session/sidebar/room_row.rs
+++ b/src/session/sidebar/room_row.rs
@@ -1,6 +1,7 @@
 use adw::subclass::prelude::BinImpl;
 use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
 
+use super::Row;
 use crate::{
     components::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl},
     session::room::{HighlightFlags, Room, RoomType},
@@ -24,8 +25,6 @@ mod imp {
         pub display_name: TemplateChild<gtk::Label>,
         #[template_child]
         pub notification_count: TemplateChild<gtk::Label>,
-        #[template_child]
-        pub room_row_menu: TemplateChild<gtk::gio::MenuModel>,
     }
 
     #[glib::object_subclass]
@@ -113,10 +112,6 @@ mod imp {
         fn constructed(&self, obj: &Self::Type) {
             self.parent_constructed(obj);
 
-            obj.set_factory(|obj, popover| {
-                popover.set_menu_model(Some(&obj.imp().room_row_menu.get()));
-            });
-
             // Allow to drag rooms
             let drag = gtk::DragSource::builder()
                 .actions(gdk::DragAction::MOVE)
@@ -146,7 +141,20 @@ mod imp {
 
     impl WidgetImpl for RoomRow {}
     impl BinImpl for RoomRow {}
-    impl ContextMenuBinImpl for RoomRow {}
+
+    impl ContextMenuBinImpl for RoomRow {
+        fn menu_opened(&self, obj: &Self::Type) {
+            if let Some(sidebar) = obj
+                .parent()
+                .as_ref()
+                .and_then(|obj| obj.downcast_ref::<Row>())
+                .map(|row| row.sidebar())
+            {
+                let popover = sidebar.room_row_popover();
+                obj.set_popover(Some(popover.to_owned()));
+            }
+        }
+    }
 }
 
 glib::wrapper! {
diff --git a/src/session/sidebar/row.rs b/src/session/sidebar/row.rs
index eda20b99f..1323bb3f5 100644
--- a/src/session/sidebar/row.rs
+++ b/src/session/sidebar/row.rs
@@ -6,19 +6,23 @@ use gtk::{gdk, glib, glib::clone, subclass::prelude::*};
 use super::EntryType;
 use crate::session::{
     room::{Room, RoomType},
-    sidebar::{Category, CategoryRow, Entry, EntryRow, RoomRow, SidebarItem, VerificationRow},
+    sidebar::{
+        Category, CategoryRow, Entry, EntryRow, RoomRow, Sidebar, SidebarItem, VerificationRow,
+    },
     verification::IdentityVerification,
 };
 
 mod imp {
     use std::cell::RefCell;
 
-    use once_cell::sync::Lazy;
+    use glib::WeakRef;
+    use once_cell::{sync::Lazy, unsync::OnceCell};
 
     use super::*;
 
     #[derive(Debug, Default)]
     pub struct Row {
+        pub sidebar: OnceCell<WeakRef<Sidebar>>,
         pub list_row: RefCell<Option<gtk::TreeListRow>>,
         pub bindings: RefCell<Vec<glib::Binding>>,
     }
@@ -52,6 +56,13 @@ mod imp {
                         gtk::TreeListRow::static_type(),
                         glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
                     ),
+                    glib::ParamSpecObject::new(
+                        "sidebar",
+                        "sidebar",
+                        "The ancestor sidebar of this row",
+                        Sidebar::static_type(),
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+                    ),
                 ]
             });
 
@@ -66,10 +77,11 @@ mod imp {
             pspec: &glib::ParamSpec,
         ) {
             match pspec.name() {
-                "list-row" => {
-                    let list_row = value.get().unwrap();
-                    obj.set_list_row(list_row);
-                }
+                "list-row" => obj.set_list_row(value.get().unwrap()),
+                "sidebar" => self
+                    .sidebar
+                    .set(value.get::<Sidebar>().unwrap().downgrade())
+                    .unwrap(),
                 _ => unimplemented!(),
             }
         }
@@ -78,6 +90,7 @@ mod imp {
             match pspec.name() {
                 "item" => obj.item().to_value(),
                 "list-row" => obj.list_row().to_value(),
+                "sidebar" => obj.sidebar().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -115,8 +128,12 @@ glib::wrapper! {
 }
 
 impl Row {
-    pub fn new() -> Self {
-        glib::Object::new(&[]).expect("Failed to create Row")
+    pub fn new(sidebar: &Sidebar) -> Self {
+        glib::Object::new(&[("sidebar", sidebar)]).expect("Failed to create Row")
+    }
+
+    pub fn sidebar(&self) -> Sidebar {
+        self.imp().sidebar.get().unwrap().upgrade().unwrap()
     }
 
     pub fn item(&self) -> Option<SidebarItem> {
@@ -315,9 +332,3 @@ impl Row {
         ret
     }
 }
-
-impl Default for Row {
-    fn default() -> Self {
-        Self::new()
-    }
-}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]