1'''lens.py - Magnifying lens.''' 2 3import math 4 5from gi.repository import Gdk, GdkPixbuf, Gtk 6 7from mcomix.preferences import prefs 8from mcomix import image_tools 9from mcomix import constants 10 11 12class MagnifyingLens(object): 13 14 '''The MagnifyingLens creates cursors from the raw pixbufs containing 15 the unscaled data for the currently displayed images. It does this by 16 looking at the cursor position and calculating what image data to put 17 in the "lens" cursor. 18 19 Note: The mapping is highly dependent on the exact layout of the main 20 window images, thus this module isn't really independent from the main 21 module as it uses implementation details not in the interface. 22 ''' 23 24 def __init__(self, window): 25 self._window = window 26 self._area = self._window._main_layout 27 self._area.connect('motion-notify-event', self._motion_event) 28 29 #: Stores lens state 30 self._enabled = False 31 #: Stores a tuple of the last mouse coordinates 32 self._point = None 33 #: Stores the last rectangle that was used to render the lens 34 self._last_lens_rect = None 35 36 def get_enabled(self): 37 return self._enabled 38 39 def set_enabled(self, enabled): 40 self._enabled = enabled 41 42 if enabled: 43 # FIXME: If no file is currently loaded, the cursor will still be hidden. 44 self._window.cursor_handler.set_cursor_type(constants.NO_CURSOR) 45 self._window.osd.clear() 46 47 if self._point: 48 self._draw_lens(*self._point) 49 else: 50 self._window.cursor_handler.set_cursor_type(constants.NORMAL_CURSOR) 51 self._clear_lens() 52 self._last_lens_rect = None 53 54 enabled = property(get_enabled, set_enabled) 55 56 def _draw_lens(self, x, y): 57 '''Calculate what image data to put in the lens and update the cursor 58 with it; <x> and <y> are the positions of the cursor within the 59 main window layout area. 60 ''' 61 if self._window.images[0].get_storage_type() not in (Gtk.ImageType.PIXBUF, Gtk.ImageType.ANIMATION): 62 return 63 64 rectangle = self._calculate_lens_rect(x, y, prefs['lens size'], prefs['lens size']) 65 pixbuf = self._get_lens_pixbuf(x, y) 66 67 draw_region = Gdk.Rectangle() 68 draw_region.x, draw_region.y, draw_region.width, draw_region.height = rectangle 69 if self._last_lens_rect: 70 last_region = Gdk.Rectangle() 71 last_region.x, last_region.y, last_region.width, last_region.height = self._last_lens_rect 72 draw_region = Gdk.rectangle_union(draw_region, last_region) 73 74 window = self._window._main_layout.get_bin_window() 75 window.begin_paint_rect(draw_region) 76 77 self._clear_lens(rectangle) 78 79 cr = window.cairo_create() 80 surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, 0, window) 81 cr.set_source_surface(surface, rectangle[0], rectangle[1]) 82 cr.paint() 83 84 window.end_paint() 85 86 self._last_lens_rect = rectangle 87 88 def _calculate_lens_rect(self, x, y, width, height): 89 ''' Calculates the area where the lens will be drawn on screen. This method takes 90 screen space into calculation and moves the rectangle accordingly when the the rectangle 91 would otherwise flow over the allocated area. ''' 92 93 lens_x = max(x - width // 2, 0) 94 lens_y = max(y - height // 2, 0) 95 96 max_width, max_height = self._window.get_visible_area_size() 97 max_width += int(self._window._hadjust.get_value()) 98 max_height += int(self._window._vadjust.get_value()) 99 lens_x = min(lens_x, max_width - width) 100 lens_y = min(lens_y, max_height - height) 101 102 # Don't forget 1 pixel border... 103 return lens_x, lens_y, width + 2, height + 2 104 105 def _clear_lens(self, current_lens_region=None): 106 ''' Invalidates the area that was damaged by the last call to draw_lens. ''' 107 108 if not self._last_lens_rect: 109 return 110 111 window = self._window._main_layout.get_bin_window() 112 113 lrect = Gdk.Rectangle() 114 lrect.x, lrect.y, lrect.width, lrect.height = self._last_lens_rect 115 116 if not current_lens_region: 117 window.invalidate_rect(lrect, True) 118 return 119 120 crect = Gdk.Rectangle() 121 crect.x, crect.y, crect.width, crect.height = current_lens_region 122 rwidth = crect.width 123 rheigt = crect.height 124 125 intersectV = Gdk.Rectangle() 126 #movement to the right 127 if crect.x - lrect.x > 0: 128 intersectV.x=lrect.x 129 intersectV.y=lrect.y 130 intersectV.width = crect.x - lrect.x 131 intersectV.height = rheigt 132 else: #movement to the left 133 intersectV.x=crect.x + rwidth 134 intersectV.y=crect.y 135 intersectV.width = lrect.x - crect.x 136 intersectV.height = rheigt 137 138 window.invalidate_rect(intersectV, True) 139 140 intersectH = Gdk.Rectangle() 141 # movement down 142 if crect.y - lrect.y > 0: 143 intersectH.x = lrect.x 144 intersectH.y = lrect.y 145 intersectH.width = rwidth 146 intersectH.height = crect.y - lrect.y 147 else: #movement up 148 intersectH.x = lrect.x 149 intersectH.y = rheigt + crect.y 150 intersectH.width = rwidth 151 intersectH.height = lrect.y - crect.y 152 153 window.invalidate_rect(intersectH, True) 154 155 window.process_updates(True) 156 self._last_lens_rect = None 157 158 def toggle(self, action): 159 '''Toggle on or off the lens depending on the state of <action>.''' 160 self.enabled = action.get_active() 161 162 def _motion_event(self, widget, event): 163 ''' Called whenever the mouse moves over the image area. ''' 164 self._point = (int(event.x), int(event.y)) 165 if self.enabled: 166 self._draw_lens(*self._point) 167 168 def _get_lens_pixbuf(self, x, y): 169 '''Get a pixbuf containing the appropiate image data for the lens 170 where <x> and <y> are the positions of the cursor. 171 ''' 172 canvas = GdkPixbuf.Pixbuf.new(colorspace=GdkPixbuf.Colorspace.RGB, 173 has_alpha=True, bits_per_sample=8, 174 width=prefs['lens size'], 175 height=prefs['lens size']) 176 r,g,b,a = [int(p*255) for p in self._window.get_bg_color()] 177 canvas.fill(image_tools.convert_rgb16list_to_rgba8int([r,g,b])) 178 cb = self._window.layout.get_content_boxes() 179 source_pixbufs = self._window.imagehandler.get_pixbufs(len(cb)) 180 for i in range(len(cb)): 181 if image_tools.is_animation(source_pixbufs[i]): 182 continue 183 cpos = cb[i].get_position() 184 self._add_subpixbuf(canvas, x - cpos[0], y - cpos[1], 185 cb[i].get_size(), source_pixbufs[i]) 186 187 return image_tools.add_border(canvas, 1) 188 189 def _add_subpixbuf(self, canvas, x, y, image_size, source_pixbuf): 190 '''Copy a subpixbuf from <source_pixbuf> to <canvas> as it should 191 be in the lens if the coordinates <x>, <y> are the mouse pointer 192 position on the main window layout area. 193 194 The displayed image (scaled from the <source_pixbuf>) must have 195 size <image_size>. 196 ''' 197 # Prevent division by zero exceptions further down 198 if not image_size[0]: 199 return 200 201 # FIXME This merely prevents Errors being raised if source_pixbuf is an 202 # animation. The result might be broken, though, since animation, 203 # rotation etc. might not match or will be ignored: 204 source_pixbuf = image_tools.static_image(source_pixbuf) 205 206 rotation = prefs['rotation'] 207 if prefs['auto rotate from exif']: 208 rotation += image_tools.get_implied_rotation(source_pixbuf) 209 rotation = rotation % 360 210 211 if rotation in [90, 270]: 212 scale = float(source_pixbuf.get_height()) / image_size[0] 213 else: 214 scale = float(source_pixbuf.get_width()) / image_size[0] 215 216 x *= scale 217 y *= scale 218 219 source_mag = prefs['lens magnification'] / scale 220 width = height = prefs['lens size'] / source_mag 221 222 paste_left = x > width / 2 223 paste_top = y > height / 2 224 dest_x = max(0, int(math.ceil((width / 2 - x) * source_mag))) 225 dest_y = max(0, int(math.ceil((height / 2 - y) * source_mag))) 226 227 if rotation == 90: 228 x, y = y, source_pixbuf.get_height() - x 229 elif rotation == 180: 230 x = source_pixbuf.get_width() - x 231 y = source_pixbuf.get_height() - y 232 elif rotation == 270: 233 x, y = source_pixbuf.get_width() - y, x 234 if prefs['horizontal flip']: 235 if rotation in (90, 270): 236 y = source_pixbuf.get_height() - y 237 else: 238 x = source_pixbuf.get_width() - x 239 if prefs['vertical flip']: 240 if rotation in (90, 270): 241 x = source_pixbuf.get_width() - x 242 else: 243 y = source_pixbuf.get_height() - y 244 245 src_x = x - width / 2 246 src_y = y - height / 2 247 if src_x < 0: 248 width += src_x 249 src_x = 0 250 if src_y < 0: 251 height += src_y 252 src_y = 0 253 width = max(0, min(source_pixbuf.get_width() - src_x, width)) 254 height = max(0, min(source_pixbuf.get_height() - src_y, height)) 255 if width < 1 or height < 1: 256 return 257 258 subpixbuf = source_pixbuf.new_subpixbuf(int(src_x), int(src_y), 259 int(width), int(height)) 260 subpixbuf = subpixbuf.scale_simple( 261 int(math.ceil(source_mag * subpixbuf.get_width())), 262 int(math.ceil(source_mag * subpixbuf.get_height())), 263 prefs['scaling quality']) 264 265 if rotation == 90: 266 subpixbuf = subpixbuf.rotate_simple( 267 Gdk.PIXBUF_ROTATE_CLOCKWISE) 268 elif rotation == 180: 269 subpixbuf = subpixbuf.rotate_simple( 270 Gdk.PIXBUF_ROTATE_UPSIDEDOWN) 271 elif rotation == 270: 272 subpixbuf = subpixbuf.rotate_simple( 273 Gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE) 274 if prefs['horizontal flip']: 275 subpixbuf = subpixbuf.flip(horizontal=True) 276 if prefs['vertical flip']: 277 subpixbuf = subpixbuf.flip(horizontal=False) 278 279 subpixbuf = self._window.enhancer.enhance(subpixbuf) 280 281 if paste_left: 282 dest_x = 0 283 else: 284 dest_x = min(canvas.get_width() - subpixbuf.get_width(), dest_x) 285 if paste_top: 286 dest_y = 0 287 else: 288 dest_y = min(canvas.get_height() - subpixbuf.get_height(), dest_y) 289 290 if subpixbuf.get_has_alpha() and prefs['checkered bg for transparent images']: 291 subpixbuf = subpixbuf.composite_color_simple(subpixbuf.get_width(), subpixbuf.get_height(), 292 GdkPixbuf.InterpType.NEAREST, 255, 8, 0x777777, 0x999999) 293 294 subpixbuf.copy_area(0, 0, subpixbuf.get_width(), 295 subpixbuf.get_height(), canvas, dest_x, dest_y) 296 297# vim: expandtab:sw=4:ts=4 298