Improve edit interface: full-screen layout with toolbars, versioned saving to edited_media folder

This commit is contained in:
Kiwy Signage Player
2025-12-05 00:25:53 +02:00
parent 72d382b96b
commit fba2007bdf
4 changed files with 316 additions and 97 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
config/resources/pencil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -127,147 +127,366 @@ class EditPopup(Popup):
except:
pass
# Build UI
self.title = 'Edit Image'
self.size_hint = (0.95, 0.95)
# Build UI - Full screen without title bar
self.title = '' # No title
self.size_hint = (1, 1) # Full screen
self.auto_dismiss = False
self.separator_height = 0 # Remove separator
# Main layout
main_layout = BoxLayout(orientation='vertical', padding=10, spacing=10)
# Main container (FloatLayout for overlays)
main_container = FloatLayout()
# Image and drawing area
image_container = FloatLayout()
# Background image
# Background image (full screen)
self.image_widget = Image(
source=image_path,
allow_stretch=True,
keep_ratio=True,
size_hint=(1, 1)
size_hint=(1, 1),
pos_hint={'x': 0, 'y': 0}
)
image_container.add_widget(self.image_widget)
main_container.add_widget(self.image_widget)
# Drawing layer on top
self.drawing_layer = DrawingLayer(size_hint=(1, 1))
image_container.add_widget(self.drawing_layer)
# Drawing layer on top of image (full screen)
self.drawing_layer = DrawingLayer(
size_hint=(1, 1),
pos_hint={'x': 0, 'y': 0}
)
main_container.add_widget(self.drawing_layer)
main_layout.add_widget(image_container)
# Top toolbar (horizontal, 56dp height - 20% smaller than 70dp, 50% transparency)
top_toolbar = BoxLayout(
orientation='horizontal',
size_hint=(1, None),
height=56,
pos_hint={'top': 1, 'x': 0},
spacing=10,
padding=[10, 8]
)
# Toolbar
toolbar = BoxLayout(size_hint_y=None, height=80, spacing=10, padding=[10, 5])
# Add semi-transparent background to toolbar
from kivy.graphics import Color, Rectangle
with top_toolbar.canvas.before:
Color(0.1, 0.1, 0.1, 0.5) # 50% transparency
top_toolbar.bg_rect = Rectangle(size=top_toolbar.size, pos=top_toolbar.pos)
# Color buttons
color_layout = BoxLayout(orientation='vertical', size_hint_x=0.15, spacing=5)
color_layout.add_widget(Label(text='Color:', size_hint_y=0.3, font_size='12sp'))
color_btns = BoxLayout(spacing=5)
color_btns.add_widget(Button(
text='Red',
background_color=(1, 0, 0, 1),
on_press=lambda x: self.drawing_layer.set_color((1, 0, 0, 1))
))
color_btns.add_widget(Button(
text='Blue',
background_color=(0, 0, 1, 1),
on_press=lambda x: self.drawing_layer.set_color((0, 0, 1, 1))
))
color_btns.add_widget(Button(
text='Green',
background_color=(0, 1, 0, 1),
on_press=lambda x: self.drawing_layer.set_color((0, 1, 0, 1))
))
color_btns.add_widget(Button(
text='Black',
background_color=(0, 0, 0, 1),
on_press=lambda x: self.drawing_layer.set_color((0, 0, 0, 1))
))
color_layout.add_widget(color_btns)
toolbar.add_widget(color_layout)
top_toolbar.bind(pos=self._update_toolbar_bg, size=self._update_toolbar_bg)
# Thickness controls
thickness_layout = BoxLayout(orientation='vertical', size_hint_x=0.15, spacing=5)
thickness_layout.add_widget(Label(text='Thickness:', size_hint_y=0.3, font_size='12sp'))
thickness_btns = BoxLayout(spacing=5)
thickness_btns.add_widget(Button(
text='Thin',
on_press=lambda x: self.drawing_layer.set_thickness(2)
))
thickness_btns.add_widget(Button(
text='Medium',
on_press=lambda x: self.drawing_layer.set_thickness(5)
))
thickness_btns.add_widget(Button(
text='Thick',
on_press=lambda x: self.drawing_layer.set_thickness(10)
))
thickness_layout.add_widget(thickness_btns)
toolbar.add_widget(thickness_layout)
# Action buttons in top toolbar (buttons with slightly rounded corners)
top_toolbar.add_widget(Widget()) # Spacer
# Action buttons
actions_layout = BoxLayout(orientation='vertical', size_hint_x=0.2, spacing=5)
actions_layout.add_widget(Label(text='Actions:', size_hint_y=0.3, font_size='12sp'))
action_btns = BoxLayout(spacing=5)
action_btns.add_widget(Button(
undo_btn = Button(
text='Undo',
background_color=(0.9, 0.6, 0.2, 1),
font_size='16sp',
size_hint=(None, 1),
width=100,
background_normal='',
background_color=(0.9, 0.6, 0.2, 0.9),
on_press=lambda x: self.drawing_layer.undo()
))
action_btns.add_widget(Button(
)
undo_btn.bind(pos=self._make_rounded_btn, size=self._make_rounded_btn)
top_toolbar.add_widget(undo_btn)
clear_btn = Button(
text='Clear',
background_color=(0.9, 0.3, 0.2, 1),
font_size='16sp',
size_hint=(None, 1),
width=100,
background_normal='',
background_color=(0.9, 0.3, 0.2, 0.9),
on_press=lambda x: self.drawing_layer.clear_all()
))
actions_layout.add_widget(action_btns)
toolbar.add_widget(actions_layout)
)
clear_btn.bind(pos=self._make_rounded_btn, size=self._make_rounded_btn)
top_toolbar.add_widget(clear_btn)
# Save and close buttons
final_btns = BoxLayout(orientation='vertical', size_hint_x=0.2, spacing=5)
final_btns.add_widget(Button(
save_btn = Button(
text='Save',
background_color=(0.2, 0.8, 0.2, 1),
font_size='16sp',
size_hint=(None, 1),
width=100,
background_normal='',
background_color=(0.2, 0.8, 0.2, 0.9),
on_press=self.save_image
))
final_btns.add_widget(Button(
)
save_btn.bind(pos=self._make_rounded_btn, size=self._make_rounded_btn)
top_toolbar.add_widget(save_btn)
cancel_btn = Button(
text='Cancel',
background_color=(0.6, 0.2, 0.2, 1),
font_size='16sp',
size_hint=(None, 1),
width=100,
background_normal='',
background_color=(0.6, 0.2, 0.2, 0.9),
on_press=self.close_without_saving
))
toolbar.add_widget(final_btns)
)
cancel_btn.bind(pos=self._make_rounded_btn, size=self._make_rounded_btn)
top_toolbar.add_widget(cancel_btn)
main_layout.add_widget(toolbar)
top_toolbar.add_widget(Widget()) # Spacer
self.content = main_layout
main_container.add_widget(top_toolbar)
# Right sidebar (vertical, 56dp width to match toolbar height, 50% transparency)
right_sidebar = BoxLayout(
orientation='vertical',
size_hint=(None, 1),
width=56,
pos_hint={'right': 1, 'y': 0},
spacing=10,
padding=[8, 66, 8, 10] # Extra top padding to avoid toolbar (56 + 10)
)
# Add semi-transparent background to sidebar
with right_sidebar.canvas.before:
Color(0.1, 0.1, 0.1, 0.5) # 50% transparency
right_sidebar.bg_rect = Rectangle(size=right_sidebar.size, pos=right_sidebar.pos)
right_sidebar.bind(pos=self._update_sidebar_bg, size=self._update_sidebar_bg)
# Color section with icon
color_header = BoxLayout(
orientation='vertical',
size_hint_y=None,
height=55,
spacing=2
)
pen_icon_path = os.path.join(self.player.resources_path, 'edit-pen.png')
color_icon_img = Image(
source=pen_icon_path,
size_hint_y=None,
height=28,
allow_stretch=True,
keep_ratio=True
)
color_label = Label(
text='Color',
font_size='11sp',
bold=True,
size_hint_y=None,
height=25
)
color_header.add_widget(color_icon_img)
color_header.add_widget(color_label)
right_sidebar.add_widget(color_header)
# Color buttons (round)
colors = [
('R', (1, 0, 0, 1)),
('B', (0, 0, 1, 1)),
('G', (0, 1, 0, 1)),
('K', (0, 0, 0, 1)),
('W', (1, 1, 1, 1))
]
for text, color in colors:
color_btn = Button(
text=text,
font_size='18sp',
size_hint=(1, None),
height=50,
background_normal='',
background_color=color,
on_press=lambda x, c=color: self.drawing_layer.set_color(c)
)
color_btn.bind(pos=self._make_round, size=self._make_round)
right_sidebar.add_widget(color_btn)
# Spacer
right_sidebar.add_widget(Widget(size_hint_y=0.2))
# Thickness section with icon
thickness_header = BoxLayout(
orientation='vertical',
size_hint_y=None,
height=55,
spacing=2
)
thickness_icon_img = Image(
source=pen_icon_path,
size_hint_y=None,
height=28,
allow_stretch=True,
keep_ratio=True
)
thickness_label = Label(
text='Size',
font_size='11sp',
bold=True,
size_hint_y=None,
height=25
)
thickness_header.add_widget(thickness_icon_img)
thickness_header.add_widget(thickness_label)
right_sidebar.add_widget(thickness_header)
# Thickness buttons (round, visual representation)
thicknesses = [
('S', 2),
('M', 5),
('L', 10)
]
for text, thickness in thicknesses:
thick_btn = Button(
text=text,
font_size='20sp',
bold=True,
size_hint=(1, None),
height=50,
background_normal='',
background_color=(0.3, 0.3, 0.3, 0.9),
on_press=lambda x, t=thickness: self.drawing_layer.set_thickness(t)
)
thick_btn.bind(pos=self._make_round, size=self._make_round)
right_sidebar.add_widget(thick_btn)
right_sidebar.add_widget(Widget()) # Bottom spacer
main_container.add_widget(right_sidebar)
self.content = main_container
self.top_toolbar = top_toolbar
self.right_sidebar = right_sidebar
# Bind to dismiss
self.bind(on_dismiss=self.on_popup_dismiss)
Logger.info(f"EditPopup: Opened for image {os.path.basename(image_path)}")
def _update_toolbar_bg(self, instance, value):
"""Update toolbar background rectangle"""
self.top_toolbar.bg_rect.pos = instance.pos
self.top_toolbar.bg_rect.size = instance.size
def _update_sidebar_bg(self, instance, value):
"""Update sidebar background rectangle"""
self.right_sidebar.bg_rect.pos = instance.pos
self.right_sidebar.bg_rect.size = instance.size
def _make_rounded_btn(self, instance, value):
"""Make toolbar button with slightly rounded corners"""
instance.canvas.before.clear()
with instance.canvas.before:
from kivy.graphics import RoundedRectangle
Color(*instance.background_color)
instance.round_rect = RoundedRectangle(
pos=instance.pos,
size=instance.size,
radius=[10]
)
def _make_round(self, instance, value):
"""Make sidebar button fully circular"""
instance.canvas.before.clear()
with instance.canvas.before:
from kivy.graphics import RoundedRectangle
Color(*instance.background_color)
instance.round_rect = RoundedRectangle(
pos=instance.pos,
size=instance.size,
radius=[instance.height / 2]
)
def save_image(self, instance):
"""Save the edited image"""
try:
# Generate output filename
import json
import re
from datetime import datetime
# Create edited_media directory if it doesn't exist
edited_dir = os.path.join(self.player.base_dir, 'media', 'edited_media')
os.makedirs(edited_dir, exist_ok=True)
# Get original filename
base_name = os.path.basename(self.image_path)
name, ext = os.path.splitext(base_name)
output_path = os.path.join(
os.path.dirname(self.image_path),
f"{name}_edited{ext}"
)
# Export the entire widget (image + drawings) as PNG
self.content.export_to_png(output_path)
# Determine version number
version_match = re.search(r'_e_v(\d+)$', name)
if version_match:
# Increment existing version
current_version = int(version_match.group(1))
new_version = current_version + 1
# Remove old version suffix
original_name = re.sub(r'_e_v\d+$', '', name)
new_name = f"{original_name}_e_v{new_version}"
else:
# First edit version
original_name = name
new_name = f"{name}_e_v1"
Logger.info(f"EditPopup: Saved edited image to {output_path}")
# Generate output path
output_filename = f"{new_name}.jpg"
output_path = os.path.join(edited_dir, output_filename)
# Show confirmation
self.title = f'Saved as {os.path.basename(output_path)}'
Clock.schedule_once(lambda dt: self.dismiss(), 1)
# Temporarily hide toolbars
self.top_toolbar.opacity = 0
self.right_sidebar.opacity = 0
# Force canvas update
self.content.canvas.ask_update()
# Small delay to ensure rendering is complete
from kivy.clock import Clock
def do_export(dt):
try:
# Export only the visible content (image + drawings, no toolbars)
self.content.export_to_png(output_path)
# Restore toolbars
self.top_toolbar.opacity = 1
self.right_sidebar.opacity = 1
# Create and save metadata
self._save_metadata(edited_dir, new_name, base_name,
new_version if version_match else 1, output_filename)
Logger.info(f"EditPopup: Saved edited image to {output_path}")
# Show confirmation
self.title = f'Saved as {output_filename}'
Clock.schedule_once(lambda dt: self.dismiss(), 1)
except Exception as e:
Logger.error(f"EditPopup: Error in export: {e}")
import traceback
Logger.error(f"EditPopup: Traceback: {traceback.format_exc()}")
self.title = f'Error saving: {e}'
# Restore toolbars
self.top_toolbar.opacity = 1
self.right_sidebar.opacity = 1
Clock.schedule_once(do_export, 0.1)
return
except Exception as e:
Logger.error(f"EditPopup: Error saving image: {e}")
import traceback
Logger.error(f"EditPopup: Traceback: {traceback.format_exc()}")
self.title = f'Error saving: {e}'
def _save_metadata(self, edited_dir, new_name, base_name, version, output_filename):
"""Save metadata JSON file"""
import json
from datetime import datetime
metadata = {
'time_of_modification': datetime.now().isoformat(),
'original_name': base_name,
'new_name': output_filename,
'original_path': self.image_path,
'version': version
}
# Save metadata JSON
json_filename = f"{new_name}_metadata.json"
json_path = os.path.join(edited_dir, json_filename)
with open(json_path, 'w') as f:
json.dump(metadata, f, indent=2)
Logger.info(f"EditPopup: Saved metadata to {json_path}")
def close_without_saving(self, instance):
"""Close without saving"""
Logger.info("EditPopup: Closed without saving")

View File

@@ -224,12 +224,12 @@
pos: self.pos
radius: [dp(15)]
# New control panel overlay (bottom center, 1/6 width, 90% transparent)
# New control panel overlay (bottom center, width for 6 buttons, 90% transparent)
BoxLayout:
id: controls_layout
orientation: 'horizontal'
size_hint: None, None
width: root.width / 6 if root.width > 0 else dp(260)
width: dp(370)
height: dp(70)
pos: (root.width - self.width) / 2, dp(10)
opacity: 1