diff --git a/config/resources/edit-pen.png b/config/resources/edit-pen.png new file mode 100644 index 0000000..d74094b Binary files /dev/null and b/config/resources/edit-pen.png differ diff --git a/config/resources/pencil.png b/config/resources/pencil.png new file mode 100644 index 0000000..bf7d970 Binary files /dev/null and b/config/resources/pencil.png differ diff --git a/src/main.py b/src/main.py index e8dba39..3586293 100644 --- a/src/main.py +++ b/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") diff --git a/src/signage_player.kv b/src/signage_player.kv index 54a415b..d21bee0 100644 --- a/src/signage_player.kv +++ b/src/signage_player.kv @@ -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