Skip to content

Commit 7b4400b

Browse files
committed
New image picker view
1 parent fe9c7d2 commit 7b4400b

File tree

4 files changed

+198
-56
lines changed

4 files changed

+198
-56
lines changed

src/application.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ mod imp {
112112
padding: 12px;
113113
}}
114114
115+
.distro-row-item {{
116+
padding: 6px;
117+
border-radius: 6px;
118+
}}
119+
115120
.output {{
116121
border-radius: 12px;
117122
border: 1px solid @borders;

src/dialogs/create_distrobox_dialog.rs

Lines changed: 174 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::sidebar_row::SidebarRow;
1212
use std::path::PathBuf;
1313
use std::{cell::RefCell, rc::Rc};
1414

15-
use crate::distro_combo_row_item;
15+
use crate::image_row_item;
1616
use glib::clone;
1717
use gtk::glib::{derived_properties, Properties};
1818

@@ -29,10 +29,13 @@ mod imp {
2929
#[property(get, set)]
3030
pub root_store: RefCell<RootStore>,
3131
pub dialog: adw::Dialog,
32+
pub navigation_view: adw::NavigationView,
3233
pub toolbar_view: adw::ToolbarView,
3334
pub content: gtk::Box,
3435
pub name_row: adw::EntryRow,
35-
pub image_row: adw::ComboRow,
36+
pub image_row: adw::ActionRow,
37+
pub images_model: gtk::StringList,
38+
pub selected_image: RefCell<String>,
3639
pub home_row_expander: adw::ExpanderRow,
3740
#[property(get, set, nullable)]
3841
pub home_folder: RefCell<Option<String>>,
@@ -93,7 +96,8 @@ mod imp {
9396
self.obj().set_title("Create a Distrobox");
9497
self.obj().set_content_width(480);
9598

96-
let toolbar_view = adw::ToolbarView::new();
99+
let navigation_view = &self.navigation_view;
100+
let toolbar_view = &self.toolbar_view;
97101
let header = adw::HeaderBar::new();
98102

99103
// Create view switcher and stack
@@ -136,35 +140,20 @@ mod imp {
136140
preferences_group.set_title("Settings");
137141
self.name_row.set_title("Name");
138142

139-
self.image_row
140-
.set_expression(Some(&gtk::PropertyExpression::new(
141-
gtk::StringObject::static_type(),
142-
None::<gtk::Expression>,
143-
"string",
144-
)));
145-
let item_factory = gtk::SignalListItemFactory::new();
146-
item_factory.connect_setup(|_, item| {
147-
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
148-
item.set_child(Some(&distro_combo_row_item::DistroComboRowItem::new()));
149-
});
150-
item_factory.connect_bind(|_, item| {
151-
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
152-
let image = item
153-
.item()
154-
.and_downcast::<gtk::StringObject>()
155-
.unwrap()
156-
.string();
157-
let child = item.child();
158-
let child: &distro_combo_row_item::DistroComboRowItem =
159-
child.and_downcast_ref().unwrap();
160-
child.set_image(&image);
161-
});
162-
self.image_row.set_factory(Some(&item_factory));
163-
self.image_row.set_enable_search(true);
164-
self.image_row
165-
.set_search_match_mode(gtk::StringFilterMatchMode::Substring);
166143
self.image_row.set_title("Base Image");
167-
self.image_row.set_use_subtitle(true);
144+
self.image_row.set_subtitle("Select an image...");
145+
self.image_row.set_activatable(true);
146+
self.image_row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));
147+
148+
let obj = self.obj().clone();
149+
self.image_row.connect_activated(clone!(
150+
#[weak]
151+
obj,
152+
move |_| {
153+
let picker = obj.build_image_picker_view();
154+
obj.imp().navigation_view.push(&picker);
155+
}
156+
));
168157

169158
let obj = self.obj().clone();
170159
let home_row = self.obj().build_file_row(
@@ -365,7 +354,9 @@ mod imp {
365354
toolbar_view.set_vexpand(true);
366355
toolbar_view.set_content(Some(&scrolled_window));
367356

368-
self.obj().set_child(Some(&toolbar_view));
357+
let page = adw::NavigationPage::new(toolbar_view, "main");
358+
navigation_view.add(&page);
359+
self.obj().set_child(Some(navigation_view));
369360
}
370361
}
371362

@@ -397,11 +388,10 @@ impl CreateDistroboxDialog {
397388
#[weak]
398389
this,
399390
move |images| {
400-
let string_list = gtk::StringList::new(&[]);
401-
for image in images {
402-
string_list.append(&image);
403-
}
404-
this.imp().image_row.set_model(Some(&string_list));
391+
let string_list = &this.imp().images_model;
392+
string_list.splice(0, string_list.n_items(), &[]);
393+
let new_items: Vec<&str> = images.iter().map(|s| s.as_str()).collect();
394+
string_list.splice(0, 0, &new_items);
405395
}
406396
));
407397
this.root_store().images_query().refetch();
@@ -485,15 +475,155 @@ impl CreateDistroboxDialog {
485475
row
486476
}
487477

478+
pub fn build_image_picker_view(&self) -> adw::NavigationPage {
479+
let view = adw::ToolbarView::new();
480+
481+
let header = adw::HeaderBar::new();
482+
view.add_top_bar(&header);
483+
484+
let search_entry = gtk::SearchEntry::new();
485+
search_entry.set_placeholder_text(Some("Search image..."));
486+
search_entry.set_hexpand(true);
487+
488+
header.set_title_widget(Some(&search_entry));
489+
490+
let model = self.imp().images_model.clone();
491+
let expression = gtk::PropertyExpression::new(
492+
gtk::StringObject::static_type(),
493+
None::<gtk::Expression>,
494+
"string",
495+
);
496+
let filter = gtk::StringFilter::builder()
497+
.expression(&expression)
498+
.match_mode(gtk::StringFilterMatchMode::Substring)
499+
.ignore_case(true)
500+
.build();
501+
502+
search_entry.bind_property("text", &filter, "search").sync_create().build();
503+
504+
let filter_model = gtk::FilterListModel::new(Some(model), Some(filter));
505+
let selection_model = gtk::SingleSelection::new(Some(filter_model.clone()));
506+
507+
let factory = gtk::SignalListItemFactory::new();
508+
factory.connect_setup(|_, item| {
509+
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
510+
let row = image_row_item::ImageRowItem::new();
511+
item.set_child(Some(&row));
512+
});
513+
factory.connect_bind(|_, item| {
514+
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
515+
let image = item
516+
.item()
517+
.and_downcast::<gtk::StringObject>()
518+
.unwrap()
519+
.string();
520+
let child = item.child();
521+
let child: &image_row_item::ImageRowItem =
522+
child.and_downcast_ref().unwrap();
523+
child.set_image(&image);
524+
});
525+
526+
let list_view = gtk::ListView::new(Some(selection_model), Some(factory));
527+
list_view.add_css_class("navigation-sidebar");
528+
list_view.set_single_click_activate(true);
529+
530+
let scrolled_window = gtk::ScrolledWindow::new();
531+
scrolled_window.set_child(Some(&list_view));
532+
scrolled_window.set_vexpand(true);
533+
534+
let stack = gtk::Stack::new();
535+
stack.add_named(&scrolled_window, Some("list"));
536+
537+
let empty_page = adw::StatusPage::new();
538+
empty_page.set_title("No images found");
539+
empty_page.set_description(Some("You can use a custom image"));
540+
empty_page.set_icon_name(Some("system-search-symbolic"));
541+
542+
let custom_image_btn = gtk::Button::with_label("Use custom image");
543+
custom_image_btn.add_css_class("pill");
544+
custom_image_btn.add_css_class("suggested-action");
545+
custom_image_btn.set_halign(gtk::Align::Center);
546+
547+
empty_page.set_child(Some(&custom_image_btn));
548+
stack.add_named(&empty_page, Some("empty"));
549+
550+
view.set_content(Some(&stack));
551+
552+
// Handle empty state
553+
let stack_clone = stack.clone();
554+
filter_model.connect_items_changed(move |model, _, _, _| {
555+
if model.n_items() > 0 {
556+
stack_clone.set_visible_child_name("list");
557+
} else {
558+
stack_clone.set_visible_child_name("empty");
559+
}
560+
});
561+
562+
// Initial state check
563+
if filter_model.n_items() == 0 {
564+
stack.set_visible_child_name("empty");
565+
}
566+
567+
// Update button label
568+
search_entry.connect_search_changed(clone!(
569+
#[weak]
570+
custom_image_btn,
571+
move |entry| {
572+
let text = entry.text();
573+
if text.is_empty() {
574+
custom_image_btn.set_label("Use custom image");
575+
custom_image_btn.set_sensitive(false);
576+
} else {
577+
custom_image_btn.set_label(&format!("Use '{}'", text));
578+
custom_image_btn.set_sensitive(true);
579+
}
580+
}
581+
));
582+
// Initial button state
583+
if search_entry.text().is_empty() {
584+
custom_image_btn.set_sensitive(false);
585+
}
586+
587+
// Handle custom image selection
588+
custom_image_btn.connect_clicked(clone!(
589+
#[weak(rename_to=this)]
590+
self,
591+
#[weak]
592+
search_entry,
593+
move |_| {
594+
let image = search_entry.text();
595+
if !image.is_empty() {
596+
this.imp().selected_image.replace(image.to_string());
597+
this.imp().image_row.set_subtitle(&image);
598+
this.imp().navigation_view.pop();
599+
}
600+
}
601+
));
602+
603+
// Handle selection
604+
list_view.connect_activate(clone!(
605+
#[weak(rename_to=this)]
606+
self,
607+
move |list_view, position| {
608+
let model = list_view.model().unwrap(); // SingleSelection
609+
let item = model.item(position).unwrap().downcast::<gtk::StringObject>().unwrap();
610+
let image = item.string();
611+
612+
this.imp().selected_image.replace(image.to_string());
613+
this.imp().image_row.set_subtitle(&image);
614+
this.imp().navigation_view.pop();
615+
}
616+
));
617+
618+
adw::NavigationPage::new(&view, "image-picker")
619+
}
620+
488621
pub async fn extract_create_args(&self) -> Result<CreateArgs, Error> {
489622
let imp = self.imp();
490-
let image = imp
491-
.image_row
492-
.selected_item()
493-
.unwrap()
494-
.downcast_ref::<gtk::StringObject>()
495-
.unwrap()
496-
.string();
623+
let image = imp.selected_image.borrow().clone();
624+
if image.is_empty() {
625+
return Err(Error::InvalidField("image".into(), "No image selected".into()));
626+
}
497627
let volumes = imp
498628
.volume_rows
499629
.borrow()
Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ use std::cell::RefCell;
77
use crate::{distro_icon, known_distros::known_distro_by_image};
88

99
mod imp {
10+
use gtk::pango;
11+
1012
use crate::known_distros::KnownDistro;
1113

1214
use super::*;
1315

1416
#[derive(Properties, Default)]
15-
#[properties(wrapper_type = super::DistroComboRowItem)]
16-
pub struct DistroComboRowItem {
17+
#[properties(wrapper_type = super::ImageRowItem)]
18+
pub struct ImageRowItem {
1719
#[property(get, construct_only)]
1820
pub image: RefCell<Option<String>>,
1921
pub distro: RefCell<Option<KnownDistro>>,
@@ -22,36 +24,40 @@ mod imp {
2224
}
2325

2426
#[glib::derived_properties]
25-
impl ObjectImpl for DistroComboRowItem {
27+
impl ObjectImpl for ImageRowItem {
2628
fn constructed(&self) {
2729
distro_icon::setup(&self.icon);
2830

2931
self.label.set_xalign(0.0);
32+
self.label.set_ellipsize(pango::EllipsizeMode::Middle);
33+
self.label.set_has_tooltip(true);
34+
3035

3136
let obj = self.obj();
37+
obj.add_css_class("distro-row-item");
3238
obj.set_spacing(6);
3339
obj.append(&self.icon);
3440
obj.append(&self.label);
3541
}
3642
}
3743

3844
#[glib::object_subclass]
39-
impl ObjectSubclass for DistroComboRowItem {
40-
const NAME: &'static str = "DistroComboRowItem";
41-
type Type = super::DistroComboRowItem;
45+
impl ObjectSubclass for ImageRowItem {
46+
const NAME: &'static str = "ImageRowItem";
47+
type Type = super::ImageRowItem;
4248
type ParentType = gtk::Box;
4349
}
4450

45-
impl WidgetImpl for DistroComboRowItem {}
46-
impl BoxImpl for DistroComboRowItem {}
51+
impl WidgetImpl for ImageRowItem {}
52+
impl BoxImpl for ImageRowItem {}
4753
}
4854

4955
glib::wrapper! {
50-
pub struct DistroComboRowItem(ObjectSubclass<imp::DistroComboRowItem>)
56+
pub struct ImageRowItem(ObjectSubclass<imp::ImageRowItem>)
5157
@extends gtk::Box, gtk::Widget,
5258
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Actionable;
5359
}
54-
impl DistroComboRowItem {
60+
impl ImageRowItem {
5561
pub fn new() -> Self {
5662
let this: Self = glib::Object::builder().build();
5763
this
@@ -65,10 +71,11 @@ impl DistroComboRowItem {
6571
imp.distro.replace(distro);
6672

6773
imp.label.set_label(image);
74+
imp.label.set_tooltip_text(Some(image));
6875
}
6976
}
7077

71-
impl Default for DistroComboRowItem {
78+
impl Default for ImageRowItem {
7279
fn default() -> Self {
7380
Self::new()
7481
}

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ mod application;
2222
mod config;
2323
mod container;
2424
mod dialogs;
25-
mod distro_combo_row_item;
25+
mod image_row_item;
2626
mod distro_icon;
2727
mod distrobox;
2828
mod distrobox_task;

0 commit comments

Comments
 (0)