import kivy from kivy.uix.screenmanager import Screen from kivy.uix.boxlayout import BoxLayout from kivy.uix.label import Label from kivy.uix.button import Button from kivy.uix.textinput import TextInput from kivy.uix.filechooser import FileChooserIconView from kivy.uix.widget import Widget from kivy.uix.image import Image from kivy.uix.carousel import Carousel from kivy.uix.progressbar import ProgressBar from kivy.graphics import Color, Rectangle, Line from kivy.uix.scrollview import ScrollView from kivy.uix.popup import Popup from kivy.uix.gridlayout import GridLayout from kivy.properties import StringProperty from kivy.clock import Clock import os import json import shutil import threading from geopy.geocoders import Nominatim from config import RESOURCES_FOLDER class PauseEditScreen(Screen): project_name = StringProperty("") def __init__(self, **kwargs): super().__init__(**kwargs) self.pauses = [] self.on_save_callback = None self.loading_popup = None self.carousel = None def on_pre_enter(self): """Called when entering the screen""" self.show_loading_popup() # Delay the layout building to show loading popup first Clock.schedule_once(self.start_loading_process, 0.1) def show_loading_popup(self): """Show loading popup while building the layout""" layout = BoxLayout(orientation='vertical', spacing=20, padding=20) # Loading animation/progress bar progress = ProgressBar( max=100, size_hint_y=None, height=20 ) # Animate the progress bar def animate_progress(dt): if progress.value < 95: progress.value += 5 else: progress.value = 10 # Reset for continuous animation Clock.schedule_interval(animate_progress, 0.1) loading_label = Label( text="Loading pause information...\nPlease wait", color=(1, 1, 1, 1), font_size=16, halign="center", text_size=(300, None) ) layout.add_widget(loading_label) layout.add_widget(progress) self.loading_popup = Popup( title="Loading Pauses", content=layout, size_hint=(0.8, 0.3), auto_dismiss=False ) self.loading_popup.open() def start_loading_process(self, dt): """Start the loading process in background""" # Run the heavy loading in a separate thread thread = threading.Thread(target=self.load_data_background) thread.daemon = True thread.start() def load_data_background(self): """Load pause data in background thread""" try: # Load pauses self.load_pauses() # Pre-process location suggestions to speed up UI for pause in self.pauses: lat = pause["location"]["latitude"] lon = pause["location"]["longitude"] # Cache the location suggestion if 'location_suggestion' not in pause: pause['location_suggestion'] = self.suggest_location_name(lat, lon) # Schedule UI update on main thread Clock.schedule_once(self.finish_loading, 0) except Exception as e: print(f"Error loading pause data: {e}") Clock.schedule_once(self.finish_loading, 0) def finish_loading(self, dt): """Finish loading and build the UI""" try: self.build_pause_layout() finally: # Close loading popup if self.loading_popup: self.loading_popup.dismiss() self.loading_popup = None def suggest_location_name(self, lat, lon): """Simplified location suggestion""" try: geolocator = Nominatim(user_agent="traccar_animation") location = geolocator.reverse((lat, lon), exactly_one=True, timeout=8, addressdetails=True) if location and location.raw: address = location.raw.get('address', {}) # Look for street address first if 'road' in address: road = address['road'] if 'house_number' in address: return f"{road} {address['house_number']}" return road # Look for area name for key in ['neighbourhood', 'suburb', 'village', 'town', 'city']: if key in address and address[key]: return address[key] return f"Location {lat:.4f}, {lon:.4f}" except Exception: return f"Location {lat:.4f}, {lon:.4f}" def load_pauses(self): """Load pauses from the project folder""" project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name) pauses_path = os.path.join(project_folder, "pauses.json") if os.path.exists(pauses_path): with open(pauses_path, "r") as f: self.pauses = json.load(f) else: self.pauses = [] def save_pauses(self): """Save pauses to the project folder""" project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name) pauses_path = os.path.join(project_folder, "pauses.json") with open(pauses_path, "w") as f: json.dump(self.pauses, f, indent=2) def build_pause_layout(self): """Build the main pause editing layout with carousel for multiple pauses""" self.clear_widgets() # Main layout with dark background main_layout = BoxLayout(orientation='vertical', spacing=5, padding=[5, 5, 5, 5]) with main_layout.canvas.before: Color(0.11, 0.10, 0.15, 1) main_layout.bg_rect = Rectangle(pos=main_layout.pos, size=main_layout.size) main_layout.bind(pos=self.update_bg_rect, size=self.update_bg_rect) # Header with back button and pause counter header = BoxLayout(orientation='horizontal', size_hint_y=None, height=40, spacing=10) back_btn = Button( text="← Back", size_hint_x=None, width=70, font_size=14, background_color=(0.341, 0.235, 0.980, 1), color=(1, 1, 1, 1) ) back_btn.bind(on_press=self.go_back) # Dynamic title based on number of pauses pause_count = len(self.pauses) if pause_count > 2: title_text = f"Edit Pauses ({pause_count} total)\nSwipe to navigate" else: title_text = "Edit Pauses" title_label = Label( text=title_text, font_size=14 if pause_count > 2 else 16, color=(1, 1, 1, 1), halign="center", bold=True ) header.add_widget(back_btn) header.add_widget(title_label) header.add_widget(Widget(size_hint_x=None, width=70)) main_layout.add_widget(header) # Choose layout based on number of pauses if pause_count > 2: # Use carousel for multiple pauses content_area = self.create_carousel_layout() else: # Use simple scroll view for 1-2 pauses content_area = self.create_simple_scroll_layout() main_layout.add_widget(content_area) # Save all button at bottom save_all_btn = Button( text="Save All Changes & Go Back", size_hint_y=None, height=45, font_size=14, background_color=(0.2, 0.7, 0.2, 1), color=(1, 1, 1, 1), bold=True ) save_all_btn.bind(on_press=self.save_all_and_close) main_layout.add_widget(save_all_btn) self.add_widget(main_layout) def create_carousel_layout(self): """Create carousel layout for multiple pauses""" # Create carousel self.carousel = Carousel( direction='right', loop=True, size_hint=(1, 1) ) # Add each pause as a slide for idx, pause in enumerate(self.pauses): # Create a slide container slide = BoxLayout(orientation='vertical', spacing=5, padding=[5, 5, 5, 5]) # Add pause indicator indicator = Label( text=f"Pause {idx + 1} of {len(self.pauses)} - Swipe for more", font_size=12, color=(0.8, 0.8, 0.8, 1), size_hint_y=None, height=25, halign="center" ) slide.add_widget(indicator) # Create pause frame pause_frame = self.create_pause_frame(idx, pause) # Wrap in scroll view for this slide scroll = ScrollView(size_hint=(1, 1)) scroll_content = BoxLayout(orientation='vertical', size_hint_y=None, padding=[2, 2, 2, 2]) scroll_content.bind(minimum_height=scroll_content.setter('height')) scroll_content.add_widget(pause_frame) scroll.add_widget(scroll_content) slide.add_widget(scroll) self.carousel.add_widget(slide) return self.carousel def create_simple_scroll_layout(self): """Create simple scroll layout for 1-2 pauses""" scroll_content = BoxLayout(orientation='vertical', spacing=10, size_hint_y=None, padding=[5, 5, 5, 5]) scroll_content.bind(minimum_height=scroll_content.setter('height')) for idx, pause in enumerate(self.pauses): pause_frame = self.create_pause_frame(idx, pause) scroll_content.add_widget(pause_frame) scroll = ScrollView(size_hint=(1, 1)) scroll.add_widget(scroll_content) return scroll def update_bg_rect(self, instance, value): """Update background rectangle""" instance.bg_rect.pos = instance.pos instance.bg_rect.size = instance.size def create_pause_frame(self, idx, pause): """Create a frame for a single pause""" # Main frame with border frame = BoxLayout( orientation='vertical', spacing=8, padding=[8, 8, 8, 8], size_hint_y=None, height=380 # Increased height for better photo scrolling ) with frame.canvas.before: Color(0.18, 0.18, 0.22, 1) # Frame background frame.bg_rect = Rectangle(pos=frame.pos, size=frame.size) Color(0.4, 0.6, 1.0, 1) # Frame border frame.border_line = Line(rectangle=(frame.x, frame.y, frame.width, frame.height), width=2) def update_frame(instance, value, frame_widget=frame): frame_widget.bg_rect.pos = frame_widget.pos frame_widget.bg_rect.size = frame_widget.size frame_widget.border_line.rectangle = (frame_widget.x, frame_widget.y, frame_widget.width, frame_widget.height) frame.bind(pos=update_frame, size=update_frame) # 1. Pause number label (centered) pause_number_label = Label( text=f"[b]PAUSE {idx + 1}[/b]", markup=True, font_size=16, color=(1, 1, 1, 1), size_hint_y=None, height=30, halign="center", valign="middle" ) pause_number_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None))) frame.add_widget(pause_number_label) # 2. Location suggestion (left aligned) - use cached version if available suggested_place = pause.get('location_suggestion') or self.suggest_location_name( pause["location"]["latitude"], pause["location"]["longitude"] ) location_label = Label( text=f"Location: {suggested_place}", font_size=12, color=(0.8, 0.9, 1, 1), size_hint_y=None, height=25, halign="left", valign="middle" ) location_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None))) frame.add_widget(location_label) # 3. Custom name entry and save button name_layout = BoxLayout(orientation='horizontal', spacing=5, size_hint_y=None, height=35) name_input = TextInput( text=pause.get('name', ''), hint_text="Enter custom location name...", multiline=False, background_color=(0.25, 0.25, 0.3, 1), foreground_color=(1, 1, 1, 1), font_size=12, padding=[8, 8, 8, 8] ) save_name_btn = Button( text="Save Name", size_hint_x=None, width=80, font_size=11, background_color=(0.341, 0.235, 0.980, 1), color=(1, 1, 1, 1) ) save_name_btn.bind(on_press=lambda x: self.save_pause_name(pause, name_input)) name_layout.add_widget(name_input) name_layout.add_widget(save_name_btn) frame.add_widget(name_layout) # 4. Photos area - vertical scrolling photos_area = self.create_photos_area_vertical(idx, pause) frame.add_widget(photos_area) # 5. Save and Delete buttons row button_layout = BoxLayout(orientation='horizontal', spacing=5, size_hint_y=None, height=30) save_pause_btn = Button( text="Save Pause Info", font_size=12, background_color=(0.2, 0.6, 0.8, 1), color=(1, 1, 1, 1) ) save_pause_btn.bind(on_press=lambda x: self.save_individual_pause(idx)) delete_pause_btn = Button( text="Delete Pause", font_size=12, background_color=(0.8, 0.2, 0.2, 1), color=(1, 1, 1, 1) ) delete_pause_btn.bind(on_press=lambda x: self.delete_pause(idx)) button_layout.add_widget(save_pause_btn) button_layout.add_widget(delete_pause_btn) frame.add_widget(button_layout) return frame def create_photos_area_vertical(self, pause_idx, pause): """Create photos area with vertical scrolling""" photos_layout = BoxLayout(orientation='vertical', spacing=5, size_hint_y=None, height=200) # Photos header with add button photos_header = BoxLayout(orientation='horizontal', size_hint_y=None, height=30) photos_title = Label( text="Photos:", font_size=12, color=(1, 1, 1, 1), size_hint_x=0.5, halign="left", valign="middle" ) photos_title.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None))) add_photos_btn = Button( text="Add Photos", size_hint_x=0.5, font_size=11, background_color=(0.2, 0.7, 0.2, 1), color=(1, 1, 1, 1) ) add_photos_btn.bind(on_press=lambda x: self.add_photos(pause_idx)) photos_header.add_widget(photos_title) photos_header.add_widget(add_photos_btn) photos_layout.add_widget(photos_header) # Get photos for this pause project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name) pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}") os.makedirs(pause_img_folder, exist_ok=True) img_list = [f for f in os.listdir(pause_img_folder) if os.path.isfile(os.path.join(pause_img_folder, f))] if img_list: # Create vertical scrolling photo gallery photos_scroll = ScrollView(size_hint=(1, 1), do_scroll_y=True, do_scroll_x=False) photos_content = BoxLayout(orientation='vertical', spacing=2, size_hint_y=None, padding=[2, 2, 2, 2]) photos_content.bind(minimum_height=photos_content.setter('height')) for img_file in img_list: photo_item = self.create_vertical_photo_item(pause_idx, img_file, photos_content) photos_content.add_widget(photo_item) photos_scroll.add_widget(photos_content) photos_layout.add_widget(photos_scroll) else: no_photos_label = Label( text="No photos added yet", font_size=12, color=(0.6, 0.6, 0.6, 1), size_hint_y=1, halign="center" ) photos_layout.add_widget(no_photos_label) return photos_layout def create_vertical_photo_item(self, pause_idx, img_file, parent_layout): """Create a photo item for vertical scrolling""" # Main container with border photo_item = BoxLayout(orientation='horizontal', spacing=5, size_hint_y=None, height=60, padding=[2, 2, 2, 2]) # Add border and background to photo item with photo_item.canvas.before: Color(0.25, 0.25, 0.30, 1) # Background photo_item.bg_rect = Rectangle(pos=photo_item.pos, size=photo_item.size) Color(0.4, 0.4, 0.5, 1) # Border photo_item.border_line = Line(rectangle=(photo_item.x, photo_item.y, photo_item.width, photo_item.height), width=1) def update_photo_item(instance, value, item=photo_item): item.bg_rect.pos = item.pos item.bg_rect.size = item.size item.border_line.rectangle = (item.x, item.y, item.width, item.height) photo_item.bind(pos=update_photo_item, size=update_photo_item) # Get full path to the image project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name) pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}") img_path = os.path.join(pause_img_folder, img_file) # Image thumbnail container image_container = BoxLayout(size_hint_x=None, width=55, padding=[2, 2, 2, 2]) try: photo_image = Image( source=img_path, size_hint=(1, 1), allow_stretch=True, keep_ratio=True ) except Exception: # Fallback to a placeholder if image can't be loaded photo_image = Widget(size_hint=(1, 1)) with photo_image.canvas: Color(0.3, 0.3, 0.3, 1) Rectangle(pos=photo_image.pos, size=photo_image.size) image_container.add_widget(photo_image) # Photo info layout (filename and details) info_layout = BoxLayout(orientation='vertical', spacing=2, size_hint_x=0.55, padding=[5, 2, 5, 2]) # Filename label (truncate if too long) display_name = img_file if len(img_file) <= 20 else f"{img_file[:17]}..." filename_label = Label( text=display_name, font_size=9, color=(1, 1, 1, 1), halign="left", valign="top", size_hint_y=0.6, bold=True ) filename_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None))) # File size and type info try: file_size = os.path.getsize(img_path) if file_size < 1024: size_text = f"{file_size} B" elif file_size < 1024*1024: size_text = f"{file_size/1024:.1f} KB" else: size_text = f"{file_size/(1024*1024):.1f} MB" # Get file extension file_ext = os.path.splitext(img_file)[1].upper().replace('.', '') info_text = f"{file_ext} • {size_text}" except: info_text = "Unknown format" size_label = Label( text=info_text, font_size=7, color=(0.8, 0.8, 0.8, 1), halign="left", valign="bottom", size_hint_y=0.4 ) size_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None))) info_layout.add_widget(filename_label) info_layout.add_widget(size_label) # Button layout for actions button_layout = BoxLayout(orientation='vertical', spacing=2, size_hint_x=0.28, padding=[2, 2, 2, 2]) # View button to show full image view_btn = Button( text="👁 View", font_size=8, background_color=(0.2, 0.6, 0.8, 1), color=(1, 1, 1, 1), size_hint_y=0.5 ) view_btn.bind(on_press=lambda x: self.view_full_image(img_path, img_file)) # Delete button delete_btn = Button( text="🗑 Del", font_size=8, background_color=(0.8, 0.2, 0.2, 1), color=(1, 1, 1, 1), size_hint_y=0.5 ) delete_btn.bind(on_press=lambda x: self.delete_single_photo(pause_idx, img_file, photo_item, parent_layout)) button_layout.add_widget(view_btn) button_layout.add_widget(delete_btn) # Add all components to photo item photo_item.add_widget(image_container) photo_item.add_widget(info_layout) photo_item.add_widget(button_layout) return photo_item def delete_single_photo(self, pause_idx, img_file, photo_item, parent_layout): """Delete a single photo with confirmation""" def confirm_delete(): project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name) pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}") file_path = os.path.join(pause_img_folder, img_file) if os.path.exists(file_path): os.remove(file_path) parent_layout.remove_widget(photo_item) parent_layout.height = max(20, len(parent_layout.children) * 62) self.show_confirmation( f"Delete Photo", f"Are you sure you want to delete '{img_file}'?", confirm_delete ) def delete_pause(self, pause_idx): """Delete an entire pause with confirmation""" def confirm_delete_pause(): try: # Remove pause from the list if 0 <= pause_idx < len(self.pauses): self.pauses.pop(pause_idx) # Save the updated pauses list self.save_pauses() # Remove pause folder and its contents project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name) pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}") if os.path.exists(pause_img_folder): shutil.rmtree(pause_img_folder) # Reorganize remaining pause folders self.reorganize_pause_folders() # Refresh the entire layout self.show_message("Pause Deleted", f"Pause {pause_idx + 1} has been deleted successfully!") Clock.schedule_once(lambda dt: self.build_pause_layout(), 0.5) except Exception as e: self.show_message("Error", f"Failed to delete pause: {str(e)}") self.show_confirmation( "Delete Pause", f"Are you sure you want to delete Pause {pause_idx + 1}?\nThis will remove the pause location and all its photos permanently.", confirm_delete_pause ) def reorganize_pause_folders(self): """Reorganize pause folders after deletion to maintain sequential numbering""" project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name) # Get all existing pause folders existing_folders = [] for item in os.listdir(project_folder): item_path = os.path.join(project_folder, item) if os.path.isdir(item_path) and item.startswith("pause_"): try: folder_num = int(item.split("_")[1]) existing_folders.append((folder_num, item_path)) except (IndexError, ValueError): continue # Sort by folder number existing_folders.sort(key=lambda x: x[0]) # Rename folders to be sequential starting from 1 temp_folders = [] for i, (old_num, old_path) in enumerate(existing_folders): new_num = i + 1 if old_num != new_num: # Create temporary name to avoid conflicts temp_path = os.path.join(project_folder, f"temp_pause_{new_num}") os.rename(old_path, temp_path) temp_folders.append((temp_path, new_num)) else: temp_folders.append((old_path, new_num)) # Final rename to correct names for temp_path, new_num in temp_folders: final_path = os.path.join(project_folder, f"pause_{new_num}") if temp_path != final_path: if os.path.exists(final_path): shutil.rmtree(final_path) os.rename(temp_path, final_path) def save_pause_name(self, pause, name_input): """Save the custom name for a pause""" pause['name'] = name_input.text def save_individual_pause(self, pause_idx): """Save individual pause info""" self.save_pauses() # Show confirmation self.show_message("Pause Saved", f"Pause {pause_idx + 1} information saved successfully!") def add_photos(self, pause_idx): """Open file browser to add photos""" project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name) pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}") layout = BoxLayout(orientation='vertical', spacing=10, padding=10) with layout.canvas.before: Color(0.13, 0.13, 0.16, 1) layout.bg_rect = Rectangle(pos=layout.pos, size=layout.size) layout.bind( pos=lambda inst, val: setattr(layout.bg_rect, 'pos', inst.pos), size=lambda inst, val: setattr(layout.bg_rect, 'size', inst.size) ) title_label = Label( text=f"Select photos for Pause {pause_idx + 1}:", color=(1, 1, 1, 1), font_size=14, size_hint_y=None, height=30 ) filechooser = FileChooserIconView( filters=['*.png', '*.jpg', '*.jpeg', '*.gif', '*.bmp'], path=os.path.expanduser('~'), multiselect=True ) btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=40) add_btn = Button( text="Add Selected", background_color=(0.2, 0.7, 0.2, 1), color=(1, 1, 1, 1), font_size=12 ) cancel_btn = Button( text="Cancel", background_color=(0.6, 0.3, 0.3, 1), color=(1, 1, 1, 1), font_size=12 ) btn_layout.add_widget(cancel_btn) btn_layout.add_widget(add_btn) layout.add_widget(title_label) layout.add_widget(filechooser) layout.add_widget(btn_layout) popup = Popup( title="Add Photos", content=layout, size_hint=(0.95, 0.9), auto_dismiss=False ) def add_selected_files(instance): if filechooser.selection: for file_path in filechooser.selection: if os.path.isfile(file_path): filename = os.path.basename(file_path) dest_path = os.path.join(pause_img_folder, filename) if not os.path.exists(dest_path): shutil.copy2(file_path, dest_path) Clock.schedule_once(lambda dt: self.refresh_photos_display(), 0.1) popup.dismiss() else: popup.dismiss() add_btn.bind(on_press=add_selected_files) cancel_btn.bind(on_press=lambda x: popup.dismiss()) popup.open() def refresh_photos_display(self): """Refresh the entire display to show updated photos""" self.build_pause_layout() def show_message(self, title, message): """Show a simple message popup""" layout = BoxLayout(orientation='vertical', spacing=10, padding=10) msg_label = Label( text=message, color=(1, 1, 1, 1), font_size=12, halign="center" ) msg_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None))) ok_btn = Button( text="OK", size_hint_y=None, height=35, background_color=(0.341, 0.235, 0.980, 1), color=(1, 1, 1, 1) ) layout.add_widget(msg_label) layout.add_widget(ok_btn) popup = Popup(title=title, content=layout, size_hint=(0.8, 0.4)) ok_btn.bind(on_press=lambda x: popup.dismiss()) popup.open() def show_confirmation(self, title, message, confirm_callback): """Show a confirmation dialog""" layout = BoxLayout(orientation='vertical', spacing=10, padding=10) msg_label = Label( text=message, color=(1, 1, 1, 1), font_size=12, halign="center" ) msg_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None))) btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=35) cancel_btn = Button( text="Cancel", background_color=(0.6, 0.3, 0.3, 1), color=(1, 1, 1, 1) ) confirm_btn = Button( text="Confirm", background_color=(0.8, 0.2, 0.2, 1), color=(1, 1, 1, 1) ) btn_layout.add_widget(cancel_btn) btn_layout.add_widget(confirm_btn) layout.add_widget(msg_label) layout.add_widget(btn_layout) popup = Popup(title=title, content=layout, size_hint=(0.8, 0.4), auto_dismiss=False) cancel_btn.bind(on_press=lambda x: popup.dismiss()) confirm_btn.bind(on_press=lambda x: (confirm_callback(), popup.dismiss())) popup.open() def save_all_and_close(self, instance): """Save all pauses and return to previous screen""" self.save_pauses() if self.on_save_callback: self.on_save_callback() self.go_back() def go_back(self, instance=None): """Return to the previous screen""" self.manager.current = "create_animation" def set_project_and_callback(self, project_name, callback=None): """Set the project name and callback for this screen""" self.project_name = project_name self.on_save_callback = callback def view_full_image(self, img_path, img_file): """Show full image in a popup""" layout = BoxLayout(orientation='vertical', spacing=10, padding=10) with layout.canvas.before: Color(0.05, 0.05, 0.08, 1) layout.bg_rect = Rectangle(pos=layout.pos, size=layout.size) layout.bind( pos=lambda inst, val: setattr(layout.bg_rect, 'pos', inst.pos), size=lambda inst, val: setattr(layout.bg_rect, 'size', inst.size) ) # Image display try: full_image = Image( source=img_path, allow_stretch=True, keep_ratio=True ) except Exception: full_image = Label( text="Unable to load image", color=(1, 1, 1, 1), font_size=16 ) # Close button close_btn = Button( text="Close", size_hint_y=None, height=40, background_color=(0.341, 0.235, 0.980, 1), color=(1, 1, 1, 1), font_size=14 ) layout.add_widget(full_image) layout.add_widget(close_btn) popup = Popup( title=f"Photo: {img_file}", content=layout, size_hint=(0.95, 0.95), auto_dismiss=False ) close_btn.bind(on_press=lambda x: popup.dismiss()) popup.open()