fractal/session/view/sidebar/
row.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::{accessible::Relation, gdk, gio, glib, glib::clone};
4use ruma::api::client::receipt::create_receipt::v3::ReceiptType;
5use tracing::error;
6
7use super::{
8    Sidebar, SidebarIconItemRow, SidebarRoomRow, SidebarSectionRow, SidebarVerificationRow,
9};
10use crate::{
11    components::{confirm_leave_room_dialog, ContextMenuBin},
12    prelude::*,
13    session::model::{
14        IdentityVerification, ReceiptPosition, Room, RoomCategory, SidebarIconItem,
15        SidebarIconItemType, SidebarSection, TargetRoomCategory, User,
16    },
17    spawn, spawn_tokio, toast,
18    utils::BoundObjectWeakRef,
19};
20
21mod imp {
22    use std::cell::RefCell;
23
24    use super::*;
25
26    #[derive(Debug, Default, glib::Properties)]
27    #[properties(wrapper_type = super::SidebarRow)]
28    pub struct SidebarRow {
29        /// The ancestor sidebar of this row.
30        #[property(get, set = Self::set_sidebar, construct_only)]
31        sidebar: BoundObjectWeakRef<Sidebar>,
32        /// The item of this row.
33        #[property(get, set = Self::set_item, explicit_notify, nullable)]
34        item: RefCell<Option<glib::Object>>,
35        room_handler: RefCell<Option<glib::SignalHandlerId>>,
36        room_join_rule_handler: RefCell<Option<glib::SignalHandlerId>>,
37        room_is_read_handler: RefCell<Option<glib::SignalHandlerId>>,
38    }
39
40    #[glib::object_subclass]
41    impl ObjectSubclass for SidebarRow {
42        const NAME: &'static str = "SidebarRow";
43        type Type = super::SidebarRow;
44        type ParentType = ContextMenuBin;
45
46        fn class_init(klass: &mut Self::Class) {
47            klass.set_css_name("sidebar-row");
48            klass.set_accessible_role(gtk::AccessibleRole::ListItem);
49        }
50    }
51
52    #[glib::derived_properties]
53    impl ObjectImpl for SidebarRow {
54        fn constructed(&self) {
55            self.parent_constructed();
56
57            // Set up drop controller
58            let drop = gtk::DropTarget::builder()
59                .actions(gdk::DragAction::MOVE)
60                .formats(&gdk::ContentFormats::for_type(Room::static_type()))
61                .build();
62            drop.connect_accept(clone!(
63                #[weak(rename_to = imp)]
64                self,
65                #[upgrade_or]
66                false,
67                move |_, drop| imp.drop_accept(drop)
68            ));
69            drop.connect_leave(clone!(
70                #[weak(rename_to = imp)]
71                self,
72                move |_| {
73                    imp.drop_leave();
74                }
75            ));
76            drop.connect_drop(clone!(
77                #[weak(rename_to = imp)]
78                self,
79                #[upgrade_or]
80                false,
81                move |_, v, _, _| imp.drop_end(v)
82            ));
83            self.obj().add_controller(drop);
84        }
85
86        fn dispose(&self) {
87            if let Some(room) = self.room() {
88                if let Some(handler) = self.room_join_rule_handler.take() {
89                    room.join_rule().disconnect(handler);
90                }
91                if let Some(handler) = self.room_is_read_handler.take() {
92                    room.disconnect(handler);
93                }
94            }
95        }
96    }
97
98    impl WidgetImpl for SidebarRow {}
99
100    impl ContextMenuBinImpl for SidebarRow {
101        fn menu_opened(&self) {
102            if !self
103                .item
104                .borrow()
105                .as_ref()
106                .is_some_and(glib::Object::is::<Room>)
107            {
108                // No context menu.
109                return;
110            }
111
112            let obj = self.obj();
113            if let Some(sidebar) = obj.sidebar() {
114                let popover = sidebar.room_row_popover();
115                obj.set_popover(Some(popover.clone()));
116            }
117        }
118    }
119
120    impl SidebarRow {
121        /// Set the ancestor sidebar of this row.
122        fn set_sidebar(&self, sidebar: &Sidebar) {
123            let drop_source_category_handler =
124                sidebar.connect_drop_source_category_changed(clone!(
125                    #[weak(rename_to = imp)]
126                    self,
127                    move |_| {
128                        imp.update_for_drop_source_category();
129                    }
130                ));
131
132            let drop_active_target_category_handler = sidebar
133                .connect_drop_active_target_category_changed(clone!(
134                    #[weak(rename_to = imp)]
135                    self,
136                    move |_| {
137                        imp.update_for_drop_active_target_category();
138                    }
139                ));
140
141            self.sidebar.set(
142                sidebar,
143                vec![
144                    drop_source_category_handler,
145                    drop_active_target_category_handler,
146                ],
147            );
148        }
149
150        /// Set the item of this row.
151        fn set_item(&self, item: Option<glib::Object>) {
152            if *self.item.borrow() == item {
153                return;
154            }
155            let obj = self.obj();
156
157            if let Some(room) = self.room() {
158                if let Some(handler) = self.room_handler.take() {
159                    room.disconnect(handler);
160                }
161                if let Some(handler) = self.room_join_rule_handler.take() {
162                    room.join_rule().disconnect(handler);
163                }
164                if let Some(handler) = self.room_is_read_handler.take() {
165                    room.disconnect(handler);
166                }
167            }
168
169            self.item.replace(item.clone());
170
171            self.update_context_menu();
172
173            if let Some(item) = item {
174                if let Some(section) = item.downcast_ref::<SidebarSection>() {
175                    let child = obj.child_or_else::<SidebarSectionRow>(|| {
176                        let child = SidebarSectionRow::new();
177                        obj.update_relation(&[Relation::LabelledBy(&[child.labelled_by()])]);
178                        child
179                    });
180                    child.set_section(Some(section.clone()));
181                } else if let Some(room) = item.downcast_ref::<Room>() {
182                    let child = obj.child_or_default::<SidebarRoomRow>();
183
184                    let room_is_direct_handler = room.connect_is_direct_notify(clone!(
185                        #[weak(rename_to = imp)]
186                        self,
187                        move |_| {
188                            imp.update_context_menu();
189                        }
190                    ));
191                    self.room_handler.replace(Some(room_is_direct_handler));
192                    let room_join_rule_handler =
193                        room.join_rule().connect_we_can_join_notify(clone!(
194                            #[weak(rename_to = imp)]
195                            self,
196                            move |_| {
197                                imp.update_context_menu();
198                            }
199                        ));
200                    self.room_join_rule_handler
201                        .replace(Some(room_join_rule_handler));
202
203                    let room_is_read_handler = room.connect_is_read_notify(clone!(
204                        #[weak(rename_to = imp)]
205                        self,
206                        move |_| {
207                            imp.update_context_menu();
208                        }
209                    ));
210                    self.room_is_read_handler
211                        .replace(Some(room_is_read_handler));
212
213                    child.set_room(Some(room.clone()));
214                } else if let Some(icon_item) = item.downcast_ref::<SidebarIconItem>() {
215                    let child = obj.child_or_default::<SidebarIconItemRow>();
216                    child.set_icon_item(Some(icon_item.clone()));
217                } else if let Some(verification) = item.downcast_ref::<IdentityVerification>() {
218                    let child = obj.child_or_default::<SidebarVerificationRow>();
219                    child.set_identity_verification(Some(verification.clone()));
220                } else {
221                    panic!("Wrong row item: {item:?}");
222                }
223
224                self.update_for_drop_source_category();
225            }
226
227            self.update_context_menu();
228            obj.notify_item();
229        }
230
231        /// Get the `Room` of this item, if this is a room row.
232        pub(super) fn room(&self) -> Option<Room> {
233            self.item.borrow().clone().and_downcast()
234        }
235
236        /// Get the `RoomCategory` of this row, if any.
237        ///
238        /// If this does not display a room or a section containing rooms,
239        /// returns `None`.
240        pub(super) fn room_category(&self) -> Option<RoomCategory> {
241            let borrowed_item = self.item.borrow();
242            let item = borrowed_item.as_ref()?;
243
244            if let Some(room) = item.downcast_ref::<Room>() {
245                Some(room.category())
246            } else {
247                item.downcast_ref::<SidebarSection>()
248                    .and_then(|section| section.name().into_room_category())
249            }
250        }
251
252        /// Get the `TargetRoomCategory` of this row, if any.
253        pub(super) fn target_room_category(&self) -> Option<TargetRoomCategory> {
254            self.room_category()
255                .and_then(RoomCategory::to_target_room_category)
256        }
257
258        /// Get the [`SidebarIconItemType`] of the icon item displayed by this
259        /// row, if any.
260        pub(super) fn item_type(&self) -> Option<SidebarIconItemType> {
261            let borrowed_item = self.item.borrow();
262            borrowed_item
263                .as_ref()?
264                .downcast_ref::<SidebarIconItem>()
265                .map(SidebarIconItem::item_type)
266        }
267
268        /// Whether this has a room context menu.
269        fn has_room_context_menu(&self) -> bool {
270            self.room().is_some_and(|r| {
271                matches!(
272                    r.category(),
273                    RoomCategory::Invited
274                        | RoomCategory::Favorite
275                        | RoomCategory::Normal
276                        | RoomCategory::LowPriority
277                        | RoomCategory::Left
278                )
279            })
280        }
281
282        /// Update the context menu according to the current state.
283        fn update_context_menu(&self) {
284            let obj = self.obj();
285
286            if !self.has_room_context_menu() {
287                obj.insert_action_group("room-row", None::<&gio::ActionGroup>);
288                obj.set_has_context_menu(false);
289                return;
290            }
291
292            obj.insert_action_group("room-row", self.room_actions().as_ref());
293            obj.set_has_context_menu(true);
294        }
295
296        /// An action group with the available room actions.
297        #[allow(clippy::too_many_lines)]
298        fn room_actions(&self) -> Option<gio::SimpleActionGroup> {
299            let room = self.room()?;
300
301            let action_group = gio::SimpleActionGroup::new();
302            let category = room.category();
303
304            match category {
305                RoomCategory::Invited => {
306                    action_group.add_action_entries([
307                        gio::ActionEntry::builder("accept-invite")
308                            .activate(clone!(
309                                #[weak(rename_to = imp)]
310                                self,
311                                move |_, _, _| {
312                                    if let Some(room) = imp.room() {
313                                        spawn!(async move {
314                                            imp.set_room_category(
315                                                &room,
316                                                TargetRoomCategory::Normal,
317                                            )
318                                            .await;
319                                        });
320                                    }
321                                }
322                            ))
323                            .build(),
324                        gio::ActionEntry::builder("decline-invite")
325                            .activate(clone!(
326                                #[weak(rename_to = imp)]
327                                self,
328                                move |_, _, _| {
329                                    if let Some(room) = imp.room() {
330                                        spawn!(async move {
331                                            imp.set_room_category(&room, TargetRoomCategory::Left)
332                                                .await;
333                                        });
334                                    }
335                                }
336                            ))
337                            .build(),
338                    ]);
339                }
340                RoomCategory::Favorite | RoomCategory::Normal | RoomCategory::LowPriority => {
341                    if matches!(category, RoomCategory::Favorite | RoomCategory::LowPriority) {
342                        action_group.add_action_entries([gio::ActionEntry::builder("set-normal")
343                            .activate(clone!(
344                                #[weak(rename_to = imp)]
345                                self,
346                                move |_, _, _| {
347                                    if let Some(room) = imp.room() {
348                                        spawn!(async move {
349                                            imp.set_room_category(
350                                                &room,
351                                                TargetRoomCategory::Normal,
352                                            )
353                                            .await;
354                                        });
355                                    }
356                                }
357                            ))
358                            .build()]);
359                    }
360
361                    if matches!(category, RoomCategory::Normal | RoomCategory::LowPriority) {
362                        action_group.add_action_entries([gio::ActionEntry::builder(
363                            "set-favorite",
364                        )
365                        .activate(clone!(
366                            #[weak(rename_to = imp)]
367                            self,
368                            move |_, _, _| {
369                                if let Some(room) = imp.room() {
370                                    spawn!(async move {
371                                        imp.set_room_category(&room, TargetRoomCategory::Favorite)
372                                            .await;
373                                    });
374                                }
375                            }
376                        ))
377                        .build()]);
378                    }
379
380                    if matches!(category, RoomCategory::Favorite | RoomCategory::Normal) {
381                        action_group.add_action_entries([gio::ActionEntry::builder(
382                            "set-lowpriority",
383                        )
384                        .activate(clone!(
385                            #[weak(rename_to = imp)]
386                            self,
387                            move |_, _, _| {
388                                if let Some(room) = imp.room() {
389                                    spawn!(async move {
390                                        imp.set_room_category(
391                                            &room,
392                                            TargetRoomCategory::LowPriority,
393                                        )
394                                        .await;
395                                    });
396                                }
397                            }
398                        ))
399                        .build()]);
400                    }
401
402                    action_group.add_action_entries([gio::ActionEntry::builder("leave")
403                        .activate(clone!(
404                            #[weak(rename_to = imp)]
405                            self,
406                            move |_, _, _| {
407                                if let Some(room) = imp.room() {
408                                    spawn!(async move {
409                                        imp.set_room_category(&room, TargetRoomCategory::Left)
410                                            .await;
411                                    });
412                                }
413                            }
414                        ))
415                        .build()]);
416
417                    if room.is_read() {
418                        action_group.add_action_entries([gio::ActionEntry::builder(
419                            "mark-as-unread",
420                        )
421                        .activate(clone!(
422                            #[weak]
423                            room,
424                            move |_, _, _| {
425                                spawn!(async move {
426                                    room.mark_as_unread().await;
427                                });
428                            }
429                        ))
430                        .build()]);
431                    } else {
432                        action_group.add_action_entries([gio::ActionEntry::builder(
433                            "mark-as-read",
434                        )
435                        .activate(clone!(
436                            #[weak]
437                            room,
438                            move |_, _, _| {
439                                spawn!(async move {
440                                    room.send_receipt(ReceiptType::Read, ReceiptPosition::End)
441                                        .await;
442                                });
443                            }
444                        ))
445                        .build()]);
446                    }
447                }
448                RoomCategory::Left => {
449                    if room.join_rule().we_can_join() {
450                        action_group.add_action_entries([gio::ActionEntry::builder("join")
451                            .activate(clone!(
452                                #[weak(rename_to = imp)]
453                                self,
454                                move |_, _, _| {
455                                    if let Some(room) = imp.room() {
456                                        spawn!(async move {
457                                            imp.set_room_category(
458                                                &room,
459                                                TargetRoomCategory::Normal,
460                                            )
461                                            .await;
462                                        });
463                                    }
464                                }
465                            ))
466                            .build()]);
467                    }
468
469                    action_group.add_action_entries([gio::ActionEntry::builder("forget")
470                        .activate(clone!(
471                            #[weak(rename_to = imp)]
472                            self,
473                            move |_, _, _| {
474                                if let Some(room) = imp.room() {
475                                    spawn!(async move {
476                                        imp.forget_room(&room).await;
477                                    });
478                                }
479                            }
480                        ))
481                        .build()]);
482                }
483                RoomCategory::Outdated | RoomCategory::Space | RoomCategory::Ignored => {}
484            }
485
486            if matches!(
487                category,
488                RoomCategory::Favorite
489                    | RoomCategory::Normal
490                    | RoomCategory::LowPriority
491                    | RoomCategory::Left
492            ) {
493                if room.is_direct() {
494                    action_group.add_action_entries([gio::ActionEntry::builder(
495                        "unset-direct-chat",
496                    )
497                    .activate(clone!(
498                        #[weak(rename_to = imp)]
499                        self,
500                        move |_, _, _| {
501                            if let Some(room) = imp.room() {
502                                spawn!(async move {
503                                    imp.set_room_is_direct(&room, false).await;
504                                });
505                            }
506                        }
507                    ))
508                    .build()]);
509                } else {
510                    action_group.add_action_entries([gio::ActionEntry::builder("set-direct-chat")
511                        .activate(clone!(
512                            #[weak(rename_to = imp)]
513                            self,
514                            move |_, _, _| {
515                                if let Some(room) = imp.room() {
516                                    spawn!(async move {
517                                        imp.set_room_is_direct(&room, true).await;
518                                    });
519                                }
520                            }
521                        ))
522                        .build()]);
523                }
524            }
525
526            Some(action_group)
527        }
528
529        /// Update the disabled or empty state of this drop target.
530        fn update_for_drop_source_category(&self) {
531            let obj = self.obj();
532            let source_category = self.sidebar.obj().and_then(|s| s.drop_source_category());
533
534            if let Some(source_category) = source_category {
535                if self
536                    .target_room_category()
537                    .is_some_and(|row_category| source_category.can_change_to(row_category))
538                {
539                    obj.remove_css_class("drop-disabled");
540
541                    if self
542                        .item
543                        .borrow()
544                        .as_ref()
545                        .and_then(glib::Object::downcast_ref)
546                        .is_some_and(SidebarSection::is_empty)
547                    {
548                        obj.add_css_class("drop-empty");
549                    } else {
550                        obj.remove_css_class("drop-empty");
551                    }
552                } else {
553                    let is_forget_item = self
554                        .item_type()
555                        .is_some_and(|item_type| item_type == SidebarIconItemType::Forget);
556                    if is_forget_item && source_category == RoomCategory::Left {
557                        obj.remove_css_class("drop-disabled");
558                    } else {
559                        obj.add_css_class("drop-disabled");
560                        obj.remove_css_class("drop-empty");
561                    }
562                }
563            } else {
564                // Clear style
565                obj.remove_css_class("drop-disabled");
566                obj.remove_css_class("drop-empty");
567                obj.remove_css_class("drop-active");
568            }
569
570            if let Some(section_row) = obj.child().and_downcast::<SidebarSectionRow>() {
571                section_row.set_show_label_for_room_category(source_category);
572            }
573        }
574
575        /// Update the active state of this drop target.
576        fn update_for_drop_active_target_category(&self) {
577            let obj = self.obj();
578
579            let Some(room_category) = self.room_category() else {
580                obj.remove_css_class("drop-active");
581                return;
582            };
583
584            let target_category = self
585                .sidebar
586                .obj()
587                .and_then(|s| s.drop_active_target_category());
588
589            if target_category.is_some_and(|target_category| target_category == room_category) {
590                obj.add_css_class("drop-active");
591            } else {
592                obj.remove_css_class("drop-active");
593            }
594        }
595
596        /// Handle the drag-n-drop hovering this row.
597        fn drop_accept(&self, drop: &gdk::Drop) -> bool {
598            let Some(sidebar) = self.sidebar.obj() else {
599                return false;
600            };
601
602            let room = drop
603                .drag()
604                .map(|drag| drag.content())
605                .and_then(|content| content.value(Room::static_type()).ok())
606                .and_then(|value| value.get::<Room>().ok());
607            if let Some(room) = room {
608                if let Some(target_category) = self.target_room_category() {
609                    if room.category().can_change_to(target_category) {
610                        sidebar.set_drop_active_target_category(Some(target_category));
611                        return true;
612                    }
613                } else if let Some(item_type) = self.item_type() {
614                    if room.category() == RoomCategory::Left
615                        && item_type == SidebarIconItemType::Forget
616                    {
617                        self.obj().add_css_class("drop-active");
618                        sidebar.set_drop_active_target_category(None);
619                        return true;
620                    }
621                }
622            }
623            false
624        }
625
626        /// Handle the drag-n-drop leaving this row.
627        fn drop_leave(&self) {
628            self.obj().remove_css_class("drop-active");
629            if let Some(sidebar) = self.sidebar.obj() {
630                sidebar.set_drop_active_target_category(None);
631            }
632        }
633
634        /// Handle the drop on this row.
635        fn drop_end(&self, value: &glib::Value) -> bool {
636            let mut ret = false;
637            if let Ok(room) = value.get::<Room>() {
638                if let Some(target_category) = self.target_room_category() {
639                    if room.category().can_change_to(target_category) {
640                        spawn!(clone!(
641                            #[weak(rename_to = imp)]
642                            self,
643                            async move {
644                                imp.set_room_category(&room, target_category).await;
645                            }
646                        ));
647                        ret = true;
648                    }
649                } else if let Some(item_type) = self.item_type() {
650                    if room.category() == RoomCategory::Left
651                        && item_type == SidebarIconItemType::Forget
652                    {
653                        spawn!(clone!(
654                            #[weak(rename_to = imp)]
655                            self,
656                            async move {
657                                imp.forget_room(&room).await;
658                            }
659                        ));
660                        ret = true;
661                    }
662                }
663            }
664            if let Some(sidebar) = self.sidebar.obj() {
665                sidebar.set_drop_source_category(None);
666            }
667            ret
668        }
669
670        /// Change the category of the given room.
671        async fn set_room_category(&self, room: &Room, category: TargetRoomCategory) {
672            let obj = self.obj();
673
674            let ignored_inviter = if category == TargetRoomCategory::Left {
675                let Some(response) = confirm_leave_room_dialog(room, &*obj).await else {
676                    return;
677                };
678
679                response.ignore_inviter.then(|| room.inviter()).flatten()
680            } else {
681                None
682            };
683
684            let previous_category = room.category();
685            if room.change_category(category).await.is_err() {
686                match previous_category {
687                    RoomCategory::Invited => {
688                        if category == RoomCategory::Left {
689                            toast!(
690                                obj,
691                                gettext(
692                                    // Translators: Do NOT translate the content between '{' and '}', this
693                                    // is a variable name.
694                                    "Could not decline invitation for {room}",
695                                ),
696                                @room,
697                            );
698                        } else {
699                            toast!(
700                                obj,
701                                gettext(
702                                    // Translators: Do NOT translate the content between '{' and '}', this
703                                    // is a variable name.
704                                    "Could not accept invitation for {room}",
705                                ),
706                                @room,
707                            );
708                        }
709                    }
710                    RoomCategory::Left => {
711                        toast!(
712                            obj,
713                            gettext(
714                                // Translators: Do NOT translate the content between '{' and '}', this is a
715                                // variable name.
716                                "Could not join {room}",
717                            ),
718                            @room,
719                        );
720                    }
721                    _ => {
722                        if category == RoomCategory::Left {
723                            toast!(
724                                obj,
725                                gettext(
726                                    // Translators: Do NOT translate the content between '{' and '}', this is a variable name.
727                                    "Could not leave {room}",
728                                ),
729                                @room,
730                            );
731                        } else {
732                            toast!(
733                                obj,
734                                gettext(
735                                    // Translators: Do NOT translate the content between '{' and '}', this is a variable name.
736                                    "Could not move {room} from {previous_category} to {new_category}",
737                                ),
738                                @room,
739                                previous_category = previous_category.to_string(),
740                                new_category = RoomCategory::from(category).to_string(),
741                            );
742                        }
743                    }
744                }
745            }
746
747            if let Some(inviter) = ignored_inviter {
748                if inviter.upcast::<User>().ignore().await.is_err() {
749                    toast!(obj, gettext("Could not ignore user"));
750                }
751            }
752        }
753
754        /// Forget the given room.
755        async fn forget_room(&self, room: &Room) {
756            if room.forget().await.is_err() {
757                toast!(
758                    self.obj(),
759                    // Translators: Do NOT translate the content between '{' and '}', this is a variable name.
760                    gettext("Could not forget {room}"),
761                    @room,
762                );
763            }
764        }
765
766        /// Set or unset the room as a direct chat.
767        async fn set_room_is_direct(&self, room: &Room, is_direct: bool) {
768            let matrix_room = room.matrix_room().clone();
769            let handle = spawn_tokio!(async move { matrix_room.set_is_direct(is_direct).await });
770
771            if let Err(error) = handle.await.unwrap() {
772                let obj = self.obj();
773
774                if is_direct {
775                    error!("Could not mark room as direct chat: {error}");
776                    // Translators: Do NOT translate the content between '{' and '}', this is a
777                    // variable name.
778                    toast!(obj, gettext("Could not mark {room} as direct chat"), @room);
779                } else {
780                    error!("Could not unmark room as direct chat: {error}");
781                    // Translators: Do NOT translate the content between '{' and '}', this is a
782                    // variable name.
783                    toast!(obj, gettext("Could not unmark {room} as direct chat"), @room);
784                }
785            }
786        }
787    }
788}
789
790glib::wrapper! {
791    /// A row of the sidebar.
792    pub struct SidebarRow(ObjectSubclass<imp::SidebarRow>)
793        @extends gtk::Widget, ContextMenuBin, @implements gtk::Accessible;
794}
795
796impl SidebarRow {
797    pub fn new(sidebar: &Sidebar) -> Self {
798        glib::Object::builder().property("sidebar", sidebar).build()
799    }
800}
801
802impl ChildPropertyExt for SidebarRow {
803    fn child_property(&self) -> Option<gtk::Widget> {
804        self.child()
805    }
806
807    fn set_child_property(&self, child: Option<&impl IsA<gtk::Widget>>) {
808        self.set_child(child);
809    }
810}