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

1""" 

2Asset drop mixin for GLWidget - handles drag-and-drop file operations 

3""" 

4 

5from pyPhotoAlbum.models import ImageData, PlaceholderData 

6from pyPhotoAlbum.commands import AddElementCommand 

7 

8 

9class AssetDropMixin: 

10 """ 

11 Mixin providing drag-and-drop asset functionality. 

12 

13 This mixin handles dragging image files into the widget and creating 

14 or updating ImageData elements. 

15 """ 

16 

17 IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp"] 

18 

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() 

29 

30 def dragMoveEvent(self, event): 

31 """Handle drag move events""" 

32 if event.mimeData().hasUrls(): 

33 event.acceptProposedAction() 

34 else: 

35 event.ignore() 

36 

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 

43 

44 x, y = event.position().x(), event.position().y() 

45 target_element = self._get_element_at(x, y) 

46 

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) 

51 

52 event.acceptProposedAction() 

53 self.update() 

54 

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 

59 

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 

65 

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 

71 

72 try: 

73 asset_path = main_window.project.asset_manager.import_asset(image_path) 

74 

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 

79 

80 print(f"Updated element with image: {asset_path}") 

81 except Exception as e: 

82 print(f"Error importing dropped image: {e}") 

83 

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 ) 

96 

97 if not main_window.project.pages: 

98 return 

99 

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 

105 

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 

111 

112 target_page, page_index, page_renderer = self._get_page_at(x, y) 

113 

114 if not (target_page and page_renderer): 

115 print("Drop location not on any page") 

116 return 

117 

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) 

123 

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}") 

129 

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 

133 

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 

138 

139 # Fallback dimensions if image cannot be read 

140 return 200, 150 

141 

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 

148 

149 page_local_x, page_local_y = page_renderer.screen_to_page(x, y) 

150 

151 new_image = ImageData(image_path=asset_path, x=page_local_x, y=page_local_y, width=img_width, height=img_height) 

152 

153 cmd = AddElementCommand(target_page.layout, new_image, asset_manager=main_window.project.asset_manager) 

154 main_window.project.history.execute(cmd) 

155 

156 print(f"Added new image to page {page_index + 1} at ({page_local_x:.1f}, {page_local_y:.1f}): {asset_path}")