1# This file is part of MyPaint.
2# Copyright (C) 2015 by Andrew Chadwick <a.t.chadwick@gmail.com>
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8
9"""Footer widget behaviour."""
10
11
12## Imports
13from __future__ import division, print_function
14
15import math
16import logging
17
18import cairo
19
20from lib.gibindings import Gdk
21from lib.gibindings import GdkPixbuf
22
23import gui.brushmanager
24from gui.quickchoice import BrushChooserPopup  # noqa
25
26import lib.xml
27from lib.gettext import C_
28from gettext import gettext as _
29
30logger = logging.getLogger(__name__)
31
32
33## Class definitions
34
35class BrushIndicatorPresenter (object):
36    """Behaviour for a clickable footer brush indicator
37
38    This presenter's view is a DrawingArea instance
39    which is used to display the current brush's preview image.
40    Its model is the BrushManager instance belonging to the main app.
41    Both the view and the model
42    must be set after construction
43    and before or during realization.
44
45    When the view DrawingArea is clicked,
46    a QuickBrushChooser is popped up near it,
47    allowing the user to change the current brush.
48
49    The code assumes that
50    a single instance of the view DrawingArea
51    is packed into the lower right corner
52    (lower left, for rtl locales)
53    of the main drawing window's footer bar.
54
55    48px is a good width for the view widget.
56    If the preview is too tall to display fully,
57    it is drawn truncated with a cute gradient effect.
58    The user can hover the pointer to show a tooltip
59    with the full preview image.
60
61    """
62
63    _TOOLTIP_ICON_SIZE = 48
64    _EDGE_HIGHLIGHT_RGBA = (1, 1, 1, 0.25)
65    _OUTLINE_RGBA = (0, 0, 0, 0.4)
66    _DEFAULT_BRUSH_DISPLAY_NAME = _("Unknown Brush")
67    # FIXME: Use brushmanager.py's source string while we are in string
68    # FIXME: freeze.
69
70    ## Initialization
71
72    def __init__(self):
73        """Basic initialization"""
74        super(BrushIndicatorPresenter, self).__init__()
75        self._brush_preview = None
76        self._brush_name = self._DEFAULT_BRUSH_DISPLAY_NAME
77        self._brush_desc = None
78        self._drawing_area = None
79        self._brush_manager = None
80        self._chooser = None
81        self._click_button = None
82
83    def set_drawing_area(self, da):
84        """Set the view DrawingArea.
85
86        :param Gtk.DrawingArea da: the drawing area
87
88        The view should be set before or during its realization.
89
90        """
91        self._drawing_area = da
92        da.set_has_window(True)
93        da.add_events(
94            Gdk.EventMask.BUTTON_PRESS_MASK |
95            Gdk.EventMask.BUTTON_RELEASE_MASK
96        )
97        da.connect("draw", self._draw_cb)
98        da.connect("query-tooltip", self._query_tooltip_cb)
99        da.set_property("has-tooltip", True)
100        da.connect("button-press-event", self._button_press_cb)
101        da.connect("button-release-event", self._button_release_cb)
102
103    def set_brush_manager(self, bm):
104        """Set the model BrushManager.
105
106        :param gui.brushmanager.BrushManager bm: the model BrushManager
107
108        """
109        self._brush_manager = bm
110        bm.brush_selected += self._brush_selected_cb
111
112    def set_chooser(self, chooser):
113        """Set an optional popup, to be shown when clicked.
114
115        :param BrushChooserPopup chooser: popup to show
116
117        """
118        self._chooser = chooser
119
120    ## View event handlers
121
122    def _draw_cb(self, da, cr):
123        """Paint a preview of the current brush to the view."""
124        if not self._brush_preview:
125            cr.set_source_rgb(1, 0, 1)
126            cr.paint()
127            return
128        aw = da.get_allocated_width()
129        ah = da.get_allocated_height()
130
131        # Work in a temporary group so that
132        # the result can be masked with a gradient later.
133        cr.push_group()
134
135        # Paint a shadow line around the edge of
136        # where the the brush preview will go.
137        # There's an additional top border of one pixel
138        # for alignment with the color preview widget
139        # in the other corner.
140        cr.rectangle(1.5, 2.5, aw-3, ah)
141        cr.set_line_join(cairo.LINE_JOIN_ROUND)
142        cr.set_source_rgba(*self._OUTLINE_RGBA)
143        cr.set_line_width(3)
144        cr.stroke()
145
146        # Scale and align the brush preview in its own saved context
147        cr.save()
148        # Clip rectangle for the bit in the middle of the shadow.
149        # Note that the bottom edge isn't shadowed.
150        cr.rectangle(1, 2, aw-2, ah)
151        cr.clip()
152        # Scale and align the preview to the top of that clip rect.
153        preview = self._brush_preview
154        pw = preview.get_width()
155        ph = preview.get_height()
156        area_size = float(max(aw, ah)) - 2
157        preview_size = float(max(pw, ph))
158        x = math.floor(-pw/2.0)
159        y = 0
160        cr.translate(aw/2.0, 2)
161        scale = area_size / preview_size
162        cr.scale(scale, scale)
163        Gdk.cairo_set_source_pixbuf(cr, preview, x, y)
164        cr.paint()
165        cr.restore()
166
167        # Finally a highlight around the edge in the house style
168        # Note that the bottom edge isn't highlighted.
169        cr.rectangle(1.5, 2.5, aw-3, ah)
170        cr.set_line_width(1)
171        cr.set_source_rgba(*self._EDGE_HIGHLIGHT_RGBA)
172        cr.stroke()
173
174        # Paint the group within a gradient mask
175        cr.pop_group_to_source()
176        mask = cairo.LinearGradient(0, 0, 0, ah)
177        mask.add_color_stop_rgba(0.0, 1, 1, 1, 1.0)
178        mask.add_color_stop_rgba(0.8, 1, 1, 1, 1.0)
179        mask.add_color_stop_rgba(0.95, 1, 1, 1, 0.5)
180        mask.add_color_stop_rgba(1.0, 1, 1, 1, 0.1)
181        cr.mask(mask)
182
183    def _query_tooltip_cb(self, da, x, y, keyboard_mode, tooltip):
184        s = self._TOOLTIP_ICON_SIZE
185        scaled_pixbuf = self._get_scaled_pixbuf(s)
186        tooltip.set_icon(scaled_pixbuf)
187        brush_name = self._brush_name
188        if not brush_name:
189            brush_name = self._DEFAULT_BRUSH_DISPLAY_NAME
190            # Rare cases, see https://github.com/mypaint/mypaint/issues/402.
191            # Probably just after init.
192        template_params = {"brush_name": lib.xml.escape(brush_name)}
193        markup_template = C_(
194            "current brush indicator: tooltip (no-description case)",
195            u"<b>{brush_name}</b>",
196        )
197        if self._brush_desc:
198            markup_template = C_(
199                "current brush indicator: tooltip (description case)",
200                u"<b>{brush_name}</b>\n{brush_desc}",
201            )
202            template_params["brush_desc"] = lib.xml.escape(self._brush_desc)
203        markup = markup_template.format(**template_params)
204        tooltip.set_markup(markup)
205        # TODO: summarize changes?
206        return True
207
208    def _button_press_cb(self, widget, event):
209        if not self._chooser:
210            return False
211        if event.button != 1:
212            return False
213        if event.type != Gdk.EventType.BUTTON_PRESS:
214            return False
215        self._click_button = event.button
216        return True
217
218    def _button_release_cb(self, widget, event):
219        if event.button != self._click_button:
220            return False
221        self._click_button = None
222        chooser = self._chooser
223        if not chooser:
224            return
225        if chooser.get_visible():
226            chooser.hide()
227        else:
228            chooser.popup(
229                widget = self._drawing_area,
230                above = True,
231                textwards = False,
232                event = event,
233            )
234        return True
235
236    ## Model event handlers
237
238    def _brush_selected_cb(self, bm, brush, brushinfo):
239        if brush is None:
240            return
241        self._brush_preview = brush.preview.copy()
242        self._brush_name = brush.get_display_name()
243        self._brush_desc = brush.description
244        self._drawing_area.queue_draw()
245
246    ## Utility methods
247
248    def _get_scaled_pixbuf(self, size):
249        if self._brush_preview is None:
250            pixbuf = GdkPixbuf.Pixbuf.new(
251                GdkPixbuf.Colorspace.RGB,
252                False, 8, size, size,
253            )
254            pixbuf.fill(0x00ff00ff)
255            return pixbuf
256        else:
257            interp = GdkPixbuf.InterpType.BILINEAR
258            return self._brush_preview.scale_simple(size, size, interp)
259