[fractal/fractal-next] components: Add widget for in-app-notification



commit b439abe55c4e1bcdb2892e0768505028f0155c68
Author: Julian Sparber <julian sparber net>
Date:   Fri May 21 18:07:05 2021 +0200

    components: Add widget for in-app-notification

 data/resources/resources.gresource.xml   |   1 +
 data/resources/style.css                 |   7 +-
 data/resources/ui/in-app-notification.ui |  35 ++++++
 po/POTFILES.in                           |   2 +
 src/components/in_app_notification.rs    | 185 +++++++++++++++++++++++++++++++
 src/components/mod.rs                    |   2 +
 src/meson.build                          |   1 +
 7 files changed, 232 insertions(+), 1 deletion(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 23a37a07..f106321d 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -20,6 +20,7 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="context-menu-bin.ui">ui/context-menu-bin.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="pill.ui">ui/pill.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="spinner-button.ui">ui/spinner-button.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="in-app-notification.ui">ui/in-app-notification.ui</file>
     <file compressed="true">style.css</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/status/welcome.svg</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index 4f385eea..8eedf99e 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -15,7 +15,7 @@
 }
 
 .app-notification .pill {
-  background-color: alpha(@theme_fg_color, 0.35);
+  background-color: alpha(@theme_bg_color, 0.2);
 }
 
 /* Login */
@@ -153,3 +153,8 @@ headerbar.flat {
 .invite-room-name {
   font-size: 24px;
 }
+
+.app-notification {
+  border-radius: 9999px;
+  padding-left: 24px;
+}
diff --git a/data/resources/ui/in-app-notification.ui b/data/resources/ui/in-app-notification.ui
new file mode 100644
index 00000000..6b695182
--- /dev/null
+++ b/data/resources/ui/in-app-notification.ui
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="InAppNotification" parent="AdwBin">
+    <property name="valign">end</property>
+    <property name="halign">center</property>
+    <property name="margin-bottom">100</property>
+    <property name="margin-start">24</property>
+    <property name="margin-end">24</property>
+    <property name="child">
+      <object class="GtkRevealer" id="revealer">
+        <property name="transition-type">crossfade</property>
+        <property name="child">
+          <object class="GtkBox" id="box_">
+            <property name="valign">center</property>
+            <child>
+              <object class="GtkButton">
+                <property name="valign">center</property>
+                <property name="icon-name">window-close-symbolic</property>
+                <property name="action-name">in-app-notification.close</property>
+                <style>
+                  <class name="flat"/>
+                  <class name="circular"/>
+                </style>
+              </object>
+            </child>
+            <style>
+              <class name="app-notification"/>
+            </style>
+          </object>
+        </property>
+      </object>
+    </property>
+  </template>
+</interface>
+
diff --git a/po/POTFILES.in b/po/POTFILES.in
index bcbe1a39..93ff03cc 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -16,6 +16,7 @@ data/resources/ui/content-state-row.ui
 data/resources/ui/content.ui
 data/resources/ui/context-menu-bin.ui
 data/resources/ui/login.ui
+data/resources/ui/in-app-notification.ui
 data/resources/ui/session.ui
 data/resources/ui/shortcuts.ui
 data/resources/ui/sidebar-category-row.ui
@@ -30,6 +31,7 @@ data/resources/ui/window.ui
 src/application.rs
 src/components/context_menu_bin.rs
 src/components/label_with_widgets.rs
+src/components/in_app_notification.rs
 src/components/mod.rs
 src/components/spinner_button.rs
 src/components/pill.rs
diff --git a/src/components/in_app_notification.rs b/src/components/in_app_notification.rs
new file mode 100644
index 00000000..821f4022
--- /dev/null
+++ b/src/components/in_app_notification.rs
@@ -0,0 +1,185 @@
+use crate::Error;
+use adw::subclass::prelude::*;
+use gtk::prelude::*;
+use gtk::subclass::prelude::*;
+use gtk::{gio, glib, glib::clone, CompositeTemplate};
+
+mod imp {
+    use super::*;
+    use glib::{signal::SignalHandlerId, subclass::InitializingObject};
+    use std::cell::{Cell, RefCell};
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/in-app-notification.ui")]
+    pub struct InAppNotification {
+        pub error_list: RefCell<Option<gio::ListStore>>,
+        pub handler: RefCell<Option<SignalHandlerId>>,
+        #[template_child]
+        pub revealer: TemplateChild<gtk::Revealer>,
+        #[template_child]
+        pub box_: TemplateChild<gtk::Box>,
+        pub current_widget: RefCell<Option<gtk::Widget>>,
+        pub shows_error: Cell<bool>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for InAppNotification {
+        const NAME: &'static str = "InAppNotification";
+        type Type = super::InAppNotification;
+        type ParentType = adw::Bin;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+
+            klass.install_action("in-app-notification.close", None, move |widget, _, _| {
+                widget.dismiss()
+            });
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for InAppNotification {
+        fn properties() -> &'static [glib::ParamSpec] {
+            use once_cell::sync::Lazy;
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![glib::ParamSpec::new_object(
+                    "error-list",
+                    "Error List",
+                    "The list of errors to display",
+                    gio::ListStore::static_type(),
+                    glib::ParamFlags::READWRITE,
+                )]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "error-list" => obj.set_error_list(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "error-list" => obj.error_list().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+            self.revealer
+                .connect_child_revealed_notify(clone!(@weak obj => move |revealer| {
+                    let priv_ = imp::InAppNotification::from_instance(&obj);
+                    revealer.set_visible(priv_.shows_error.get());
+                }));
+        }
+
+        fn dispose(&self, _obj: &Self::Type) {
+            if let Some(id) = self.handler.take() {
+                self.error_list.borrow().as_ref().unwrap().disconnect(id);
+            }
+        }
+    }
+
+    impl WidgetImpl for InAppNotification {}
+
+    impl BinImpl for InAppNotification {}
+}
+
+glib::wrapper! {
+    pub struct InAppNotification(ObjectSubclass<imp::InAppNotification>)
+        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl InAppNotification {
+    pub fn new(error_list: &gio::ListStore) -> Self {
+        glib::Object::new(&[("error-list", &error_list)])
+            .expect("Failed to create InAppNotification")
+    }
+
+    pub fn set_error_list(&self, error_list: Option<gio::ListStore>) {
+        let priv_ = imp::InAppNotification::from_instance(self);
+        if self.error_list() == error_list {
+            return;
+        }
+
+        if let Some(id) = priv_.handler.take() {
+            priv_.error_list.borrow().as_ref().unwrap().disconnect(id);
+        }
+
+        if let Some(ref error_list) = error_list {
+            let handler = error_list.connect_items_changed(
+                clone!(@weak self as obj => move |_, position, removed, added| {
+                        let priv_ = imp::InAppNotification::from_instance(&obj);
+                        // If the first error is removed we need to display the next error
+                        if position == 0 && removed > 0 {
+                                obj.next();
+                        }
+
+                        if added > 0  && !priv_.shows_error.get() {
+                                obj.next();
+                        }
+
+                }),
+            );
+            priv_.handler.replace(Some(handler));
+        }
+        priv_.error_list.replace(error_list);
+
+        self.next();
+        self.notify("error-list");
+    }
+
+    pub fn error_list(&self) -> Option<gio::ListStore> {
+        let priv_ = imp::InAppNotification::from_instance(self);
+        priv_.error_list.borrow().to_owned()
+    }
+
+    /// Show the next message in the `error-list`
+    fn next(&self) {
+        let priv_ = imp::InAppNotification::from_instance(self);
+
+        let shows_error = if let Some(widget) = priv_
+            .error_list
+            .borrow()
+            .as_ref()
+            .and_then(|error_list| error_list.item(0))
+            .and_then(|obj| obj.downcast::<Error>().ok())
+            .and_then(|error| error.widget())
+        {
+            if let Some(current_widget) = priv_.current_widget.take() {
+                priv_.box_.remove(&current_widget);
+            }
+            priv_.box_.prepend(&widget);
+            priv_.current_widget.replace(Some(widget));
+            true
+        } else {
+            false
+        };
+
+        priv_.shows_error.set(shows_error);
+        if shows_error {
+            priv_.revealer.show();
+        }
+        priv_.revealer.set_reveal_child(shows_error);
+    }
+
+    fn dismiss(&self) {
+        let priv_ = imp::InAppNotification::from_instance(self);
+        if let Some(error_list) = &*priv_.error_list.borrow() {
+            error_list.remove(0);
+        }
+    }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 04674049..158f2a50 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -1,9 +1,11 @@
 mod context_menu_bin;
+mod in_app_notification;
 mod label_with_widgets;
 mod pill;
 mod spinner_button;
 
 pub use self::context_menu_bin::{ContextMenuBin, ContextMenuBinImpl};
+pub use self::in_app_notification::InAppNotification;
 pub use self::label_with_widgets::LabelWithWidgets;
 pub use self::pill::Pill;
 pub use self::spinner_button::SpinnerButton;
diff --git a/src/meson.build b/src/meson.build
index b2c8fd8c..e149e2bc 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -24,6 +24,7 @@ sources = files(
   'components/label_with_widgets.rs',
   'components/mod.rs',
   'components/pill.rs',
+  'components/in_app_notification.rs',
   'components/spinner_button.rs',
   'config.rs',
   'error.rs',


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