relm4/typed_view/
column.rs

1//! Idiomatic and high-level abstraction over [`gtk::ColumnView`].
2
3use super::{
4    Filter, OrdFn, RelmSelectionExt, TypedListItem, get_mut_value, get_value,
5    iterator::TypedIterator,
6};
7use gtk::{
8    gio, glib,
9    prelude::{Cast, CastNone, FilterExt, IsA, ListItemExt, ListModelExt, ObjectExt},
10};
11use std::{
12    any::Any,
13    cmp::Ordering,
14    collections::HashMap,
15    fmt::{Debug, Display},
16    marker::PhantomData,
17};
18
19/// An item of a [`TypedColumnView`].
20pub trait RelmColumn: Any {
21    /// The top-level widget for the list item.
22    type Root: IsA<gtk::Widget>;
23
24    /// The widgets created for the list item.
25    type Widgets;
26
27    /// Item whose data is shown in this column.
28    type Item: Any;
29
30    /// The columns created for this list item.
31    const COLUMN_NAME: &'static str;
32    /// Whether to enable resizing for this column
33    const ENABLE_RESIZE: bool = false;
34    /// Whether to enable automatic expanding for this column
35    const ENABLE_EXPAND: bool = false;
36
37    /// Returns the shown title for the column. By default shows [`RelmColumn::COLUMN_NAME`]. Useful for translations
38    #[must_use]
39    fn header_title() -> String {
40        String::from(Self::COLUMN_NAME)
41    }
42
43    /// Construct the widgets.
44    fn setup(list_item: &gtk::ListItem) -> (Self::Root, Self::Widgets);
45
46    /// Bind the widgets to match the data of the list item.
47    fn bind(_item: &mut Self::Item, _widgets: &mut Self::Widgets, _root: &mut Self::Root) {}
48
49    /// Undo the steps of [`RelmColumn::bind()`] if necessary.
50    fn unbind(_item: &mut Self::Item, _widgets: &mut Self::Widgets, _root: &mut Self::Root) {}
51
52    /// Undo the steps of [`RelmColumn::setup()`] if necessary.
53    fn teardown(_list_item: &gtk::ListItem) {}
54
55    /// Sorter for column.
56    #[must_use]
57    fn sort_fn() -> OrdFn<Self::Item> {
58        None
59    }
60}
61
62/// Simplified trait for creating columns with only one `gtk::Label` widget per-entry (i.e. a text cell)
63pub trait LabelColumn: 'static {
64    /// Item of the model
65    type Item: Any;
66    /// Value of the column
67    type Value: PartialOrd + Display;
68
69    /// Name of the column
70    const COLUMN_NAME: &'static str;
71    /// Whether to enable the sorting for this column
72    const ENABLE_SORT: bool;
73    /// Whether to enable resizing for this column
74    const ENABLE_RESIZE: bool = false;
75    /// Whether to enable automatic expanding for this column
76    const ENABLE_EXPAND: bool = false;
77
78    /// Get the value that this column represents.
79    fn get_cell_value(item: &Self::Item) -> Self::Value;
80    /// Format the value for presentation in the text cell.
81    fn format_cell_value(value: &Self::Value) -> String {
82        value.to_string()
83    }
84}
85
86impl<C> RelmColumn for C
87where
88    C: LabelColumn,
89{
90    type Root = gtk::Label;
91    type Widgets = ();
92    type Item = C::Item;
93
94    const COLUMN_NAME: &'static str = C::COLUMN_NAME;
95    const ENABLE_RESIZE: bool = C::ENABLE_RESIZE;
96    const ENABLE_EXPAND: bool = C::ENABLE_EXPAND;
97
98    fn setup(_: &gtk::ListItem) -> (Self::Root, Self::Widgets) {
99        (gtk::Label::new(None), ())
100    }
101
102    fn bind(item: &mut Self::Item, _: &mut Self::Widgets, label: &mut Self::Root) {
103        label.set_label(&C::format_cell_value(&C::get_cell_value(item)));
104    }
105
106    fn sort_fn() -> OrdFn<Self::Item> {
107        if C::ENABLE_SORT {
108            Some(Box::new(|a, b| {
109                let a = C::get_cell_value(a);
110                let b = C::get_cell_value(b);
111                a.partial_cmp(&b).unwrap_or(Ordering::Equal)
112            }))
113        } else {
114            None
115        }
116    }
117}
118
119/// A high-level wrapper around [`gio::ListStore`],
120/// [`gtk::SignalListItemFactory`] and [`gtk::ColumnView`].
121///
122/// [`TypedColumnView`] aims at keeping nearly the same functionality and
123/// flexibility of the raw bindings while introducing a more idiomatic
124/// and type-safe interface.
125pub struct TypedColumnView<T, S> {
126    /// The internal list view.
127    pub view: gtk::ColumnView,
128    /// The internal selection model.
129    pub selection_model: S,
130    columns: HashMap<&'static str, gtk::ColumnViewColumn>,
131    store: gio::ListStore,
132    filters: Vec<Filter>,
133    active_model: gio::ListModel,
134    base_model: gio::ListModel,
135    _ty: PhantomData<*const T>,
136}
137
138impl<T, S> Debug for TypedColumnView<T, S>
139where
140    T: Debug,
141    S: Debug,
142{
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        f.debug_struct("TypedColumnView")
145            .field("store", &self.store)
146            .field("view", &self.view)
147            .field("filters", &"<Vec<gtk::Filter>>")
148            .field("active_model", &self.active_model)
149            .field("base_model", &self.base_model)
150            .field("selection_model", &self.selection_model)
151            .finish()
152    }
153}
154
155impl<T, S> Default for TypedColumnView<T, S>
156where
157    T: Any,
158    S: RelmSelectionExt,
159{
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165impl<T, S> TypedColumnView<T, S>
166where
167    T: Any,
168    S: RelmSelectionExt,
169{
170    /// Create a new, empty [`TypedColumnView`].
171    #[must_use]
172    pub fn new() -> Self {
173        let store = gio::ListStore::new::<glib::BoxedAnyObject>();
174
175        let model: gio::ListModel = store.clone().upcast();
176
177        let b = gtk::SortListModel::new(Some(model), None::<gtk::Sorter>);
178
179        let base_model: gio::ListModel = b.clone().upcast();
180
181        let selection_model = S::new_model(base_model.clone());
182        let view = gtk::ColumnView::new(Some(selection_model.clone()));
183        b.set_sorter(view.sorter().as_ref());
184
185        Self {
186            store,
187            view,
188            columns: HashMap::new(),
189            filters: Vec::new(),
190            active_model: base_model.clone(),
191            base_model,
192            _ty: PhantomData,
193            selection_model,
194        }
195    }
196
197    /// Append column to this typed view
198    pub fn append_column<C>(&mut self)
199    where
200        C: RelmColumn<Item = T>,
201    {
202        let factory = gtk::SignalListItemFactory::new();
203        factory.connect_setup(move |_, list_item| {
204            let list_item = list_item
205                .downcast_ref::<gtk::ListItem>()
206                .expect("Needs to be ListItem");
207
208            let (root, widgets) = C::setup(list_item);
209            unsafe { root.set_data("widgets", widgets) };
210            list_item.set_child(Some(&root));
211        });
212
213        #[inline]
214        fn modify_widgets<T, C>(
215            list_item: &glib::Object,
216            f: impl FnOnce(&mut T, &mut C::Widgets, &mut C::Root),
217        ) where
218            T: Any,
219            C: RelmColumn<Item = T>,
220        {
221            let list_item = list_item
222                .downcast_ref::<gtk::ListItem>()
223                .expect("Needs to be ListItem");
224
225            let widget = list_item.child();
226
227            let obj = list_item.item().unwrap();
228            let mut obj = get_mut_value::<T>(&obj);
229
230            let mut root = widget.and_downcast::<C::Root>().unwrap();
231
232            let mut widgets = unsafe { root.steal_data("widgets") }.unwrap();
233            (f)(&mut *obj, &mut widgets, &mut root);
234            unsafe { root.set_data("widgets", widgets) };
235        }
236
237        factory.connect_bind(move |_, list_item| {
238            modify_widgets::<T, C>(list_item.upcast_ref(), |obj, widgets, root| {
239                C::bind(obj, widgets, root);
240            });
241        });
242
243        factory.connect_unbind(move |_, list_item| {
244            modify_widgets::<T, C>(list_item.upcast_ref(), |obj, widgets, root| {
245                C::unbind(obj, widgets, root);
246            });
247        });
248
249        factory.connect_teardown(move |_, list_item| {
250            let list_item = list_item
251                .downcast_ref::<gtk::ListItem>()
252                .expect("Needs to be ListItem");
253
254            C::teardown(list_item);
255        });
256
257        let sort_fn = C::sort_fn();
258
259        let c = gtk::ColumnViewColumn::new(Some(&C::header_title()), Some(factory));
260        c.set_resizable(C::ENABLE_RESIZE);
261        c.set_expand(C::ENABLE_EXPAND);
262
263        if let Some(sort_fn) = sort_fn {
264            c.set_sorter(Some(&gtk::CustomSorter::new(move |first, second| {
265                let first = get_value::<T>(first);
266                let second = get_value::<T>(second);
267
268                sort_fn(&first, &second).into()
269            })))
270        }
271
272        self.view.append_column(&c);
273        self.columns.insert(C::COLUMN_NAME, c);
274    }
275
276    /// Add a function to filter the stored items.
277    /// Returning `false` will simply hide the item.
278    ///
279    /// Note that several filters can be added on top of each other.
280    pub fn add_filter<F: Fn(&T) -> bool + 'static>(&mut self, f: F) {
281        let filter = gtk::CustomFilter::new(move |obj| {
282            let value = get_value::<T>(obj);
283            f(&value)
284        });
285        let filter_model =
286            gtk::FilterListModel::new(Some(self.active_model.clone()), Some(filter.clone()));
287        self.active_model = filter_model.clone().upcast();
288        self.selection_model.set_list_model(&self.active_model);
289        self.filters.push(Filter {
290            filter,
291            model: filter_model,
292        });
293    }
294
295    /// Get columns currently associated with this view.
296    pub fn get_columns(&self) -> &HashMap<&'static str, gtk::ColumnViewColumn> {
297        &self.columns
298    }
299
300    /// Returns the amount of filters that were added.
301    pub fn filters_len(&self) -> usize {
302        self.filters.len()
303    }
304
305    /// Set a certain filter as active or inactive.
306    pub fn set_filter_status(&mut self, idx: usize, active: bool) -> bool {
307        if let Some(filter) = self.filters.get(idx) {
308            if active {
309                filter.model.set_filter(Some(&filter.filter));
310            } else {
311                filter.model.set_filter(None::<&gtk::CustomFilter>);
312            }
313            true
314        } else {
315            false
316        }
317    }
318
319    /// Notify that a certain filter has changed.
320    /// This causes the filter expression to be re-evaluated.
321    ///
322    /// Returns true if a filter was notified.
323    pub fn notify_filter_changed(&self, idx: usize) -> bool {
324        if let Some(filter) = self.filters.get(idx) {
325            filter.filter.changed(gtk::FilterChange::Different);
326            true
327        } else {
328            false
329        }
330    }
331
332    /// Remove the last filter.
333    pub fn pop_filter(&mut self) {
334        let filter = self.filters.pop();
335        if let Some(filter) = filter {
336            self.active_model = filter.model.model().unwrap();
337            self.selection_model.set_list_model(&self.active_model);
338        }
339    }
340
341    /// Remove all filters.
342    pub fn clear_filters(&mut self) {
343        self.filters.clear();
344        self.active_model = self.base_model.clone();
345        self.selection_model.set_list_model(&self.active_model);
346    }
347
348    /// Add a new item at the end of the list.
349    pub fn append(&mut self, value: T) {
350        self.store.append(&glib::BoxedAnyObject::new(value));
351    }
352
353    /// Add new items from an iterator the the end of the list.
354    pub fn extend_from_iter<I: IntoIterator<Item = T>>(&mut self, init: I) {
355        let objects: Vec<glib::BoxedAnyObject> =
356            init.into_iter().map(glib::BoxedAnyObject::new).collect();
357        self.store.extend_from_slice(&objects);
358    }
359
360    #[cfg(feature = "gnome_43")]
361    #[cfg_attr(docsrs, doc(cfg(feature = "gnome_43")))]
362    /// Find the index of the first item that matches a certain function.
363    pub fn find<F: FnMut(&T) -> bool>(&self, mut equal_func: F) -> Option<u32> {
364        self.store.find_with_equal_func(move |obj| {
365            let value = get_value::<T>(obj);
366            equal_func(&value)
367        })
368    }
369
370    /// Returns true if the list is empty.
371    pub fn is_empty(&self) -> bool {
372        self.len() == 0
373    }
374
375    /// Returns the length of the list (without filters).
376    pub fn len(&self) -> u32 {
377        self.store.n_items()
378    }
379
380    /// Get the [`TypedListItem`] at the specified position.
381    ///
382    /// Returns [`None`] if the position is invalid.
383    pub fn get(&self, position: u32) -> Option<TypedListItem<T>> {
384        if let Some(obj) = self.store.item(position) {
385            let wrapper = obj.downcast::<glib::BoxedAnyObject>().unwrap();
386            Some(TypedListItem::new(wrapper))
387        } else {
388            None
389        }
390    }
391
392    /// Get the visible [`TypedListItem`] at the specified position,
393    /// (the item at the given position after filtering and sorting).
394    ///
395    /// Returns [`None`] if the position is invalid.
396    pub fn get_visible(&self, position: u32) -> Option<TypedListItem<T>> {
397        if let Some(obj) = self.active_model.item(position) {
398            let wrapper = obj.downcast::<glib::BoxedAnyObject>().unwrap();
399            Some(TypedListItem::new(wrapper))
400        } else {
401            None
402        }
403    }
404
405    /// Insert an item at a specific position.
406    pub fn insert(&mut self, position: u32, value: T) {
407        self.store
408            .insert(position, &glib::BoxedAnyObject::new(value));
409    }
410
411    /// Insert an item into the list and calculate its position from
412    /// a sorting function.
413    pub fn insert_sorted<F>(&self, value: T, mut compare_func: F) -> u32
414    where
415        F: FnMut(&T, &T) -> Ordering,
416    {
417        let item = glib::BoxedAnyObject::new(value);
418
419        let compare = move |first: &glib::Object, second: &glib::Object| -> Ordering {
420            let first = get_value::<T>(first);
421            let second = get_value::<T>(second);
422            compare_func(&first, &second)
423        };
424
425        self.store.insert_sorted(&item, compare)
426    }
427
428    /// Remove an item at a specific position.
429    pub fn remove(&mut self, position: u32) {
430        self.store.remove(position);
431    }
432
433    /// Remove all items.
434    pub fn clear(&mut self) {
435        self.store.remove_all();
436    }
437
438    /// Returns an iterator that allows modifying each [`TypedListItem`].
439    pub fn iter(&self) -> TypedIterator<'_, TypedColumnView<T, S>> {
440        TypedIterator {
441            list: self,
442            index: 0,
443        }
444    }
445}