diff --git a/src/edit_drowing.py b/src/edit_drowing.py new file mode 100644 index 0000000..8e37aac --- /dev/null +++ b/src/edit_drowing.py @@ -0,0 +1,100 @@ +Kiwy drawing + +from kivy.app import App +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.image import Image +from kivy.uix.button import Button +from kivy.uix.widget import Widget +from kivy.graphics import Color, Line +from kivy.core.window import Window + +class DrawLayer(Widget): +def init(self, **kwargs): +super().init(**kwargs) +self.strokes = [] # store all drawn lines +self.current_color = (1, 0, 0) # default red +self.current_width = 2 # default thickness +self.drawing_enabled = False # drawing toggle + + +def on_touch_down(self, touch): + if not self.drawing_enabled: + return False + + with self.canvas: + Color(*self.current_color) + new_line = Line(points=[touch.x, touch.y], width=self.current_width) + self.strokes.append(new_line) + return True + +def on_touch_move(self, touch): + if self.strokes and self.drawing_enabled: + self.strokes[-1].points += [touch.x, touch.y] + +# ========================== +# UNDO LAST LINE +# ========================== +def undo(self): + if self.strokes: + last = self.strokes.pop() + self.canvas.remove(last) + +# ========================== +# CHANGE COLOR +# ========================== +def set_color(self, color_tuple): + self.current_color = color_tuple + +# ========================== +# CHANGE LINE WIDTH +# ========================== +def set_thickness(self, value): + self.current_width = value +class EditorUI(BoxLayout): +def init(self, **kwargs): +super().init(orientation="vertical", **kwargs) + + + # Background image + self.img = Image(source="graph.png", allow_stretch=True) + self.add_widget(self.img) + + # Drawing layer above image + self.draw = DrawLayer() + self.add_widget(self.draw) + + # Toolbar + toolbar = BoxLayout(size_hint_y=0.15) + + toolbar.add_widget(Button(text="Draw On", on_press=self.toggle_draw)) + toolbar.add_widget(Button(text="Red", on_press=lambda x: self.draw.set_color((1,0,0)))) + toolbar.add_widget(Button(text="Blue", on_press=lambda x: self.draw.set_color((0,0,1)))) + toolbar.add_widget(Button(text="Green", on_press=lambda x: self.draw.set_color((0,1,0)))) + + toolbar.add_widget(Button(text="Thin", on_press=lambda x: self.draw.set_thickness(2))) + toolbar.add_widget(Button(text="Thick", on_press=lambda x: self.draw.set_thickness(6))) + + toolbar.add_widget(Button(text="Undo", on_press=lambda x: self.draw.undo())) + toolbar.add_widget(Button(text="Save", on_press=lambda x: self.save_image())) + + self.add_widget(toolbar) + +# ========================== +# TOGGLE DRAWING MODE +# ========================== +def toggle_draw(self, btn): + self.draw.drawing_enabled = not self.draw.drawing_enabled + btn.text = "Draw Off" if self.draw.drawing_enabled else "Draw On" + +# ========================== +# SAVE MERGED IMAGE +# ========================== +def save_image(self): + self.export_to_png("edited_graph.png") + print("Saved as edited_graph.png") +class AnnotatorApp(App): +def build(self): +return EditorUI() + +AnnotatorApp().run() + diff --git a/src/main.py b/src/main.py index b27aff7..e8dba39 100644 --- a/src/main.py +++ b/src/main.py @@ -46,12 +46,245 @@ from get_playlists import ( send_player_error_feedback ) from keyboard_widget import KeyboardWidget +from kivy.graphics import Color, Line, Ellipse +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.slider import Slider # Load the KV file Builder.load_file('signage_player.kv') # Removed VLCVideoWidget - using Kivy's built-in Video widget instead +class DrawingLayer(Widget): + """Layer for drawing on top of images""" + def __init__(self, **kwargs): + super(DrawingLayer, self).__init__(**kwargs) + self.strokes = [] # Store all drawn lines + self.current_color = (1, 0, 0, 1) # Default red + self.current_width = 3 # Default thickness + self.drawing_enabled = True # Drawing always enabled in edit mode + + def on_touch_down(self, touch): + if not self.drawing_enabled or not self.collide_point(*touch.pos): + return False + + with self.canvas: + Color(*self.current_color) + new_line = Line(points=[touch.x, touch.y], width=self.current_width) + self.strokes.append({'line': new_line, 'color': self.current_color, 'width': self.current_width}) + touch.ud['line'] = new_line + return True + + def on_touch_move(self, touch): + if 'line' in touch.ud and self.drawing_enabled: + touch.ud['line'].points += [touch.x, touch.y] + return True + + def undo(self): + """Remove the last stroke""" + if self.strokes: + last_stroke = self.strokes.pop() + self.canvas.remove(last_stroke['line']) + Logger.info("DrawingLayer: Undid last stroke") + + def clear_all(self): + """Clear all strokes""" + for stroke in self.strokes: + self.canvas.remove(stroke['line']) + self.strokes = [] + Logger.info("DrawingLayer: Cleared all strokes") + + def set_color(self, color_tuple): + """Set drawing color (RGBA)""" + self.current_color = color_tuple + Logger.debug(f"DrawingLayer: Color set to {color_tuple}") + + def set_thickness(self, value): + """Set line thickness""" + self.current_width = value + Logger.debug(f"DrawingLayer: Thickness set to {value}") + + +class EditPopup(Popup): + """Popup for editing/annotating images""" + def __init__(self, player_instance, image_path, **kwargs): + super(EditPopup, self).__init__(**kwargs) + self.player = player_instance + self.image_path = image_path + self.drawing_layer = None + + # Pause playback + self.was_paused = self.player.is_paused + if not self.was_paused: + self.player.is_paused = True + Clock.unschedule(self.player.next_media) + if self.player.current_widget and isinstance(self.player.current_widget, Video): + self.player.current_widget.state = 'pause' + + # Show cursor + try: + Window.show_cursor = True + except: + pass + + # Build UI + self.title = 'Edit Image' + self.size_hint = (0.95, 0.95) + self.auto_dismiss = False + + # Main layout + main_layout = BoxLayout(orientation='vertical', padding=10, spacing=10) + + # Image and drawing area + image_container = FloatLayout() + + # Background image + self.image_widget = Image( + source=image_path, + allow_stretch=True, + keep_ratio=True, + size_hint=(1, 1) + ) + image_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) + + main_layout.add_widget(image_container) + + # Toolbar + toolbar = BoxLayout(size_hint_y=None, height=80, spacing=10, padding=[10, 5]) + + # 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) + + # 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 + 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( + text='Undo', + background_color=(0.9, 0.6, 0.2, 1), + on_press=lambda x: self.drawing_layer.undo() + )) + action_btns.add_widget(Button( + text='Clear', + background_color=(0.9, 0.3, 0.2, 1), + on_press=lambda x: self.drawing_layer.clear_all() + )) + actions_layout.add_widget(action_btns) + toolbar.add_widget(actions_layout) + + # Save and close buttons + final_btns = BoxLayout(orientation='vertical', size_hint_x=0.2, spacing=5) + final_btns.add_widget(Button( + text='Save', + background_color=(0.2, 0.8, 0.2, 1), + font_size='16sp', + on_press=self.save_image + )) + final_btns.add_widget(Button( + text='Cancel', + background_color=(0.6, 0.2, 0.2, 1), + font_size='16sp', + on_press=self.close_without_saving + )) + toolbar.add_widget(final_btns) + + main_layout.add_widget(toolbar) + + self.content = main_layout + + # Bind to dismiss + self.bind(on_dismiss=self.on_popup_dismiss) + + Logger.info(f"EditPopup: Opened for image {os.path.basename(image_path)}") + + def save_image(self, instance): + """Save the edited image""" + try: + # Generate output 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) + + Logger.info(f"EditPopup: Saved edited image to {output_path}") + + # Show confirmation + self.title = f'Saved as {os.path.basename(output_path)}' + Clock.schedule_once(lambda dt: self.dismiss(), 1) + + except Exception as e: + Logger.error(f"EditPopup: Error saving image: {e}") + self.title = f'Error saving: {e}' + + def close_without_saving(self, instance): + """Close without saving""" + Logger.info("EditPopup: Closed without saving") + self.dismiss() + + def on_popup_dismiss(self, *args): + """Resume playback when popup closes""" + # Resume playback if it wasn't paused before + if not self.was_paused: + self.player.is_paused = False + self.player.play_current_media() + + # Restart control hide timer + self.player.schedule_hide_controls() + + Logger.info("EditPopup: Dismissed, playback resumed") + # Custom keyboard container with close button class KeyboardContainer(BoxLayout): def __init__(self, vkeyboard, **kwargs): @@ -1070,6 +1303,39 @@ class SignagePlayer(Widget): popup = SettingsPopup(player_instance=self, was_paused=was_paused) popup.open() + def show_edit_interface(self, instance=None): + """Show edit interface for current image""" + # Check if current media is an image + if not self.playlist or self.current_index >= len(self.playlist): + Logger.warning("SignagePlayer: No media to edit") + return + + media_item = self.playlist[self.current_index] + file_name = media_item.get('file_name', '') + file_extension = os.path.splitext(file_name)[1].lower() + + # Only allow editing images + if file_extension not in ['.jpg', '.jpeg', '.png', '.bmp']: + Logger.warning(f"SignagePlayer: Cannot edit {file_extension} files, only images") + # Show error message briefly + self.ids.status_label.text = 'Can only edit image files' + self.ids.status_label.opacity = 1 + Clock.schedule_once(lambda dt: setattr(self.ids.status_label, 'opacity', 0), 2) + return + + # Get full path to current image + image_path = os.path.join(self.media_dir, file_name) + + if not os.path.exists(image_path): + Logger.error(f"SignagePlayer: Image not found: {image_path}") + return + + Logger.info(f"SignagePlayer: Opening edit interface for {file_name}") + + # Open edit popup + popup = EditPopup(player_instance=self, image_path=image_path) + popup.open() + def show_exit_popup(self, instance=None): """Show exit password popup""" # Pause playback when exit popup opens diff --git a/src/signage_player.kv b/src/signage_player.kv index d095cb4..54a415b 100644 --- a/src/signage_player.kv +++ b/src/signage_player.kv @@ -261,6 +261,14 @@ border: (0, 0, 0, 0) on_press: root.toggle_pause() + Button: + id: edit_btn + size_hint: None, None + size: dp(50), dp(50) + background_normal: root.resources_path + '/pencil.png' + background_down: root.resources_path + '/pencil.png' + border: (0, 0, 0, 0) + on_press: root.show_edit_interface() Button: id: settings_btn size_hint: None, None