Coverage for pyPhotoAlbum/mixins/asset_drop.py: 95%
87 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-20 12:55 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-20 12:55 +0000
1"""
2Asset drop mixin for GLWidget - handles drag-and-drop file operations
3"""
5from pyPhotoAlbum.models import ImageData, PlaceholderData
6from pyPhotoAlbum.commands import AddElementCommand
9class AssetDropMixin:
10 """
11 Mixin providing drag-and-drop asset functionality.
13 This mixin handles dragging image files into the widget and creating
14 or updating ImageData elements.
15 """
17 IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp"]
19 def dragEnterEvent(self, event):
20 """Handle drag enter events"""
21 if event.mimeData().hasUrls():
22 urls = event.mimeData().urls()
23 for url in urls:
24 file_path = url.toLocalFile()
25 if any(file_path.lower().endswith(ext) for ext in self.IMAGE_EXTENSIONS):
26 event.acceptProposedAction()
27 return
28 event.ignore()
30 def dragMoveEvent(self, event):
31 """Handle drag move events"""
32 if event.mimeData().hasUrls():
33 event.acceptProposedAction()
34 else:
35 event.ignore()
37 def dropEvent(self, event):
38 """Handle drop events - delegates to specialized handlers"""
39 image_path = self._extract_image_path(event)
40 if not image_path:
41 event.ignore()
42 return
44 x, y = event.position().x(), event.position().y()
45 target_element = self._get_element_at(x, y)
47 if target_element and isinstance(target_element, (ImageData, PlaceholderData)):
48 self._handle_drop_on_element(image_path, target_element)
49 else:
50 self._handle_drop_on_empty_space(image_path, x, y)
52 event.acceptProposedAction()
53 self.update()
55 def _extract_image_path(self, event):
56 """Extract the first valid image path from drop event"""
57 if not event.mimeData().hasUrls():
58 return None
60 for url in event.mimeData().urls():
61 file_path = url.toLocalFile()
62 if any(file_path.lower().endswith(ext) for ext in self.IMAGE_EXTENSIONS):
63 return file_path
64 return None
66 def _handle_drop_on_element(self, image_path, target_element):
67 """Handle dropping an image onto an existing element"""
68 main_window = self.window()
69 if not (hasattr(main_window, "project") and main_window.project):
70 return
72 try:
73 asset_path = main_window.project.asset_manager.import_asset(image_path)
75 if isinstance(target_element, PlaceholderData):
76 self._replace_placeholder_with_image(target_element, asset_path, main_window)
77 else:
78 target_element.image_path = asset_path
80 print(f"Updated element with image: {asset_path}")
81 except Exception as e:
82 print(f"Error importing dropped image: {e}")
84 def _replace_placeholder_with_image(self, placeholder, asset_path, main_window):
85 """Replace a placeholder element with an ImageData element"""
86 new_image = ImageData(
87 image_path=asset_path,
88 x=placeholder.position[0],
89 y=placeholder.position[1],
90 width=placeholder.size[0],
91 height=placeholder.size[1],
92 z_index=placeholder.z_index,
93 # Inherit styling from placeholder (for templatable styles)
94 style=placeholder.style.copy(),
95 )
97 if not main_window.project.pages:
98 return
100 for page in main_window.project.pages:
101 if placeholder in page.layout.elements:
102 page.layout.elements.remove(placeholder)
103 page.layout.add_element(new_image)
104 break
106 def _handle_drop_on_empty_space(self, image_path, x, y):
107 """Handle dropping an image onto empty space"""
108 main_window = self.window()
109 if not (hasattr(main_window, "project") and main_window.project and main_window.project.pages):
110 return
112 target_page, page_index, page_renderer = self._get_page_at(x, y)
114 if not (target_page and page_renderer):
115 print("Drop location not on any page")
116 return
118 try:
119 # Import asset first, then calculate dimensions from imported asset
120 asset_path = main_window.project.asset_manager.import_asset(image_path)
121 full_asset_path = self.get_asset_full_path(asset_path)
122 img_width, img_height = self._calculate_image_dimensions(full_asset_path)
124 self._add_new_image_to_page(
125 asset_path, target_page, page_index, page_renderer, x, y, img_width, img_height, main_window
126 )
127 except Exception as e:
128 print(f"Error importing dropped image: {e}")
130 def _calculate_image_dimensions(self, image_path):
131 """Calculate scaled image dimensions for new image using centralized utility."""
132 from pyPhotoAlbum.async_backend import get_image_dimensions
134 # Use centralized utility (max 300px for UI display)
135 dimensions = get_image_dimensions(image_path, max_size=300)
136 if dimensions:
137 return dimensions
139 # Fallback dimensions if image cannot be read
140 return 200, 150
142 def _add_new_image_to_page(
143 self, asset_path, target_page, page_index, page_renderer, x, y, img_width, img_height, main_window
144 ):
145 """Add a new image element to the target page (asset already imported)"""
146 if page_index >= 0:
147 self.current_page_index = page_index
149 page_local_x, page_local_y = page_renderer.screen_to_page(x, y)
151 new_image = ImageData(image_path=asset_path, x=page_local_x, y=page_local_y, width=img_width, height=img_height)
153 cmd = AddElementCommand(target_page.layout, new_image, asset_manager=main_window.project.asset_manager)
154 main_window.project.history.execute(cmd)
156 print(f"Added new image to page {page_index + 1} at ({page_local_x:.1f}, {page_local_y:.1f}): {asset_path}")