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