[fractal/fractal-next] room-details: Add menu for members



commit f26413bf4d62873991e2a37851f2df453cdb7fd4
Author: Julian Sparber <julian sparber net>
Date:   Fri Dec 10 15:14:23 2021 +0100

    room-details: Add menu for members

 data/resources/resources.gresource.xml             |   1 +
 data/resources/ui/content-member-row.ui            |   6 +
 data/resources/ui/member-menu.ui                   |  34 +++++
 src/meson.build                                    |   1 +
 .../room_details/member_page/member_menu.rs        | 164 +++++++++++++++++++++
 .../content/room_details/member_page/member_row.rs |  34 ++++-
 .../content/room_details/member_page/mod.rs        |  47 +++++-
 src/session/content/room_details/mod.rs            |  10 +-
 8 files changed, 287 insertions(+), 10 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index be75fc61..f754a62c 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -32,6 +32,7 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="event-source-dialog.ui">ui/event-source-dialog.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="login.ui">ui/login.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="media-viewer.ui">ui/media-viewer.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" alias="member-menu.ui">ui/member-menu.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="session.ui">ui/session.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="sidebar.ui">ui/sidebar.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-account-switcher.ui">ui/sidebar-account-switcher.ui</file>
diff --git a/data/resources/ui/content-member-row.ui b/data/resources/ui/content-member-row.ui
index 2e745ee5..209779b8 100644
--- a/data/resources/ui/content-member-row.ui
+++ b/data/resources/ui/content-member-row.ui
@@ -71,6 +71,12 @@
             </child>
           </object>
         </child>
+        <child>
+          <object class="GtkToggleButton" id="menu_btn">
+            <property name="has-frame">False</property>
+            <property name="icon-name">view-more-symbolic</property>
+          </object>
+        </child>
       </object>
     </property>
   </template>
diff --git a/data/resources/ui/member-menu.ui b/data/resources/ui/member-menu.ui
new file mode 100644
index 00000000..e64e1511
--- /dev/null
+++ b/data/resources/ui/member-menu.ui
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="menu_model">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Verify</attribute>
+        <attribute name="action">member.verify</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+        <attribute name="hidden-when">action-missing</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">Make _Mod</attribute>
+        <attribute name="action">member.make-mod</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+        <attribute name="hidden-when">action-missing</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Make _Admin</attribute>
+        <attribute name="action">member.make-admin</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+        <attribute name="hidden-when">action-missing</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Kick</attribute>
+        <attribute name="action">member.kick</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+        <attribute name="hidden-when">action-missing</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
+
diff --git a/src/meson.build b/src/meson.build
index 97cafe3b..72ba437d 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -79,6 +79,7 @@ sources = files(
   'session/content/room_details/invite_subpage/invitee_row.rs',
   'session/content/room_details/member_page/mod.rs',
   'session/content/room_details/member_page/member_row.rs',
+  'session/content/room_details/member_page/member_menu.rs',
   'session/content/room_details/mod.rs',
   'session/content/verification/emoji.rs',
   'session/content/verification/mod.rs',
diff --git a/src/session/content/room_details/member_page/member_menu.rs 
b/src/session/content/room_details/member_page/member_menu.rs
new file mode 100644
index 00000000..6e9e05ce
--- /dev/null
+++ b/src/session/content/room_details/member_page/member_menu.rs
@@ -0,0 +1,164 @@
+use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
+
+use crate::session::room::Member;
+
+mod imp {
+    use super::*;
+    use once_cell::sync::Lazy;
+    use once_cell::unsync::OnceCell;
+    use std::cell::RefCell;
+
+    #[derive(Debug, Default)]
+    pub struct MemberMenu {
+        pub member: RefCell<Option<Member>>,
+        pub popover: OnceCell<gtk::PopoverMenu>,
+        pub destroy_handler: RefCell<Option<glib::signal::SignalHandlerId>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for MemberMenu {
+        const NAME: &'static str = "ContentMemberMenu";
+        type Type = super::MemberMenu;
+        type ParentType = glib::Object;
+    }
+
+    impl ObjectImpl for MemberMenu {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![glib::ParamSpec::new_object(
+                    "member",
+                    "Member",
+                    "The member this row is showing",
+                    Member::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() {
+                "member" => obj.set_member(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "member" => obj.member().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+
+            obj.popover_menu()
+                .connect_closed(clone!(@weak obj => move |_| {
+                    obj.close_popover();
+                }));
+        }
+    }
+}
+
+glib::wrapper! {
+    pub struct MemberMenu(ObjectSubclass<imp::MemberMenu>);
+}
+
+impl MemberMenu {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create MemberMenu")
+    }
+
+    pub fn member(&self) -> Option<Member> {
+        let priv_ = imp::MemberMenu::from_instance(self);
+        priv_.member.borrow().clone()
+    }
+
+    pub fn set_member(&self, member: Option<Member>) {
+        let priv_ = imp::MemberMenu::from_instance(self);
+
+        if self.member() == member {
+            return;
+        }
+
+        priv_.member.replace(member);
+        self.notify("member");
+    }
+
+    fn popover_menu(&self) -> &gtk::PopoverMenu {
+        let priv_ = imp::MemberMenu::from_instance(self);
+        priv_.popover.get_or_init(|| {
+            gtk::PopoverMenu::from_model(Some(
+                &gtk::Builder::from_resource("/org/gnome/FractalNext/member-menu.ui")
+                    .object::<gio::MenuModel>("menu_model")
+                    .unwrap(),
+            ))
+        })
+    }
+
+    /// Show the menu on the specific button
+    ///
+    /// For convenience it allows to set the member for which the popover is shown
+    pub fn present_popover(&self, button: &gtk::ToggleButton, member: Option<Member>) {
+        let priv_ = imp::MemberMenu::from_instance(self);
+        let popover = self.popover_menu();
+        let _guard = popover.freeze_notify();
+
+        self.close_popover();
+        self.unparent_popover();
+
+        self.set_member(member);
+
+        let handler = button.connect_destroy(clone!(@weak self as obj => move |_| {
+            obj.unparent_popover();
+        }));
+
+        priv_.destroy_handler.replace(Some(handler));
+
+        popover.set_parent(button);
+        popover.show();
+    }
+
+    fn unparent_popover(&self) {
+        let priv_ = imp::MemberMenu::from_instance(self);
+        let popover = self.popover_menu();
+
+        if let Some(parent) = popover.parent() {
+            if let Some(handler) = priv_.destroy_handler.take() {
+                parent.disconnect(handler);
+            }
+
+            popover.unparent();
+        }
+    }
+
+    /// Closes the popover
+    pub fn close_popover(&self) {
+        let popover = self.popover_menu();
+        let _guard = popover.freeze_notify();
+
+        if let Some(button) = popover.parent() {
+            if popover.is_visible() {
+                popover.hide();
+            }
+            button
+                .downcast::<gtk::ToggleButton>()
+                .expect("The parent of a MemberMenu needs to ba a gtk::ToggleButton")
+                .set_active(false);
+        }
+    }
+}
+
+impl Default for MemberMenu {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/src/session/content/room_details/member_page/member_row.rs 
b/src/session/content/room_details/member_page/member_row.rs
index b87e871c..f3509f83 100644
--- a/src/session/content/room_details/member_page/member_row.rs
+++ b/src/session/content/room_details/member_page/member_row.rs
@@ -1,5 +1,6 @@
-use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
+use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
 
+use crate::session::content::RoomDetails;
 use crate::session::room::Member;
 use adw::subclass::prelude::BinImpl;
 
@@ -13,6 +14,8 @@ mod imp {
     #[template(resource = "/org/gnome/FractalNext/content-member-row.ui")]
     pub struct MemberRow {
         pub member: RefCell<Option<Member>>,
+        #[template_child]
+        pub menu_btn: TemplateChild<gtk::ToggleButton>,
     }
 
     #[glib::object_subclass]
@@ -66,6 +69,21 @@ mod imp {
                 _ => unimplemented!(),
             }
         }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+
+            self.menu_btn
+                .connect_toggled(clone!(@weak obj => move |btn| {
+                    if let Some(details) = obj.details() {
+                        let page = details.member_page();
+                        let menu = page.member_menu();
+                        if btn.is_active() {
+                            menu.present_popover(btn, obj.member());
+                        }
+                    }
+                }));
+        }
     }
     impl WidgetImpl for MemberRow {}
     impl BinImpl for MemberRow {}
@@ -93,7 +111,21 @@ impl MemberRow {
             return;
         }
 
+        // We need to update the member of the menu if it's shown for this row
+        if priv_.menu_btn.is_active() {
+            if let Some(details) = self.details() {
+                let page = details.member_page();
+                let menu = page.member_menu();
+
+                menu.set_member(member.clone());
+            }
+        }
+
         priv_.member.replace(member);
         self.notify("member");
     }
+
+    fn details(&self) -> Option<RoomDetails> {
+        Some(self.root()?.downcast::<RoomDetails>().unwrap())
+    }
 }
diff --git a/src/session/content/room_details/member_page/mod.rs 
b/src/session/content/room_details/member_page/mod.rs
index f84f79f3..70c27883 100644
--- a/src/session/content/room_details/member_page/mod.rs
+++ b/src/session/content/room_details/member_page/mod.rs
@@ -5,13 +5,16 @@ use gtk::glib::{self, clone};
 use gtk::subclass::prelude::*;
 use gtk::CompositeTemplate;
 
+mod member_menu;
 mod member_row;
+use self::member_menu::MemberMenu;
 use self::member_row::MemberRow;
 use crate::components::{Avatar, Badge};
 use crate::prelude::*;
 use crate::session::content::RoomDetails;
 use crate::session::room::{Member, RoomAction};
 use crate::session::Room;
+use log::warn;
 
 mod imp {
     use super::*;
@@ -31,6 +34,7 @@ mod imp {
         pub members_search_entry: TemplateChild<gtk::SearchEntry>,
         #[template_child]
         pub members_list_view: TemplateChild<gtk::ListView>,
+        pub member_menu: OnceCell<MemberMenu>,
     }
 
     #[glib::object_subclass]
@@ -44,6 +48,14 @@ mod imp {
             Badge::static_type();
             MemberRow::static_type();
             Self::bind_template(klass);
+
+            klass.install_action("member.verify", None, move |widget, _, _| {
+                if let Some(member) = widget.member_menu().member() {
+                    widget.verify_member(member);
+                } else {
+                    warn!("No member was selected to be verified");
+                }
+            });
         }
 
         fn instance_init(obj: &InitializingObject<Self>) {
@@ -54,13 +66,22 @@ mod imp {
     impl ObjectImpl for MemberPage {
         fn properties() -> &'static [glib::ParamSpec] {
             static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
-                vec![glib::ParamSpec::new_object(
-                    "room",
-                    "Room",
-                    "The room backing all details of the member page",
-                    Room::static_type(),
-                    glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
-                )]
+                vec![
+                    glib::ParamSpec::new_object(
+                        "room",
+                        "Room",
+                        "The room backing all details of the member page",
+                        Room::static_type(),
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+                    ),
+                    glib::ParamSpec::new_object(
+                        "member-menu",
+                        "Member Menu",
+                        "The object holding information needed for the menu of each MemberRow",
+                        MemberMenu::static_type(),
+                        glib::ParamFlags::READABLE,
+                    ),
+                ]
             });
 
             PROPERTIES.as_ref()
@@ -79,9 +100,10 @@ mod imp {
             }
         }
 
-        fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
             match pspec.name() {
                 "room" => self.room.get().to_value(),
+                "member-menu" => obj.member_menu().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -210,4 +232,13 @@ impl MemberPage {
                 window.present_invite_subpage();
             }));
     }
+
+    pub fn member_menu(&self) -> &MemberMenu {
+        let priv_ = imp::MemberPage::from_instance(self);
+        priv_.member_menu.get_or_init(|| MemberMenu::new())
+    }
+
+    fn verify_member(&self, _member: Member) {
+        todo!("Show member verification");
+    }
 }
diff --git a/src/session/content/room_details/mod.rs b/src/session/content/room_details/mod.rs
index 14d85bce..b35191bf 100644
--- a/src/session/content/room_details/mod.rs
+++ b/src/session/content/room_details/mod.rs
@@ -39,6 +39,7 @@ mod imp {
         pub room_name_entry: TemplateChild<gtk::Entry>,
         #[template_child]
         pub room_topic_text_view: TemplateChild<gtk::TextView>,
+        pub member_page: OnceCell<MemberPage>,
     }
 
     #[glib::object_subclass]
@@ -102,7 +103,9 @@ mod imp {
         fn constructed(&self, obj: &Self::Type) {
             self.parent_constructed(obj);
 
-            obj.add(&MemberPage::new(obj.room()));
+            let member_page = MemberPage::new(obj.room());
+            obj.add(&member_page);
+            self.member_page.set(member_page).unwrap();
 
             obj.init_avatar();
             obj.init_edit_toggle();
@@ -258,4 +261,9 @@ impl RoomDetails {
         self.set_title(Some(&gettext("Room Details")));
         self.close_subpage();
     }
+
+    pub fn member_page(&self) -> &MemberPage {
+        let priv_ = imp::RoomDetails::from_instance(self);
+        priv_.member_page.get().unwrap()
+    }
 }


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