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::() {