fractal/components/rows/
combo_loading_row.rsuse adw::{prelude::*, subclass::prelude::*};
use gtk::{glib, glib::clone, pango, CompositeTemplate};
use crate::{components::LoadingBin, utils::BoundObject};
mod imp {
use std::{
cell::{Cell, RefCell},
marker::PhantomData,
};
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/components/rows/combo_loading_row.ui")]
#[properties(wrapper_type = super::ComboLoadingRow)]
pub struct ComboLoadingRow {
#[template_child]
pub loading_bin: TemplateChild<LoadingBin>,
#[template_child]
pub popover: TemplateChild<gtk::Popover>,
#[template_child]
pub list: TemplateChild<gtk::ListBox>,
#[property(get, set = Self::set_string_model, explicit_notify, nullable)]
pub string_model: BoundObject<gtk::StringList>,
#[property(get, default = gtk::INVALID_LIST_POSITION)]
pub selected: Cell<u32>,
#[property(get, set = Self::set_selected_string, explicit_notify, nullable)]
pub selected_string: RefCell<Option<String>>,
#[property(get = Self::is_loading, set = Self::set_is_loading)]
pub is_loading: PhantomData<bool>,
#[property(get, set = Self::set_read_only, explicit_notify)]
pub read_only: Cell<bool>,
selected_handlers: RefCell<Vec<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for ComboLoadingRow {
const NAME: &'static str = "ComboLoadingRow";
type Type = super::ComboLoadingRow;
type ParentType = adw::ActionRow;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
klass.set_accessible_role(gtk::AccessibleRole::ComboBox);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for ComboLoadingRow {}
impl WidgetImpl for ComboLoadingRow {}
impl ListBoxRowImpl for ComboLoadingRow {}
impl PreferencesRowImpl for ComboLoadingRow {}
impl ActionRowImpl for ComboLoadingRow {
fn activate(&self) {
if !self.is_loading() {
self.popover.popup();
}
}
}
impl ComboLoadingRow {
fn set_string_model(&self, model: Option<gtk::StringList>) {
if self.string_model.obj() == model {
return;
}
let obj = self.obj();
for handler in self.selected_handlers.take() {
obj.disconnect(handler);
}
self.string_model.disconnect_signals();
self.list.bind_model(
model.as_ref(),
clone!(
#[weak]
obj,
#[upgrade_or_else]
|| { gtk::ListBoxRow::new().upcast() },
move |item| {
let Some(item) = item.downcast_ref::<gtk::StringObject>() else {
return gtk::ListBoxRow::new().upcast();
};
let string = item.string();
let child = gtk::Box::new(gtk::Orientation::Horizontal, 6);
let label = gtk::Label::builder()
.xalign(0.0)
.ellipsize(pango::EllipsizeMode::End)
.max_width_chars(40)
.valign(gtk::Align::Center)
.label(string)
.build();
child.append(&label);
let icon = gtk::Image::builder()
.accessible_role(gtk::AccessibleRole::Presentation)
.icon_name("object-select-symbolic")
.build();
let selected_handler = obj.connect_selected_string_notify(clone!(
#[weak]
label,
#[weak]
icon,
move |obj| {
let is_selected =
obj.selected_string().is_some_and(|s| s == label.label());
let opacity = if is_selected { 1.0 } else { 0.0 };
icon.set_opacity(opacity);
}
));
obj.imp()
.selected_handlers
.borrow_mut()
.push(selected_handler);
let is_selected = obj.selected_string().is_some_and(|s| s == label.label());
let opacity = if is_selected { 1.0 } else { 0.0 };
icon.set_opacity(opacity);
child.append(&icon);
gtk::ListBoxRow::builder().child(&child).build().upcast()
}
),
);
if let Some(model) = model {
let items_changed_handler = model.connect_items_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_, _, _, _| {
imp.update_selected();
}
));
self.string_model.set(model, vec![items_changed_handler]);
}
self.update_selected();
obj.notify_string_model();
}
fn set_selected_string(&self, string: Option<String>) {
if *self.selected_string.borrow() == string {
return;
}
let obj = self.obj();
obj.set_subtitle(string.as_deref().unwrap_or_default());
self.selected_string.replace(string);
self.update_selected();
obj.notify_selected_string();
}
fn update_selected(&self) {
let mut selected = gtk::INVALID_LIST_POSITION;
if let Some((string_model, selected_string)) = self
.string_model
.obj()
.zip(self.selected_string.borrow().clone())
{
for (pos, item) in string_model.iter::<glib::Object>().enumerate() {
let Some(item) = item.ok().and_downcast::<gtk::StringObject>() else {
break;
};
if item.string() == selected_string {
selected = pos as u32;
break;
}
}
}
if self.selected.get() == selected {
return;
}
self.selected.set(selected);
self.obj().notify_selected();
}
fn is_loading(&self) -> bool {
self.loading_bin.is_loading()
}
fn set_is_loading(&self, loading: bool) {
if self.is_loading() == loading {
return;
}
self.loading_bin.set_is_loading(loading);
self.obj().notify_is_loading();
}
fn set_read_only(&self, read_only: bool) {
if self.read_only.get() == read_only {
return;
}
let obj = self.obj();
self.read_only.set(read_only);
obj.update_property(&[gtk::accessible::Property::ReadOnly(read_only)]);
obj.notify_read_only();
}
}
}
glib::wrapper! {
pub struct ComboLoadingRow(ObjectSubclass<imp::ComboLoadingRow>)
@extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow,
@implements gtk::Actionable, gtk::Accessible;
}
#[gtk::template_callbacks]
impl ComboLoadingRow {
pub fn new() -> Self {
glib::Object::new()
}
#[template_callback]
fn row_activated(&self, row: >k::ListBoxRow) {
let Some(string) = row
.child()
.and_downcast::<gtk::Box>()
.and_then(|b| b.first_child())
.and_downcast::<gtk::Label>()
.map(|l| l.label())
else {
return;
};
self.imp().popover.popdown();
self.set_selected_string(Some(string));
}
#[template_callback]
fn popover_visible(&self) {
let is_visible = self.imp().popover.is_visible();
if is_visible {
self.add_css_class("has-open-popup");
} else {
self.remove_css_class("has-open-popup");
}
}
}