[fractal/multi-account: 4/4] Account switcher: Show hints in account entry for visible session




commit 4a5f478bd11a017d7fbcf1bfe169e211961f3ee6
Author: Alejandro Domínguez <adomu net-c com>
Date:   Mon Aug 16 05:41:05 2021 +0200

    Account switcher: Show hints in account entry for visible session

 data/resources/resources.gresource.xml             |   1 +
 data/resources/style.css                           |  10 ++
 .../ui/components-avatar-with-selection.ui         |  12 ++
 data/resources/ui/user-entry-row.ui                |   2 +-
 po/POTFILES.in                                     |   2 +
 src/components/avatar_with_selection.rs            | 141 +++++++++++++++++++++
 src/components/mod.rs                              |   2 +
 src/meson.build                                    |   1 +
 src/session/mod.rs                                 |   4 +-
 src/session/sidebar/account_switcher/item.rs       |  23 +++-
 src/session/sidebar/account_switcher/mod.rs        |  76 ++++++-----
 src/session/sidebar/account_switcher/user_entry.rs |  15 ++-
 src/session/sidebar/sidebar.rs                     |  10 +-
 13 files changed, 253 insertions(+), 46 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index ca7c77b3..0139dbb9 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -33,6 +33,7 @@
     <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" preprocess="xml-stripblanks" 
alias="components-avatar.ui">ui/components-avatar.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="components-avatar-with-selection.ui">ui/components-avatar-with-selection.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 08e2583f..f9486b67 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -55,6 +55,16 @@
   background-color: alpha(@theme_bg_color, 0.2);
 }
 
+.selected-avatar avatar {
+  border: 2px solid @accent_bg_color;
+}
+
+.blue-checkmark {
+  color: @accent_bg_color;
+  border-radius: 9999px;
+  background-color: white;
+}
+
 /* Login */
 .login {
   min-width: 250px;
diff --git a/data/resources/ui/components-avatar-with-selection.ui 
b/data/resources/ui/components-avatar-with-selection.ui
new file mode 100644
index 00000000..ed1d4c75
--- /dev/null
+++ b/data/resources/ui/components-avatar-with-selection.ui
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ComponentsAvatarWithSelection" parent="AdwBin">
+    <property name="child">
+      <object class="GtkOverlay" id="checkmark_overlay">
+        <child>
+          <object class="ComponentsAvatar" id="child_avatar"></object>
+        </child>
+      </object>
+    </property>
+  </template>
+</interface>
diff --git a/data/resources/ui/user-entry-row.ui b/data/resources/ui/user-entry-row.ui
index 611fc607..551cb454 100644
--- a/data/resources/ui/user-entry-row.ui
+++ b/data/resources/ui/user-entry-row.ui
@@ -5,7 +5,7 @@
       <object class="GtkBox">
         <property name="spacing">10</property>
         <child>
-          <object class="ComponentsAvatar" id="avatar_component">
+          <object class="ComponentsAvatarWithSelection" id="avatar_component">
             <property name="size">40</property>
             <binding name="item">
               <lookup name="avatar" type="User">
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 554422f7..8070be19 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -7,6 +7,7 @@ data/org.gnome.FractalNext.metainfo.xml.in.in
 # UI files
 data/resources/ui/add_account.ui
 data/resources/ui/components-avatar.ui
+data/resources/ui/components-avatar-with-selection.ui
 data/resources/ui/content-divider-row.ui
 data/resources/ui/content-item-row-menu.ui
 data/resources/ui/content-item.ui
@@ -36,6 +37,7 @@ data/resources/ui/window.ui
 # Rust files
 src/application.rs
 src/components/avatar.rs
+src/components/avatar_with_selection.rs
 src/components/context_menu_bin.rs
 src/components/custom_entry.rs
 src/components/label_with_widgets.rs
diff --git a/src/components/avatar_with_selection.rs b/src/components/avatar_with_selection.rs
new file mode 100644
index 00000000..ecf47dd0
--- /dev/null
+++ b/src/components/avatar_with_selection.rs
@@ -0,0 +1,141 @@
+use adw::subclass::prelude::*;
+use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+use super::Avatar;
+use crate::session::Avatar as AvatarItem;
+
+mod imp {
+    use super::*;
+    use glib::subclass::InitializingObject;
+    use once_cell::sync::{Lazy, OnceCell};
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/components-avatar-with-selection.ui")]
+    pub struct AvatarWithSelection {
+        #[template_child]
+        pub child_avatar: TemplateChild<Avatar>,
+        #[template_child]
+        pub checkmark_overlay: TemplateChild<gtk::Overlay>,
+        pub checkmark: OnceCell<gtk::Image>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for AvatarWithSelection {
+        const NAME: &'static str = "ComponentsAvatarWithSelection";
+        type Type = super::AvatarWithSelection;
+        type ParentType = adw::Bin;
+
+        fn class_init(klass: &mut Self::Class) {
+            Avatar::static_type();
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for AvatarWithSelection {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpec::new_object(
+                        "item",
+                        "Item",
+                        "The Avatar item displayed by this widget",
+                        AvatarItem::static_type(),
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpec::new_int(
+                        "size",
+                        "Size",
+                        "The size of the Avatar",
+                        -1,
+                        i32::MAX,
+                        -1,
+                        glib::ParamFlags::READWRITE,
+                    ),
+                    glib::ParamSpec::new_boolean(
+                        "selected",
+                        "Selected",
+                        "Style helper for the inner Avatar",
+                        false,
+                        glib::ParamFlags::WRITABLE,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "item" => self.child_avatar.set_item(value.get().unwrap()),
+                "size" => self.child_avatar.set_size(value.get().unwrap()),
+                "selected" => obj.set_selected(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "item" => self.child_avatar.item().to_value(),
+                "size" => self.child_avatar.size().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+    }
+
+    impl WidgetImpl for AvatarWithSelection {}
+    impl BinImpl for AvatarWithSelection {}
+}
+
+glib::wrapper! {
+    /// A widget displaying an `Avatar` for a `Room` or `User`.
+    pub struct AvatarWithSelection(ObjectSubclass<imp::AvatarWithSelection>)
+        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl AvatarWithSelection {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create AvatarWithSelection")
+    }
+
+    pub fn set_selected(&self, selected: bool) {
+        let priv_ = imp::AvatarWithSelection::from_instance(self);
+        let checkmark = priv_.checkmark.get_or_init(|| {
+            gtk::Image::builder()
+                .halign(gtk::Align::End)
+                .valign(gtk::Align::End)
+                .icon_name("emblem-default-symbolic")
+                .pixel_size(14)
+                .css_classes(vec!["blue-checkmark".into()])
+                .build()
+        });
+
+        if selected {
+            priv_.child_avatar.add_css_class("selected-avatar");
+            priv_.checkmark_overlay.add_overlay(checkmark);
+        } else {
+            priv_.child_avatar.remove_css_class("selected-avatar");
+            priv_.checkmark_overlay.remove_overlay(checkmark);
+        }
+    }
+
+    pub fn avatar(&self) -> &Avatar {
+        let priv_ = imp::AvatarWithSelection::from_instance(self);
+        &priv_.child_avatar
+    }
+}
+
+impl Default for AvatarWithSelection {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index d80ef5a3..3e1a2915 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -1,4 +1,5 @@
 mod avatar;
+mod avatar_with_selection;
 mod context_menu_bin;
 mod custom_entry;
 mod in_app_notification;
@@ -8,6 +9,7 @@ mod room_title;
 mod spinner_button;
 
 pub use self::avatar::Avatar;
+pub use self::avatar_with_selection::AvatarWithSelection;
 pub use self::context_menu_bin::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl};
 pub use self::custom_entry::CustomEntry;
 pub use self::in_app_notification::InAppNotification;
diff --git a/src/meson.build b/src/meson.build
index 193238ca..f9eb7fc2 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -21,6 +21,7 @@ run_command(
 sources = files(
   'application.rs',
   'components/avatar.rs',
+  'components/avatar_with_selection.rs',
   'components/context_menu_bin.rs',
   'components/custom_entry.rs',
   'components/label_with_widgets.rs',
diff --git a/src/session/mod.rs b/src/session/mod.rs
index 436fa2f6..b888ccf0 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -452,7 +452,9 @@ impl Session {
 
     pub fn set_logged_in_users(&self, sessions_stack_pages: &SelectionModel) {
         let priv_ = &imp::Session::from_instance(self);
-        priv_.sidebar.set_logged_in_users(sessions_stack_pages);
+        priv_
+            .sidebar
+            .set_logged_in_users(sessions_stack_pages, self);
     }
 }
 
diff --git a/src/session/sidebar/account_switcher/item.rs b/src/session/sidebar/account_switcher/item.rs
index 332fa3e8..987fa82e 100644
--- a/src/session/sidebar/account_switcher/item.rs
+++ b/src/session/sidebar/account_switcher/item.rs
@@ -1,5 +1,6 @@
 use super::add_account::AddAccountRow;
 use super::user_entry::UserEntryRow;
+use crate::session::Session;
 use gtk::{gio::ListStore, glib, prelude::*, subclass::prelude::*};
 use std::convert::TryFrom;
 
@@ -110,9 +111,9 @@ impl ExtraItemObj {
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub enum Item {
-    User(gtk::StackPage),
+    User(gtk::StackPage, bool),
     Separator,
     AddAccount,
 }
@@ -132,15 +133,29 @@ impl TryFrom<glib::Object> for Item {
     fn try_from(object: glib::Object) -> Result<Self, Self::Error> {
         object
             .downcast::<gtk::StackPage>()
-            .map(Self::User)
+            .map(|sp| Self::User(sp, false))
             .or_else(|object| object.downcast::<ExtraItemObj>().map(|it| it.get().into()))
     }
 }
 
 impl Item {
+    pub fn set_hint(self, session_root: Session) -> Self {
+        match self {
+            Self::User(session_page, _) => {
+                let hinted = session_root == session_page.child();
+                Self::User(session_page, hinted)
+            }
+            other => other,
+        }
+    }
+
     pub fn build_widget(&self) -> gtk::Widget {
         match self {
-            Self::User(ref session_page) => UserEntryRow::new(session_page).upcast(),
+            Self::User(ref session_page, hinted) => {
+                let user_entry = UserEntryRow::new(session_page);
+                user_entry.set_hint(hinted.clone());
+                user_entry.upcast()
+            }
             Self::Separator => gtk::Separator::new(gtk::Orientation::Vertical).upcast(),
             Self::AddAccount => AddAccountRow::new().upcast(),
         }
diff --git a/src/session/sidebar/account_switcher/mod.rs b/src/session/sidebar/account_switcher/mod.rs
index d35fa04e..e7ce4185 100644
--- a/src/session/sidebar/account_switcher/mod.rs
+++ b/src/session/sidebar/account_switcher/mod.rs
@@ -1,6 +1,6 @@
 use gtk::{
     gio::{self, ListModel, ListStore},
-    glib,
+    glib::{self, clone},
     prelude::*,
     subclass::prelude::*,
     CompositeTemplate, SelectionModel,
@@ -8,6 +8,7 @@ use gtk::{
 use std::convert::TryFrom;
 
 use super::account_switcher::item::{ExtraItemObj, Item as AccountSwitcherItem};
+use crate::session::Session;
 
 pub mod add_account;
 pub mod item;
@@ -50,7 +51,7 @@ mod imp {
                     .map(AccountSwitcherItem::try_from)
                     .and_then(Result::ok)
                     .map(|item| match item {
-                        AccountSwitcherItem::User(session_page) => {
+                        AccountSwitcherItem::User(session_page, _) => {
                             let session_widget = session_page.child();
                             session_widget
                                 .parent()
@@ -65,37 +66,6 @@ mod imp {
                         _ => {}
                     });
             });
-
-            // There is no permanent stuff to take care of,
-            // so only bind and unbind are connected.
-            let ref factory = gtk::SignalListItemFactory::new();
-            factory.connect_bind(|_, list_item| {
-                list_item.set_selectable(false);
-                let child = list_item
-                    .item()
-                    .map(AccountSwitcherItem::try_from)
-                    .and_then(Result::ok)
-                    .as_ref()
-                    .map(|item| {
-                        match item {
-                            AccountSwitcherItem::Separator => {
-                                list_item.set_activatable(false);
-                            }
-                            _ => {}
-                        }
-
-                        item
-                    })
-                    .map(AccountSwitcherItem::build_widget);
-
-                list_item.set_child(child.as_ref());
-            });
-
-            factory.connect_unbind(|_, list_item| {
-                list_item.set_child::<gtk::Widget>(None);
-            });
-
-            self.entries.set_factory(Some(factory));
         }
     }
 
@@ -109,9 +79,47 @@ glib::wrapper! {
 }
 
 impl AccountSwitcher {
-    pub fn set_logged_in_users(&self, sessions_stack_pages: &SelectionModel) {
+    pub fn set_logged_in_users(
+        &self,
+        sessions_stack_pages: &SelectionModel,
+        session_root: &Session,
+    ) {
         let entries = imp::AccountSwitcher::from_instance(self).entries.get();
 
+        // There is no permanent stuff to take care of,
+        // so only bind and unbind are connected.
+        let ref factory = gtk::SignalListItemFactory::new();
+        factory.connect_bind(clone!(@weak session_root => move |_, list_item| {
+            list_item.set_selectable(false);
+            let child = list_item
+                .item()
+                .map(AccountSwitcherItem::try_from)
+                .and_then(Result::ok)
+                .map(|item| {
+                    // Given that all the account switchers are built per-session widget
+                    // there is no need for callbacks or data bindings; just set the hint
+                    // when building the entries and they will show correctly marked in
+                    // each session widget.
+                    let item = item.set_hint(session_root);
+
+                    if item == AccountSwitcherItem::Separator {
+                        list_item.set_activatable(false);
+                    }
+
+                    item
+                })
+                .as_ref()
+                .map(AccountSwitcherItem::build_widget);
+
+            list_item.set_child(child.as_ref());
+        }));
+
+        factory.connect_unbind(|_, list_item| {
+            list_item.set_child::<gtk::Widget>(None);
+        });
+
+        entries.set_factory(Some(factory));
+
         let ref end_items = ExtraItemObj::list_store();
         let ref items_split = ListStore::new(ListModel::static_type());
         items_split.append(sessions_stack_pages);
diff --git a/src/session/sidebar/account_switcher/user_entry.rs 
b/src/session/sidebar/account_switcher/user_entry.rs
index 6edeaf14..c0a42ee3 100644
--- a/src/session/sidebar/account_switcher/user_entry.rs
+++ b/src/session/sidebar/account_switcher/user_entry.rs
@@ -1,4 +1,4 @@
-use crate::components::Avatar;
+use crate::components::AvatarWithSelection;
 use adw::subclass::prelude::BinImpl;
 use gtk::{self, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
 
@@ -12,7 +12,7 @@ mod imp {
     #[template(resource = "/org/gnome/FractalNext/user-entry-row.ui")]
     pub struct UserEntryRow {
         #[template_child]
-        pub avatar_component: TemplateChild<Avatar>,
+        pub avatar_component: TemplateChild<AvatarWithSelection>,
         #[template_child]
         pub display_name: TemplateChild<gtk::Label>,
         #[template_child]
@@ -27,7 +27,7 @@ mod imp {
         type ParentType = adw::Bin;
 
         fn class_init(klass: &mut Self::Class) {
-            Avatar::static_type();
+            AvatarWithSelection::static_type();
             Self::bind_template(klass);
         }
 
@@ -88,4 +88,13 @@ impl UserEntryRow {
     pub fn new(session_page: &gtk::StackPage) -> Self {
         glib::Object::new(&[("session-page", session_page)]).expect("Failed to create UserEntryRow")
     }
+
+    pub fn set_hint(&self, hinted: bool) {
+        let priv_ = imp::UserEntryRow::from_instance(self);
+
+        priv_.avatar_component.set_selected(hinted);
+        priv_
+            .display_name
+            .set_css_classes(if hinted { &["bold"] } else { &[] });
+    }
 }
diff --git a/src/session/sidebar/sidebar.rs b/src/session/sidebar/sidebar.rs
index 6d465273..a44c924b 100644
--- a/src/session/sidebar/sidebar.rs
+++ b/src/session/sidebar/sidebar.rs
@@ -6,7 +6,7 @@ use crate::session::{
     content::ContentType,
     room::Room,
     sidebar::{Category, Entry, ItemList, RoomRow, Row, Selection},
-    RoomList,
+    RoomList, Session,
 };
 
 mod imp {
@@ -252,10 +252,14 @@ impl Sidebar {
         self.notify("selected-room");
     }
 
-    pub fn set_logged_in_users(&self, sessions_stack_pages: &SelectionModel) {
+    pub fn set_logged_in_users(
+        &self,
+        sessions_stack_pages: &SelectionModel,
+        session_root: &Session,
+    ) {
         imp::Sidebar::from_instance(self)
             .account_switcher
-            .set_logged_in_users(sessions_stack_pages);
+            .set_logged_in_users(sessions_stack_pages, session_root);
     }
 }
 


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