[fractal/fractal-next] sidebar: Improve interactions with a custom SelectionModel



commit f414a6f5bac3d97da6fbe8551cb1b10cd1328717
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Tue May 4 21:08:52 2021 +0200

    sidebar: Improve interactions with a custom SelectionModel
    
    Closes #749

 data/resources/ui/sidebar.ui     |   1 +
 src/session/sidebar/mod.rs       |   2 +
 src/session/sidebar/selection.rs | 341 +++++++++++++++++++++++++++++++++++++++
 src/session/sidebar/sidebar.rs   |  37 +++--
 4 files changed, 364 insertions(+), 17 deletions(-)
---
diff --git a/data/resources/ui/sidebar.ui b/data/resources/ui/sidebar.ui
index e644a4ec..8228b87d 100644
--- a/data/resources/ui/sidebar.ui
+++ b/data/resources/ui/sidebar.ui
@@ -56,6 +56,7 @@
                 <style>
                   <class name="navigation-sidebar"/>
                 </style>
+                <property name="single-click-activate">true</property>
                 <property name="factory">
                   <object class="GtkBuilderListItemFactory">
                     <property name="resource">/org/gnome/FractalNext/sidebar-item.ui</property>
diff --git a/src/session/sidebar/mod.rs b/src/session/sidebar/mod.rs
index 34b3697a..705d8f32 100644
--- a/src/session/sidebar/mod.rs
+++ b/src/session/sidebar/mod.rs
@@ -1,9 +1,11 @@
 mod category_row;
 mod room_row;
 mod row;
+mod selection;
 mod sidebar;
 
 use self::category_row::CategoryRow;
 use self::room_row::RoomRow;
 use self::row::Row;
+use self::selection::Selection;
 pub use self::sidebar::Sidebar;
diff --git a/src/session/sidebar/selection.rs b/src/session/sidebar/selection.rs
new file mode 100644
index 00000000..4af219d0
--- /dev/null
+++ b/src/session/sidebar/selection.rs
@@ -0,0 +1,341 @@
+use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
+
+use crate::session::room::Room;
+
+// FIXME Could not find it in gtk
+pub const GTK_INVALID_LIST_POSITION: u32 = u32::MAX;
+
+mod imp {
+    use super::*;
+    use once_cell::sync::Lazy;
+    use std::cell::{Cell, RefCell};
+
+    #[derive(Debug, Default)]
+    pub struct Selection {
+        pub model: RefCell<Option<gio::ListModel>>,
+        pub selected: Cell<u32>,
+        pub selected_room: RefCell<Option<Room>>,
+        pub signal_handler: RefCell<Option<glib::SignalHandlerId>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for Selection {
+        const NAME: &'static str = "SidebarSelection";
+        type Type = super::Selection;
+        type ParentType = glib::Object;
+        type Interfaces = (gio::ListModel, gtk::SelectionModel);
+
+        fn new() -> Self {
+            Self {
+                selected: Cell::new(GTK_INVALID_LIST_POSITION),
+                ..Default::default()
+            }
+        }
+    }
+
+    impl ObjectImpl for Selection {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpec::new_object(
+                        "model",
+                        "Model",
+                        "The model being managed",
+                        gio::ListModel::static_type(),
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpec::new_uint(
+                        "selected",
+                        "Selected",
+                        "The position of the selected item",
+                        0,
+                        u32::MAX,
+                        GTK_INVALID_LIST_POSITION,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpec::new_object(
+                        "selected-room",
+                        "Selected Room",
+                        "The selected room",
+                        Room::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() {
+                "model" => {
+                    let model: Option<gio::ListModel> = value.get().unwrap();
+                    obj.set_model(model.as_ref());
+                }
+                "selected" => {
+                    let selected = value.get().unwrap();
+                    obj.set_selected(selected);
+                }
+                "selected-room" => {
+                    let selected_room = value.get().unwrap();
+                    obj.set_selected_room(selected_room);
+                }
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "model" => obj.model().to_value(),
+                "selected" => obj.selected().to_value(),
+                "selected-room" => obj.selected_room().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+    }
+
+    impl ListModelImpl for Selection {
+        fn item_type(&self, _list_model: &Self::Type) -> glib::Type {
+            gtk::TreeListRow::static_type()
+        }
+        fn n_items(&self, _list_model: &Self::Type) -> u32 {
+            self.model
+                .borrow()
+                .as_ref()
+                .map(|m| m.n_items())
+                .unwrap_or(0)
+        }
+        fn item(&self, _list_model: &Self::Type, position: u32) -> Option<glib::Object> {
+            self.model.borrow().as_ref().and_then(|m| m.item(position))
+        }
+    }
+
+    impl SelectionModelImpl for Selection {
+        fn selection_in_range(
+            &self,
+            _model: &Self::Type,
+            _position: u32,
+            _n_items: u32,
+        ) -> gtk::Bitset {
+            let bitset = gtk::Bitset::new_empty();
+            let selected = self.selected.get();
+
+            if selected != GTK_INVALID_LIST_POSITION {
+                bitset.add(selected);
+            }
+
+            bitset
+        }
+
+        fn is_selected(&self, _model: &Self::Type, position: u32) -> bool {
+            self.selected.get() == position
+        }
+    }
+}
+
+glib::wrapper! {
+    pub struct Selection(ObjectSubclass<imp::Selection>)
+        @implements gio::ListModel, gtk::SelectionModel;
+}
+
+impl Selection {
+    pub fn new<P: IsA<gio::ListModel>>(model: Option<&P>) -> Selection {
+        let model = model.map(|m| m.clone().upcast::<gio::ListModel>());
+        glib::Object::new(&[("model", &model)]).expect("Failed to create Selection")
+    }
+
+    pub fn model(&self) -> Option<gio::ListModel> {
+        let priv_ = imp::Selection::from_instance(self);
+        priv_.model.borrow().clone()
+    }
+
+    pub fn selected(&self) -> u32 {
+        let priv_ = imp::Selection::from_instance(self);
+        priv_.selected.get()
+    }
+
+    pub fn selected_room(&self) -> Option<Room> {
+        let priv_ = imp::Selection::from_instance(self);
+        priv_.selected_room.borrow().clone()
+    }
+
+    pub fn set_model<P: IsA<gio::ListModel>>(&self, model: Option<&P>) {
+        let priv_ = imp::Selection::from_instance(self);
+
+        let model = model.map(|m| m.clone().upcast::<gio::ListModel>());
+
+        let old_model = self.model();
+        if old_model == model {
+            return;
+        }
+
+        let n_items_before = old_model
+            .map(|model| {
+                if let Some(id) = priv_.signal_handler.take() {
+                    model.disconnect(id);
+                }
+                model.n_items()
+            })
+            .unwrap_or(0);
+
+        if let Some(model) = model {
+            priv_
+                .signal_handler
+                .replace(Some(model.connect_items_changed(
+                    clone!(@weak self as obj => move |m, p, r, a| {
+                            obj.items_changed_cb(m, p, r, a);
+                    }),
+                )));
+
+            self.items_changed_cb(&model, 0, n_items_before, model.n_items());
+
+            priv_.model.replace(Some(model));
+        } else {
+            priv_.model.replace(None);
+
+            if self.selected() != GTK_INVALID_LIST_POSITION {
+                priv_.selected.replace(GTK_INVALID_LIST_POSITION);
+                self.notify("selected");
+            }
+            if self.selected_room().is_some() {
+                priv_.selected_room.replace(None);
+                self.notify("selected-room");
+            }
+
+            self.items_changed(0, n_items_before, 0);
+        }
+
+        self.notify("model");
+    }
+
+    pub fn set_selected(&self, position: u32) {
+        let priv_ = imp::Selection::from_instance(self);
+
+        let old_selected = self.selected();
+        if old_selected == position {
+            return;
+        }
+
+        let selected_room = self
+            .model()
+            .and_then(|m| m.item(position))
+            .and_then(|o| o.downcast::<gtk::TreeListRow>().ok())
+            .and_then(|r| r.item())
+            .and_then(|o| o.downcast::<Room>().ok());
+        let selected = if selected_room.is_none() {
+            GTK_INVALID_LIST_POSITION
+        } else {
+            position
+        };
+
+        if old_selected == selected {
+            return;
+        }
+
+        priv_.selected.replace(selected);
+        priv_.selected_room.replace(selected_room);
+
+        if old_selected == GTK_INVALID_LIST_POSITION {
+            self.selection_changed(selected, 1);
+        } else if selected == GTK_INVALID_LIST_POSITION {
+            self.selection_changed(old_selected, 1);
+        } else if selected < old_selected {
+            self.selection_changed(selected, old_selected - selected + 1);
+        } else {
+            self.selection_changed(old_selected, selected - old_selected + 1);
+        }
+
+        self.notify("selected");
+        self.notify("selected-room");
+    }
+
+    pub fn set_selected_room(&self, room: Option<Room>) {
+        let priv_ = imp::Selection::from_instance(self);
+
+        let selected_room = self.selected_room();
+        if selected_room == room {
+            return;
+        }
+
+        let old_selected = self.selected();
+
+        let mut selected = GTK_INVALID_LIST_POSITION;
+
+        if let Some(model) = self.model() {
+            for i in 0..=model.n_items() {
+                let room = model
+                    .item(i)
+                    .and_then(|o| o.downcast::<gtk::TreeListRow>().ok())
+                    .and_then(|r| r.item())
+                    .and_then(|o| o.downcast::<Room>().ok());
+                if room == selected_room {
+                    selected = i;
+                    break;
+                }
+            }
+        }
+
+        priv_.selected_room.replace(selected_room);
+
+        if old_selected != selected {
+            priv_.selected.replace(selected);
+
+            if old_selected == GTK_INVALID_LIST_POSITION {
+                self.selection_changed(selected, 1);
+            } else if selected == GTK_INVALID_LIST_POSITION {
+                self.selection_changed(old_selected, 1);
+            } else if selected < old_selected {
+                self.selection_changed(selected, old_selected - selected + 1);
+            } else {
+                self.selection_changed(old_selected, selected - old_selected + 1);
+            }
+            self.notify("selected");
+        }
+
+        self.notify("selected-room");
+    }
+
+    fn items_changed_cb(&self, model: &gio::ListModel, position: u32, removed: u32, added: u32) {
+        let priv_ = imp::Selection::from_instance(self);
+
+        let selected = self.selected();
+        let selected_room = self.selected_room();
+
+        if selected_room.is_none() || selected < position {
+            // unchanged
+        } else if selected != GTK_INVALID_LIST_POSITION && selected >= position + removed {
+            priv_.selected.replace(selected + added - removed);
+            self.notify("selected");
+        } else {
+            for i in 0..=added {
+                if i == added {
+                    // the item really was deleted
+                    priv_.selected.replace(GTK_INVALID_LIST_POSITION);
+                    self.notify("selected");
+                } else {
+                    let room = model
+                        .item(position + i)
+                        .and_then(|o| o.downcast::<gtk::TreeListRow>().ok())
+                        .and_then(|r| r.item())
+                        .and_then(|o| o.downcast::<Room>().ok());
+                    if room == selected_room {
+                        // the item moved
+                        if selected != position + i {
+                            priv_.selected.replace(position + i);
+                            self.notify("selected");
+                        }
+                        break;
+                    }
+                }
+            }
+        }
+
+        self.items_changed(position, removed, added);
+    }
+}
diff --git a/src/session/sidebar/sidebar.rs b/src/session/sidebar/sidebar.rs
index 07caf238..ae3ac38c 100644
--- a/src/session/sidebar/sidebar.rs
+++ b/src/session/sidebar/sidebar.rs
@@ -1,10 +1,10 @@
 use adw::subclass::prelude::BinImpl;
-use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
 
 use crate::session::{
     categories::{Categories, Category},
     room::Room,
-    sidebar::{RoomRow, Row},
+    sidebar::{RoomRow, Row, Selection},
 };
 
 mod imp {
@@ -110,18 +110,23 @@ mod imp {
             self.parent_constructed(obj);
 
             self.listview.get().connect_activate(move |listview, pos| {
-                if let Some(row) = listview
+                if let Some(model) = listview
                     .model()
-                    .and_then(|m| m.downcast::<gtk::SingleSelection>().ok())
-                    .and_then(|m| m.item(pos))
-                    .and_then(|o| o.downcast::<gtk::TreeListRow>().ok())
+                    .and_then(|m| m.downcast::<Selection>().ok())
                 {
-                    if row
-                        .item()
-                        .and_then(|o| o.downcast::<Category>().ok())
-                        .is_some()
+                    if let Some(row) = model
+                        .item(pos)
+                        .and_then(|o| o.downcast::<gtk::TreeListRow>().ok())
                     {
-                        row.set_expanded(!row.is_expanded());
+                        if row
+                            .item()
+                            .and_then(|o| o.downcast::<Category>().ok())
+                            .is_some()
+                        {
+                            row.set_expanded(!row.is_expanded());
+                        } else if row.item().and_then(|o| o.downcast::<Room>().ok()).is_some() {
+                            model.set_selected(pos);
+                        }
                     }
                 }
             });
@@ -183,12 +188,10 @@ impl Sidebar {
                 .flags(glib::BindingFlags::SYNC_CREATE)
                 .build();
 
-            let selection = gtk::SingleSelection::new(Some(&filter_model));
-            selection.connect_notify_local(Some("selected-item"), clone!(@weak self as obj => move |model, 
_| {
-                if let Some(room) = model.selected_item().and_then(|row| 
row.downcast_ref::<gtk::TreeListRow>().unwrap().item()).and_then(|o| o.downcast::<Room>().ok()) {
-                        obj.set_selected_room(Some(room));
-                }
-            }));
+            let selection = Selection::new(Some(&filter_model));
+            self.bind_property("selected-room", &selection, "selected-room")
+                .flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL)
+                .build();
 
             priv_.listview.set_model(Some(&selection));
         } else {


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