[fractal/fractal-next] sidebar: Hide empty categories



commit f13e6b3a22a682801a496576ba4828f8c5e9546d
Author: Julian Sparber <julian sparber net>
Date:   Fri Nov 19 17:54:51 2021 +0100

    sidebar: Hide empty categories
    
    This also adds a property to the `ItemList` called `show-all` to show
    all categories (expect invites). Once we implement dnd of rooms between
    categories we can use this property to display all categories.

 src/session/sidebar/category.rs  |  51 ++++++++++++-----
 src/session/sidebar/item_list.rs | 120 +++++++++++++++++++++++++++++++++++++--
 src/session/sidebar/mod.rs       |   1 -
 3 files changed, 152 insertions(+), 20 deletions(-)
---
diff --git a/src/session/sidebar/category.rs b/src/session/sidebar/category.rs
index 41ad6334..b604107d 100644
--- a/src/session/sidebar/category.rs
+++ b/src/session/sidebar/category.rs
@@ -13,6 +13,7 @@ mod imp {
     pub struct Category {
         pub model: OnceCell<gio::ListModel>,
         pub type_: Cell<CategoryType>,
+        pub is_empty: Cell<bool>,
     }
 
     #[glib::object_subclass]
@@ -50,6 +51,13 @@ mod imp {
                         gio::ListModel::static_type(),
                         glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
                     ),
+                    glib::ParamSpec::new_boolean(
+                        "empty",
+                        "Empty",
+                        "Whether this category is empty",
+                        false,
+                        glib::ParamFlags::READABLE,
+                    ),
                 ]
             });
 
@@ -81,6 +89,7 @@ mod imp {
                 "type" => obj.type_().to_value(),
                 "display-name" => obj.type_().to_string().to_value(),
                 "model" => self.model.get().to_value(),
+                "empty" => obj.is_empty().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -122,7 +131,7 @@ impl Category {
         let type_ = self.type_();
 
         // Special case room lists so that they are sorted and in the right category
-        if model.is::<RoomList>() {
+        let model = if model.is::<RoomList>() {
             let filter = gtk::CustomFilter::new(move |o| {
                 o.downcast_ref::<Room>()
                     .filter(|r| CategoryType::from(r.category()) == type_)
@@ -136,20 +145,34 @@ impl Category {
                 b.latest_change().cmp(&a.latest_change()).into()
             });
             let sort_model = gtk::SortListModel::new(Some(&filter_model), Some(&sorter));
-
-            sort_model.connect_items_changed(
-                clone!(@weak self as obj => move |_, pos, removed, added| {
-                    obj.items_changed(pos, removed, added);
-                }),
-            );
-            priv_.model.set(sort_model.upcast()).unwrap();
+            sort_model.upcast()
         } else {
-            model.connect_items_changed(
-                clone!(@weak self as obj => move |_, pos, removed, added| {
-                    obj.items_changed(pos, removed, added);
-                }),
-            );
-            priv_.model.set(model).unwrap();
+            model
+        };
+
+        model.connect_items_changed(
+            clone!(@weak self as obj => move |model, pos, removed, added| {
+                obj.items_changed(pos, removed, added);
+                obj.set_is_empty(model.n_items() == 0);
+            }),
+        );
+
+        self.set_is_empty(model.n_items() == 0);
+        priv_.model.set(model).unwrap();
+    }
+
+    fn set_is_empty(&self, is_empty: bool) {
+        let priv_ = imp::Category::from_instance(self);
+        if is_empty == self.is_empty() {
+            return;
         }
+
+        priv_.is_empty.set(is_empty);
+        self.notify("empty");
+    }
+
+    pub fn is_empty(&self) -> bool {
+        let priv_ = imp::Category::from_instance(self);
+        priv_.is_empty.get()
     }
 }
diff --git a/src/session/sidebar/item_list.rs b/src/session/sidebar/item_list.rs
index 7fa61cfd..5a583aba 100644
--- a/src/session/sidebar/item_list.rs
+++ b/src/session/sidebar/item_list.rs
@@ -1,4 +1,4 @@
-use gtk::{gio, glib, prelude::*, subclass::prelude::*};
+use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
 
 use crate::session::{
     room_list::RoomList,
@@ -11,14 +11,16 @@ use crate::session::{
 mod imp {
     use once_cell::sync::Lazy;
     use once_cell::unsync::OnceCell;
+    use std::cell::Cell;
 
     use super::*;
 
     #[derive(Debug, Default)]
     pub struct ItemList {
-        pub list: OnceCell<[glib::Object; 7]>,
+        pub list: OnceCell<[(glib::Object, Cell<bool>); 7]>,
         pub room_list: OnceCell<RoomList>,
         pub verification_list: OnceCell<VerificationList>,
+        pub show_all: Cell<bool>,
     }
 
     #[glib::object_subclass]
@@ -47,6 +49,13 @@ mod imp {
                         VerificationList::static_type(),
                         glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
                     ),
+                    glib::ParamSpec::new_boolean(
+                        "show-all",
+                        "Show All",
+                        "Whether all room categories should be shown",
+                        false,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
                 ]
             });
 
@@ -63,6 +72,7 @@ mod imp {
             match pspec.name() {
                 "room-list" => obj.set_room_list(value.get().unwrap()),
                 "verification-list" => obj.set_verification_list(value.get().unwrap()),
+                "show-all" => obj.set_show_all(value.get().unwrap()),
                 _ => unimplemented!(),
             }
         }
@@ -71,6 +81,7 @@ mod imp {
             match pspec.name() {
                 "room-list" => obj.room_list().to_value(),
                 "verification-list" => obj.verification_list().to_value(),
+                "show-all" => obj.show_all().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -92,6 +103,26 @@ mod imp {
                 Category::new(CategoryType::Left, room_list).upcast::<glib::Object>(),
             ];
 
+            for (index, item) in list.iter().enumerate() {
+                if let Some(category) = item.downcast_ref::<Category>() {
+                    category.connect_notify_local(
+                        Some("empty"),
+                        clone!(@weak obj => move |_, _| {
+                            obj.update_category(index);
+                        }),
+                    );
+                }
+            }
+
+            let list = list.map(|item| {
+                let visible = if let Some(category) = item.downcast_ref::<Category>() {
+                    !category.is_empty()
+                } else {
+                    true
+                };
+                (item, Cell::new(visible))
+            });
+
             self.list.set(list).unwrap();
         }
     }
@@ -101,13 +132,28 @@ mod imp {
             glib::Object::static_type()
         }
         fn n_items(&self, _list_model: &Self::Type) -> u32 {
-            self.list.get().map(|l| l.len()).unwrap_or(0) as u32
+            self.list
+                .get()
+                .unwrap()
+                .iter()
+                .filter(|(_, visible)| visible.get())
+                .count() as u32
         }
         fn item(&self, _list_model: &Self::Type, position: u32) -> Option<glib::Object> {
             self.list
                 .get()
-                .and_then(|l| l.get(position as usize))
-                .map(glib::object::Cast::upcast_ref::<glib::Object>)
+                .unwrap()
+                .iter()
+                .filter_map(
+                    |(item, visible)| {
+                        if visible.get() {
+                            Some(item)
+                        } else {
+                            None
+                        }
+                    },
+                )
+                .nth(position as usize)
                 .cloned()
         }
     }
@@ -131,6 +177,58 @@ impl ItemList {
         .expect("Failed to create ItemList")
     }
 
+    fn update_category(&self, position: usize) {
+        let priv_ = imp::ItemList::from_instance(self);
+        let (item, old_visible) = priv_.list.get().unwrap().get(position).unwrap();
+        let category = item.downcast_ref::<Category>().unwrap();
+
+        let visible = !category.is_empty() || (self.show_all() && is_show_all_category(category));
+        if visible != old_visible.get() {
+            old_visible.set(visible);
+            let hidden_before_position = priv_
+                .list
+                .get()
+                .unwrap()
+                .iter()
+                .take(position)
+                .filter(|(_, visible)| !visible.get())
+                .count();
+            let real_position = position - hidden_before_position;
+
+            let (removed, added) = if visible { (0, 1) } else { (1, 0) };
+
+            self.items_changed(real_position as u32, removed, added);
+        }
+    }
+
+    // Whether all room categories are shown
+    // This doesn't include `CategoryType::Invite` since the user can't move rooms to it.
+    pub fn show_all(&self) -> bool {
+        let priv_ = imp::ItemList::from_instance(self);
+        priv_.show_all.get()
+    }
+
+    // Set whether all room categories should be shown
+    // This doesn't include `CategoryType::Invite` since the user can't move rooms to it.
+    pub fn set_show_all(&self, show_all: bool) {
+        let priv_ = imp::ItemList::from_instance(self);
+        if show_all == self.show_all() {
+            return;
+        }
+
+        priv_.show_all.set(show_all);
+
+        for (index, (item, _)) in priv_.list.get().unwrap().iter().enumerate() {
+            if let Some(category) = item.downcast_ref::<Category>() {
+                if is_show_all_category(category) {
+                    self.update_category(index);
+                }
+            }
+        }
+
+        self.notify("show-all");
+    }
+
     fn set_room_list(&self, room_list: RoomList) {
         let priv_ = imp::ItemList::from_instance(self);
         priv_.room_list.set(room_list).unwrap();
@@ -151,3 +249,15 @@ impl ItemList {
         priv_.verification_list.get().unwrap()
     }
 }
+
+// Wheter this category should be shown when `show-all` is `true`
+// This doesn't include `CategoryType::Invite` since the user can't move rooms to it.
+fn is_show_all_category(category: &Category) -> bool {
+    matches!(
+        category.type_(),
+        CategoryType::Favorite
+            | CategoryType::Normal
+            | CategoryType::LowPriority
+            | CategoryType::Left
+    )
+}
diff --git a/src/session/sidebar/mod.rs b/src/session/sidebar/mod.rs
index 606f33fc..fe3bd4b2 100644
--- a/src/session/sidebar/mod.rs
+++ b/src/session/sidebar/mod.rs
@@ -208,7 +208,6 @@ impl Sidebar {
             }
         };
 
-        // TODO: hide empty categories
         let tree_model = gtk::TreeListModel::new(&item_list, false, true, |item| {
             item.clone().downcast::<gio::ListModel>().ok()
         });


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