From aa843458e33b129c293ac69ed763886ced660fdd Mon Sep 17 00:00:00 2001 From: Marcus Behrendt Date: Thu, 7 Nov 2024 16:13:10 +0100 Subject: [PATCH 01/11] feat(model/container-status): Implement Ord --- src/model/container.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/model/container.rs b/src/model/container.rs index 11431b77..d5514670 100644 --- a/src/model/container.rs +++ b/src/model/container.rs @@ -39,6 +39,35 @@ pub(crate) enum Status { Unknown, } +impl std::cmp::Ord for Status { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self { + Self::Running => { + if let Self::Running = other { + std::cmp::Ordering::Equal + } else { + std::cmp::Ordering::Greater + } + } + Self::Paused => match other { + Self::Running => std::cmp::Ordering::Less, + Self::Paused => std::cmp::Ordering::Equal, + _ => std::cmp::Ordering::Greater, + }, + _ => match other { + Self::Running | Self::Paused => std::cmp::Ordering::Less, + _ => std::cmp::Ordering::Equal, + }, + } + } +} + +impl std::cmp::PartialOrd for Status { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl FromStr for Status { type Err = Self; From 414dabfb553cf8e7a21492136504d237f14a6b84 Mon Sep 17 00:00:00 2001 From: Marcus Behrendt Date: Thu, 7 Nov 2024 16:13:29 +0100 Subject: [PATCH 02/11] feat(model/pod-status): Implement Ord --- src/model/pod.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/model/pod.rs b/src/model/pod.rs index 3d9d63c9..e27c2082 100644 --- a/src/model/pod.rs +++ b/src/model/pod.rs @@ -39,6 +39,35 @@ pub(crate) enum Status { Unknown, } +impl std::cmp::Ord for Status { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self { + Self::Running => { + if let Self::Running = other { + std::cmp::Ordering::Equal + } else { + std::cmp::Ordering::Greater + } + } + Self::Paused => match other { + Self::Running => std::cmp::Ordering::Less, + Self::Paused => std::cmp::Ordering::Equal, + _ => std::cmp::Ordering::Greater, + }, + _ => match other { + Self::Running | Self::Paused => std::cmp::Ordering::Less, + _ => std::cmp::Ordering::Equal, + }, + } + } +} + +impl std::cmp::PartialOrd for Status { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl FromStr for Status { type Err = Self; From ee8aff74b6fb24fa21005056f6c462a546c3cdea Mon Sep 17 00:00:00 2001 From: Marcus Behrendt Date: Thu, 7 Nov 2024 16:21:26 +0100 Subject: [PATCH 03/11] feat(model/pod-list): Add signal `containers-in-pod-changed` --- src/model/pod_list.rs | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/model/pod_list.rs b/src/model/pod_list.rs index d64e6eb8..d1045cb1 100644 --- a/src/model/pod_list.rs +++ b/src/model/pod_list.rs @@ -46,9 +46,14 @@ mod imp { fn signals() -> &'static [Signal] { static SIGNALS: OnceLock> = OnceLock::new(); SIGNALS.get_or_init(|| { - vec![Signal::builder("pod-added") - .param_types([model::Pod::static_type()]) - .build()] + vec![ + Signal::builder("pod-added") + .param_types([model::Pod::static_type()]) + .build(), + Signal::builder("containers-in-pod-changed") + .param_types([model::Pod::static_type()]) + .build(), + ] }) } @@ -94,6 +99,14 @@ mod imp { let obj = &*self.obj(); model::SelectableList::bootstrap(obj.upcast_ref()); obj.connect_items_changed(|self_, _, _, _| self_.notify("len")); + + obj.connect_pod_added(|list, pod| { + pod.connect_num_containers_notify(clone!( + #[weak] + list, + move |pod| list.emit_by_name::<()>("containers-in-pod-changed", &[pod]) + )); + }); } } @@ -314,7 +327,22 @@ impl PodList { &self, f: F, ) -> glib::SignalHandlerId { - self.connect_local("pod-added", true, move |values| { + self.connect_signal("pod-added", f) + } + + pub(crate) fn connect_containers_in_pod_changed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_signal("containers-in-pod-changed", f) + } + + fn connect_signal( + &self, + signal: &str, + f: F, + ) -> glib::SignalHandlerId { + self.connect_local(signal, true, move |values| { let obj = values[0].get::().unwrap(); let pod = values[1].get::().unwrap(); f(&obj, &pod); From 61ffc7ae272616abd1e2e5057f94e7070cdd8758 Mon Sep 17 00:00:00 2001 From: Marcus Behrendt Date: Thu, 7 Nov 2024 16:22:49 +0100 Subject: [PATCH 04/11] feat(model/volume-list): Add signal `containers-of-volume-changed` --- src/model/volume_list.rs | 42 +++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/model/volume_list.rs b/src/model/volume_list.rs index 67214219..ab0766a2 100644 --- a/src/model/volume_list.rs +++ b/src/model/volume_list.rs @@ -56,6 +56,9 @@ mod imp { Signal::builder("volume-removed") .param_types([model::Volume::static_type()]) .build(), + Signal::builder("containers-of-volume-changed") + .param_types([model::Volume::static_type()]) + .build(), ] }) } @@ -189,7 +192,7 @@ impl VolumeList { drop(list); self.items_changed(idx as u32, 1, 0); - self.volume_removed(&volume); + self.emit_by_name::<()>("volume-removed", &[&volume]); volume.emit_deleted(); } } @@ -269,30 +272,47 @@ impl VolumeList { fn volume_added(&self, volume: &model::Volume) { self.emit_by_name::<()>("volume-added", &[volume]); + volume.container_list().connect_notify_local( + Some("len"), + clone!( + #[weak(rename_to=obj)] + self, + #[weak] + volume, + move |_, _| { + obj.emit_by_name::<()>("containers-of-volume-changed", &[&volume]); + } + ), + ); } pub(crate) fn connect_volume_added( &self, f: F, ) -> glib::SignalHandlerId { - self.connect_local("volume-added", true, move |values| { - let obj = values[0].get::().unwrap(); - let volume = values[1].get::().unwrap(); - f(&obj, &volume); + self.connect_signal("volume-added", f) + } - None - }) + pub(crate) fn connect_volume_removed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_signal("volume-removed", f) } - fn volume_removed(&self, volume: &model::Volume) { - self.emit_by_name::<()>("volume-removed", &[volume]); + pub(crate) fn connect_containers_of_volume_changed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_signal("containers-of-volume-changed", f) } - pub(crate) fn connect_volume_removed( + fn connect_signal( &self, + signal: &str, f: F, ) -> glib::SignalHandlerId { - self.connect_local("volume-removed", true, move |values| { + self.connect_local(signal, true, move |values| { let obj = values[0].get::().unwrap(); let volume = values[1].get::().unwrap(); f(&obj, &volume); From 6494dc97c1a427dc5ee4b61daf24c7c43555133b Mon Sep 17 00:00:00 2001 From: Marcus Behrendt Date: Thu, 7 Nov 2024 16:24:54 +0100 Subject: [PATCH 05/11] feat(model/image-list): Add some containers signals --- src/model/image_list.rs | 43 +++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/model/image_list.rs b/src/model/image_list.rs index 9602f75c..5c1a6109 100644 --- a/src/model/image_list.rs +++ b/src/model/image_list.rs @@ -54,6 +54,12 @@ mod imp { Signal::builder("image-removed") .param_types([model::Image::static_type()]) .build(), + Signal::builder("containers-of-image-changed") + .param_types([model::Image::static_type()]) + .build(), + Signal::builder("tags-of-image-changed") + .param_types([model::Image::static_type()]) + .build(), ] }) } @@ -98,9 +104,6 @@ mod imp { model::SelectableList::bootstrap(obj.upcast_ref()); obj.connect_items_changed(|self_, _, _, _| self_.notify("len")); - - obj.connect_image_added(|list, _| list.notify_num_images()); - obj.connect_image_removed(|list, _| list.notify_num_images()); } } @@ -209,7 +212,8 @@ impl ImageList { drop(list); self.items_changed(idx as u32, 1, 0); - self.image_removed(&image); + self.emit_by_name::<()>("image-removed", &[&image]); + self.notify_num_images(); image.emit_deleted(); } } @@ -333,7 +337,11 @@ impl ImageList { clone!( #[weak(rename_to = obj)] self, - move |_, _| obj.notify_num_images() + move |image, _| { + obj.notify_num_images(); + obj.emit_by_name::<()>("containers-of-image-changed", &[&image]); + obj.emit_by_name::<()>("tags-of-image-changed", &[&image]); + } ), ); self.emit_by_name::<()>("image-added", &[image]); @@ -343,24 +351,29 @@ impl ImageList { &self, f: F, ) -> glib::SignalHandlerId { - self.connect_local("image-added", true, move |values| { - let obj = values[0].get::().unwrap(); - let image = values[1].get::().unwrap(); - f(&obj, &image); + self.connect_signal("image-added", f) + } - None - }) + pub(crate) fn connect_containers_of_image_changed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_signal("containers-of-image-changed", f) } - fn image_removed(&self, image: &model::Image) { - self.emit_by_name::<()>("image-removed", &[image]); + pub(crate) fn connect_tags_of_image_changed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_signal("tags-of-image-changed", f) } - pub(crate) fn connect_image_removed( + fn connect_signal( &self, + signal: &str, f: F, ) -> glib::SignalHandlerId { - self.connect_local("image-removed", true, move |values| { + self.connect_local(signal, true, move |values| { let obj = values[0].get::().unwrap(); let image = values[1].get::().unwrap(); f(&obj, &image); From 5bc7baf305754e6e1544450df972404c81d05256 Mon Sep 17 00:00:00 2001 From: Marcus Behrendt Date: Thu, 7 Nov 2024 16:50:33 +0100 Subject: [PATCH 06/11] feat(gschema): Add new keys --- data/com.github.marhkb.Pods.gschema.xml.in | 96 ++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/data/com.github.marhkb.Pods.gschema.xml.in b/data/com.github.marhkb.Pods.gschema.xml.in index 69453405..fe91dd75 100644 --- a/data/com.github.marhkb.Pods.gschema.xml.in +++ b/data/com.github.marhkb.Pods.gschema.xml.in @@ -106,4 +106,100 @@ + + + + + + + + 'grid' + Containers representation + + + + + + + + 'name-asc' + sort attribute for containers + + + + false + Whether to show running containers first + + + + + + + + + + + 'asc' + Sort direction for pods + + + + + + + + 'name' + Sort attribute for pods + + + + false + Whether to show running pods first + + + + + + + + + + + 'asc' + Sort direction for images + + + + + + + + + 'name' + Sort attribute for images + + + + + + + + + + + 'asc' + Sort direction for volumes + + + + + + + + + 'name' + Sort attribute for volumes + + + From 59d96bf8a7bf579d13d181174180bf29706e3762 Mon Sep 17 00:00:00 2001 From: Marcus Behrendt Date: Thu, 7 Nov 2024 16:51:46 +0100 Subject: [PATCH 07/11] feat(containers-panel): Offer sorting --- src/view/containers_panel.rs | 229 +++++++++++++++++++++++------------ src/view/containers_panel.ui | 75 ++++++------ 2 files changed, 193 insertions(+), 111 deletions(-) diff --git a/src/view/containers_panel.rs b/src/view/containers_panel.rs index 9ccbf6c0..34a66a06 100644 --- a/src/view/containers_panel.rs +++ b/src/view/containers_panel.rs @@ -1,6 +1,7 @@ use std::cell::Cell; use std::cell::OnceCell; use std::cell::RefCell; +use std::ops::Deref; use adw::prelude::*; use adw::subclass::prelude::*; @@ -14,6 +15,7 @@ use gtk::gio; use gtk::glib; use gtk::CompositeTemplate; +use crate::config; use crate::model; use crate::model::AbstractContainerListExt; use crate::model::SelectableListExt; @@ -33,8 +35,9 @@ const ACTION_START_OR_RESUME_SELECTION: &str = "containers-panel.start-or-resume const ACTION_STOP_SELECTION: &str = "containers-panel.stop-selection"; const ACTION_PAUSE_SELECTION: &str = "containers-panel.pause-selection"; const ACTION_DELETE_SELECTION: &str = "containers-panel.delete-selection"; -const ACTION_TOGGLE_SHOW_ONLY_RUNNING_CONTAINERS: &str = - "containers-panel.toggle-show-only-running-containers"; +const ACTION_CHANGE_SORT_ATTRIBUTE: &str = "containers-panel.change-sort-attribute"; +const ACTION_TOGGLE_SHOW_RUNNING_CONTAINERS_FIRST: &str = + "containers-panel.toggle-show-running-containers-first"; const ACTION_SHOW_ALL_CONTAINERS: &str = "containers-panel.show-all-containers"; const ACTIONS_SELECTION: &[&str] = &[ @@ -46,6 +49,24 @@ const ACTIONS_SELECTION: &[&str] = &[ ACTION_DELETE_SELECTION, ]; +#[derive(Debug)] +pub(crate) struct Settings(gio::Settings); +impl Default for Settings { + fn default() -> Self { + Self(gio::Settings::new(&format!( + "{}.view.panels.containers", + config::APP_ID + ))) + } +} +impl Deref for Settings { + type Target = gio::Settings; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[derive(Debug)] enum ContainersView { Grid(view::ContainersGridView), @@ -75,6 +96,16 @@ impl ContainersView { } } +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, glib::Enum)] +#[enum_type(name = "ContainersPanelSortAttribute")] +pub(crate) enum SortAttribute { + #[default] + #[enum_value(nick = "name-asc")] + NameAsc, + #[enum_value(nick = "name-desc")] + NameDesc, +} + mod imp { use super::*; @@ -82,7 +113,7 @@ mod imp { #[properties(wrapper_type = super::ContainersPanel)] #[template(resource = "/com/github/marhkb/Pods/ui/view/containers_panel.ui")] pub(crate) struct ContainersPanel { - pub(super) settings: utils::PodsSettings, + pub(super) settings: Settings, pub(super) containers_view: RefCell>, pub(super) filter: OnceCell, pub(super) sorter: OnceCell, @@ -92,8 +123,16 @@ mod imp { pub(super) container_list: glib::WeakRef, #[property(get, set)] pub(super) collapsed: Cell, + #[property(get, set, builder(SortAttribute::default()))] + pub(super) sort_attribute: RefCell, #[property(get, set)] - pub(super) show_only_running_containers: Cell, + pub(super) show_running_containers_first: Cell, + #[template_child] + pub(super) create_container_button: TemplateChild, + #[template_child] + pub(super) prune_button: TemplateChild, + #[template_child] + pub(super) view_options_split_button: TemplateChild, #[template_child] pub(super) main_stack: TemplateChild, #[template_child] @@ -101,9 +140,13 @@ mod imp { #[template_child] pub(super) header_stack: TemplateChild, #[template_child] + pub(super) create_container_button_top_bin: TemplateChild, + #[template_child] + pub(super) prune_button_top_bin: TemplateChild, + #[template_child] pub(super) window_title: TemplateChild, #[template_child] - pub(super) view_options_split_button_1: TemplateChild, + pub(super) view_options_split_button_top_bin: TemplateChild, #[template_child] pub(super) selected_containers_button: TemplateChild, #[template_child] @@ -117,7 +160,11 @@ mod imp { #[template_child] pub(super) overhang_action_bar: TemplateChild, #[template_child] - pub(super) view_options_split_button_2: TemplateChild, + pub(super) create_container_button_bottom_bin: TemplateChild, + #[template_child] + pub(super) prune_button_bottom_bin: TemplateChild, + #[template_child] + pub(super) view_options_split_button_bottom_bin: TemplateChild, } #[glib::object_subclass] @@ -184,9 +231,11 @@ mod imp { widget.delete_selection(); }); + klass.install_property_action(ACTION_CHANGE_SORT_ATTRIBUTE, "sort-attribute"); + klass.install_property_action( - ACTION_TOGGLE_SHOW_ONLY_RUNNING_CONTAINERS, - "show-only-running-containers", + ACTION_TOGGLE_SHOW_RUNNING_CONTAINERS_FIRST, + "show-running-containers-first", ); klass.install_action(ACTION_SHOW_ALL_CONTAINERS, None, |widget, _, _| { @@ -219,7 +268,7 @@ mod imp { self.set_containers_view(); self.settings.connect_changed( - Some("containers-view"), + Some("view"), clone!( #[weak] obj, @@ -230,11 +279,11 @@ mod imp { ); self.settings - .bind( - "show-only-running-containers", - obj, - "show-only-running-containers", - ) + .bind("sort-attribute", obj, "sort-attribute") + .build(); + + self.settings + .bind("show-running-first", obj, "show-running-containers-first") .build(); let container_list_expr = Self::Type::this_expression("container-list"); @@ -342,7 +391,14 @@ mod imp { ) .bind(&self.toolbar_view.get(), "reveal-bottom-bars", Some(obj)); - let search_filter = gtk::CustomFilter::new(clone!( + let filter = gtk::EveryFilter::new(); + filter.append( + gtk::BoolFilter::builder() + .expression(model::Container::this_expression("is-infra")) + .invert(true) + .build(), + ); + filter.append(gtk::CustomFilter::new(clone!( #[weak] obj, #[upgrade_or] @@ -363,48 +419,28 @@ mod imp { || container.image_id().contains(term) } } - )); + ))); - let state_filter = gtk::AnyFilter::new(); - state_filter.append(gtk::CustomFilter::new(clone!( + let sorter = gtk::CustomSorter::new(clone!( #[weak] obj, #[upgrade_or] - false, - move |_| !obj.show_only_running_containers() - ))); - state_filter.append(gtk::BoolFilter::new(Some( - model::Container::this_expression("status").chain_closure::(closure!( - |_: model::Container, status: model::ContainerStatus| status - == model::ContainerStatus::Running - )), - ))); - - let filter = gtk::EveryFilter::new(); - filter.append( - gtk::BoolFilter::builder() - .expression(model::Container::this_expression("is-infra")) - .invert(true) - .build(), - ); - filter.append(search_filter); - filter.append(state_filter); - - let sorter = gtk::CustomSorter::new(|item1, item2| { - item1 - .downcast_ref::() - .unwrap() - .name() - .to_lowercase() - .cmp( - &item2 - .downcast_ref::() - .unwrap() - .name() - .to_lowercase(), - ) + gtk::Ordering::Equal, + move |item1, item2| { + let container1 = item1.downcast_ref::().unwrap(); + let container2 = item2.downcast_ref::().unwrap(); + + if obj.show_running_containers_first() { + match container2.status().cmp(&container1.status()) { + std::cmp::Ordering::Equal => obj.imp().cmp(container1, container2), + other => other, + } + } else { + obj.imp().cmp(container1, container2) + } .into() - }); + } + )); self.filter.set(filter.upcast()).unwrap(); self.sorter.set(sorter.upcast()).unwrap(); @@ -421,30 +457,20 @@ mod imp { impl ContainersPanel { fn set_containers_view(&self) { let model = self.model.borrow(); - let view = if self.settings.string("containers-view") == "grid" { - self.view_options_split_button_1 - .set_icon_name("view-list-symbolic"); - self.view_options_split_button_2 + let view = if self.settings.string("view") == "grid" { + self.view_options_split_button .set_icon_name("view-list-symbolic"); - let tooltip = gettext("List View"); - self.view_options_split_button_1 - .set_tooltip_text(Some(&tooltip)); - self.view_options_split_button_2 - .set_tooltip_text(Some(&tooltip)); + self.view_options_split_button + .set_tooltip_text(Some(&gettext("List View"))); ContainersView::Grid(view::ContainersGridView::from(model.as_ref())) } else { - self.view_options_split_button_1 - .set_icon_name("view-grid-symbolic"); - self.view_options_split_button_2 + self.view_options_split_button .set_icon_name("view-grid-symbolic"); - let tooltip = gettext("Grid View"); - self.view_options_split_button_1 - .set_tooltip_text(Some(&tooltip)); - self.view_options_split_button_2 - .set_tooltip_text(Some(&tooltip)); + self.view_options_split_button + .set_tooltip_text(Some(&gettext("Grid View"))); ContainersView::List(view::ContainersListView::from(model.as_ref())) }; @@ -453,13 +479,62 @@ mod imp { self.containers_view.replace(Some(view)); } + fn cmp( + &self, + container1: &model::Container, + container2: &model::Container, + ) -> std::cmp::Ordering { + match self.obj().sort_attribute() { + SortAttribute::NameAsc => container1 + .name() + .to_lowercase() + .cmp(&container2.name().to_lowercase()), + SortAttribute::NameDesc => container2 + .name() + .to_lowercase() + .cmp(&container1.name().to_lowercase()), + } + } + #[template_callback] - fn on_notify_show_only_running_containers(&self) { - self.update_filter(if self.obj().show_only_running_containers() { - gtk::FilterChange::MoreStrict + fn on_notify_collapsed(&self) { + if self.obj().collapsed() { + self.create_container_button_top_bin + .set_child(gtk::Widget::NONE); + self.prune_button_top_bin.set_child(gtk::Widget::NONE); + self.view_options_split_button_top_bin + .set_child(gtk::Widget::NONE); + + self.create_container_button_bottom_bin + .set_child(Some(&self.create_container_button.get())); + self.prune_button_bottom_bin + .set_child(Some(&self.prune_button.get())); + self.view_options_split_button_bottom_bin + .set_child(Some(&self.view_options_split_button.get())); } else { - gtk::FilterChange::LessStrict - }); + self.create_container_button_bottom_bin + .set_child(gtk::Widget::NONE); + self.prune_button_bottom_bin.set_child(gtk::Widget::NONE); + self.view_options_split_button_bottom_bin + .set_child(gtk::Widget::NONE); + + self.create_container_button_top_bin + .set_child(Some(&self.create_container_button.get())); + self.prune_button_top_bin + .set_child(Some(&self.prune_button.get())); + self.view_options_split_button_top_bin + .set_child(Some(&self.view_options_split_button.get())); + } + } + + #[template_callback] + fn on_notify_sort_attribute(&self) { + self.update_sorter(); + } + + #[template_callback] + fn on_notify_show_running_containers_first(&self) { + self.update_sorter(); } #[template_callback] @@ -621,7 +696,7 @@ impl ContainersPanel { } pub(crate) fn show_all_containers(&self) { - self.set_show_only_running_containers(false); + self.set_show_running_containers_first(false); self.set_search_mode(false); } @@ -655,8 +730,8 @@ impl ContainersPanel { let settings = &self.imp().settings; settings .set_string( - "containers-view", - if settings.string("containers-view") == "grid" { + "view", + if settings.string("view") == "grid" { "list" } else { "grid" diff --git a/src/view/containers_panel.ui b/src/view/containers_panel.ui index 41f4a3ce..a2b44873 100644 --- a/src/view/containers_panel.ui +++ b/src/view/containers_panel.ui @@ -3,9 +3,22 @@
+ Sort - _Hide Non-Running - containers-panel.toggle-show-only-running-containers + A-Z + containers-panel.change-sort-attribute + name-asc + + + Z-A + containers-panel.change-sort-attribute + name-desc + +
+
+ + _Show Running First + containers-panel.toggle-show-running-containers-first
@@ -21,12 +34,31 @@ + + containers-panel.create-container + list-add-symbolic + Create Container + + + + containers-panel.prune-unused-containers + eraser5-symbolic + Prune Stopped Containers + + + + containers-panel.toggle-containers-view + view-options-menu + +