@@ -12,7 +12,7 @@ use crate::sidebar_row::SidebarRow;
1212use std:: path:: PathBuf ;
1313use std:: { cell:: RefCell , rc:: Rc } ;
1414
15- use crate :: distro_combo_row_item ;
15+ use crate :: image_row_item ;
1616use glib:: clone;
1717use 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 ( )
0 commit comments