[fractal/fractal-next] sidebar: Improve interactions with a custom SelectionModel
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] sidebar: Improve interactions with a custom SelectionModel
- Date: Wed, 5 May 2021 09:29:50 +0000 (UTC)
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]