Coverage for pyPhotoAlbum/mixins/image_pan.py: 95%

42 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-20 12:55 +0000

1""" 

2Image pan mixin for GLWidget - handles panning images within frames 

3""" 

4 

5from typing import TYPE_CHECKING, Optional, Tuple 

6 

7 

8from pyPhotoAlbum.models import ImageData 

9 

10 

11class ImagePanMixin: 

12 # Type hints for expected attributes from mixing class 

13 drag_start_pos: Optional[Tuple[float, float]] 

14 zoom_level: float 

15 """ 

16 Mixin providing image panning functionality. 

17 

18 This mixin handles Control+drag to pan an image within its frame by 

19 adjusting the crop_info property. 

20 """ 

21 

22 def __init__(self, *args, **kwargs): 

23 super().__init__(*args, **kwargs) 

24 

25 # Image pan state (for panning image within frame with Control key) 

26 self.image_pan_mode: bool = False # True when Control+dragging an ImageData element 

27 self.image_pan_start_crop: Optional[Tuple[float, float, float, float]] = None # Starting crop_info 

28 

29 def _handle_image_pan_move(self, x: float, y: float, element: "ImageData"): 

30 """ 

31 Handle image panning within a frame during mouse move. 

32 

33 Args: 

34 x: Current mouse X position in screen coordinates 

35 y: Current mouse Y position in screen coordinates 

36 element: The ImageData element being panned 

37 """ 

38 if not self.image_pan_mode or not isinstance(element, ImageData): 

39 return 

40 

41 if not self.drag_start_pos: 

42 return 

43 

44 # Calculate mouse movement in screen pixels 

45 screen_dx = x - self.drag_start_pos[0] 

46 screen_dy = y - self.drag_start_pos[1] 

47 

48 # Get element size in page-local coordinates 

49 elem_w, elem_h = element.size 

50 

51 # Convert screen movement to normalized crop coordinates 

52 # Negative because moving mouse right should pan image left (show more of right side) 

53 # Scale by zoom level and element size 

54 crop_dx = -screen_dx / (elem_w * self.zoom_level) 

55 crop_dy = -screen_dy / (elem_h * self.zoom_level) 

56 

57 # Get starting crop info 

58 start_crop = self.image_pan_start_crop 

59 if not start_crop: 

60 start_crop = (0, 0, 1, 1) 

61 

62 # Calculate new crop_info 

63 crop_width = start_crop[2] - start_crop[0] 

64 crop_height = start_crop[3] - start_crop[1] 

65 

66 new_x_min = start_crop[0] + crop_dx 

67 new_y_min = start_crop[1] + crop_dy 

68 new_x_max = new_x_min + crop_width 

69 new_y_max = new_y_min + crop_height 

70 

71 # Clamp to valid range (0-1) to prevent panning beyond image boundaries 

72 if new_x_min < 0: 

73 new_x_min = 0 

74 new_x_max = crop_width 

75 if new_x_max > 1: 

76 new_x_max = 1 

77 new_x_min = 1 - crop_width 

78 

79 if new_y_min < 0: 

80 new_y_min = 0 

81 new_y_max = crop_height 

82 if new_y_max > 1: 

83 new_y_max = 1 

84 new_y_min = 1 - crop_height 

85 

86 # Update element's crop_info 

87 element.crop_info = (new_x_min, new_y_min, new_x_max, new_y_max)