1 """

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

3 """

4

5 from pyPhotoAlbum.models import ImageData, PlaceholderData

6 from pyPhotoAlbum.commands import AddElementCommand

7

8

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