diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 4811cc1c..7099baa2 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -15,6 +15,7 @@ profiles.yml style.css ui/area-selector.ui + ui/item_row.ui ui/preferences-dialog.ui ui/shortcuts.ui ui/view-port.ui diff --git a/data/resources/ui/item_row.ui b/data/resources/ui/item_row.ui new file mode 100644 index 00000000..525afb7f --- /dev/null +++ b/data/resources/ui/item_row.ui @@ -0,0 +1,31 @@ + + + + diff --git a/src/item_row.rs b/src/item_row.rs new file mode 100644 index 00000000..c44afed6 --- /dev/null +++ b/src/item_row.rs @@ -0,0 +1,166 @@ +use gtk::{glib, prelude::*, subclass::prelude::*}; + +mod imp { + use std::cell::{Cell, RefCell}; + + use super::*; + + #[derive(Default, glib::Properties, gtk::CompositeTemplate)] + #[properties(wrapper_type = super::ItemRow)] + #[template(resource = "/io/github/seadve/Kooha/ui/item_row.ui")] + pub struct ItemRow { + #[property(get, set = Self::set_title, explicit_notify)] + pub(super) title: RefCell, + #[property(get, set = Self::set_warning_tooltip_text, explicit_notify)] + pub(super) warning_tooltip_text: RefCell, + #[property(get, set = Self::set_shows_warning_icon, explicit_notify)] + pub(super) shows_warning_icon: Cell, + #[property(get, set = Self::set_shows_selected_icon, explicit_notify)] + pub(super) shows_selected_icon: Cell, + #[property(get, set = Self::set_is_selected, explicit_notify)] + pub(super) is_selected: Cell, + + #[template_child] + pub(super) warning_icon: TemplateChild, + #[template_child] + pub(super) title_label: TemplateChild, + #[template_child] + pub(super) selected_icon: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for ItemRow { + const NAME: &'static str = "KoohaItemRow"; + type Type = super::ItemRow; + type ParentType = gtk::Widget; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for ItemRow { + fn constructed(&self) { + self.parent_constructed(); + + let obj = self.obj(); + + obj.update_title_label(); + obj.update_warning_icon_tooltip_text(); + obj.update_warning_icon_visibility(); + obj.update_selected_icon_visibility(); + obj.update_selected_icon_opacity(); + } + + fn dispose(&self) { + self.dispose_template(); + } + } + + impl WidgetImpl for ItemRow {} + + impl ItemRow { + fn set_title(&self, title: String) { + let obj = self.obj(); + + if title == obj.title() { + return; + } + + self.title.set(title); + obj.update_title_label(); + obj.notify_title(); + } + + fn set_warning_tooltip_text(&self, warning_tooltip_text: String) { + let obj = self.obj(); + + if warning_tooltip_text == obj.warning_tooltip_text() { + return; + } + + self.warning_tooltip_text.set(warning_tooltip_text); + obj.update_warning_icon_tooltip_text(); + obj.notify_warning_tooltip_text(); + } + + fn set_shows_warning_icon(&self, shows_warning_icon: bool) { + let obj = self.obj(); + + if shows_warning_icon == obj.shows_warning_icon() { + return; + } + + self.shows_warning_icon.set(shows_warning_icon); + obj.update_warning_icon_visibility(); + obj.notify_shows_warning_icon(); + } + + fn set_shows_selected_icon(&self, shows_selected_icon: bool) { + let obj = self.obj(); + + if shows_selected_icon == obj.shows_selected_icon() { + return; + } + + self.shows_selected_icon.set(shows_selected_icon); + obj.update_selected_icon_visibility(); + obj.notify_shows_selected_icon(); + } + + fn set_is_selected(&self, is_selected: bool) { + let obj = self.obj(); + + if is_selected == obj.is_selected() { + return; + } + + self.is_selected.set(is_selected); + obj.update_selected_icon_opacity(); + obj.notify_is_selected(); + } + } +} + +glib::wrapper! { + pub struct ItemRow(ObjectSubclass) + @extends gtk::Widget; +} + +impl ItemRow { + pub fn new() -> Self { + glib::Object::new() + } + + fn update_title_label(&self) { + let imp = self.imp(); + imp.title_label.set_label(&self.title()); + } + + fn update_warning_icon_tooltip_text(&self) { + let imp = self.imp(); + imp.warning_icon + .set_tooltip_text(Some(&self.warning_tooltip_text())); + } + + fn update_warning_icon_visibility(&self) { + let imp = self.imp(); + imp.warning_icon.set_visible(self.shows_warning_icon()); + } + + fn update_selected_icon_visibility(&self) { + let imp = self.imp(); + imp.selected_icon.set_visible(self.shows_selected_icon()); + } + + fn update_selected_icon_opacity(&self) { + let imp = self.imp(); + let opacity = if self.is_selected() { 1.0 } else { 0.0 }; + imp.selected_icon.set_opacity(opacity); + } +} diff --git a/src/main.rs b/src/main.rs index 3e5ca77e..64b91c17 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ mod config; mod format_time; mod help; mod i18n; +mod item_row; mod pipeline; mod preferences_dialog; mod profile; diff --git a/src/preferences_dialog.rs b/src/preferences_dialog.rs index 82528d3f..2a7380e1 100644 --- a/src/preferences_dialog.rs +++ b/src/preferences_dialog.rs @@ -5,14 +5,16 @@ use gettextrs::gettext; use gtk::{ gio, glib::{self, clone, closure, BoxedAnyObject}, - pango, }; -use crate::{profile::Profile, settings::Settings, IS_EXPERIMENTAL_MODE}; +use crate::{item_row::ItemRow, profile::Profile, settings::Settings, IS_EXPERIMENTAL_MODE}; /// Used to represent "none" profile in the profiles model type NoneProfile = BoxedAnyObject; +const PROFILE_ROW_SELECTED_ITEM_NOTIFY_HANDLER_ID_KEY: &str = + "kooha-profile-row-selected-item-notify-handler-id"; + mod imp { use std::cell::OnceCell; @@ -84,9 +86,7 @@ mod imp { let active_profile = settings.profile(); self.profile_row - .set_factory(Some(&profile_row_factory(&self.profile_row, false))); - self.profile_row - .set_list_factory(Some(&profile_row_factory(&self.profile_row, true))); + .set_factory(Some(&profile_row_factory(&self.profile_row))); let profiles = Profile::all() .inspect_err(|err| tracing::error!("Failed to load profiles: {:?}", err)) .unwrap_or_default(); @@ -214,79 +214,76 @@ impl PreferencesDialog { } } -fn profile_row_factory( - profile_row: &adw::ComboRow, - show_selected_indicator: bool, -) -> gtk::SignalListItemFactory { +fn profile_row_factory(profile_row: &adw::ComboRow) -> gtk::SignalListItemFactory { let factory = gtk::SignalListItemFactory::new(); + factory.connect_setup(clone!(@weak profile_row => move |_, list_item| { let list_item = list_item.downcast_ref::().unwrap(); - let item_expression = list_item.property_expression("item"); - - let hbox = gtk::Box::builder().spacing(12).build(); - - let warning_indicator = gtk::Image::builder() - .tooltip_text(gettext("This format is experimental and unsupported.")) - .icon_name("warning-symbolic") - .build(); - warning_indicator.add_css_class("warning"); - hbox.append(&warning_indicator); - - item_expression - .chain_closure::(closure!( - |_: Option, obj: Option| { - obj.as_ref() - .and_then(|obj| profile_from_obj(obj)) - .is_some_and(|profile| profile.is_experimental()) - } - )) - .bind(&warning_indicator, "visible", glib::Object::NONE); - - let label = gtk::Label::builder() - .valign(gtk::Align::Center) - .xalign(0.0) - .ellipsize(pango::EllipsizeMode::End) - .max_width_chars(20) - .build(); - hbox.append(&label); - - item_expression - .chain_closure::(closure!( - |_: Option, obj: Option| { - obj.as_ref() - .and_then(|o| profile_from_obj(o)) - .map_or(gettext("None"), |profile| profile.name().to_string()) - } - )) - .bind(&label, "label", glib::Object::NONE); - - if show_selected_indicator { - let selected_indicator = gtk::Image::from_icon_name("object-select-symbolic"); - hbox.append(&selected_indicator); - - gtk::ClosureExpression::new::( - &[ - profile_row.property_expression("selected-item"), - item_expression, - ], - closure!(|_: Option, - selected_item: Option, - item: Option| { - if item == selected_item { - 1.0 - } else { - 0.0 - } - }), - ) - .bind(&selected_indicator, "opacity", glib::Object::NONE); + + let item_row = ItemRow::new(); + item_row.set_warning_tooltip_text(gettext("This format is experimental and unsupported.")); + + list_item.set_child(Some(&item_row)); + })); + + factory.connect_bind(clone!(@weak profile_row => move |_, list_item| { + let list_item = list_item.downcast_ref::().unwrap(); + + let item_row = list_item.child().unwrap().downcast::().unwrap(); + + let item = list_item.item().unwrap(); + let profile = profile_from_obj(&item); + + item_row.set_shows_warning_icon(profile.is_some_and(|profile| profile.is_experimental())); + item_row.set_title( + profile.map_or_else(|| gettext("None"), |profile| profile.name().to_string()), + ); + + // Only show the selected icon when it is inside the given row's popover. This assumes that + // the parent of the given row is not a popover, so we can tell which is which. + if item_row.ancestor(gtk::Popover::static_type()).is_some() { + debug_assert!(profile_row.ancestor(gtk::Popover::static_type()).is_none()); + + item_row.set_shows_selected_icon(true); + + unsafe { + list_item.set_data( + PROFILE_ROW_SELECTED_ITEM_NOTIFY_HANDLER_ID_KEY, + profile_row.connect_selected_item_notify( + clone!(@weak list_item => move |profile_row| { + update_item_row_is_selected(profile_row, &list_item); + }), + ), + ); + } + + update_item_row_is_selected(&profile_row, list_item); + } else { + item_row.set_shows_selected_icon(false); } + })); + + factory.connect_unbind(clone!(@weak profile_row => move |_, list_item| { + let list_item = list_item.downcast_ref::().unwrap(); - list_item.set_child(Some(&hbox)); + unsafe { + if let Some(handler_id) = + list_item.steal_data(PROFILE_ROW_SELECTED_ITEM_NOTIFY_HANDLER_ID_KEY) + { + profile_row.disconnect(handler_id); + } + } })); + factory } +fn update_item_row_is_selected(row: &adw::ComboRow, list_item: >k::ListItem) { + let item_row = list_item.child().unwrap().downcast::().unwrap(); + + item_row.set_is_selected(row.selected_item() == list_item.item()); +} + /// Returns `Some` if the object is a `Profile`, otherwise `None`, if the object is a `NoneProfile`. fn profile_from_obj(obj: &glib::Object) -> Option<&Profile> { if let Some(profile) = obj.downcast_ref::() {