[fractal/fractal-next] session: Add Media Viewer



commit f21eccfc15fca049efada7bd4bd3a55d436ab39e
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Tue Nov 30 19:32:24 2021 +0100

    session: Add Media Viewer

 data/resources/resources.gresource.xml    |   1 +
 data/resources/ui/content-item.ui         |  10 +-
 data/resources/ui/content-room-history.ui |   2 +-
 data/resources/ui/media-viewer.ui         |  93 ++++++++++
 data/resources/ui/session.ui              |   4 +-
 po/POTFILES.in                            |   2 +
 src/meson.build                           |   2 +-
 src/session/content/room_history/mod.rs   |  17 +-
 src/session/media_viewer.rs               | 272 ++++++++++++++++++++++++++++++
 src/session/mod.rs                        |  14 +-
 src/session/room/event.rs                 |  32 +++-
 src/session/room/item.rs                  |  21 +++
 src/window.rs                             |  11 ++
 13 files changed, 470 insertions(+), 11 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 607d1544..bfb305e4 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -22,6 +22,7 @@
     <file compressed="true" preprocess="xml-stripblanks" alias="event-menu.ui">ui/event-menu.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="event-source-dialog.ui">ui/event-source-dialog.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="login.ui">ui/login.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" alias="media-viewer.ui">ui/media-viewer.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="session.ui">ui/session.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="sidebar.ui">ui/sidebar.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-account-switcher.ui">ui/sidebar-account-switcher.ui</file>
diff --git a/data/resources/ui/content-item.ui b/data/resources/ui/content-item.ui
index 3a2c3bce..19ef73bd 100644
--- a/data/resources/ui/content-item.ui
+++ b/data/resources/ui/content-item.ui
@@ -1,12 +1,16 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
   <template class="GtkListItem">
-    <property name="activatable">False</property>
+    <binding name="activatable">
+      <lookup name="activatable">
+        <lookup name="item">row</lookup>
+      </lookup>
+    </binding>
     <property name="selectable">False</property>
     <property name="child">
-      <object class="ContentItemRow">
+      <object class="ContentItemRow" id="row">
         <binding name="item">
-            <lookup name="item">GtkListItem</lookup>
+          <lookup name="item">GtkListItem</lookup>
         </binding>
       </object>
     </property>
diff --git a/data/resources/ui/content-room-history.ui b/data/resources/ui/content-room-history.ui
index f02d5f67..b4d9cd09 100644
--- a/data/resources/ui/content-room-history.ui
+++ b/data/resources/ui/content-room-history.ui
@@ -142,6 +142,7 @@
                                 <property name="resource">/org/gnome/FractalNext/content-item.ui</property>
                               </object>
                             </property>
+                            <property name="single-click-activate">True</property>
                             <accessibility>
                               <property name="label" translatable="yes">Room History</property>
                             </accessibility>
@@ -227,4 +228,3 @@
     </child>
   </template>
 </interface>
-
diff --git a/data/resources/ui/media-viewer.ui b/data/resources/ui/media-viewer.ui
new file mode 100644
index 00000000..c470ae48
--- /dev/null
+++ b/data/resources/ui/media-viewer.ui
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="MediaViewer" parent="ContextMenuBin">
+    <property name="child">
+      <object class="GtkOverlay">
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkHeaderBar">
+                <property name="visible" bind-source="MediaViewer" bind-property="fullscreened" 
bind-flags="sync-create | invert-boolean"/>
+                <property name="title-widget">
+                  <object class="GtkLabel">
+                    <binding name="label">
+                      <lookup name="body">MediaViewer</lookup>
+                    </binding>
+                    <property name="single-line-mode">True</property>
+                    <property name="ellipsize">end</property>
+                    <style>
+                      <class name="title"/>
+                    </style>
+                  </object>
+                </property>
+                <child type="start">
+                  <object class="GtkButton" id="back">
+                    <property name="icon-name">go-previous-symbolic</property>
+                    <property name="action-name">session.show-content</property>
+                  </object>
+                </child>
+                <child type="end">
+                  <object class="GtkMenuButton" id="menu_unfull">
+                    <property name="icon-name">view-more-symbolic</property>
+                    <property name="menu-model" bind-source="MediaViewer" bind-property="context-menu" 
bind-flags="sync-create"/>
+                  </object>
+                </child>
+                <child type="end">
+                  <object class="GtkButton">
+                    <property name="icon-name">view-fullscreen-symbolic</property>
+                    <property name="action-name">win.toggle-fullscreen</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="AdwBin" id="media">
+                <property name="halign">center</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child type="overlay">
+          <object class="GtkRevealer" id="headerbar_revealer">
+            <property name="visible" bind-source="MediaViewer" bind-property="fullscreened" 
bind-flags="sync-create"/>
+            <property name="valign">GTK_ALIGN_START</property>
+            <property name="transition-type">GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN</property>
+            <child>
+              <object class="GtkHeaderBar">
+                <property name="show-title-buttons">false</property>
+                <property name="title-widget">
+                  <object class="GtkLabel">
+                    <binding name="label">
+                      <lookup name="body">MediaViewer</lookup>
+                    </binding>
+                    <property name="single-line-mode">True</property>
+                    <property name="ellipsize">end</property>
+                    <style>
+                      <class name="title"/>
+                    </style>
+                  </object>
+                </property>
+                <child type="end">
+                  <object class="GtkMenuButton" id="menu_full">
+                    <property name="icon-name">view-more-symbolic</property>
+                    <property name="menu-model" bind-source="MediaViewer" bind-property="context-menu" 
bind-flags="sync-create"/>
+                  </object>
+                </child>
+                <child type="end">
+                  <object class="GtkButton">
+                    <property name="icon-name">view-restore-symbolic</property>
+                    <property name="action-name">win.toggle-fullscreen</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <layout>
+              <property name="measure">True</property>
+            </layout>
+          </object>
+        </child>
+      </object>
+    </property>
+  </template>
+</interface>
\ No newline at end of file
diff --git a/data/resources/ui/session.ui b/data/resources/ui/session.ui
index f8dee1d9..ca7d10a5 100644
--- a/data/resources/ui/session.ui
+++ b/data/resources/ui/session.ui
@@ -61,8 +61,10 @@
             </child>
           </object>
         </child>
+        <child>
+          <object class="MediaViewer" id="media_viewer" />
+        </child>
       </object>
     </property>
   </template>
 </interface>
-
diff --git a/po/POTFILES.in b/po/POTFILES.in
index c47338ca..bd618fcd 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -28,6 +28,7 @@ data/resources/ui/event-menu.ui
 data/resources/ui/event-source-dialog.ui
 data/resources/ui/login.ui
 data/resources/ui/in-app-notification.ui
+data/resources/ui/media-viewer.ui
 data/resources/ui/room-creation.ui
 data/resources/ui/session.ui
 data/resources/ui/session-verification.ui
@@ -86,6 +87,7 @@ src/session/content/room_history/message_row/mod.rs
 src/session/content/room_history/message_row/text.rs
 src/session/content/room_history/mod.rs
 src/session/content/room_history/state_row.rs
+src/session/media_viewer.rs
 src/session/mod.rs
 src/session/room_creation/mod.rs
 src/session/room_list.rs
diff --git a/src/meson.build b/src/meson.build
index ccc4245e..229e3a88 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -71,7 +71,7 @@ sources = files(
   'session/content/mod.rs',
   'session/content/room_details/member_page.rs',
   'session/content/room_details/mod.rs',
-  'session/room/event_actions.rs',
+  'session/media_viewer.rs',
   'session/room/event.rs',
   'session/room/highlight_flags.rs',
   'session/room/item.rs',
diff --git a/src/session/content/room_history/mod.rs b/src/session/content/room_history/mod.rs
index 715b9d08..176346a1 100644
--- a/src/session/content/room_history/mod.rs
+++ b/src/session/content/room_history/mod.rs
@@ -16,7 +16,7 @@ use sourceview::prelude::*;
 
 use crate::components::{CustomEntry, RoomTitle};
 use crate::session::content::{MarkdownPopover, RoomDetails};
-use crate::session::room::{Room, RoomType};
+use crate::session::room::{Item, Room, RoomType};
 
 mod imp {
     use super::*;
@@ -193,6 +193,21 @@ mod imp {
             self.listview
                 .set_vscroll_policy(gtk::ScrollablePolicy::Natural);
 
+            self.listview
+                .connect_activate(clone!(@weak obj => move |listview, pos| {
+                    if let Some(item) = listview
+                        .model()
+                        .and_then(|model| model.item(pos))
+                        .and_then(|o| o.downcast::<Item>().ok())
+                    {
+                        if let Some(event) = item.event() {
+                            if let Some(room) = obj.room() {
+                                room.session().show_media(event);
+                            }
+                        }
+                    }
+                }));
+
             obj.set_sticky(true);
             let adj = self.listview.vadjustment().unwrap();
 
diff --git a/src/session/media_viewer.rs b/src/session/media_viewer.rs
new file mode 100644
index 00000000..aaa84659
--- /dev/null
+++ b/src/session/media_viewer.rs
@@ -0,0 +1,272 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gettextrs::gettext;
+use gtk::{
+    gdk, gdk_pixbuf::Pixbuf, gio, glib, glib::clone, subclass::prelude::*, CompositeTemplate,
+};
+use log::warn;
+use matrix_sdk::ruma::events::{room::message::MessageType, AnyMessageEventContent};
+
+use crate::{
+    components::{ContextMenuBin, ContextMenuBinImpl},
+    session::room::Event,
+    spawn, Window,
+};
+
+use super::room::EventActions;
+
+mod imp {
+    use crate::components::ContextMenuBinExt;
+
+    use super::*;
+    use glib::object::WeakRef;
+    use glib::subclass::InitializingObject;
+    use once_cell::sync::Lazy;
+    use std::cell::{Cell, RefCell};
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/media-viewer.ui")]
+    pub struct MediaViewer {
+        pub fullscreened: Cell<bool>,
+        pub event: RefCell<Option<WeakRef<Event>>>,
+        pub body: RefCell<Option<String>>,
+        #[template_child]
+        pub headerbar_revealer: TemplateChild<gtk::Revealer>,
+        #[template_child]
+        pub menu_full: TemplateChild<gtk::MenuButton>,
+        #[template_child]
+        pub media: TemplateChild<adw::Bin>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for MediaViewer {
+        const NAME: &'static str = "MediaViewer";
+        type Type = super::MediaViewer;
+        type ParentType = ContextMenuBin;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for MediaViewer {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpec::new_boolean(
+                        "fullscreened",
+                        "Fullscreened",
+                        "Whether the viewer is fullscreen",
+                        false,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpec::new_object(
+                        "event",
+                        "Event",
+                        "The media event to display",
+                        Event::static_type(),
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpec::new_string(
+                        "body",
+                        "Body",
+                        "The body of the media event",
+                        None,
+                        glib::ParamFlags::READABLE,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "fullscreened" => obj.set_fullscreened(value.get().unwrap()),
+                "event" => obj.set_event(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "fullscreened" => obj.fullscreened().to_value(),
+                "event" => obj.event().to_value(),
+                "body" => obj.body().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+
+            obj.set_context_menu(Some(Self::Type::event_menu_model()));
+
+            // Bind `fullscreened` to the window property of the same name.
+            obj.connect_notify_local(Some("root"), |obj, _| {
+                if let Some(window) = obj.root().and_then(|root| root.downcast::<Window>().ok()) {
+                    window
+                        .bind_property("fullscreened", obj, "fullscreened")
+                        .flags(glib::BindingFlags::SYNC_CREATE)
+                        .build();
+                }
+            });
+
+            // Toggle fullscreen on double click.
+            let click_gesture = gtk::GestureClick::builder().button(1).build();
+            click_gesture.connect_pressed(clone!(@weak obj => move |_, n_pressed, _, _| {
+                if n_pressed == 2 {
+                    obj.activate_action("win.toggle-fullscreen", None);
+                }
+            }));
+            obj.add_controller(&click_gesture);
+
+            // Show headerbar when revealer is hovered.
+            let revealer: &gtk::Revealer = &*self.headerbar_revealer;
+            let menu: &gtk::MenuButton = &*self.menu_full;
+            let motion_controller = gtk::EventControllerMotion::new();
+            motion_controller.connect_enter(clone!(@weak revealer => move |_, _, _| {
+                revealer.set_reveal_child(true);
+            }));
+            // Hide the headerbar when revealer is not hovered and header menu is closed.
+            motion_controller.connect_leave(clone!(@weak revealer, @weak menu => move |_| {
+                if menu.popover().filter(|popover| popover.is_visible()).is_none() {
+                    revealer.set_reveal_child(false);
+                }
+            }));
+            menu.popover().unwrap().connect_closed(
+                clone!(@weak revealer, @weak motion_controller, => move |_| {
+                    if !motion_controller.contains_pointer() {
+                        revealer.set_reveal_child(false);
+                    }
+                }),
+            );
+            revealer.add_controller(&motion_controller);
+        }
+    }
+
+    impl WidgetImpl for MediaViewer {}
+    impl BinImpl for MediaViewer {}
+    impl ContextMenuBinImpl for MediaViewer {}
+}
+
+glib::wrapper! {
+    pub struct MediaViewer(ObjectSubclass<imp::MediaViewer>)
+        @extends gtk::Widget, adw::Bin, ContextMenuBin, @implements gtk::Accessible;
+}
+
+impl MediaViewer {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create MediaViewer")
+    }
+
+    pub fn event(&self) -> Option<Event> {
+        let priv_ = imp::MediaViewer::from_instance(self);
+        priv_
+            .event
+            .borrow()
+            .as_ref()
+            .and_then(|event| event.upgrade())
+    }
+
+    pub fn set_event(&self, event: Option<Event>) {
+        let priv_ = imp::MediaViewer::from_instance(self);
+
+        if event == self.event() {
+            return;
+        }
+
+        priv_.event.replace(event.map(|event| event.downgrade()));
+        self.build();
+        self.notify("event");
+    }
+
+    pub fn body(&self) -> Option<String> {
+        let priv_ = imp::MediaViewer::from_instance(self);
+        priv_.body.borrow().clone()
+    }
+
+    pub fn set_body(&self, body: Option<String>) {
+        let priv_ = imp::MediaViewer::from_instance(self);
+
+        if body == self.body() {
+            return;
+        }
+
+        priv_.body.replace(body);
+        self.notify("body");
+    }
+
+    pub fn fullscreened(&self) -> bool {
+        let priv_ = imp::MediaViewer::from_instance(self);
+        priv_.fullscreened.get()
+    }
+
+    pub fn set_fullscreened(&self, fullscreened: bool) {
+        let priv_ = imp::MediaViewer::from_instance(self);
+
+        if fullscreened == self.fullscreened() {
+            return;
+        }
+
+        priv_.fullscreened.set(fullscreened);
+
+        // Upscale the media on fullscreen
+        if fullscreened {
+            priv_.media.set_halign(gtk::Align::Fill);
+        } else {
+            priv_.media.set_halign(gtk::Align::Center);
+        }
+
+        self.notify("fullscreened");
+    }
+
+    fn build(&self) {
+        if let Some(event) = self.event() {
+            self.set_event_actions(Some(&event));
+            if let Some(AnyMessageEventContent::RoomMessage(content)) = event.message_content() {
+                match content.msgtype {
+                    MessageType::Image(image) => {
+                        self.set_body(Some(image.body.clone()));
+
+                        spawn!(
+                            glib::PRIORITY_LOW,
+                            clone!(@weak self as obj => async move {
+                                let priv_ = imp::MediaViewer::from_instance(&obj);
+
+                                match event.get_media_content().await {
+                                    Ok((_, data)) => {
+                                        let stream = 
gio::MemoryInputStream::from_bytes(&glib::Bytes::from(&data));
+                                        let texture = Pixbuf::from_stream(&stream, gio::NONE_CANCELLABLE)
+                                            .ok()
+                                            .map(|pixbuf| gdk::Texture::for_pixbuf(&pixbuf));
+                                        let child = gtk::Picture::for_paintable(texture.as_ref());
+
+                                        priv_.media.set_child(Some(&child));
+                                    }
+                                    Err(error) => {
+                                        warn!("Could not retrieve image file: {}", error);
+                                        let child = gtk::Label::new(Some(&gettext("Could not retrieve 
image")));
+                                        priv_.media.set_child(Some(&child));
+                                    }
+                                }
+                            })
+                        );
+                    }
+                    _ => {}
+                }
+            }
+        }
+    }
+}
+
+impl EventActions for MediaViewer {}
diff --git a/src/session/mod.rs b/src/session/mod.rs
index 0d35cc34..572cdb29 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -2,6 +2,7 @@ mod account_settings;
 mod avatar;
 mod content;
 mod event_source_dialog;
+mod media_viewer;
 pub mod room;
 mod room_creation;
 mod room_list;
@@ -12,7 +13,8 @@ pub mod verification;
 use self::account_settings::AccountSettings;
 pub use self::avatar::Avatar;
 use self::content::Content;
-pub use self::room::Room;
+use self::media_viewer::MediaViewer;
+pub use self::room::{Event, Item, Room};
 pub use self::room_creation::RoomCreation;
 use self::room_list::RoomList;
 use self::sidebar::Sidebar;
@@ -75,6 +77,8 @@ mod imp {
         pub content: TemplateChild<adw::Leaflet>,
         #[template_child]
         pub sidebar: TemplateChild<Sidebar>,
+        #[template_child]
+        pub media_viewer: TemplateChild<MediaViewer>,
         pub client: RefCell<Option<Client>>,
         pub item_list: OnceCell<ItemList>,
         pub user: OnceCell<User>,
@@ -728,6 +732,14 @@ impl Session {
 
         self.emit_by_name("ready", &[]).unwrap();
     }
+
+    /// Show a media event
+    pub fn show_media(&self, event: &Event) {
+        let priv_ = imp::Session::from_instance(self);
+        priv_.media_viewer.set_event(Some(event.clone()));
+
+        priv_.stack.set_visible_child(&*priv_.media_viewer);
+    }
 }
 
 impl Default for Session {
diff --git a/src/session/room/event.rs b/src/session/room/event.rs
index 57440472..9c63fb78 100644
--- a/src/session/room/event.rs
+++ b/src/session/room/event.rs
@@ -107,6 +107,13 @@ mod imp {
                         None,
                         glib::ParamFlags::READABLE,
                     ),
+                    glib::ParamSpec::new_boolean(
+                        "can-view-media",
+                        "Can View Media",
+                        "Whether this is a media event that can be viewed",
+                        false,
+                        glib::ParamFlags::READABLE,
+                    ),
                 ]
             });
 
@@ -146,6 +153,7 @@ mod imp {
                 "show-header" => obj.show_header().to_value(),
                 "can-hide-header" => obj.can_hide_header().to_value(),
                 "time" => obj.time().to_value(),
+                "can-view-media" => obj.can_view_media().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -200,6 +208,7 @@ impl Event {
         priv_.pure_event.replace(Some(event));
 
         self.notify("event");
+        self.notify("can-view-media");
     }
 
     pub fn matrix_sender(&self) -> UserId {
@@ -506,6 +515,7 @@ impl Event {
     /// Compatible events:
     ///
     /// - File message (`MessageType::File`).
+    /// - Image message (`MessageType::Image`).
     ///
     /// Returns `Ok((filename, binary_content))` on success, `Err` if an error occured while
     /// fetching the content. Panics on an incompatible event.
@@ -513,11 +523,17 @@ impl Event {
         if let AnyMessageEventContent::RoomMessage(content) = self.message_content().unwrap() {
             let client = self.room().session().client();
             match content.msgtype {
-                MessageType::File(file_content) => {
-                    let content = file_content.clone();
+                MessageType::File(content) => {
+                    let filename = content.filename.clone().unwrap_or(content.body.clone());
+                    let handle = spawn_tokio!(async move { client.get_file(content, true).await });
+                    let data = handle.await.unwrap()?.unwrap();
+                    return Ok((filename, data));
+                }
+                MessageType::Image(content) => {
+                    let filename = content.body.clone();
                     let handle = spawn_tokio!(async move { client.get_file(content, true).await });
                     let data = handle.await.unwrap()?.unwrap();
-                    return Ok((file_content.filename.unwrap_or(file_content.body), data));
+                    return Ok((filename, data));
                 }
                 _ => {}
             };
@@ -525,4 +541,14 @@ impl Event {
 
         panic!("Trying to get the media content of an event of incompatible type");
     }
+
+    /// Whether this is a media event that can be viewed.
+    pub fn can_view_media(&self) -> bool {
+        match self.message_content() {
+            Some(AnyMessageEventContent::RoomMessage(message)) => {
+                matches!(message.msgtype, MessageType::Image(_))
+            }
+            _ => false,
+        }
+    }
 }
diff --git a/src/session/room/item.rs b/src/session/room/item.rs
index ad1709e1..ccb32b4e 100644
--- a/src/session/room/item.rs
+++ b/src/session/room/item.rs
@@ -27,12 +27,15 @@ impl From<ItemType> for BoxedItemType {
 }
 
 mod imp {
+    use std::cell::Cell;
+
     use super::*;
     use once_cell::{sync::Lazy, unsync::OnceCell};
 
     #[derive(Debug, Default)]
     pub struct Item {
         pub type_: OnceCell<ItemType>,
+        pub activatable: Cell<bool>,
     }
 
     #[glib::object_subclass]
@@ -74,6 +77,13 @@ mod imp {
                         false,
                         glib::ParamFlags::READABLE,
                     ),
+                    glib::ParamSpec::new_boolean(
+                        "activatable",
+                        "Activatable",
+                        "Whether this item is activatable.",
+                        false,
+                        glib::ParamFlags::READWRITE,
+                    ),
                 ]
             });
 
@@ -96,6 +106,7 @@ mod imp {
                     let show_header = value.get().unwrap();
                     let _ = obj.set_show_header(show_header);
                 }
+                "activatable" => self.activatable.set(value.get().unwrap()),
                 _ => unimplemented!(),
             }
         }
@@ -105,9 +116,19 @@ mod imp {
                 "selectable" => obj.selectable().to_value(),
                 "show-header" => obj.show_header().to_value(),
                 "can-hide-header" => obj.can_hide_header().to_value(),
+                "activatable" => self.activatable.get().to_value(),
                 _ => unimplemented!(),
             }
         }
+
+        fn constructed(&self, obj: &Self::Type) {
+            if let Some(event) = obj.event() {
+                event
+                    .bind_property("can-view-media", obj, "activatable")
+                    .flags(glib::BindingFlags::SYNC_CREATE)
+                    .build();
+            }
+        }
     }
 }
 
diff --git a/src/window.rs b/src/window.rs
index 2e3ded05..7cea5193 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -74,6 +74,17 @@ mod imp {
             );
 
             obj.set_default_by_child();
+
+            // Ask for the toggle fullscreen state
+            let fullscreen = gio::SimpleAction::new("toggle-fullscreen", None);
+            fullscreen.connect_activate(clone!(@weak obj as window => move |_, _| {
+                if window.is_fullscreened() {
+                    window.unfullscreen();
+                } else {
+                    window.fullscreen();
+                }
+            }));
+            obj.add_action(&fullscreen);
         }
     }
 


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