Esempio n. 1
0
 def _configure_cb(self, widget, event):
     """Internal: Update size and prefs when window is adjusted"""
     # Constrain window to fit on its current monitor, if possible.
     screen = event.get_screen()
     mon = screen.get_monitor_at_point(event.x, event.y)
     mon_geom = screen.get_monitor_geometry(mon)
     # Constrain width and height
     w = clamp(int(event.width), self.MIN_WIDTH, self.MAX_WIDTH)
     h = clamp(int(event.height), self.MIN_HEIGHT, self.MAX_HEIGHT)
     # Constrain position
     x, y = event.x, event.y
     if y + h > mon_geom.y + mon_geom.height:
         y = mon_geom.y + mon_geom.height - h
     if x + w > mon_geom.x + mon_geom.width:
         x = mon_geom.x + mon_geom.width - w
     if x < mon_geom.x:
         x = mon_geom.x
     if y < mon_geom.y:
         y = mon_geom.y
     event_size = (event.x, event.y, event.width, event.height)
     ex, ey, ew, eh = [int(c) for c in event_size]
     x, y, w, h = [int(c) for c in (x, y, w, h)]
     if not self._corrected_pos:
         if (x, y) != (ex, ey):
             GObject.idle_add(self.move, x, y)
         if (w, h) != (ew, eh):
             GObject.idle_add(self.resize, w, h)
         self._corrected_pos = True
     # Record size
     self._size = (x, y, w, h)
     self.app.preferences[self._prefs_size_key] = (w, h)
Esempio n. 2
0
 def _configure_cb(self, widget, event):
     """Internal: Update size and prefs when window is adjusted"""
     # Constrain window to fit on its current monitor, if possible.
     screen = event.get_screen()
     mon = screen.get_monitor_at_point(event.x, event.y)
     mon_geom = screen.get_monitor_geometry(mon)
     # Constrain width and height
     w = clamp(int(event.width), self.MIN_WIDTH, self.MAX_WIDTH)
     h = clamp(int(event.height), self.MIN_HEIGHT, self.MAX_HEIGHT)
     # Constrain position
     x, y = event.x, event.y
     if y+h > mon_geom.y + mon_geom.height:
         y = mon_geom.y + mon_geom.height - h
     if x+w > mon_geom.x + mon_geom.width:
         x = mon_geom.x + mon_geom.width - w
     if x < mon_geom.x:
         x = mon_geom.x
     if y < mon_geom.y:
         y = mon_geom.y
     event_size = (event.x, event.y, event.width, event.height)
     ex, ey, ew, eh = [int(c) for c in event_size]
     x, y, w, h = [int(c) for c in (x, y, w, h)]
     if not self._corrected_pos:
         if (x, y) != (ex, ey):
             GLib.idle_add(self.move, x, y)
         if (w, h) != (ew, eh):
             GLib.idle_add(self.resize, w, h)
         self._corrected_pos = True
     # Record size
     self._size = (x, y, w, h)
     self.app.preferences[self._prefs_size_key] = (w, h)
Esempio n. 3
0
 def _interpolate_p0_p1(self):
     """Interpolate between p0 and p1, but do not step or clear"""
     pt0p, pt0 = self._pt0_prev, self._pt0
     pt1, pt1n = self._pt1, self._pt1_next
     can_interp = ( pt0 is not None and pt1 is not None and
                    len(self._np) > 0 )
     if can_interp:
         if pt0p is None:
             pt0p = pt0
         if pt1n is None:
             pt1n = pt1
         t0 = pt0[0]
         t1 = pt1[0]
         dt = t1 - t0
         can_interp = dt > 0
     if can_interp:
         for np in self._np:
             t, x, y = np[0:3]
             p, xt, yt = spline_4p(
                 float(t - t0) / dt,
                 array(pt0p[3:]), array(pt0[3:]),
                 array(pt1[3:]), array(pt1n[3:])
             )
             p = clamp(p, 0.0, 1.0)
             xt = clamp(xt, -1.0, 1.0)
             yt = clamp(yt, -1.0, 1.0)
             yield (t, x, y, p, xt, yt)
     if pt1 is not None:
         yield pt1
Esempio n. 4
0
    def _sizeify_flag_columns(self):
        """Sneakily scale the fixed size of the flag icons to match texts.

        This can only be called after the list has rendered once, because
        GTK doesn't know how tall the treeview's rows will be till then.
        Therefore it's called in an idle callback after the first show.

        """
        # Get the maximum height for all columns.
        s = 0
        for col in self._columns:
            ox, oy, w, h = col.cell_get_size(None)
            if h > s:
                s = h
        if not s:
            return

        # Set that as the fixed size of the flag icon columns,
        # within reason, and force a re-layout.
        h = helpers.clamp(s, 24, 48)
        w = helpers.clamp(s, 24, 48)
        for col in [self._flags1_col, self._flags2_col]:
            for cell in col.get_cells():
                cell.set_fixed_size(w, h)
            col.set_min_width(w)
        for col in self._columns:
            col.queue_resize()
Esempio n. 5
0
    def _sizeify_flag_columns(self):
        """Sneakily scale the fixed size of the flag icons to match texts.

        This can only be called after the list has rendered once, because
        GTK doesn't know how tall the treeview's rows will be till then.
        Therefore it's called in an idle callback after the first show.

        """
        # Get the maximum height for all columns.
        s = 0
        for col in self._columns:
            ox, oy, w, h = col.cell_get_size(None)
            if h > s:
                s = h
        if not s:
            return

        # Set that as the fixed size of the flag icon columns,
        # within reason, and force a re-layout.
        h = helpers.clamp(s, 24, 48)
        w = helpers.clamp(s, 24, 48)
        for col in [self._flags1_col, self._flags2_col]:
            for cell in col.get_cells():
                cell.set_fixed_size(w, h)
            col.set_min_width(w)
        for col in self._columns:
            col.queue_resize()
Esempio n. 6
0
 def _interpolate_p0_p1(self):
     """Interpolate between p0 and p1, but do not step or clear"""
     pt0p, pt0 = self._pt0_prev, self._pt0
     pt1, pt1n = self._pt1, self._pt1_next
     can_interp = (pt0 is not None and pt1 is not None
                   and len(self._np) > 0)
     if can_interp:
         if pt0p is None:
             pt0p = pt0
         if pt1n is None:
             pt1n = pt1
         t0 = pt0[0]
         t1 = pt1[0]
         dt = t1 - t0
         can_interp = dt > 0
     if can_interp:
         for np in self._np:
             t, x, y = np[0:3]
             p, xt, yt = spline_4p(
                 float(t - t0) / dt, array(pt0p[3:]), array(pt0[3:]),
                 array(pt1[3:]), array(pt1n[3:]))
             p = clamp(p, 0.0, 1.0)
             xt = clamp(xt, -1.0, 1.0)
             yt = clamp(yt, -1.0, 1.0)
             yield (t, x, y, p, xt, yt)
     if pt1 is not None:
         yield pt1
Esempio n. 7
0
def _get_paint_chip_highlight(color):
    """Paint chip highlight edge color"""
    highlight = HCYColor(color=color)
    ky = gui.style.PAINT_CHIP_HIGHLIGHT_HCY_Y_MULT
    kc = gui.style.PAINT_CHIP_HIGHLIGHT_HCY_C_MULT
    highlight.y = clamp(highlight.y * ky, 0, 1)
    highlight.c = clamp(highlight.c * kc, 0, 1)
    return highlight
Esempio n. 8
0
def _get_paint_chip_highlight(color):
    """Paint chip highlight edge color"""
    highlight = HCYColor(color=color)
    ky = gui.style.PAINT_CHIP_HIGHLIGHT_HCY_Y_MULT
    kc = gui.style.PAINT_CHIP_HIGHLIGHT_HCY_C_MULT
    highlight.y = clamp(highlight.y * ky, 0, 1)
    highlight.c = clamp(highlight.c * kc, 0, 1)
    return highlight
Esempio n. 9
0
def _get_paint_chip_shadow(color):
    """Paint chip shadow edge color"""
    shadow = HCYColor(color=color)
    ky = gui.style.PAINT_CHIP_SHADOW_HCY_Y_MULT
    kc = gui.style.PAINT_CHIP_SHADOW_HCY_C_MULT
    shadow.y = clamp(shadow.y * ky, 0, 1)
    shadow.c = clamp(shadow.c * kc, 0, 1)
    return shadow
Esempio n. 10
0
def _get_paint_chip_shadow(color):
    """Paint chip shadow edge color"""
    shadow = HCYColor(color=color)
    ky = gui.style.PAINT_CHIP_SHADOW_HCY_Y_MULT
    kc = gui.style.PAINT_CHIP_SHADOW_HCY_C_MULT
    shadow.y = clamp(shadow.y * ky, 0, 1)
    shadow.c = clamp(shadow.c * kc, 0, 1)
    return shadow
Esempio n. 11
0
 def set_color_hsv(self, hsv):
     h, s, v = hsv
     while h > 1.0: h -= 1.0
     while h < 0.0: h += 1.0
     s = clamp(s, 0.0, 1.0)
     v = clamp(v, 0.0, 1.0)
     if self.hsv_widget is not None:
         self.hsv_widget.set_color(h, s, v)
     else:
         color = gdk.color_from_hsv(h, s, v)
         self.color_sel.set_current_color(color)
Esempio n. 12
0
    def _process_queued_event(self, tdw, event_data):
        """Process one motion event from the motion queue"""
        drawstate = self._get_drawing_state(tdw)
        (time, x, y, pressure, xtilt, ytilt, viewzoom,
         viewrotation, barrel_rotation) = event_data
        model = tdw.doc

        # Calculate time delta for the brush engine
        last_event_time = drawstate.last_handled_event_time
        drawstate.last_handled_event_time = time
        if not last_event_time:
            return
        dtime = (time - last_event_time) / 1000.0
        if self._debug:
            cavg = drawstate.avgtime
            if cavg is not None:
                tavg, nevents = cavg
                nevents += 1
                tavg += (dtime - tavg) / nevents
            else:
                tavg = dtime
                nevents = 1
            if ((nevents * tavg) > 1.0) and nevents > 20:
                logger.debug("Processing at %d events/s (t_avg=%0.3fs)",
                             nevents, tavg)
                drawstate.avgtime = None
            else:
                drawstate.avgtime = (tavg, nevents)

        current_layer = model._layers.current
        if not current_layer.get_paintable():
            return

        # Feed data to the brush engine.  Pressure and tilt cleanup
        # needs to be done here to catch all forwarded data after the
        # earlier interpolations. The interpolation method used for
        # filling in missing axis data is known to generate
        # OverflowErrors for legitimate but pathological input streams.
        # https://github.com/mypaint/mypaint/issues/344

        pressure = clamp(pressure, 0.0, 1.0)
        xtilt = clamp(xtilt, -1.0, 1.0)
        ytilt = clamp(ytilt, -1.0, 1.0)
        self.stroke_to(model, dtime, x, y, pressure,
                       xtilt, ytilt, viewzoom,
                       viewrotation, barrel_rotation)

        # Update the TDW's idea of where we last painted
        # FIXME: this should live in the model, not the view
        if pressure:
            tdw.set_last_painting_pos((x, y))
Esempio n. 13
0
    def __unicode__(self):
        """Py2-era serialization as a Unicode string.

        Used by the Py3 __str__() while we are in transition.

        """
        result = u"GIMP Palette\n"
        if self._name is not None:
            result += u"Name: %s\n" % self._name
        if self._columns > 0:
            result += u"Columns: %d\n" % self._columns
        result += u"#\n"
        for col in self._colors:
            if col is self._EMPTY_SLOT_ITEM:
                col_name = self._EMPTY_SLOT_NAME
                r = g = b = 0
            else:
                col_name = col.__name
                r, g, b = [
                    clamp(int(c * 0xff), 0, 0xff) for c in col.get_rgb()
                ]
            if col_name is None:
                result += u"%d %d %d\n" % (r, g, b)
            else:
                result += u"%d %d %d    %s\n" % (r, g, b, col_name)
        return result
Esempio n. 14
0
 def _datafunc_get_pixbuf_height(initial, column, multiple=8, maximum=256):
     """Nearest multiple-of-n height for a pixbuf data cell."""
     ox, oy, w, h = column.cell_get_size(None)
     s = initial
     if h is not None:
         s = helpers.clamp((int(h // 8) * 8), s, maximum)
     return s
Esempio n. 15
0
 def _datafunc_get_pixbuf_height(initial, column, multiple=8, maximum=256):
     """Nearest multiple-of-n height for a pixbuf data cell."""
     ox, oy, w, h = column.cell_get_size(None)
     s = initial
     if h is not None:
         s = helpers.clamp((int(h // 8) * 8), s, maximum)
     return s
Esempio n. 16
0
    def _fixed_center(self, center=None, ongoing=True):
        """Keep a fixed center when zoom or rotation changes

        :param tuple center: Center of the rotation, display (X, Y)
        :param bool ongoing: Hint that this is in an ongoing change

        This context manager's cleanup phase applies a corrective
        transform which keeps the specified center in the same position
        on the screen. If the center isn't specified, the center pixel
        of the widget itself is used.

        It also queues a redraw.

        """
        # Determine the requested (or default) center in model space
        cx = None
        cy = None
        if center is not None:
            cx, cy = center
        if cx is None or cy is None:
            cx, cy = self.renderer.get_center()
        cx_model, cy_model = self.renderer.display_to_model(cx, cy)
        # Execute the changes in the body of the with statement
        yield
        # Corrective transform (& limits)
        self.renderer.scale = helpers.clamp(self.renderer.scale,
                                            self.renderer.zoom_min,
                                            self.renderer.zoom_max)
        cx_new, cy_new = self.renderer.model_to_display(cx_model, cy_model)
        self.renderer.translation_x += cx - cx_new
        self.renderer.translation_y += cy - cy_new
        # Redraw handling
        if ongoing:
            self.renderer.defer_hq_rendering()
        self.renderer.queue_draw()
Esempio n. 17
0
    def _fixed_center(self, center=None, ongoing=True):
        """Keep a fixed center when zoom or rotation changes

        :param tuple center: Center of the rotation, display (X, Y)
        :param bool ongoing: Hint that this is in an ongoing change

        This context manager's cleanup phase applies a corrective
        transform which keeps the specified center in the same position
        on the screen. If the center isn't specified, the center pixel
        of the widget itself is used.

        It also queues a redraw.

        """
        # Determine the requested (or default) center in model space
        cx = None
        cy = None
        if center is not None:
            cx, cy = center
        if cx is None or cy is None:
            cx, cy = self.renderer.get_center()
        cx_model, cy_model = self.renderer.display_to_model(cx, cy)
        # Execute the changes in the body of the with statement
        yield
        # Corrective transform (& limits)
        self.renderer.scale = helpers.clamp(self.renderer.scale,
                                            self.renderer.zoom_min,
                                            self.renderer.zoom_max)
        cx_new, cy_new = self.renderer.model_to_display(cx_model, cy_model)
        self.renderer.translation_x += cx - cx_new
        self.renderer.translation_y += cy - cy_new
        # Redraw handling
        if ongoing:
            self.renderer.defer_hq_rendering()
        self.renderer.queue_draw()
Esempio n. 18
0
def from_gdk_rgba(gdk_rgba):
    """Construct a new UIColor from a `Gdk.RGBA` (omitting alpha)

    >>> from_gdk_rgba(Gdk.RGBA(0.5, 0.8, 0.2, 1))
    <RGBColor r=0.5000, g=0.8000, b=0.2000>

    """
    rgbflt = (gdk_rgba.red, gdk_rgba.green, gdk_rgba.blue)
    return RGBColor(*[clamp(c, 0., 1.) for c in rgbflt])
Esempio n. 19
0
def from_gdk_rgba(gdk_rgba):
    """Construct a new UIColor from a `Gdk.RGBA` (omitting alpha)

    >>> from_gdk_rgba(Gdk.RGBA(0.5, 0.8, 0.2, 1))
    <RGBColor r=0.5000, g=0.8000, b=0.2000>

    """
    rgbflt = (gdk_rgba.red, gdk_rgba.green, gdk_rgba.blue)
    return RGBColor(*[clamp(c, 0., 1.) for c in rgbflt])
Esempio n. 20
0
 def motion_notify_cb(self, widget, event):
     if self.grabbed is None:
         return
     x, y = self.eventpoint(event.x, event.y)
     i = self.grabbed
     points = self.points
     # XXX this may fail for non contiguous groups.
     if i in self._ylock:
         i_candidate = None
         if x > points[max(self._ylock[i])][0]:
             i_candidate = max((i, ) + self._ylock[i])
         elif x < points[min(self._ylock[i])][0]:
             i_candidate = min((i, ) + self._ylock[i])
         if (i_candidate is not None
                 and abs(points[i][0] - points[i_candidate][0]) < 0.001):
             i = i_candidate
     out = False  # by default, points cannot be removed
     if i == len(points) - 1:
         # last point stays right
         left_bound = right_bound = 1.0
     elif i == 0:
         # first point stays left
         left_bound = right_bound = 0.0
     else:
         # other points can be dragged out
         left_bound = points[i - 1][0]
         right_bound = points[i + 1][0]
         margin = 0.02
         inside_x_bounds = left_bound - margin < x < right_bound + margin
         inside_y_bounds = -0.1 <= y <= 1.1
         out = not (self.npoints or (inside_x_bounds and inside_y_bounds))
     if out:
         points[i] = None
     else:
         y = clamp(y, 0.0, 1.0)
         if self.magnetic:
             x_diff = [abs(x - v) for v in self._SNAP_TO]
             y_diff = [abs(y - v) for v in self._SNAP_TO]
             if min(x_diff) < 0.015 and min(y_diff) < 0.015:
                 y = self._SNAP_TO[y_diff.index(min(y_diff))]
                 x = self._SNAP_TO[x_diff.index(min(x_diff))]
         x = clamp(x, left_bound, right_bound)
         self.set_point(i, (x, y))
     self.queue_draw()
Esempio n. 21
0
 def _load_common_flags_from_ora_elem(self, elem):
     attrs = elem.attrib
     self.name = unicode(attrs.get("name", ""))
     compop = str(attrs.get("composite-op", ""))
     self.mode = ORA_MODES_BY_OPNAME.get(compop, DEFAULT_MODE)
     self.opacity = helpers.clamp(float(attrs.get("opacity", "1.0")), 0.0, 1.0)
     visible = attrs.get("visibility", "visible").lower()
     self.visible = visible != "hidden"
     locked = attrs.get("edit-locked", "false").lower()
     self.locked = helpers.xsd2bool(locked)
     selected = attrs.get("selected", "false").lower()
     self.initially_selected = helpers.xsd2bool(selected)
Esempio n. 22
0
 def _load_common_flags_from_ora_elem(self, elem):
     attrs = elem.attrib
     self.name = unicode(attrs.get('name', ''))
     compop = str(attrs.get('composite-op', ''))
     self.mode = ORA_MODES_BY_OPNAME.get(compop, DEFAULT_MODE)
     self.opacity = helpers.clamp(float(attrs.get('opacity', '1.0')),
                                  0.0, 1.0)
     visible = attrs.get('visibility', 'visible').lower()
     self.visible = (visible != "hidden")
     locked = attrs.get("edit-locked", 'false').lower()
     self.locked = lib.xml.xsd2bool(locked)
     selected = attrs.get("selected", 'false').lower()
     self.initially_selected = lib.xml.xsd2bool(selected)
Esempio n. 23
0
 def _load_common_flags_from_ora_elem(self, elem):
     attrs = elem.attrib
     self.name = unicode(attrs.get('name', ''))
     compop = str(attrs.get('composite-op', ''))
     self.mode = ORA_MODES_BY_OPNAME.get(compop, DEFAULT_MODE)
     self.opacity = helpers.clamp(float(attrs.get('opacity', '1.0')),
                                  0.0, 1.0)
     visible = attrs.get('visibility', 'visible').lower()
     self.visible = (visible != "hidden")
     locked = attrs.get("edit-locked", 'false').lower()
     self.locked = lib.xml.xsd2bool(locked)
     selected = attrs.get("selected", 'false').lower()
     self.initially_selected = lib.xml.xsd2bool(selected)
Esempio n. 24
0
 def opacity(self, opacity):
     opacity = helpers.clamp(float(opacity), 0.0, 1.0)
     if opacity == self._opacity:
         return
     if self.mode == PASS_THROUGH_MODE:
         warn("Cannot change the change the opacity of a layer "
              "group using PASS_THROUGH_MODE",
              RuntimeWarning, stacklevel=2)
         return
     self._opacity = opacity
     self._properties_changed(["opacity"])
     bbox = tuple(self.get_full_redraw_bbox())
     self._content_changed(*bbox)
Esempio n. 25
0
 def rotozoom_with_center(self, function, at_pointer=False):
     if at_pointer and self.has_pointer and self.last_event_x is not None:
         cx, cy = self.last_event_x, self.last_event_y
     else:
         w, h = self.window.get_size()
         cx, cy = self.get_center()
     cx_model, cy_model = self.display_to_model(cx, cy)
     function()
     self.scale = helpers.clamp(self.scale, self.zoom_min, self.zoom_max)
     cx_new, cy_new = self.model_to_display(cx_model, cy_model)
     self.translation_x += cx - cx_new
     self.translation_y += cy - cy_new
     self.queue_draw()
Esempio n. 26
0
 def rotozoom_with_center(self, function, center=None):
     cx = None
     cy = None
     if center is not None:
         cx, cy = center
     if cx is None or cy is None:
         cx, cy = self.renderer.get_center()
     cx_model, cy_model = self.renderer.display_to_model(cx, cy)
     function()
     self.renderer.scale = helpers.clamp(self.renderer.scale, self.renderer.zoom_min, self.renderer.zoom_max)
     cx_new, cy_new = self.renderer.model_to_display(cx_model, cy_model)
     self.renderer.translation_x += cx - cx_new
     self.renderer.translation_y += cy - cy_new
     self.renderer.queue_draw()
 def rotozoom_with_center(self, function, at_pointer=False):
     if at_pointer and self.has_pointer and self.event_box.last_event_x is not None:
         cx, cy = self.event_box.last_event_x, self.event_box.last_event_y
     else:
         allocation = self.get_allocation()
         w, h = allocation.width, allocation.height
         cx, cy = self.renderer.get_center()
     cx_model, cy_model = self.renderer.display_to_model(cx, cy)
     function()
     self.renderer.scale = helpers.clamp(self.renderer.scale, self.renderer.zoom_min, self.renderer.zoom_max)
     cx_new, cy_new = self.renderer.model_to_display(cx_model, cy_model)
     self.renderer.translation_x += cx - cx_new
     self.renderer.translation_y += cy - cy_new
     self.renderer.queue_draw()
Esempio n. 28
0
 def opacity(self, opacity):
     opacity = helpers.clamp(float(opacity), 0.0, 1.0)
     if opacity == self._opacity:
         return
     if self.mode == PASS_THROUGH_MODE:
         warn(
             "Cannot change the change the opacity of a layer "
             "group using PASS_THROUGH_MODE",
             RuntimeWarning,
             stacklevel=2)
         return
     self._opacity = opacity
     self._properties_changed(["opacity"])
     bbox = tuple(self.get_full_redraw_bbox())
     self._content_changed(*bbox)
Esempio n. 29
0
 def nearest_move_target(self, x,y):
     """
     Returns the nearest (x, y) the selection indicator can move to during a
     move with the button held down.
     """
     area_source = self.area_at(self.press_x, self.press_y)
     area = self.area_at(x,y)
     if area == area_source and area in [AREA_CIRCLE, AREA_SQUARE]:
         return x,y
     dx = x-self.x0
     dy = y-self.y0
     d = sqrt(dx*dx+dy*dy)
     x1 = dx/d
     y1 = dy/d
     if area_source == AREA_CIRCLE:
         rx = self.x0 + (self.r2+3.0)*x1
         ry = self.y0 + (self.r2+3.0)*y1
     else:
         m = self.m
         dx = clamp(dx, -m, m)
         dy = clamp(dy, -m, m)
         rx = self.x0 + dx
         ry = self.y0 + dy
     return rx,ry
Esempio n. 30
0
    def anim_cb(self):
        """Animation callback.

        Each step fades the alpha multiplier slightly and invalidates the area
        last painted.
        """
        self.alpha -= 1 / (self.fade_fps * self.fade_duration)
        self.alpha = clamp(self.alpha, 0.0, 1.0)

        if self.__area:
            self.tdw.queue_draw_area(*self.__area)
        if self.alpha <= 0.0:
            self.__anim_srcid = None
            return False
        else:
            return True
    def rotozoom_with_center(self, function, at_pointer=False):
        if at_pointer and self.has_pointer and self.last_event_x is not None:
            cx, cy = self.last_event_x, self.last_event_y
        else:
            w, h = self.window.get_size()
            cx, cy = self.get_center()
        cr = self.get_model_coordinates_cairo_context()
        cx_device, cy_device = cr.device_to_user(cx, cy)
        function()
        self.scale = helpers.clamp(self.scale, self.zoom_min, self.zoom_max)
        cr = self.get_model_coordinates_cairo_context()
        cx_new, cy_new = cr.user_to_device(cx_device, cy_device)
        self.translation_x += cx - cx_new
        self.translation_y += cy - cy_new

        self.queue_draw()
Esempio n. 32
0
 def rotozoom_with_center(self, function, center=None):
     cx = None
     cy = None
     if center is not None:
         cx, cy = center
     if cx is None or cy is None:
         cx, cy = self.renderer.get_center()
     cx_model, cy_model = self.renderer.display_to_model(cx, cy)
     function()
     self.renderer.scale = helpers.clamp(self.renderer.scale,
                                         self.renderer.zoom_min,
                                         self.renderer.zoom_max)
     cx_new, cy_new = self.renderer.model_to_display(cx_model, cy_model)
     self.renderer.translation_x += cx - cx_new
     self.renderer.translation_y += cy - cy_new
     self.renderer.queue_draw()
Esempio n. 33
0
    def anim_cb(self):
        """Animation callback.

        Each step fades the alpha multiplier slightly and invalidates the area
        last painted.
        """
        self.alpha -= 1.0 / (float(self.fade_fps) * self.fade_duration)
        self.alpha = clamp(self.alpha, 0.0, 1.0)

        if self.__area:
            self.tdw.queue_draw_area(*self.__area)
        if self.alpha <= 0.0:
            self.__anim_srcid = None
            return False
        else:
            return True
    def rotozoom_with_center(self, function, at_pointer=False):
        if at_pointer and self.has_pointer and self.last_event_x is not None:
            cx, cy = self.last_event_x, self.last_event_y
        else:
            w, h = self.window.get_size()
            cx, cy = self.get_center()
        cr = self.get_model_coordinates_cairo_context()
        cx_device, cy_device = cr.device_to_user(cx, cy)
        function()
        self.scale = helpers.clamp(self.scale, self.zoom_min, self.zoom_max)
        cr = self.get_model_coordinates_cairo_context()
        cx_new, cy_new = cr.user_to_device(cx_device, cy_device)
        self.translation_x += cx - cx_new
        self.translation_y += cy - cy_new

        self.queue_draw()
Esempio n. 35
0
    def anim_cb(self):
        """Animation callback.

        Each step fades the alpha multiplier slightly and invalidates the area
        last painted.
        """
        self.alpha -= 1.0 / (float(self.fade_fps) * self.fade_duration)
        self.alpha = clamp(self.alpha, 0.0, 1.0)
        win = self.tdw.get_window()
        if win is not None:
            if self.__area:
                win.invalidate_rect(gdk.Rectangle(*self.__area), True)
        if self.alpha <= 0.0:
            self.__anim_srcid = None
            return False
        else:
            return True
Esempio n. 36
0
def new_blank_pixbuf(rgb, w, h):
    """Create a blank pixbuf with all pixels set to a color

    :param tuple rgb: Color to blank the pixbuf to (``R,G,B``, floats)
    :param int w: Width for the new pixbuf
    :param int h: Width for the new pixbuf

    The returned pixbuf has no alpha channel.

    """
    pixbuf = GdkPixbuf.Pixbuf.new(
        GdkPixbuf.Colorspace.RGB, False, 8,
        w, h,
    )
    r, g, b = (helpers.clamp(int(round(0xff*x)), 0, 0xff) for x in rgb)
    rgba_pixel = (r<<24) + (g<<16) + (b<<8) + 0xff
    pixbuf.fill(rgba_pixel)
    return pixbuf
Esempio n. 37
0
 def opacity(self, opacity):
     opacity = helpers.clamp(float(opacity), 0.0, 1.0)
     if opacity == self._opacity:
         return
     if self.mode == PASS_THROUGH_MODE:
         warn("Cannot change the change the opacity multiplier "
              "of a layer group in PASS_THROUGH_MODE",
              RuntimeWarning, stacklevel=2)
         return
     self._opacity = opacity
     self._properties_changed(["opacity"])
     # Note: not the full_redraw_bbox here.
     # Changing a layer's opacity multiplier alone cannot change the
     # calculated alpha of an outlying empty tile in the layer.
     # Those are always zero. Even if the layer has a fancy masking
     # mode, that won't affect redraws arising from mere opacity
     # multiplier updates.
     bbox = tuple(self.get_bbox())
     self._content_changed(*bbox)
Esempio n. 38
0
 def opacity(self, opacity):
     opacity = helpers.clamp(float(opacity), 0.0, 1.0)
     if opacity == self._opacity:
         return
     if self.mode == PASS_THROUGH_MODE:
         warn("Cannot change the change the opacity multiplier "
              "of a layer group in PASS_THROUGH_MODE",
              RuntimeWarning, stacklevel=2)
         return
     self._opacity = opacity
     self._properties_changed(["opacity"])
     # Note: not the full_redraw_bbox here.
     # Changing a layer's opacity multiplier alone cannot change the
     # calculated alpha of an outlying empty tile in the layer.
     # Those are always zero. Even if the layer has a fancy masking
     # mode, that won't affect redraws arising from mere opacity
     # multiplier updates.
     bbox = tuple(self.get_bbox())
     self._content_changed(*bbox)
Esempio n. 39
0
def new_blank_pixbuf(rgb, w, h):
    """Create a blank pixbuf with all pixels set to a color

    :param tuple rgb: Color to blank the pixbuf to (``R,G,B``, floats)
    :param int w: Width for the new pixbuf
    :param int h: Width for the new pixbuf

    The returned pixbuf has no alpha channel.

    """
    pixbuf = GdkPixbuf.Pixbuf.new(
        GdkPixbuf.Colorspace.RGB,
        False,
        8,
        w,
        h,
    )
    r, g, b = (helpers.clamp(int(round(0xff * x)), 0, 0xff) for x in rgb)
    rgba_pixel = (r << 24) + (g << 16) + (b << 8) + 0xff
    pixbuf.fill(rgba_pixel)
    return pixbuf
Esempio n. 40
0
 def __unicode__(self):
     result = u"GIMP Palette\n"
     if self._name is not None:
         result += u"Name: %s\n" % self._name
     if self._columns > 0:
         result += u"Columns: %d\n" % self._columns
     result += u"#\n"
     for col in self._colors:
         if col is self._EMPTY_SLOT_ITEM:
             col_name = self._EMPTY_SLOT_NAME
             r = g = b = 0
         else:
             col_name = col.__name
             r, g, b = [
                 clamp(int(c * 0xff), 0, 0xff) for c in col.get_rgb()
             ]
         if col_name is None:
             result += u"%d %d %d\n" % (r, g, b)
         else:
             result += u"%d %d %d    %s\n" % (r, g, b, col_name)
     return result
Esempio n. 41
0
    def button_press_cb(self, widget, event):
        if not (self.points or event.button == 1):
            return
        x, y = self.eventpoint(event.x, event.y)

        # Note: Squared distance used for comparisons
        def dist_squared(p):
            return abs(x - p[0])**2 + abs(y - p[1])**2
        points = self.points
        dsq, pos = min((dist_squared(p), i) for i, p in enumerate(points))

        # Unless the number of points are fixed, maxed out, or the intent
        # was to move an existing point, insert a new curve point.
        if not (self.npoints or dsq <= 0.003 or len(points) >= self.maxpoints):
            candidates = [i+1 for i, (px, _) in enumerate(points) if px < x]
            insert_pos = candidates and candidates[-1]
            if insert_pos and insert_pos < len(points):
                points.insert(insert_pos, (x, clamp(y, 0.0, 1.0)))
                pos = insert_pos
                self.queue_draw()

        self.grabbed = pos
Esempio n. 42
0
    def __unicode__(self):
        """Py2-era serialization as a Unicode string.

        Used by the Py3 __str__() while we are in transition.

        """
        result = u"GIMP Palette\n"
        if self._name is not None:
            result += u"Name: %s\n" % self._name
        if self._columns > 0:
            result += u"Columns: %d\n" % self._columns
        result += u"#\n"
        for col in self._colors:
            if col is self._EMPTY_SLOT_ITEM:
                col_name = self._EMPTY_SLOT_NAME
                r = g = b = 0
            else:
                col_name = col.__name
                r, g, b = [clamp(int(c*0xff), 0, 0xff) for c in col.get_rgb()]
            if col_name is None:
                result += u"%d %d %d\n" % (r, g, b)
            else:
                result += u"%d %d %d    %s\n" % (r, g, b, col_name)
        return result
Esempio n. 43
0
    def motion_notify_cb(self, tdw, event, fakepressure=None):
        """Motion event handler: queues raw input and returns

        :param tdw: The TiledDrawWidget receiving the event
        :param event: the MotionNotify event being handled
        :param fakepressure: fake pressure to use if no real pressure

        Fake pressure is passed with faked motion events, e.g.
        button-press and button-release handlers for mouse events.

        """

        # Do nothing if painting is inactivated
        current_layer = tdw.doc._layers.current
        if not (tdw.is_sensitive and current_layer.get_paintable()):
            return False

        # If the device has changed and the last pressure value from the
        # previous device is not equal to 0.0, this can leave a visible
        # stroke on the layer even if the 'new' device is not pressed on
        # the tablet and has a pressure axis == 0.0.  Resetting the brush
        # when the device changes fixes this issue, but there may be a
        # much more elegant solution that only resets the brush on this
        # edge-case.
        same_device = True
        if tdw.app is not None:
            device = event.get_source_device()
            same_device = tdw.app.device_monitor.device_used(device)
            if not same_device:
                tdw.doc.brush.reset()

        # Extract the raw readings for this event
        x = event.x
        y = event.y
        time = event.time
        pressure = event.get_axis(Gdk.AxisUse.PRESSURE)
        xtilt = event.get_axis(Gdk.AxisUse.XTILT)
        ytilt = event.get_axis(Gdk.AxisUse.YTILT)
        viewzoom = tdw.scale
        viewrotation = tdw.rotation
        barrel_rotation = event.get_axis(Gdk.AxisUse.WHEEL)
        state = event.state

        # Workaround for buggy evdev behaviour.
        # Events sometimes get a zero raw pressure reading when the
        # pressure reading has not changed. This results in broken
        # lines. As a workaround, forbid zero pressures if there is a
        # button pressed down, and substitute the last-known good value.
        # Detail: https://github.com/mypaint/mypaint/issues/29
        drawstate = self._get_drawing_state(tdw)
        if drawstate.button_down is not None:
            if pressure == 0.0:
                pressure = drawstate.last_good_raw_pressure
            elif pressure is not None and np.isfinite(pressure):
                drawstate.last_good_raw_pressure = pressure

        # Ensure each event has a defined pressure
        if pressure is not None:
            # Using the reported pressure. Apply some sanity checks
            if not np.isfinite(pressure):
                # infinity/nan: use button state (instead of clamping in
                # brush.hpp) https://gna.org/bugs/?14709
                pressure = None
            else:
                pressure = clamp(pressure, 0.0, 1.0)
            drawstate.last_event_had_pressure = True

        # Fake the pressure if we have none, or if infinity was reported
        if pressure is None:
            if fakepressure is not None:
                pressure = clamp(fakepressure, 0.0, 1.0)
            else:
                pressure = ((state & Gdk.ModifierType.BUTTON1_MASK)
                            and tdw.app.fakepressure or 0.0)
            drawstate.last_event_had_pressure = False

        # Check whether tilt is present.  For some tablets without
        # tilt support GTK reports a tilt axis with value nan, instead
        # of None.  https://gna.org/bugs/?17084
        if xtilt is None or ytilt is None or not np.isfinite(xtilt + ytilt):
            xtilt = 0.0
            ytilt = 0.0

        # Switching from a non-tilt device to a device which reports
        # tilt can cause GDK to return out-of-range tilt values, on X11.
        xtilt = clamp(xtilt, -1.0, 1.0)
        ytilt = clamp(ytilt, -1.0, 1.0)

        tilt_ascension = 0.5 * math.atan2(-xtilt, ytilt) / math.pi

        # Offset barrel rotation if wanted
        # This could be used to correct for different devices,
        # Left vs Right handed, etc.
        b_offset = tdw.app.preferences.get("input.barrel_rotation_offset")
        if (barrel_rotation is not None):
            barrel_rotation = (barrel_rotation + b_offset) % 1.0

        # barrel_rotation is likely affected by ascension (a bug?)
        # lets compensate but allow disabling
        if (barrel_rotation is not None and tdw.app.preferences.get(
                "input.barrel_rotation_subtract_ascension")):
            barrel_rotation = (barrel_rotation - tilt_ascension) % 1.0

        # If WHEEL is missing (barrel_rotation)
        # Use the fakerotation controller to allow keyboard control
        # We can't trust None so also look at preference
        if (barrel_rotation is None
                or not tdw.app.preferences.get("input.use_barrel_rotation")):
            barrel_rotation = (tdw.app.fakerotation + b_offset) % 1.0

        # Evdev workaround. X and Y tilts suffer from the same
        # problem as pressure for fancier devices.
        if drawstate.button_down is not None:
            if xtilt == 0.0:
                xtilt = drawstate.last_good_raw_xtilt
            else:
                drawstate.last_good_raw_xtilt = xtilt
            if ytilt == 0.0:
                ytilt = drawstate.last_good_raw_ytilt
            else:
                drawstate.last_good_raw_ytilt = ytilt

        if tdw.mirrored:
            xtilt *= -1.0
            barrel_rotation *= -1.0

        # Apply pressure mapping if we're running as part of a full
        # MyPaint application (and if there's one defined).
        if tdw.app is not None and tdw.app.pressure_mapping:
            pressure = tdw.app.pressure_mapping(pressure)

        # Apply any configured while-drawing cursor
        if pressure > 0:
            self._hide_drawing_cursor(tdw)
        else:
            self._reinstate_drawing_cursor(tdw)

        # Queue this event
        x, y = tdw.display_to_model(x, y)

        event_data = (time, x, y, pressure, xtilt, ytilt, viewzoom,
                      viewrotation, barrel_rotation)
        drawstate.queue_motion(event_data)
        # Start the motion event processor, if it isn't already running
        if not drawstate.motion_processing_cbid:
            cbid = GLib.idle_add(
                self._motion_queue_idle_cb,
                tdw,
                priority=self.MOTION_QUEUE_PRIORITY,
            )
            drawstate.motion_processing_cbid = cbid
Esempio n. 44
0
 def layer_decrease_opacity(self, action):
     opa = helpers.clamp(self.model.layer.opacity - 0.08, 0.0, 1.0)
     self.model.set_layer_opacity(opa)
Esempio n. 45
0
    def __init__(self, app, actions, config_name):
        """Initialize.

        :param app: the main Application object.
        :param iterable actions: keyboard action names to pass through.
        :param str config_name: config prefix for saving window size.

        Use a simple "lowercase_with_underscores" name for the
        configuration key prefix.

        See also: `gui.keyboard.KeyboardManager.add_window()`.
        """
        # Superclass
        Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP)
        self.set_modal(True)

        # Internal state
        self.app = app
        self._size = None  # last recorded size from any show()
        self._motion_handler_id = None
        self._prefs_size_key = "%s.window_size" % (config_name, )
        self._resize_info = None  # state during an edge resize
        self._outside_grab_active = False
        self._outside_cursor = Gdk.Cursor(Gdk.CursorType.LEFT_PTR)
        self._popup_info = None

        # Initial positioning
        self._initial_move_pos = None  # used when forcing a specific position
        self._corrected_pos = None  # used when keeping the widget on-screen

        # Resize cursors
        self._edge_cursors = {}
        for edge, cursor in self.EDGE_CURSORS.iteritems():
            if cursor is not None:
                cursor = Gdk.Cursor(cursor)
            self._edge_cursors[edge] = cursor

        # Default size
        self.set_gravity(Gdk.Gravity.NORTH_WEST)
        default_size = (self.MIN_WIDTH, self.MIN_HEIGHT)
        w, h = app.preferences.get(self._prefs_size_key, default_size)
        w = clamp(int(w), self.MIN_WIDTH, self.MAX_WIDTH)
        h = clamp(int(h), self.MIN_HEIGHT, self.MAX_HEIGHT)
        default_size = (w, h)
        self.set_transient_for(app.drawWindow)
        self.set_default_size(*default_size)
        self.set_position(Gtk.WindowPosition.MOUSE)

        # Register with the keyboard manager, but only let certain actions be
        # driven from the keyboard.
        app.kbm.add_window(self, actions)

        # Event handlers
        self.connect("realize", self._realize_cb)
        self.connect("configure-event", self._configure_cb)
        self.connect("enter-notify-event", self._crossing_cb)
        self.connect("leave-notify-event", self._crossing_cb)
        self.connect("show", self._show_cb)
        self.connect("hide", self._hide_cb)
        self.connect("button-press-event", self._button_press_cb)
        self.connect("button-release-event", self._button_release_cb)
        self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK
                        | Gdk.EventMask.BUTTON_RELEASE_MASK)

        # Appearance
        self._frame = Gtk.Frame()
        self._frame.set_shadow_type(Gtk.ShadowType.OUT)
        self._align = Gtk.Alignment(0.5, 0.5, 1.0, 1.0)
        self._align.set_padding(self.EDGE_SIZE, self.EDGE_SIZE, self.EDGE_SIZE,
                                self.EDGE_SIZE)
        self._frame.add(self._align)
        Gtk.Window.add(self, self._frame)
Esempio n. 46
0
    def __init__(self, app, actions, config_name):
        """Initialize.

        :param app: the main Application object.
        :param iterable actions: keyboard action names to pass through.
        :param str config_name: config prefix for saving window size.

        Use a simple "lowercase_with_underscores" name for the
        configuration key prefix.

        See also: `gui.keyboard.KeyboardManager.add_window()`.
        """
        # Superclass
        Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP)
        self.set_modal(True)

        # Internal state
        self.app = app
        self._size = None  # last recorded size from any show()
        self._motion_handler_id = None
        self._prefs_size_key = "%s.window_size" % (config_name,)
        self._resize_info = None   # state during an edge resize
        self._outside_grab_active = False
        self._outside_cursor = Gdk.Cursor(Gdk.CursorType.LEFT_PTR)
        self._popup_info = None

        # Initial positioning
        self._initial_move_pos = None  # used when forcing a specific position
        self._corrected_pos = None  # used when keeping the widget on-screen

        # Resize cursors
        self._edge_cursors = {}
        for edge, cursor in self.EDGE_CURSORS.iteritems():
            if cursor is not None:
                cursor = Gdk.Cursor(cursor)
            self._edge_cursors[edge] = cursor

        # Default size
        self.set_gravity(Gdk.Gravity.NORTH_WEST)
        default_size = (self.MIN_WIDTH, self.MIN_HEIGHT)
        w, h = app.preferences.get(self._prefs_size_key, default_size)
        w = clamp(int(w), self.MIN_WIDTH, self.MAX_WIDTH)
        h = clamp(int(h), self.MIN_HEIGHT, self.MAX_HEIGHT)
        default_size = (w, h)
        self.set_transient_for(app.drawWindow)
        self.set_default_size(*default_size)
        self.set_position(Gtk.WindowPosition.MOUSE)

        # Register with the keyboard manager, but only let certain actions be
        # driven from the keyboard.
        app.kbm.add_window(self, actions)

        # Event handlers
        self.connect("realize", self._realize_cb)
        self.connect("configure-event", self._configure_cb)
        self.connect("enter-notify-event", self._crossing_cb)
        self.connect("leave-notify-event", self._crossing_cb)
        self.connect("show", self._show_cb)
        self.connect("hide", self._hide_cb)
        self.connect("button-press-event", self._button_press_cb)
        self.connect("button-release-event", self._button_release_cb)
        self.add_events( Gdk.EventMask.BUTTON_PRESS_MASK |
                         Gdk.EventMask.BUTTON_RELEASE_MASK )

        # Appearance
        self._frame = Gtk.Frame()
        self._frame.set_shadow_type(Gtk.ShadowType.OUT)
        self._align = Gtk.Alignment.new(0.5, 0.5, 1.0, 1.0)
        self._align.set_padding( self.EDGE_SIZE, self.EDGE_SIZE,
                                 self.EDGE_SIZE, self.EDGE_SIZE )
        self._frame.add(self._align)
        Gtk.Window.add(self, self._frame)
Esempio n. 47
0
    def render_execute(self, cr, surface, sparse, mipmap_level, clip_region):
        translation_only = self.is_translation_only()
        model_bbox = surface.x, surface.y, surface.w, surface.h

        #print 'model bbox', model_bbox

        # not sure if it is a good idea to clip so tightly
        # has no effect right now because device_bbox is always smaller
        cr.rectangle(*model_bbox)
        cr.clip()

        layers = self.get_visible_layers()

        if self.visualize_rendering:
            surface.pixbuf.fill((int(random.random()*0xff)<<16)+0x00000000)

        background = None
        if self.current_layer_solo:
            background = self.neutral_background_pixbuf
            layers = [self.doc.layer]
            # this is for hiding instead
            #layers.pop(self.doc.layer_idx)
        if self.overlay_layer:
            idx = layers.index(self.doc.layer)
            layers.insert(idx+1, self.overlay_layer)

        # Composite
        tiles = [(tx, ty) for tx, ty in surface.get_tiles() if tile_is_visible(cr, tx, ty, clip_region, sparse, translation_only)]
        self.doc.render_into(surface, tiles, mipmap_level, layers, background)

        if translation_only and not pygtkcompat.USE_GTK3:
            # not sure why, but using gdk directly is notably faster than the same via cairo
            x, y = cr.user_to_device(surface.x, surface.y)
            self.window.draw_pixbuf(None, surface.pixbuf, 0, 0, int(x), int(y),
                                    dither=gdk.RGB_DITHER_MAX)
        else:
            #print 'Position (screen coordinates):', cr.user_to_device(surface.x, surface.y)
            if pygtkcompat.USE_GTK3:
                gdk.cairo_set_source_pixbuf(cr, surface.pixbuf,
                                            round(surface.x), round(surface.y))
            else:
                cr.set_source_pixbuf(surface.pixbuf, round(surface.x), round(surface.y))
            pattern = cr.get_source()

            # We could set interpolation mode here (eg nearest neighbour)
            #pattern.set_filter(cairo.FILTER_NEAREST)  # 1.6s
            #pattern.set_filter(cairo.FILTER_FAST)     # 2.0s
            #pattern.set_filter(cairo.FILTER_GOOD)     # 3.1s
            #pattern.set_filter(cairo.FILTER_BEST)     # 3.1s
            #pattern.set_filter(cairo.FILTER_BILINEAR) # 3.1s

            if self.scale > 2.8:
                # pixelize at high zoom-in levels
                pattern.set_filter(cairo.FILTER_NEAREST)

            cr.paint()

        if self.doc.frame_enabled:
            # Draw a overlay for all the area outside the "document area"
            cr.save()
            frame_rgba = self.app.preferences["frame.color_rgba"]
            frame_rgba = [helpers.clamp(c, 0, 1) for c in frame_rgba]
            cr.set_source_rgba(*frame_rgba)
            cr.set_operator(cairo.OPERATOR_OVER)
            mipmap_factor = 2**mipmap_level
            frame = self.doc.get_frame()
            cr.rectangle(frame[0]/mipmap_factor, frame[1]/mipmap_factor,
                            frame[2]/mipmap_factor, frame[3]/mipmap_factor)
            cr.rectangle(*model_bbox)
            cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
            cr.fill()
            cr.restore()

        if self.visualize_rendering:
            # visualize painted bboxes (blue)
            cr.set_source_rgba(0, 0, random.random(), 0.4)
            cr.paint()
Esempio n. 48
0
    def motion_notify_cb(self, tdw, event, fakepressure=None):
        """Motion event handler: queues raw input and returns

        :param tdw: The TiledDrawWidget receiving the event
        :param event: the MotionNotify event being handled
        :param fakepressure: fake pressure to use if no real pressure

        Fake pressure is passed with faked motion events, e.g.
        button-press and button-release handlers for mouse events.

        GTK 3.8 and above does motion compression, forcing our use of
        event filter hackery to obtain the high-resolution event
        positions required for making brushstrokes. This handler is
        still called for the events the GDK compression code lets
        through, and it is the only source of pressure and tilt info
        available when motion compression is active.
        """

        # Do nothing if painting is inactivated
        current_layer = tdw.doc._layers.current
        if not ( tdw.is_sensitive and current_layer.get_paintable() ):
            return False

        # Try and initialize an event filter, used to circumvent the
        # unhelpful motion event compression of newer GDKs. This filter
        # passes through all events, but motion events are translated
        # and passed to queue_motion_event separately.
        drawstate = self._get_drawing_state(tdw)
        if drawstate.evhack_data is None:
            self._add_evhack(tdw)

        # If the device has changed and the last pressure value from the
        # previous device is not equal to 0.0, this can leave a visible
        # stroke on the layer even if the 'new' device is not pressed on
        # the tablet and has a pressure axis == 0.0.  Reseting the brush
        # when the device changes fixes this issue, but there may be a
        # much more elegant solution that only resets the brush on this
        # edge-case.
        same_device = True
        if tdw.app is not None:
            device = event.get_source_device()
            same_device = tdw.app.device_monitor.device_used(device)
            if not same_device:
                tdw.doc.brush.reset()

        # Extract the raw readings for this event
        x = event.x
        y = event.y
        time = event.time
        pressure = event.get_axis(gdk.AXIS_PRESSURE)
        xtilt = event.get_axis(gdk.AXIS_XTILT)
        ytilt = event.get_axis(gdk.AXIS_YTILT)
        state = event.state

        # Ensure each non-evhack event has a defined pressure
        if pressure is not None:
            # Using the reported pressure. Apply some sanity checks
            if not isfinite(pressure):
                # infinity/nan: use button state (instead of clamping in
                # brush.hpp) https://gna.org/bugs/?14709
                pressure = None
            else:
                pressure = clamp(pressure, 0.0, 1.0)
            drawstate.last_event_had_pressure = True

        # Fake the pressure if we have none, or if infinity was reported
        if pressure is None:
            if fakepressure is not None:
                pressure = clamp(fakepressure, 0.0, 1.0)
            else:
                pressure = (state & gdk.BUTTON1_MASK) and 0.5 or 0.0
            drawstate.last_event_had_pressure = False

        # Check whether tilt is present.  For some tablets without
        # tilt support GTK reports a tilt axis with value nan, instead
        # of None.  https://gna.org/bugs/?17084
        if xtilt is None or ytilt is None or not isfinite(xtilt+ytilt):
            xtilt = 0.0
            ytilt = 0.0
        else:
            # Tilt inputs are assumed to be relative to the viewport,
            # but the canvas may be rotated or mirrored, or both.
            # Compensate before passing them to the brush engine.
            # https://gna.org/bugs/?19988
            if tdw.mirrored:
                xtilt *= -1.0
            if tdw.rotation != 0:
                tilt_angle = math.atan2(ytilt, xtilt) - tdw.rotation
                tilt_magnitude = math.sqrt((xtilt**2) + (ytilt**2))
                xtilt = tilt_magnitude * math.cos(tilt_angle)
                ytilt = tilt_magnitude * math.sin(tilt_angle)

        # HACK: color picking, do not paint
        # TEST: Does this ever happen now?
        if state & gdk.CONTROL_MASK or state & gdk.MOD1_MASK:
            # Don't simply return; this is a workaround for unwanted
            # lines in https://gna.org/bugs/?16169
            pressure = 0.0

        # Apply pressure mapping if we're running as part of a full
        # MyPaint application (and if there's one defined).
        if tdw.app is not None and tdw.app.pressure_mapping:
            pressure = tdw.app.pressure_mapping(pressure)

        # HACK: straight line mode?
        # TEST: Does this ever happen?
        if state & gdk.SHIFT_MASK:
            pressure = 0.0

        # If the eventhack filter caught more than one event, push them
        # onto the motion event queue. Pressures and tilts will be
        # interpolated from surrounding motion-notify events.
        if len(drawstate.evhack_positions) > 1:
            # Remove the last item: it should be the one corresponding
            # to the current motion-notify-event.
            hx0, hy0, ht0 = drawstate.evhack_positions.pop(-1)
            # Check that we can use the eventhack data uncorrected
            if (hx0, hy0, ht0) == (x, y, time):
                for hx, hy, ht in drawstate.evhack_positions:
                    hx, hy = tdw.display_to_model(hx, hy)
                    event_data = (ht, hx, hy, None, None, None)
                    drawstate.queue_motion(event_data)
            else:
                logger.warning(
                    "Final evhack event (%0.2f, %0.2f, %d) doesn't match its "
                    "corresponding motion-notify-event (%0.2f, %0.2f, %d). "
                    "This can be ignored if it's just a one-off occurrence.",
                    hx0, hy0, ht0, x, y, time)
        # Reset the eventhack queue
        if len(drawstate.evhack_positions) > 0:
            drawstate.evhack_positions = []

        # Queue this event
        x, y = tdw.display_to_model(x, y)
        event_data = (time, x, y, pressure, xtilt, ytilt)
        drawstate.queue_motion(event_data)
        # Start the motion event processor, if it isn't already running
        if not drawstate.motion_processing_cbid:
            cbid = gobject.idle_add(self._motion_queue_idle_cb, tdw,
                                    priority=self.MOTION_QUEUE_PRIORITY)
            drawstate.motion_processing_cbid = cbid
Esempio n. 49
0
    def motion_notify_cb(self, tdw, event, fakepressure=None):
        """Motion event handler: queues raw input and returns

        :param tdw: The TiledDrawWidget receiving the event
        :param event: the MotionNotify event being handled
        :param fakepressure: fake pressure to use if no real pressure

        Fake pressure is passed with faked motion events, e.g.
        button-press and button-release handlers for mouse events.

        GTK 3.8 and above does motion compression, forcing our use of
        event filter hackery to obtain the high-resolution event
        positions required for making brushstrokes. This handler is
        still called for the events the GDK compression code lets
        through, and it is the only source of pressure and tilt info
        available when motion compression is active.
        """

        # Do nothing if painting is inactivated
        current_layer = tdw.doc._layers.current
        if not (tdw.is_sensitive and current_layer.get_paintable()):
            return False

        # Disable or work around GDK's motion event compression
        if self._event_compression_supported is None:
            win = tdw.get_window()
            mc_supported = hasattr(win, "set_event_compression")
            self._event_compression_supported = mc_supported
        drawstate = self._get_drawing_state(tdw)
        if drawstate.event_compression_workaround is None:
            self._add_event_compression_workaround(tdw)

        # If the device has changed and the last pressure value from the
        # previous device is not equal to 0.0, this can leave a visible
        # stroke on the layer even if the 'new' device is not pressed on
        # the tablet and has a pressure axis == 0.0.  Reseting the brush
        # when the device changes fixes this issue, but there may be a
        # much more elegant solution that only resets the brush on this
        # edge-case.
        same_device = True
        if tdw.app is not None:
            device = event.get_source_device()
            same_device = tdw.app.device_monitor.device_used(device)
            if not same_device:
                tdw.doc.brush.reset()

        # Extract the raw readings for this event
        x = event.x
        y = event.y
        time = event.time
        pressure = event.get_axis(Gdk.AxisUse.PRESSURE)
        xtilt = event.get_axis(Gdk.AxisUse.XTILT)
        ytilt = event.get_axis(Gdk.AxisUse.YTILT)
        state = event.state

        # Workaround for buggy evdev behaviour.
        # Events sometimes get a zero raw pressure reading when the
        # pressure reading has not changed. This results in broken
        # lines. As a workaround, forbid zero pressures if there is a
        # button pressed down, and substitute the last-known good value.
        # Detail: https://github.com/mypaint/mypaint/issues/29
        if drawstate.button_down is not None:
            if pressure == 0.0:
                pressure = drawstate.last_good_raw_pressure
            elif pressure is not None and np.isfinite(pressure):
                drawstate.last_good_raw_pressure = pressure

        # Ensure each non-evhack event has a defined pressure
        if pressure is not None:
            # Using the reported pressure. Apply some sanity checks
            if not np.isfinite(pressure):
                # infinity/nan: use button state (instead of clamping in
                # brush.hpp) https://gna.org/bugs/?14709
                pressure = None
            else:
                pressure = clamp(pressure, 0.0, 1.0)
            drawstate.last_event_had_pressure = True

        # Fake the pressure if we have none, or if infinity was reported
        if pressure is None:
            if fakepressure is not None:
                pressure = clamp(fakepressure, 0.0, 1.0)
            else:
                pressure = (
                    (state & Gdk.ModifierType.BUTTON1_MASK) and 0.5 or 0.0)
            drawstate.last_event_had_pressure = False

        # Check whether tilt is present.  For some tablets without
        # tilt support GTK reports a tilt axis with value nan, instead
        # of None.  https://gna.org/bugs/?17084
        if xtilt is None or ytilt is None or not np.isfinite(xtilt + ytilt):
            xtilt = 0.0
            ytilt = 0.0

        # Switching from a non-tilt device to a device which reports
        # tilt can cause GDK to return out-of-range tilt values, on X11.
        xtilt = clamp(xtilt, -1.0, 1.0)
        ytilt = clamp(ytilt, -1.0, 1.0)

        # Evdev workaround. X and Y tilts suffer from the same
        # problem as pressure for fancier devices.
        if drawstate.button_down is not None:
            if xtilt == 0.0:
                xtilt = drawstate.last_good_raw_xtilt
            else:
                drawstate.last_good_raw_xtilt = xtilt
            if ytilt == 0.0:
                ytilt = drawstate.last_good_raw_ytilt
            else:
                drawstate.last_good_raw_ytilt = ytilt

        # Tilt inputs are assumed to be relative to the viewport,
        # but the canvas may be rotated or mirrored, or both.
        # Compensate before passing them to the brush engine.
        # https://gna.org/bugs/?19988
        if tdw.mirrored:
            xtilt *= -1.0
        if tdw.rotation != 0:
            tilt_angle = math.atan2(ytilt, xtilt) - tdw.rotation
            tilt_magnitude = math.sqrt((xtilt**2) + (ytilt**2))
            xtilt = tilt_magnitude * math.cos(tilt_angle)
            ytilt = tilt_magnitude * math.sin(tilt_angle)

        # HACK: color picking, do not paint
        # TEST: Does this ever happen now?
        if (state & Gdk.ModifierType.CONTROL_MASK or
                state & Gdk.ModifierType.MOD1_MASK):
            # Don't simply return; this is a workaround for unwanted
            # lines in https://gna.org/bugs/?16169
            pressure = 0.0

        # Apply pressure mapping if we're running as part of a full
        # MyPaint application (and if there's one defined).
        if tdw.app is not None and tdw.app.pressure_mapping:
            pressure = tdw.app.pressure_mapping(pressure)

        # Apply any configured while-drawing cursor
        if pressure > 0:
            self._hide_drawing_cursor(tdw)
        else:
            self._reinstate_drawing_cursor(tdw)

        # HACK: straight line mode?
        # TEST: Does this ever happen?
        if state & Gdk.ModifierType.SHIFT_MASK:
            pressure = 0.0

        # If the eventhack filter caught more than one event, push them
        # onto the motion event queue. Pressures and tilts will be
        # interpolated from surrounding motion-notify events.
        if len(drawstate.evhack_positions) > 1:
            # Remove the last item: it should be the one corresponding
            # to the current motion-notify-event.
            hx0, hy0, ht0 = drawstate.evhack_positions.pop(-1)
            # Check that we can use the eventhack data uncorrected
            if (hx0, hy0, ht0) == (x, y, time):
                for hx, hy, ht in drawstate.evhack_positions:
                    hx, hy = tdw.display_to_model(hx, hy)
                    event_data = (ht, hx, hy, None, None, None)
                    drawstate.queue_motion(event_data)
            else:
                logger.warning(
                    "Final evhack event (%0.2f, %0.2f, %d) doesn't match its "
                    "corresponding motion-notify-event (%0.2f, %0.2f, %d). "
                    "This can be ignored if it's just a one-off occurrence.",
                    hx0, hy0, ht0, x, y, time)
        # Reset the eventhack queue
        if len(drawstate.evhack_positions) > 0:
            drawstate.evhack_positions = []

        # Queue this event
        x, y = tdw.display_to_model(x, y)
        event_data = (time, x, y, pressure, xtilt, ytilt)
        drawstate.queue_motion(event_data)
        # Start the motion event processor, if it isn't already running
        if not drawstate.motion_processing_cbid:
            cbid = GLib.idle_add(
                self._motion_queue_idle_cb,
                tdw,
                priority = self.MOTION_QUEUE_PRIORITY,
            )
            drawstate.motion_processing_cbid = cbid
Esempio n. 50
0
    def paint(self, cr):
        """Paints the frame, and edit boxes if the app is in FrameEditMode"""
        if not self.doc.model.frame_enabled:
            return

        tdw = self.doc.tdw
        allocation = tdw.get_allocation()
        w, h = allocation.width, allocation.height
        canvas_bbox = (-1, -1, w + 2, h + 2)

        cr.rectangle(*canvas_bbox)

        corners = self._frame_corners()
        p1, p2, p3, p4 = [(int(x) + 0.5, int(y) + 0.5) for x, y in corners]
        cr.move_to(*p1)
        cr.line_to(*p2)
        cr.line_to(*p3)
        cr.line_to(*p4)
        cr.close_path()
        cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)

        frame_rgba = self.app.preferences["frame.color_rgba"]
        frame_rgba = [helpers.clamp(c, 0, 1) for c in frame_rgba]
        cr.set_source_rgba(*frame_rgba)
        editmode = None
        for m in self.doc.modes:
            if isinstance(m, FrameEditMode):
                editmode = m
                break
        if not editmode:
            # Frame mask
            cr.fill_preserve()
            # Doublestrike the edge for emphasis, assuming an alpha frame
            cr.set_line_width(self.OUTLINE_WIDTH)
            cr.stroke()
        else:
            # Frame mask
            cr.fill()
            # Editable frame outline
            cr.set_line_cap(cairo.LINE_CAP_ROUND)
            cr.set_line_width(self.EDIT_BOX_WIDTH + 2)
            r, g, b = self.EDIT_BOX_OUTLINE_RGB
            cr.set_source_rgba(r, g, b, 0.666)
            cr.move_to(*p1)
            cr.line_to(*p2)
            cr.line_to(*p3)
            cr.line_to(*p4)
            cr.close_path()
            cr.stroke()
            # Line corresponding to editable zones
            zonelines = [(FrameEditMode.TOP, p1, p2),
                         (FrameEditMode.RIGHT, p2, p3),
                         (FrameEditMode.BOTTOM, p3, p4),
                         (FrameEditMode.LEFT, p4, p1)]
            cr.set_line_width(self.EDIT_BOX_WIDTH)
            for zone, p, q in zonelines:
                if editmode._zone and (editmode._zone == zone):
                    r, g, b = self.EDIT_BOX_PRELIGHT_RGB
                else:
                    r, g, b = self.EDIT_BOX_RGB
                cr.set_source_rgba(r, g, b, 1)
                cr.move_to(*p)
                cr.line_to(*q)
                cr.stroke()
            # Corner dot outline
            radius = self.EDIT_CORNER_SIZE + 1
            r, g, b = self.EDIT_BOX_OUTLINE_RGB
            cr.set_source_rgba(r, g, b, 0.666)
            for p in [p1, p2, p3, p4]:
                x, y = p
                cr.arc(x, y, radius, 0, 2 * math.pi)
                cr.fill()
                cr.stroke()
            # Dots corresponding to editable corners
            zonecorners = [
                (p1, FrameEditMode.TOP | FrameEditMode.LEFT),
                (p2, FrameEditMode.TOP | FrameEditMode.RIGHT),
                (p3, FrameEditMode.BOTTOM | FrameEditMode.RIGHT),
                (p4, FrameEditMode.BOTTOM | FrameEditMode.LEFT),
            ]
            radius = self.EDIT_CORNER_SIZE
            for p, zonemask in zonecorners:
                x, y = p
                if editmode._zone and (editmode._zone == zonemask):
                    r, g, b = self.EDIT_BOX_PRELIGHT_RGB
                else:
                    r, g, b = self.EDIT_BOX_RGB
                cr.set_source_rgba(r, g, b, 1)
                cr.arc(x, y, radius, 0, 2 * math.pi)
                cr.fill()
Esempio n. 51
0
    def paint(self, cr):
        """Paints the frame, and edit boxes if the app is in FrameEditMode"""
        if not self.doc.model.frame_enabled:
            return

        tdw = self.doc.tdw
        allocation = tdw.get_allocation()
        w, h = allocation.width, allocation.height
        canvas_bbox = (-1, -1, w+2, h+2)

        cr.rectangle(*canvas_bbox)

        corners = self._frame_corners()
        p1, p2, p3, p4 = [(int(x)+0.5, int(y)+0.5) for x, y in corners]
        cr.move_to(*p1)
        cr.line_to(*p2)
        cr.line_to(*p3)
        cr.line_to(*p4)
        cr.close_path()
        cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)

        frame_rgba = self.app.preferences["frame.color_rgba"]
        frame_rgba = [helpers.clamp(c, 0, 1) for c in frame_rgba]
        cr.set_source_rgba(*frame_rgba)
        editmode = None
        for m in self.doc.modes:
            if isinstance(m, FrameEditMode):
                editmode = m
                break
        if not editmode:
            # Frame mask
            cr.fill_preserve()
            # Doublestrike the edge for emphasis, assuming an alpha frame
            cr.set_line_width(self.OUTLINE_WIDTH)
            cr.stroke()
        else:
            # Frame mask
            cr.fill()
            # Editable/active frame outline
            cr.set_line_cap(cairo.LINE_CAP_ROUND)
            zonelines = [(FrameEditMode.TOP,    p1, p2),
                         (FrameEditMode.RIGHT,  p2, p3),
                         (FrameEditMode.BOTTOM, p3, p4),
                         (FrameEditMode.LEFT,   p4, p1)]
            edge_width = gui.style.DRAGGABLE_EDGE_WIDTH
            for zone, p, q in zonelines:
                gui.drawutils.draw_draggable_edge_drop_shadow(
                    cr=cr,
                    p0=p,
                    p1=q,
                    width=edge_width,
                )
            cr.set_line_width(edge_width)
            for zone, p, q in zonelines:
                if editmode._zone and (editmode._zone == zone):
                    rgb = gui.style.ACTIVE_ITEM_COLOR.get_rgb()
                else:
                    rgb = gui.style.EDITABLE_ITEM_COLOR.get_rgb()
                cr.set_source_rgb(*rgb)
                cr.move_to(*p)
                cr.line_to(*q)
                cr.stroke()
            # Dots corresponding to editable corners
            zonecorners = [
                (p1, FrameEditMode.TOP | FrameEditMode.LEFT),
                (p2, FrameEditMode.TOP | FrameEditMode.RIGHT),
                (p3, FrameEditMode.BOTTOM | FrameEditMode.RIGHT),
                (p4, FrameEditMode.BOTTOM | FrameEditMode.LEFT)
            ]
            radius = gui.style.DRAGGABLE_POINT_HANDLE_SIZE
            for p, zonemask in zonecorners:
                x, y = p
                if editmode._zone and (editmode._zone == zonemask):
                    col = gui.style.ACTIVE_ITEM_COLOR
                else:
                    col = gui.style.EDITABLE_ITEM_COLOR
                gui.drawutils.render_round_floating_color_chip(
                    cr=cr,
                    x=x, y=y,
                    color=col,
                    radius=radius,
                )
Esempio n. 52
0
    def load(self, filehandle, silent=False):
        """Load contents from a file handle containing a GIMP palette.

        :param filehandle: File-like object (.readline, line iteration)
        :param bool silent: If true, don't emit any events.

        >>> pal = Palette()
        >>> with open("palettes/MyPaint_Default.gpl", "r") as fp:
        ...     pal.load(fp)
        >>> len(pal) > 1
        True

        If the file format is incorrect, a RuntimeError will be raised.

        """
        comment_line_re = re.compile(r'^#')
        field_line_re = re.compile(r'^(\w+)\s*:\s*(.*)$')
        color_line_re = re.compile(r'^(\d+)\s+(\d+)\s+(\d+)\s*(?:\b(.*))$')
        fp = filehandle
        self.clear(silent=True)   # method fires events itself
        line = fp.readline()
        if line.strip() != "GIMP Palette":
            raise RuntimeError("Not a valid GIMP Palette")
        header_done = False
        line_num = 0
        for line in fp:
            line = line.strip()
            line_num += 1
            if line == '':
                continue
            if comment_line_re.match(line):
                continue
            if not header_done:
                match = field_line_re.match(line)
                if match:
                    key, value = match.groups()
                    key = key.lower()
                    if key == 'name':
                        self._name = value.strip()
                    elif key == 'columns':
                        self._columns = int(value)
                    else:
                        logger.warning("Unknown 'key:value' pair %r", line)
                    continue
                else:
                    header_done = True
            match = color_line_re.match(line)
            if not match:
                logger.warning("Expected 'R G B [Name]', not %r", line)
                continue
            r, g, b, col_name = match.groups()
            col_name = col_name.strip()
            r = clamp(int(r), 0, 0xff) / 0xff
            g = clamp(int(g), 0, 0xff) / 0xff
            b = clamp(int(b), 0, 0xff) / 0xff
            if r == g == b == 0 and col_name == self._EMPTY_SLOT_NAME:
                self.append(None)
            else:
                col = RGBColor(r, g, b)
                col.__name = col_name
                self._colors.append(col)
        if not silent:
            self.info_changed()
            self.sequence_changed()
            self.match_changed()
Esempio n. 53
0
    def motion_notify_cb(self, tdw, event, fakepressure=None):
        """Motion event handler: queues raw input and returns

        :param tdw: The TiledDrawWidget receiving the event
        :param event: the MotionNotify event being handled
        :param fakepressure: fake pressure to use if no real pressure

        Fake pressure is passed with faked motion events, e.g.
        button-press and button-release handlers for mouse events.

        """

        # Do nothing if painting is inactivated
        current_layer = tdw.doc._layers.current
        if not (tdw.is_sensitive and current_layer.get_paintable()):
            return False

        # If the device has changed and the last pressure value from the
        # previous device is not equal to 0.0, this can leave a visible
        # stroke on the layer even if the 'new' device is not pressed on
        # the tablet and has a pressure axis == 0.0.  Reseting the brush
        # when the device changes fixes this issue, but there may be a
        # much more elegant solution that only resets the brush on this
        # edge-case.
        same_device = True
        if tdw.app is not None:
            device = event.get_source_device()
            same_device = tdw.app.device_monitor.device_used(device)
            if not same_device:
                tdw.doc.brush.reset()

        # Extract the raw readings for this event
        x = event.x
        y = event.y
        time = event.time
        pressure = event.get_axis(Gdk.AxisUse.PRESSURE)
        xtilt = event.get_axis(Gdk.AxisUse.XTILT)
        ytilt = event.get_axis(Gdk.AxisUse.YTILT)
        viewzoom = tdw.scale
        viewrotation = tdw.rotation
        state = event.state

        # Workaround for buggy evdev behaviour.
        # Events sometimes get a zero raw pressure reading when the
        # pressure reading has not changed. This results in broken
        # lines. As a workaround, forbid zero pressures if there is a
        # button pressed down, and substitute the last-known good value.
        # Detail: https://github.com/mypaint/mypaint/issues/29
        drawstate = self._get_drawing_state(tdw)
        if drawstate.button_down is not None:
            if pressure == 0.0:
                pressure = drawstate.last_good_raw_pressure
            elif pressure is not None and np.isfinite(pressure):
                drawstate.last_good_raw_pressure = pressure

        # Ensure each event has a defined pressure
        if pressure is not None:
            # Using the reported pressure. Apply some sanity checks
            if not np.isfinite(pressure):
                # infinity/nan: use button state (instead of clamping in
                # brush.hpp) https://gna.org/bugs/?14709
                pressure = None
            else:
                pressure = clamp(pressure, 0.0, 1.0)
            drawstate.last_event_had_pressure = True

        # Fake the pressure if we have none, or if infinity was reported
        if pressure is None:
            if fakepressure is not None:
                pressure = clamp(fakepressure, 0.0, 1.0)
            else:
                pressure = ((state & Gdk.ModifierType.BUTTON1_MASK) and 0.5
                            or 0.0)
            drawstate.last_event_had_pressure = False

        # Check whether tilt is present.  For some tablets without
        # tilt support GTK reports a tilt axis with value nan, instead
        # of None.  https://gna.org/bugs/?17084
        if xtilt is None or ytilt is None or not np.isfinite(xtilt + ytilt):
            xtilt = 0.0
            ytilt = 0.0

        # Switching from a non-tilt device to a device which reports
        # tilt can cause GDK to return out-of-range tilt values, on X11.
        xtilt = clamp(xtilt, -1.0, 1.0)
        ytilt = clamp(ytilt, -1.0, 1.0)

        # Evdev workaround. X and Y tilts suffer from the same
        # problem as pressure for fancier devices.
        if drawstate.button_down is not None:
            if xtilt == 0.0:
                xtilt = drawstate.last_good_raw_xtilt
            else:
                drawstate.last_good_raw_xtilt = xtilt
            if ytilt == 0.0:
                ytilt = drawstate.last_good_raw_ytilt
            else:
                drawstate.last_good_raw_ytilt = ytilt

        # Tilt inputs are assumed to be relative to the viewport,
        # but the canvas may be rotated or mirrored, or both.
        # Compensate before passing them to the brush engine.
        # https://gna.org/bugs/?19988
        if tdw.mirrored:
            xtilt *= -1.0
        if tdw.rotation != 0:
            tilt_angle = math.atan2(ytilt, xtilt) - tdw.rotation
            tilt_magnitude = math.sqrt((xtilt**2) + (ytilt**2))
            xtilt = tilt_magnitude * math.cos(tilt_angle)
            ytilt = tilt_magnitude * math.sin(tilt_angle)

        # HACK: color picking, do not paint
        # TEST: Does this ever happen now?
        if (state & Gdk.ModifierType.CONTROL_MASK
                or state & Gdk.ModifierType.MOD1_MASK):
            # Don't simply return; this is a workaround for unwanted
            # lines in https://gna.org/bugs/?16169
            pressure = 0.0

        # Apply pressure mapping if we're running as part of a full
        # MyPaint application (and if there's one defined).
        if tdw.app is not None and tdw.app.pressure_mapping:
            pressure = tdw.app.pressure_mapping(pressure)

        # Apply any configured while-drawing cursor
        if pressure > 0:
            self._hide_drawing_cursor(tdw)
        else:
            self._reinstate_drawing_cursor(tdw)

        # HACK: straight line mode?
        # TEST: Does this ever happen?
        if state & Gdk.ModifierType.SHIFT_MASK:
            pressure = 0.0

        # Queue this event
        x, y = tdw.display_to_model(x, y)
        event_data = (time, x, y, pressure, xtilt, ytilt, viewzoom,
                      viewrotation)
        drawstate.queue_motion(event_data)
        # Start the motion event processor, if it isn't already running
        if not drawstate.motion_processing_cbid:
            cbid = GLib.idle_add(
                self._motion_queue_idle_cb,
                tdw,
                priority=self.MOTION_QUEUE_PRIORITY,
            )
            drawstate.motion_processing_cbid = cbid
Esempio n. 54
0
    def paint(self, cr):
        """Paints the frame, and edit boxes if the app is in FrameEditMode"""
        if not self.doc.model.frame_enabled:
            return

        tdw = self.doc.tdw
        allocation = tdw.get_allocation()
        w, h = allocation.width, allocation.height
        canvas_bbox = (-1, -1, w+2, h+2)

        cr.rectangle(*canvas_bbox)

        corners = self._frame_corners()
        p1, p2, p3, p4 = [(int(x)+0.5, int(y)+0.5) for x, y in corners]
        cr.move_to(*p1)
        cr.line_to(*p2)
        cr.line_to(*p3)
        cr.line_to(*p4)
        cr.close_path()
        cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)

        frame_rgba = self.app.preferences["frame.color_rgba"]
        frame_rgba = [helpers.clamp(c, 0, 1) for c in frame_rgba]
        cr.set_source_rgba(*frame_rgba)
        editmode = None
        for m in self.doc.modes:
            if isinstance(m, FrameEditMode):
                editmode = m
                break
        if not editmode:
            # Frame mask
            cr.fill_preserve()
            # Doublestrike the edge for emphasis, assuming an alpha frame
            cr.set_line_width(self.OUTLINE_WIDTH)
            cr.stroke()
        else:
            # Frame mask
            cr.fill()
            # Editable frame outline
            cr.set_line_cap(cairo.LINE_CAP_ROUND)
            cr.set_line_width(self.EDIT_BOX_WIDTH + 2)
            r, g, b = self.EDIT_BOX_OUTLINE_RGB
            cr.set_source_rgba(r, g, b, 0.666)
            cr.move_to(*p1)
            cr.line_to(*p2)
            cr.line_to(*p3)
            cr.line_to(*p4)
            cr.close_path()
            cr.stroke()
            # Line corresponding to editable zones
            zonelines = [(FrameEditMode.TOP,    p1, p2),
                         (FrameEditMode.RIGHT,  p2, p3),
                         (FrameEditMode.BOTTOM, p3, p4),
                         (FrameEditMode.LEFT,   p4, p1)]
            cr.set_line_width(self.EDIT_BOX_WIDTH)
            for zone, p, q in zonelines:
                if editmode._zone and (editmode._zone == zone):
                    r, g, b = self.EDIT_BOX_PRELIGHT_RGB
                else:
                    r, g, b = self.EDIT_BOX_RGB
                cr.set_source_rgba(r, g, b, 1)
                cr.move_to(*p)
                cr.line_to(*q)
                cr.stroke()
            # Corner dot outline
            radius = self.EDIT_CORNER_SIZE + 1
            r, g, b = self.EDIT_BOX_OUTLINE_RGB
            cr.set_source_rgba(r, g, b, 0.666)
            for p in [p1, p2, p3, p4]:
                x, y = p
                cr.arc(x, y, radius, 0, 2*math.pi)
                cr.fill()
                cr.stroke()
            # Dots corresponding to editable corners
            zonecorners = [
                (p1, FrameEditMode.TOP | FrameEditMode.LEFT),
                (p2, FrameEditMode.TOP | FrameEditMode.RIGHT),
                (p3, FrameEditMode.BOTTOM | FrameEditMode.RIGHT),
                (p4, FrameEditMode.BOTTOM | FrameEditMode.LEFT)
            ]
            radius = self.EDIT_CORNER_SIZE
            for p, zonemask in zonecorners:
                x, y = p
                if editmode._zone and (editmode._zone == zonemask):
                    r, g, b = self.EDIT_BOX_PRELIGHT_RGB
                else:
                    r, g, b = self.EDIT_BOX_RGB
                cr.set_source_rgba(r, g, b, 1)
                cr.arc(x, y, radius, 0, 2*math.pi)
                cr.fill()
Esempio n. 55
0
    def motion_notify_cb(self, tdw, event, fakepressure=None):
        """Motion event handler: queues raw input and returns

        :param tdw: The TiledDrawWidget receiving the event
        :param event: the MotionNotify event being handled
        :param fakepressure: fake pressure to use if no real pressure

        Fake pressure is passed with faked motion events, e.g.
        button-press and button-release handlers for mouse events.

        GTK 3.8 and above does motion compression, forcing our use of
        event filter hackery to obtain the high-resolution event
        positions required for making brushstrokes. This handler is
        still called for the events the GDK compression code lets
        through, and it is the only source of pressure and tilt info
        available when motion compression is active.
        """

        # Do nothing if painting is inactivated
        current_layer = tdw.doc._layers.current
        if not (tdw.is_sensitive and current_layer.get_paintable()):
            return False

        # Disable or work around GDK's motion event compression
        if self._event_compression_supported is None:
            win = tdw.get_window()
            mc_supported = hasattr(win, "set_event_compression")
            self._event_compression_supported = mc_supported
        drawstate = self._get_drawing_state(tdw)
        if drawstate.event_compression_workaround is None:
            self._add_event_compression_workaround(tdw)

        # If the device has changed and the last pressure value from the
        # previous device is not equal to 0.0, this can leave a visible
        # stroke on the layer even if the 'new' device is not pressed on
        # the tablet and has a pressure axis == 0.0.  Reseting the brush
        # when the device changes fixes this issue, but there may be a
        # much more elegant solution that only resets the brush on this
        # edge-case.
        same_device = True
        if tdw.app is not None:
            device = event.get_source_device()
            same_device = tdw.app.device_monitor.device_used(device)
            if not same_device:
                tdw.doc.brush.reset()

        # Extract the raw readings for this event
        x = event.x
        y = event.y
        time = event.time
        pressure = event.get_axis(gdk.AXIS_PRESSURE)
        xtilt = event.get_axis(gdk.AXIS_XTILT)
        ytilt = event.get_axis(gdk.AXIS_YTILT)
        state = event.state

        # Workaround for buggy evdev behaviour.
        # Events sometimes get a zero raw pressure reading when the
        # pressure reading has not changed. This results in broken
        # lines. As a workaround, forbid zero pressures if there is a
        # button pressed down, and substitute the last-known good value.
        # Detail: https://github.com/mypaint/mypaint/issues/29
        if drawstate.button_down is not None:
            if pressure == 0.0:
                pressure = drawstate.last_good_raw_pressure
            elif pressure is not None and isfinite(pressure):
                drawstate.last_good_raw_pressure = pressure

        # Ensure each non-evhack event has a defined pressure
        if pressure is not None:
            # Using the reported pressure. Apply some sanity checks
            if not isfinite(pressure):
                # infinity/nan: use button state (instead of clamping in
                # brush.hpp) https://gna.org/bugs/?14709
                pressure = None
            else:
                pressure = clamp(pressure, 0.0, 1.0)
            drawstate.last_event_had_pressure = True

        # Fake the pressure if we have none, or if infinity was reported
        if pressure is None:
            if fakepressure is not None:
                pressure = clamp(fakepressure, 0.0, 1.0)
            else:
                pressure = (state & gdk.BUTTON1_MASK) and 0.5 or 0.0
            drawstate.last_event_had_pressure = False

        # Check whether tilt is present.  For some tablets without
        # tilt support GTK reports a tilt axis with value nan, instead
        # of None.  https://gna.org/bugs/?17084
        if xtilt is None or ytilt is None or not isfinite(xtilt + ytilt):
            xtilt = 0.0
            ytilt = 0.0
        else:
            # Evdev workaround. X and Y tilts suffer from the same
            # problem as pressure for fancier devices.
            if drawstate.button_down is not None:
                if xtilt == 0.0:
                    xtilt = drawstate.last_good_raw_xtilt
                else:
                    drawstate.last_good_raw_xtilt = xtilt
                if ytilt == 0.0:
                    ytilt = drawstate.last_good_raw_ytilt
                else:
                    drawstate.last_good_raw_ytilt = ytilt

            # Tilt inputs are assumed to be relative to the viewport,
            # but the canvas may be rotated or mirrored, or both.
            # Compensate before passing them to the brush engine.
            # https://gna.org/bugs/?19988
            if tdw.mirrored:
                xtilt *= -1.0
            if tdw.rotation != 0:
                tilt_angle = math.atan2(ytilt, xtilt) - tdw.rotation
                tilt_magnitude = math.sqrt((xtilt**2) + (ytilt**2))
                xtilt = tilt_magnitude * math.cos(tilt_angle)
                ytilt = tilt_magnitude * math.sin(tilt_angle)

        # HACK: color picking, do not paint
        # TEST: Does this ever happen now?
        if state & gdk.CONTROL_MASK or state & gdk.MOD1_MASK:
            # Don't simply return; this is a workaround for unwanted
            # lines in https://gna.org/bugs/?16169
            pressure = 0.0

        # Apply pressure mapping if we're running as part of a full
        # MyPaint application (and if there's one defined).
        if tdw.app is not None and tdw.app.pressure_mapping:
            pressure = tdw.app.pressure_mapping(pressure)

        # Apply any configured while-drawing cursor
        if pressure > 0:
            self._hide_drawing_cursor(tdw)
        else:
            self._reinstate_drawing_cursor(tdw)

        # HACK: straight line mode?
        # TEST: Does this ever happen?
        if state & gdk.SHIFT_MASK:
            pressure = 0.0

        # If the eventhack filter caught more than one event, push them
        # onto the motion event queue. Pressures and tilts will be
        # interpolated from surrounding motion-notify events.
        if len(drawstate.evhack_positions) > 1:
            # Remove the last item: it should be the one corresponding
            # to the current motion-notify-event.
            hx0, hy0, ht0 = drawstate.evhack_positions.pop(-1)
            # Check that we can use the eventhack data uncorrected
            if (hx0, hy0, ht0) == (x, y, time):
                for hx, hy, ht in drawstate.evhack_positions:
                    hx, hy = tdw.display_to_model(hx, hy)
                    event_data = (ht, hx, hy, None, None, None)
                    drawstate.queue_motion(event_data)
            else:
                logger.warning(
                    "Final evhack event (%0.2f, %0.2f, %d) doesn't match its "
                    "corresponding motion-notify-event (%0.2f, %0.2f, %d). "
                    "This can be ignored if it's just a one-off occurrence.",
                    hx0, hy0, ht0, x, y, time)
        # Reset the eventhack queue
        if len(drawstate.evhack_positions) > 0:
            drawstate.evhack_positions = []

        # Queue this event
        x, y = tdw.display_to_model(x, y)
        event_data = (time, x, y, pressure, xtilt, ytilt)
        drawstate.queue_motion(event_data)
        # Start the motion event processor, if it isn't already running
        if not drawstate.motion_processing_cbid:
            cbid = gobject.idle_add(self._motion_queue_idle_cb,
                                    tdw,
                                    priority=self.MOTION_QUEUE_PRIORITY)
            drawstate.motion_processing_cbid = cbid
Esempio n. 56
0
    def paint(self, cr):
        """Paints the frame, and edit boxes if the app is in FrameEditMode"""
        if not self.doc.model.frame_enabled:
            return

        tdw = self.doc.tdw
        allocation = tdw.get_allocation()
        w, h = allocation.width, allocation.height
        canvas_bbox = (-1, -1, w+2, h+2)

        cr.rectangle(*canvas_bbox)

        corners = self._frame_corners()
        p1, p2, p3, p4 = [(int(x)+0.5, int(y)+0.5) for x, y in corners]
        cr.move_to(*p1)
        cr.line_to(*p2)
        cr.line_to(*p3)
        cr.line_to(*p4)
        cr.close_path()
        cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)

        frame_rgba = self.app.preferences["frame.color_rgba"]
        frame_rgba = [helpers.clamp(c, 0, 1) for c in frame_rgba]
        cr.set_source_rgba(*frame_rgba)
        editmode = None
        for m in self.doc.modes:
            if isinstance(m, FrameEditMode):
                editmode = m
                break
        if not editmode:
            # Frame mask
            cr.fill_preserve()
            # Doublestrike the edge for emphasis, assuming an alpha frame
            cr.set_line_width(self.OUTLINE_WIDTH)
            cr.stroke()
        else:
            # Frame mask
            cr.fill()
            # Editable/active frame outline
            cr.set_line_cap(cairo.LINE_CAP_ROUND)
            zonelines = [(FrameEditMode.TOP,    p1, p2),
                         (FrameEditMode.RIGHT,  p2, p3),
                         (FrameEditMode.BOTTOM, p3, p4),
                         (FrameEditMode.LEFT,   p4, p1)]
            edge_width = gui.style.DRAGGABLE_EDGE_WIDTH
            for zone, p, q in zonelines:
                gui.drawutils.draw_draggable_edge_drop_shadow(
                    cr=cr,
                    p0=p,
                    p1=q,
                    width=edge_width,
                )
            cr.set_line_width(edge_width)
            for zone, p, q in zonelines:
                if editmode._zone and (editmode._zone == zone):
                    rgb = gui.style.ACTIVE_ITEM_COLOR.get_rgb()
                else:
                    rgb = gui.style.EDITABLE_ITEM_COLOR.get_rgb()
                cr.set_source_rgb(*rgb)
                cr.move_to(*p)
                cr.line_to(*q)
                cr.stroke()
            # Dots corresponding to editable corners
            zonecorners = [
                (p1, FrameEditMode.TOP | FrameEditMode.LEFT),
                (p2, FrameEditMode.TOP | FrameEditMode.RIGHT),
                (p3, FrameEditMode.BOTTOM | FrameEditMode.RIGHT),
                (p4, FrameEditMode.BOTTOM | FrameEditMode.LEFT)
            ]
            radius = gui.style.DRAGGABLE_POINT_HANDLE_SIZE
            for p, zonemask in zonecorners:
                x, y = p
                if editmode._zone and (editmode._zone == zonemask):
                    col = gui.style.ACTIVE_ITEM_COLOR
                else:
                    col = gui.style.EDITABLE_ITEM_COLOR
                gui.drawutils.render_round_floating_color_chip(
                    cr=cr,
                    x=x, y=y,
                    color=col,
                    radius=radius,
                )