Files
traccar_animation/screens/pause_edit_screen_improved.py
2025-07-02 16:41:44 +03:00

890 lines
33 KiB
Python

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