Improve edit interface: full-screen layout with toolbars, versioned saving to edited_media folder
This commit is contained in:
BIN
config/resources/edit-pen.png
Normal file
BIN
config/resources/edit-pen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
config/resources/pencil.png
Normal file
BIN
config/resources/pencil.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
409
src/main.py
409
src/main.py
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user