Example #1
0
def fmt_time_period_abbr(t):
    """Get a localized abbreviated minutes+seconds string

    :param int t: A positive number of seconds
    :returns: short localized string
    :rtype: unicode

    The result looks like like "<minutes>m<seconds>s",
    or just "<seconds>s".

    """
    if t < 0:
        raise ValueError("Parameter t cannot be negative")
    days = int(t / (24 * 60 * 60))
    hours = int(t - days * 24 * 60 * 60) / (60 * 60)
    minutes = int(t - hours * 60 * 60) / 60
    seconds = int(t - minutes * 60)
    # TRANSLATORS: I'm assuming that time periods in places where
    # TRANSLATORS: abbreviations make sense don't need ngettext()
    if t > 24 * 60 * 60:
        template = C_("Time period abbreviations", u"{days}d{hours}h")
    elif t > 60 * 60:
        template = C_("Time period abbreviations", u"{hours}h{minutes}m")
    elif t > 60:
        template = C_("Time period abbreviation", u"{minutes}m{seconds}s")
    else:
        template = C_("Time period abbreviation", u"{seconds}s")
    return template.format(
        days = days,
        hours = hours,
        minutes = minutes,
        seconds = seconds,
    )
Example #2
0
 def _query_tooltip_cb(self, da, x, y, keyboard_mode, tooltip):
     s = self._TOOLTIP_ICON_SIZE
     scaled_pixbuf = self._get_scaled_pixbuf(s)
     tooltip.set_icon(scaled_pixbuf)
     template_params = {"brush_name": escape(self._brush_name)}
     markup_template = C_(
         "current brush indicator: tooltip (no-description case)",
         u"<b>{brush_name}</b>",
     )
     if self._brush_desc:
         markup_template = C_(
             "current brush indicator: tooltip (description case)",
             u"<b>{brush_name}</b>\n{brush_desc}",
         )
         template_params["brush_desc"] = escape(self._brush_desc)
     markup = markup_template.format(**template_params)
     tooltip.set_markup(markup)
     # TODO: summarize changes?
     return True
Example #3
0
 def _motion_notify_cb(self, widget, event):
     x, y = event.x, event.y
     i = self.get_index_at_pos(x, y)
     # Set the tooltip.
     # Passing the tooltip through a value of None is necessary for its
     # position on the screen to be updated to where the pointer is. Setting
     # it to None, and then to the desired value must happen in two separate
     # events for the tooltip window position update to be honoured.
     if i is None:
         # Not over a color, so use the static default
         if self._tooltip_index not in (-1, -2):
             # First such event: reset the tooltip.
             self._tooltip_index = -1
             self.set_has_tooltip(False)
             self.set_tooltip_text("")
         elif self._tooltip_index != -2:
             # Second event over a non-color: set the tooltip text.
             self._tooltip_index = -2
             self.set_has_tooltip(True)
             self.set_tooltip_text(self.STATIC_TOOLTIP_TEXT)
     elif self._tooltip_index != i:
         # Mouse pointer has moved to a different color, or away
         # from the two states above.
         if self._tooltip_index is not None:
             # First event for this i: reset the tooltip.
             self._tooltip_index = None
             self.set_has_tooltip(False)
             self.set_tooltip_text("")
         else:
             # Second event for this i: set the desired tooltip text.
             self._tooltip_index = i
             mgr = self.get_color_manager()
             tip = mgr.palette.get_color_name(i)
             color = mgr.palette.get_color(i)
             if color is None:
                 tip = C_(
                     "palette view",
                     "Empty palette slot (drag a color here)",
                 )
             elif tip is None or tip.strip() == "":
                 tip = ""  # Anonymous colors don't get tooltips
             self.set_has_tooltip(True)
             self.set_tooltip_text(tip)
Example #4
0
 def _query_tooltip_cb(self, da, x, y, keyboard_mode, tooltip):
     s = self._TOOLTIP_ICON_SIZE
     scaled_pixbuf = self._get_scaled_pixbuf(s)
     tooltip.set_icon(scaled_pixbuf)
     brush_name = self._brush_name
     if not brush_name:
         brush_name = self._DEFAULT_BRUSH_DISPLAY_NAME
         # Rare cases, see https://github.com/mypaint/mypaint/issues/402.
         # Probably just after init.
     template_params = {"brush_name": lib.xml.escape(brush_name)}
     markup_template = C_("current brush indicator: tooltip (no-description case)", u"<b>{brush_name}</b>")
     if self._brush_desc:
         markup_template = C_(
             "current brush indicator: tooltip (description case)", u"<b>{brush_name}</b>\n{brush_desc}"
         )
         template_params["brush_desc"] = lib.xml.escape(self._brush_desc)
     markup = markup_template.format(**template_params)
     tooltip.set_markup(markup)
     # TODO: summarize changes?
     return True
Example #5
0
 def _populate_settings_treestore(self):
     # Populate the treestore
     store = self._builder.get_object("settings_treestore")
     root_iter = store.get_iter_first()
     self._treestore = store
     # Editable string fields
     # Columns: [cname, displayname, is_selectable, font_weight]
     row_data = [
         None,
         C_("brush settings list: brush metadata texts group", "About"),
         True,
         Pango.Weight.NORMAL,
     ]
     group_iter = store.append(root_iter, row_data)
     # Groups for settings.
     groups = [{
         'id':
         'experimental',
         'title':
         C_(
             'brush settings list: setting group',
             'Experimental',
         ),
         'settings': [],
     }, {
         'id':
         'basic',
         'title':
         C_(
             'brush settings list: setting group',
             'Basic',
         ),
         'settings': [
             'radius_logarithmic',
             'radius_by_random',
             'hardness',
             'snap_to_pixel',
             'anti_aliasing',
             'eraser',
             'offset_by_random',
             'elliptical_dab_angle',
             'elliptical_dab_ratio',
             'direction_filter',
             'pressure_gain_log',
         ],
     }, {
         'id':
         'opacity',
         'title':
         C_(
             'brush settings list: setting group',
             'Opacity',
         ),
         'settings': [
             'opaque',
             'opaque_multiply',
             'opaque_linearize',
             'lock_alpha',
         ],
     }, {
         'id':
         'dabs',
         'title':
         C_(
             'brush settings list: setting group',
             'Dabs',
         ),
         'settings': [
             'dabs_per_basic_radius',
             'dabs_per_actual_radius',
             'dabs_per_second',
         ],
     }, {
         'id':
         'smudge',
         'title':
         C_(
             'brush settings list: setting group',
             'Smudge',
         ),
         'settings': [
             'smudge',
             'smudge_length',
             'smudge_radius_log',
         ],
     }, {
         'id':
         'speed',
         'title':
         C_(
             'brush settings list: setting group',
             'Speed',
         ),
         'settings': [
             'speed1_slowness',
             'speed2_slowness',
             'speed1_gamma',
             'speed2_gamma',
             'offset_by_speed',
             'offset_by_speed_slowness',
         ],
     }, {
         'id':
         'tracking',
         'title':
         C_(
             'brush settings list: setting group',
             'Tracking',
         ),
         'settings': [
             'slow_tracking',
             'slow_tracking_per_dab',
             'tracking_noise',
         ],
     }, {
         'id':
         'stroke',
         'title':
         C_(
             'brush settings list: setting group',
             'Stroke',
         ),
         'settings': [
             'stroke_threshold',
             'stroke_duration_logarithmic',
             'stroke_holdtime',
         ],
     }, {
         'id':
         'color',
         'title':
         C_(
             'brush settings list: setting group',
             'Color',
         ),
         'settings': [
             'change_color_h',
             'change_color_l',
             'change_color_hsl_s',
             'change_color_v',
             'change_color_hsv_s',
             'restore_color',
             'colorize',
         ],
     }, {
         'id': 'custom',
         'title': C_(
             'brush settings list: setting group',
             'Custom',
         ),
         'settings': ['custom_input', 'custom_input_slowness'],
     }]
     hidden_settings = ['color_h', 'color_s', 'color_v']
     # Add new settings to the "experimental" group
     grouped_settings = set(hidden_settings)
     for g in groups:
         grouped_settings.update(g['settings'])
     for s in brushsettings.settings:
         n = s.cname
         if n not in grouped_settings:
             groups[0]['settings'].append(n)
             logger.warning('Setting %r should be added to a group', n)
     # Hide experimental group if empty
     if not groups[0]['settings']:
         groups.pop(0)
     # Groups to open by default
     open_paths = []
     open_ids = set(["experimental", "basic"])
     # Add groups to the treestore
     for group_num, group in enumerate(groups):
         group_id = group["id"]
         # Columns: [cname, displayname, is_selectable, font_weight]
         row_data = [None, group["title"], False, Pango.Weight.NORMAL]
         group_iter = store.append(root_iter, row_data)
         group_path = store.get_path(group_iter)
         self._group_treepath[group_id] = group_path
         for i, cname in enumerate(group['settings']):
             self._setting_group[cname] = group_id
             s = brushsettings.settings_dict[cname]
             row_data = [cname, s.name, True, Pango.Weight.NORMAL]
             setting_iter = store.append(group_iter, row_data)
             setting_path = store.get_path(setting_iter)
             self._setting_treepath[cname] = setting_path
         if group_id in open_ids:
             open_paths.append([group_num + 1])
     # Connect signals and handler functions
     v = self._builder.get_object("settings_treeview")
     sel = v.get_selection()
     sel.set_select_function(self._settings_treeview_selectfunc, None)
     # Select the first (description)
     sel.select_iter(store.get_iter_first())
     # Process the paths-to-open
     for path in open_paths:
         v.expand_to_path(Gtk.TreePath(path))
Example #6
0
class HCYMaskEditorWheel(HCYHueChromaWheel):
    """HCY wheel specialized for mask editing."""

    ## Instance vars
    __last_cursor = None  # previously set cursor (determines some actions)
    # Objects which are active or being manipulated
    __tmp_new_ctrlpoint = None  # new control-point color
    __active_ctrlpoint = None  # active point in active_void
    __active_shape = None  # list of colors or None
    # Drag state
    __drag_func = None
    __drag_start_pos = None

    ## Class-level constants and variables
    # Specialized cursors for different actions
    __add_cursor = None
    __move_cursor = None
    __move_point_cursor = None
    __rotate_cursor = None
    # Constrain the range of allowable lumas
    __MAX_LUMA = 0.75
    __MIN_LUMA = 0.25

    # Drawing constraints and activity proximities
    __ctrlpoint_radius = 2.5
    __ctrlpoint_grab_radius = 10
    __max_num_shapes = 6  # how many shapes are allowed

    # Tooltip text. Is here a better way of explaining this? It obscures the
    # editor quite a lot.
    STATIC_TOOLTIP_TEXT = C_(
        "HCY Mask Editor Wheel: tooltip",
        u"Gamut mask editor. Click in the middle to create "
        u"or manipulate shapes, or rotate the mask using "
        u"the edges of the disc.",
    )

    def __init__(self):
        """Instantiate, and connect the editor events.
        """
        super(HCYMaskEditorWheel, self).__init__()

        self.connect("realize", self._realize_cb)
        self.connect("button-press-event", self.__button_press_cb)
        self.connect("button-release-event", self.__button_release_cb)
        self.connect("motion-notify-event", self.__motion_cb)
        self.connect("leave-notify-event", self.__leave_cb)
        self.add_events(Gdk.EventMask.POINTER_MOTION_MASK
                        | Gdk.EventMask.LEAVE_NOTIFY_MASK)

    def _realize_cb(self, widget):
        display = self.get_window().get_display()

        self.__add_cursor = Gdk.Cursor.new_for_display(display,
                                                       Gdk.CursorType.PLUS)
        self.__move_cursor = Gdk.Cursor.new_for_display(
            display, Gdk.CursorType.FLEUR)
        self.__move_point_cursor = Gdk.Cursor.new_for_display(
            display, Gdk.CursorType.CROSSHAIR)
        self.__rotate_cursor = Gdk.Cursor.new_for_display(
            display, Gdk.CursorType.EXCHANGE)

    def __leave_cb(self, widget, event):
        # Reset the active objects when the pointer leaves.
        if self.__drag_func is not None:
            return
        self.__active_shape = None
        self.__active_ctrlpoint = None
        self.__tmp_new_ctrlpoint = None
        self.queue_draw()
        self.__set_cursor(None)

    def __set_cursor(self, cursor):
        # Sets the window cursor, retaining a record.
        if cursor != self.__last_cursor:
            self.get_window().set_cursor(cursor)
            self.__last_cursor = cursor

    def __update_active_objects(self, x, y):
        # Decides what a click or a drag at (x, y) would do, and updates the
        # mouse cursor and draw state to match.

        assert self.__drag_func is None
        self.__active_shape = None
        self.__active_ctrlpoint = None
        self.__tmp_new_ctrlpoint = None
        self.queue_draw()  # yes, always

        # Possible mask void manipulations
        mask = self.get_mask()
        for mask_idx in xrange(len(mask)):
            colors = mask[mask_idx]
            if len(colors) < 3:
                continue

            # If the pointer is near an existing control point, clicking and
            # dragging will move it.
            void = []
            for col_idx in xrange(len(colors)):
                col = colors[col_idx]
                px, py = self.get_pos_for_color(col)
                dp = math.sqrt((x - px)**2 + (y - py)**2)
                if dp <= self.__ctrlpoint_grab_radius:
                    mask.remove(colors)
                    mask.insert(0, colors)
                    self.__active_shape = colors
                    self.__active_ctrlpoint = col_idx
                    self.__set_cursor(None)
                    return
                void.append((px, py))

            # If within a certain distance of an edge, dragging will create and
            # then move a new control point.
            void = geom.convex_hull(void)
            for p1, p2 in geom.pairwise(void):
                isect = geom.nearest_point_in_segment(p1, p2, (x, y))
                if isect is not None:
                    ix, iy = isect
                    di = math.sqrt((ix - x)**2 + (iy - y)**2)
                    if di <= self.__ctrlpoint_grab_radius:
                        newcol = self.get_color_at_position(ix, iy)
                        self.__tmp_new_ctrlpoint = newcol
                        mask.remove(colors)
                        mask.insert(0, colors)
                        self.__active_shape = colors
                        self.__set_cursor(None)
                        return

            # If the mouse is within a mask void, then dragging would move that
            # shape around within the mask.
            if geom.point_in_convex_poly((x, y), void):
                mask.remove(colors)
                mask.insert(0, colors)
                self.__active_shape = colors
                self.__set_cursor(None)
                return

        # Away from shapes, clicks and drags manipulate the entire mask: adding
        # cutout voids to it, or rotating the whole mask around its central
        # axis.
        alloc = self.get_allocation()
        cx, cy = self.get_center(alloc=alloc)
        radius = self.get_radius(alloc=alloc)
        dx, dy = x - cx, y - cy
        r = math.sqrt(dx**2 + dy**2)
        if r < radius * (1.0 - self.min_shape_size):
            if len(mask) < self.__max_num_shapes:
                d = self.__dist_to_nearest_shape(x, y)
                minsize = radius * self.min_shape_size
                if d is None or d > minsize:
                    # Clicking will result in a new void
                    self.__set_cursor(self.__add_cursor)
        else:
            # Click-drag to rotate the entire mask
            self.__set_cursor(self.__rotate_cursor)

    def __drag_active_shape(self, px, py):
        # Updates the position of the active shape during drags.
        x0, y0 = self.__drag_start_pos
        dx = px - x0
        dy = py - y0
        self.__active_shape[:] = []
        for col in self.__active_shape_predrag:
            cx, cy = self.get_pos_for_color(col)
            cx += dx
            cy += dy
            col2 = super(HCYMaskEditorWheel,
                         self).get_color_at_position(cx, cy, ignore_mask=True)
            self.__active_shape.append(col2)

    def __drag_active_ctrlpoint(self, px, py):
        # Moves the highlighted control point during drags.
        x0, y0 = self.__drag_start_pos
        dx = px - x0
        dy = py - y0
        col = self.__active_ctrlpoint_predrag
        cx, cy = self.get_pos_for_color(col)
        cx += dx
        cy += dy
        col = super(HCYMaskEditorWheel,
                    self).get_color_at_position(cx, cy, ignore_mask=True)
        self.__active_shape[self.__active_ctrlpoint] = col

    def __rotate_mask(self, px, py):
        # Rotates the entire mask around the grey axis during drags.
        cx, cy = self.get_center()
        x0, y0 = self.__drag_start_pos
        theta0 = math.atan2(x0 - cx, y0 - cy)
        theta = math.atan2(px - cx, py - cy)
        dntheta = (theta0 - theta) / (2 * math.pi)
        while dntheta <= 0:
            dntheta += 1.0
        if self.__mask_predrag is None:
            self.__mask_predrag = []
            for shape in self.get_mask():
                shape_hcy = [HCYColor(color=c) for c in shape]
                self.__mask_predrag.append(shape_hcy)
        mgr = self.get_color_manager()
        newmask = []
        for shape in self.__mask_predrag:
            shape_rot = []
            for col in shape:
                col_r = HCYColor(color=col)
                h = mgr.distort_hue(col_r.h)
                h += dntheta
                h %= 1.0
                col_r.h = mgr.undistort_hue(h)
                shape_rot.append(col_r)
            newmask.append(shape_rot)
        self.set_mask(newmask)

    def __button_press_cb(self, widget, event):
        # Begins drags.
        if self.__drag_func is None:
            self.__update_active_objects(event.x, event.y)
            self.__drag_start_pos = event.x, event.y
            if self.__tmp_new_ctrlpoint is not None:
                self.__active_ctrlpoint = len(self.__active_shape)
                self.__active_shape.append(self.__tmp_new_ctrlpoint)
                self.__tmp_new_ctrlpoint = None
            if self.__active_ctrlpoint is not None:
                self.__active_shape_predrag = self.__active_shape[:]
                ctrlpt = self.__active_shape[self.__active_ctrlpoint]
                self.__active_ctrlpoint_predrag = ctrlpt
                self.__drag_func = self.__drag_active_ctrlpoint
                self.__set_cursor(self.__move_point_cursor)
            elif self.__active_shape is not None:
                self.__active_shape_predrag = self.__active_shape[:]
                self.__drag_func = self.__drag_active_shape
                self.__set_cursor(self.__move_cursor)
            elif self.__last_cursor is self.__rotate_cursor:
                self.__mask_predrag = None
                self.__drag_func = self.__rotate_mask

    def __button_release_cb(self, widget, event):
        # Ends the current drag & cleans up, or handle other clicks.
        if self.__drag_func is None:
            # Clicking when not in a drag adds a new shape
            if self.__last_cursor is self.__add_cursor:
                self.__add_void(event.x, event.y)
        else:
            # Cleanup when dragging ends
            self.__drag_func = None
            self.__drag_start_pos = None
            self.__cleanup_mask()
        self.__update_active_objects(event.x, event.y)

    def __motion_cb(self, widget, event):
        # Fire the current drag function if one's active.
        if self.__drag_func is not None:
            self.__drag_func(event.x, event.y)
            self.queue_draw()
        else:
            self.__update_active_objects(event.x, event.y)

    def __cleanup_mask(self):
        mask = self.get_mask()

        # Drop points from all shapes which are not part of the convex hulls.
        for shape in mask:
            if len(shape) <= 3:
                continue
            points = [self.get_pos_for_color(c) for c in shape]
            edge_points = geom.convex_hull(points)
            for col, point in zip(shape, points):
                if point in edge_points:
                    continue
                shape.remove(col)

        # Drop shapes smaller than the minimum size.
        newmask = []
        min_size = self.get_radius() * self.min_shape_size
        for shape in mask:
            points = [self.get_pos_for_color(c) for c in shape]
            void = geom.convex_hull(points)
            size = self._get_void_size(void)
            if size >= min_size:
                newmask.append(shape)
        mask = newmask

        # Drop shapes whose points entirely lie within other shapes
        newmask = []
        maskvoids = [
            (shape,
             geom.convex_hull([self.get_pos_for_color(c) for c in shape]))
            for shape in mask
        ]
        for shape1, void1 in maskvoids:
            shape1_subsumed = True
            for p1 in void1:
                p1_subsumed = False
                for shape2, void2 in maskvoids:
                    if shape1 is shape2:
                        continue
                    if geom.point_in_convex_poly(p1, void2):
                        p1_subsumed = True
                        break
                if not p1_subsumed:
                    shape1_subsumed = False
                    break
            if not shape1_subsumed:
                newmask.append(shape1)
        mask = newmask

        self.set_mask(mask)
        self.queue_draw()

    def __dist_to_nearest_shape(self, x, y):
        # Distance from `x`, `y` to the nearest edge or vertex of any shape.
        dists = []
        for hull in self.get_mask_voids():
            # cx, cy = geom.poly_centroid(hull)
            for p1, p2 in geom.pairwise(hull):
                nearest_point = geom.nearest_point_in_segment(p1, p2, (x, y))
                if nearest_point is not None:
                    nx, ny = nearest_point
                    d = math.sqrt((x - nx)**2 + (y - ny)**2)
                    dists.append(d)
            # Segment end too
            d = math.sqrt((p1[0] - x)**2 + (p1[1] - y)**2)
            dists.append(d)
        if not dists:
            return None
        dists.sort()
        return dists[0]

    def __add_void(self, x, y):
        # Adds a new shape into the empty space centred at `x`, `y`.
        self.queue_draw()
        # Pick a nice size for the new shape, taking care not to
        # overlap any other shapes, at least initially.
        alloc = self.get_allocation()
        cx, cy = self.get_center(alloc=alloc)
        radius = self.get_radius(alloc=alloc)
        dx, dy = x - cx, y - cy
        r = math.sqrt(dx**2 + dy**2)
        d = self.__dist_to_nearest_shape(x, y)
        if d is None:
            d = radius
        size = min((radius - r), d) * 0.95
        minsize = radius * self.min_shape_size
        if size < minsize:
            return
        # Create a regular polygon with one of its edges facing the
        # middle of the wheel.
        shape = []
        nsides = 3 + len(self.get_mask())
        psi = math.atan2(dy, dx) + (math.pi / nsides)
        psi += math.pi
        for i in xrange(nsides):
            theta = 2.0 * math.pi * i / nsides
            theta += psi
            px = int(x + size * math.cos(theta))
            py = int(y + size * math.sin(theta))
            col = self.get_color_at_position(px, py, ignore_mask=True)
            shape.append(col)
        mask = self.get_mask()
        mask.append(shape)
        self.set_mask(mask)

    def draw_mask_control_points(self, cr, wd, ht):
        # Draw active and inactive control points on the active shape.

        if self.__active_shape is None:
            return

        cr.save()
        active_rgb = 1, 1, 1
        normal_rgb = 0, 0, 0
        delete_rgb = 1, 0, 0
        cr.set_line_width(1.0)
        void = self.colors_to_mask_void(self.__active_shape)

        # Highlight the objects that would be directly or indirectly affected
        # if the shape were dragged, and how.
        min_size = self.get_radius(wd=wd, ht=ht) * self.min_shape_size
        void_rgb = normal_rgb
        if self._get_void_size(void) < min_size:
            # Shape will be deleted
            void_rgb = delete_rgb
        elif ((self.__active_ctrlpoint is None)
              and (self.__tmp_new_ctrlpoint is None)):
            # The entire shape would be moved
            void_rgb = active_rgb
        # Outline the current shape
        cr.set_source_rgb(*void_rgb)
        for p_idx, p in enumerate(void):
            if p_idx == 0:
                cr.move_to(*p)
            else:
                cr.line_to(*p)
        cr.close_path()
        cr.stroke()

        # Control points
        colors = self.__active_shape
        for col_idx, col in enumerate(colors):
            px, py = self.get_pos_for_color(col)
            if (px, py) not in void:
                # not in convex hull (is it worth doing this fragile test?)
                continue
            point_rgb = void_rgb
            if col_idx == self.__active_ctrlpoint:
                point_rgb = active_rgb
            cr.set_source_rgb(*point_rgb)
            cr.arc(px, py, self.__ctrlpoint_radius, 0, 2 * math.pi)
            cr.fill()
        if self.__tmp_new_ctrlpoint:
            px, py = self.get_pos_for_color(self.__tmp_new_ctrlpoint)
            cr.set_source_rgb(*active_rgb)
            cr.arc(px, py, self.__ctrlpoint_radius, 0, 2 * math.pi)
            cr.fill()

        # Centroid
        cr.set_source_rgb(*void_rgb)
        cx, cy = geom.poly_centroid(void)
        cr.save()
        cr.set_line_cap(cairo.LINE_CAP_SQUARE)
        cr.set_line_width(0.5)
        cr.translate(int(cx) + 0.5, int(cy) + 0.5)
        cr.move_to(-2, 0)
        cr.line_to(2, 0)
        cr.stroke()
        cr.move_to(0, -2)
        cr.line_to(0, 2)
        cr.stroke()

        cr.restore()

    def paint_foreground_cb(self, cr, wd, ht):
        """Foreground drawing override.
        """
        self.draw_mask(cr, wd, ht)
        self.draw_mask_control_points(cr, wd, ht)

    def get_managed_color(self):
        """Override, with a limited range or returned luma.
        """
        col = super(HCYMaskEditorWheel, self).get_managed_color()
        col = HCYColor(color=col)
        col.y = clamp(col.y, self.__MIN_LUMA, self.__MAX_LUMA)
        return col

    def set_managed_color(self, color):
        """Override, limiting the luma range.
        """
        col = HCYColor(color=color)
        col.y = clamp(col.y, self.__MIN_LUMA, self.__MAX_LUMA)
        super(HCYMaskEditorWheel, self).set_managed_color(col)
Example #7
0
 def get_page_title(cls):
     return C_(
         "HCY Wheel color adjuster page: title for tooltips etc.",
         u"HCY Wheel",
     )
Example #8
0
    def __init__(self, parent, target):
        super(HCYMaskPropertiesDialog, self).__init__(
            title=C_(
                "HCY Gamut Mask Editor dialog: window title",
                u"Gamut Mask Editor",
            ),
            transient_for=parent,
            modal=True,
            destroy_with_parent=True,
            window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
            buttons=(
                Gtk.STOCK_CANCEL,
                Gtk.ResponseType.REJECT,
                Gtk.STOCK_OK,
                Gtk.ResponseType.ACCEPT,
            ),
        )
        self.target = target
        ed = HCYMaskEditorWheel()
        target_mgr = target.get_color_manager()
        prefs_ro = deepcopy(target_mgr.get_prefs())
        datapath = target_mgr.get_data_path()
        ed_mgr = ColorManager(prefs=prefs_ro, datapath=datapath)
        ed.set_color_manager(ed_mgr)
        self.editor = ed
        ed.set_size_request(300, 300)
        ed.mask_toggle.set_active(True)
        self.mask_toggle_ctrl = Gtk.CheckButton(
            C_(
                "HCY Gamut Mask Editor dialog: mask-is-active checkbox",
                u"Active",
            ),
            use_underline=False,
        )
        self.mask_toggle_ctrl.set_tooltip_text(ed.mask_toggle.get_tooltip())
        ed.mask_observers.append(self.__mask_changed_cb)

        hbox = Gtk.HBox()
        hbox.set_spacing(3)

        # Sidebar buttonbox
        # On the right and packed to the top. This places its secondary
        # control, a mask toggle button, next to the "OK" button so it's less
        # likely to be missed.
        bbox = Gtk.VButtonBox()
        new_btn = self.__new_button = Gtk.Button(stock=Gtk.STOCK_NEW)
        load_btn = self.__load_button = Gtk.Button(stock=Gtk.STOCK_OPEN)
        save_btn = self.__save_button = Gtk.Button(stock=Gtk.STOCK_SAVE)
        clear_btn = self.__clear_button = Gtk.Button(stock=Gtk.STOCK_CLEAR)

        help_btn = self.__help_button = Gtk.LinkButton.new_with_label(
            uri=MASK_EDITOR_HELP_URI,
            label=C_(
                "HCY Mask Editor: action button labels",
                u"Help…",
            ),
        )

        new_btn.set_tooltip_text(
            C_("HCY Mask Editor: action button tooltips",
               u"Create mask from template."), )
        load_btn.set_tooltip_text(
            C_("HCY Mask Editor: action button tooltips",
               u"Load mask from a GIMP palette file."), )
        save_btn.set_tooltip_text(
            C_("HCY Mask Editor: action button tooltips",
               u"Save mask to a GIMP palette file."), )
        clear_btn.set_tooltip_text(
            C_("HCY Mask Editor: action button tooltips",
               u"Erase the mask."), )
        help_btn.set_tooltip_text(
            C_("HCY Mask Editor: action button tooltips",
               u"Open the online help for this dialog in a web browser."), )

        new_btn.connect("clicked", self.__new_clicked)
        save_btn.connect("clicked", self.__save_clicked)
        load_btn.connect("clicked", self.__load_clicked)
        clear_btn.connect("clicked", self.__clear_clicked)

        bbox.pack_start(new_btn, True, True, 0)
        bbox.pack_start(load_btn, True, True, 0)
        bbox.pack_start(save_btn, True, True, 0)
        bbox.pack_start(clear_btn, True, True, 0)

        action_area = self.get_action_area()
        if isinstance(action_area, Gtk.ButtonBox):
            action_area.pack_start(help_btn, True, True, 0)
            action_area.set_child_secondary(help_btn, True)
            action_area.set_child_non_homogeneous(help_btn, True)
            bbox.pack_start(self.mask_toggle_ctrl, True, True, 0)
            bbox.set_child_secondary(self.mask_toggle_ctrl, True)
        else:
            bbox.pack_start(self.mask_toggle_ctrl, True, True, 0)
            bbox.pack_start(help_btn, True, True, 0)
            bbox.set_child_secondary(help_btn, True)

        bbox.set_layout(Gtk.ButtonBoxStyle.START)

        hbox.pack_start(ed, True, True, 0)
        hbox.pack_start(bbox, False, False, 0)
        hbox.set_border_width(9)

        self.vbox.pack_start(hbox, True, True, 0)

        self.connect("response", self.__response_cb)
        self.connect("show", self.__show_cb)
        for w in self.vbox:
            w.show_all()
Example #9
0
 def picking_status_text(self):
     """The statusbar text to use during the grab."""
     return C_(
         "color picker: statusbar text during grab",
         u"Pick color…",
     )
Example #10
0
# TRANSLATORS: Statusbar message shown when the app chosen for
# TRANSLATORS: "Edit in External App" (for a layer) failed to open.
_LAUNCH_FAILED_MSG = _(u"Error: failed to launch {app_name} to edit "
                       u"layer “{layer_name}”")

# TRANSLATORS: Statusbar message for when an "Edit in External App"
# TRANSLATORS: operation is cancelled by the user before it starts.
_LAUNCH_CANCELLED_MSG = _(u"Editing cancelled. You can still edit "
                          u"“{layer_name}” from the Layers menu.")

# TRANSLATORS: This is a statusbar message shown when
# TRANSLATORS:  a layer is updated from an external edit
_LAYER_UPDATED_MSG = _(u"Updated layer “{layer_name}” with external edits")

_LAYER_UPDATE_FAILED_MSG = C_(
    "Edit in External App (statusbar message)",
    u"Failed to update layer with external edits from “{file_basename}”.",
)

# Restoration of environment variables for launch context

_MYP_ENV_NAME = 'MYPAINT_ENV_CLEAN'


def restore_env(ctx):
    """Clear existing envvars in context & use the given envvars instead

    This is used to restore the original environment when starting
    external editing with an environment that may lead to e.g.
    incompatible or non-existing versions of libraries being linked
    by the external application, instead of the correct ones.
Example #11
0
class SymmetryEditOptionsWidget (Gtk.Alignment):

    _POSITION_LABEL_TEXT = C_(
        "symmetry axis options panel: labels",
        u"Position:",
    )
    _POSITION_BUTTON_TEXT_INACTIVE = C_(
        "symmetry axis options panel: position button: no axis pos.",
        u"None",
    )
    _POSITION_BUTTON_TEXT_TEMPLATE = C_(
        "symmetry axis options panel: position button: axis pos. in pixels",
        u"%d px",
    )
    _ALPHA_LABEL_TEXT = C_(
        "symmetry axis options panel: labels",
        u"Alpha:",
    )

    def __init__(self):
        super(SymmetryEditOptionsWidget, self).__init__(
            xalign=0.5,
            yalign=0.5,
            xscale=1.0,
            yscale=1.0,
        )
        self._axis_pos_dialog = None
        self._axis_pos_button = None
        from application import get_app
        self.app = get_app()
        rootstack = self.app.doc.model.layer_stack
        self._axis_pos_adj = Gtk.Adjustment(
            rootstack.symmetry_axis,
            upper=32000,
            lower=-32000,
            step_incr=1,
            page_incr=100,
        )
        self._axis_pos_adj.connect(
            'value-changed',
            self._axis_pos_adj_changed,
        )
        self._init_ui()
        rootstack.symmetry_state_changed += self._symmetry_state_changed_cb
        self._update_axis_pos_button_label(rootstack.symmetry_axis)

    def _init_ui(self):
        app = self.app

        # Dialog for showing and editing the axis value directly
        buttons = (Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)
        dialog = gui.windowing.Dialog(
            app, C_(
                "symmetry axis options panel: axis position dialog: window title",
                u"Axis Position",
            ),
            app.drawWindow,
            buttons=buttons,
        )
        dialog.connect('response', self._axis_pos_dialog_response_cb)
        grid = Gtk.Grid()
        grid.set_border_width(gui.widgets.SPACING_LOOSE)
        grid.set_column_spacing(gui.widgets.SPACING)
        grid.set_row_spacing(gui.widgets.SPACING)
        label = Gtk.Label(self._POSITION_LABEL_TEXT)
        label.set_hexpand(False)
        label.set_vexpand(False)
        grid.attach(label, 0, 0, 1, 1)
        entry = Gtk.SpinButton(
            adjustment=self._axis_pos_adj,
            climb_rate=0.25,
            digits=0
        )
        entry.set_hexpand(True)
        entry.set_vexpand(False)
        grid.attach(entry, 1, 0, 1, 1)
        dialog_content_box = dialog.get_content_area()
        dialog_content_box.pack_start(grid, True, True, 0)
        self._axis_pos_dialog = dialog

        # Layout grid
        row = 0
        grid = Gtk.Grid()
        grid.set_border_width(gui.widgets.SPACING_CRAMPED)
        grid.set_row_spacing(gui.widgets.SPACING_CRAMPED)
        grid.set_column_spacing(gui.widgets.SPACING_CRAMPED)
        self.add(grid)

        row += 1
        label = Gtk.Label(self._ALPHA_LABEL_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        grid.attach(label, 0, row, 1, 1)
        scale = Gtk.Scale.new_with_range(
            orientation = Gtk.Orientation.HORIZONTAL,
            min = 0,
            max = 1,
            step = 0.1,
        )
        scale.set_draw_value(False)
        line_alpha = self.app.preferences.get(_ALPHA_PREFS_KEY, _DEFAULT_ALPHA)
        scale.set_value(line_alpha)
        scale.set_hexpand(True)
        scale.set_vexpand(False)
        scale.connect("value-changed", self._scale_value_changed_cb)
        grid.attach(scale, 1, row, 1, 1)

        row += 1
        label = Gtk.Label(self._POSITION_LABEL_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        button = Gtk.Button(self._POSITION_BUTTON_TEXT_INACTIVE)
        button.set_vexpand(False)
        button.connect("clicked", self._axis_pos_button_clicked_cb)
        button.set_hexpand(True)
        button.set_vexpand(False)
        grid.attach(label, 0, row, 1, 1)
        grid.attach(button, 1, row, 1, 1)
        self._axis_pos_button = button

        row += 1
        button = Gtk.CheckButton()
        toggle_action = self.app.find_action("SymmetryActive")
        button.set_related_action(toggle_action)
        button.set_label(C_(
            "symmetry axis options panel: axis active checkbox",
            u'Enabled',
        ))
        button.set_hexpand(True)
        button.set_vexpand(False)
        grid.attach(button, 1, row, 2, 1)
        self._axis_active_button = button


    def _symmetry_state_changed_cb(self, rootstack, active, x):
        self._update_axis_pos_button_label(x)
        dialog = self._axis_pos_dialog
        dialog_content_box = dialog.get_content_area()
        if x is None:
            dialog_content_box.set_sensitive(False)
        else:
            dialog_content_box.set_sensitive(True)
            adj = self._axis_pos_adj
            adj_pos = int(adj.get_value())
            model_pos = int(x)
            if adj_pos != model_pos:
                adj.set_value(model_pos)

    def _update_axis_pos_button_label(self, x):
        if x is None:
            text = self._POSITION_BUTTON_TEXT_INACTIVE
        else:
            text = self._POSITION_BUTTON_TEXT_TEMPLATE % (x,)
        self._axis_pos_button.set_label(text)

    def _axis_pos_adj_changed(self, adj):
        rootstack = self.app.doc.model.layer_stack
        model_pos = int(rootstack.symmetry_axis)
        adj_pos = int(adj.get_value())
        if adj_pos != model_pos:
            rootstack.symmetry_axis = adj_pos

    def _axis_pos_button_clicked_cb(self, button):
        self._axis_pos_dialog.show_all()

    def _axis_pos_dialog_response_cb(self, dialog, response_id):
        if response_id == Gtk.ResponseType.ACCEPT:
            dialog.hide()

    def _scale_value_changed_cb(self, scale):
        alpha = scale.get_value()
        prefs = self.app.preferences
        prefs[_ALPHA_PREFS_KEY] = alpha
        for tdw in self._tdws_with_symmetry_overlays():
            tdw.queue_draw()

    @staticmethod
    def _tdws_with_symmetry_overlays():
        for tdw in gui.tileddrawwidget.TiledDrawWidget.get_visible_tdws():
            for ov in tdw.display_overlays:
                if isinstance(ov, SymmetryOverlay):
                    yield tdw
Example #12
0
class SymmetryEditMode (gui.mode.ScrollableModeMixin, gui.mode.DragMode):
    """Tool/mode for editing the axis of symmetry used when painting"""

    # Class-level config

    ACTION_NAME = 'SymmetryEditMode'

    pointer_behavior = gui.mode.Behavior.EDIT_OBJECTS
    scroll_behavior = gui.mode.Behavior.CHANGE_VIEW

    # These will be overridden on enter()
    inactive_cursor = None
    active_cursor = None

    unmodified_persist = True
    permitted_switch_actions = {
        'ShowPopupMenu', 'RotateViewMode', 'ZoomViewMode', 'PanViewMode',
    }

    # Statusbar stuff
    _STATUSBAR_CONTEXT = 'symmetry-mode'
    _STATUSBAR_CREATE_AXIS_MSG = C_(
        "symmetry axis edit mode: instructions shown in statusbar",
        u"Place axis",
    )
    _STATUSBAR_MOVE_AXIS_MSG = C_(
        "symmetry axis edit mode: instructions shown in statusbar",
        u"Move axis",
    )
    _STATUSBAR_DELETE_AXIS_MSG = C_(
        "symmetry axis edit mode: instructions shown in statusbar",
        u"Remove axis",
    )

    # Options widget singleton
    _OPTIONS_WIDGET = None

    # Info strings

    @classmethod
    def get_name(cls):
        return C_(
            "symmetry axis edit mode: mode name (tooltips)",
            u"Edit Symmetry Axis",
        )

    def get_usage(self):
        return C_(
            "symmetry axis edit mode: mode description (tooltips)",
            u"Adjust the painting symmetry axis.",
        )

    # Initization and mode interface

    def __init__(self, **kwds):
        """Initialize."""
        super(SymmetryEditMode, self).__init__(**kwds)
        from gui.application import get_app
        app = get_app()
        self.app = app

        # The overlay is always present and stores the information required to
        # draw the axes, as well as information about what the active zone is.
        self._overlay = [
            o for o in app.doc.tdw.display_overlays
            if isinstance(o, SymmetryOverlay)][0]

        statusbar_cid = app.statusbar.get_context_id(self._STATUSBAR_CONTEXT)
        self._statusbar_context_id = statusbar_cid
        self._last_msg_zone = None
        self._zone = None

        # Symmetry center location at the beginning of the drag
        self._drag_start_pos = None
        self._drag_prev_pos = None
        self._active_axis_points = None
        self._drag_factors = None
        self._click_info = None
        self._entered_before = False

        self._move_item = None
        self._move_timeout_id = None

        # Initialize/fetch cursors
        self.cursor_remove = self._get_cursor(gui.cursor.Name.ARROW)
        self.cursor_add = self._get_cursor(gui.cursor.Name.ADD)
        self.cursor_normal = self._get_cursor(gui.cursor.Name.ARROW)
        self.cursor_movable = self._get_cursor(gui.cursor.Name.HAND_OPEN)
        self.cursor_moving = self._get_cursor(gui.cursor.Name.HAND_CLOSED)

    def _get_cursor(self, name):
        return self.app.cursors.get_action_cursor(self.ACTION_NAME, name)

    def enter(self, doc, **kwds):
        """Enter the mode"""
        super(SymmetryEditMode, self).enter(doc, **kwds)
        # Set overlay to draw edit controls (center point, disable button)
        self._overlay.enable_edit_mode()
        # Turn on the axis, if it happens to be off right now
        if not self._entered_before:
            self.app.find_action("SymmetryActive").set_active(True)
            self._entered_before = True

    def popped(self):
        # Set overlay to draw normally, without controls
        self._overlay.disable_edit_mode()
        super(SymmetryEditMode, self).popped()

    def _update_statusbar(self, zone):
        if self.in_drag:
            return
        if self._last_msg_zone == zone:
            return
        self._last_msg_zone = zone
        statusbar = self.app.statusbar
        statusbar_cid = self._statusbar_context_id
        statusbar.remove_all(statusbar_cid)
        msgs = {
            _EditZone.CREATE_AXIS: self._STATUSBAR_CREATE_AXIS_MSG,
            _EditZone.MOVE_AXIS: self._STATUSBAR_MOVE_AXIS_MSG,
            _EditZone.MOVE_CENTER: self._STATUSBAR_MOVE_AXIS_MSG,
            _EditZone.DISABLE: self._STATUSBAR_DELETE_AXIS_MSG,
        }
        msg = msgs.get(zone, None)
        if msg:
            statusbar.push(statusbar_cid, msg)

    def get_options_widget(self):
        """Get the (class singleton) options widget"""
        cls = self.__class__
        if cls._OPTIONS_WIDGET is None:
            widget = SymmetryEditOptionsWidget()
            cls._OPTIONS_WIDGET = widget
        return cls._OPTIONS_WIDGET

    # Events and internals

    def button_press_cb(self, tdw, event):
        if self._zone in (_EditZone.CREATE_AXIS, _EditZone.DISABLE):
            button = event.button
            if button == 1 and event.type == Gdk.EventType.BUTTON_PRESS:
                self._click_info = (button, self._zone)
                return False
        return super(SymmetryEditMode, self).button_press_cb(tdw, event)

    def button_release_cb(self, tdw, event):
        # If the corresponding press was not on a clickable zone, or the cursor
        # was moved away from it before the button was released.
        info = self._click_info
        if not info or (event.button, self._zone) != info:
            return super(SymmetryEditMode, self).button_release_cb(tdw, event)

        _, zone_pressed = info
        self._click_info = None
        layer_stack = tdw.doc.layer_stack
        # Disable button clicked
        if zone_pressed == _EditZone.DISABLE:
            layer_stack.symmetry_active = False
        # Symmetry was inactive - create axis based on cursor position
        elif zone_pressed == _EditZone.CREATE_AXIS:
            new_center = tuple(
                int(round(c)) for c in
                tdw.display_to_model(event.x, event.y))
            if layer_stack.symmetry_unset:
                layer_stack.symmetry_unset = False
            layer_stack.set_symmetry_state(True, center=new_center)
        self._update_zone_and_cursor(event.x, event.y)
        return False

    def _update_zone_and_cursor(self, x, y):
        """Update UI & some internal zone flags from pointer position

        :param x: cursor x position
        :param y: cursor y position

        See also: `SymmetryOverlay`.

        """
        if self.in_drag:
            return

        zone_cursors = {  # Active and inactive cursors respectively
            _EditZone.CREATE_AXIS: (self.cursor_add, self.cursor_add),
            _EditZone.MOVE_CENTER: (self.cursor_moving, self.cursor_movable),
            _EditZone.DISABLE: (self.cursor_remove, self.cursor_remove),
            _EditZone.NONE: (self.cursor_normal, self.cursor_normal),
        }

        changed, zone, data = self._overlay.update_zone_data(x, y)
        self._zone = zone

        if changed:
            self._update_statusbar(zone)
            if zone in zone_cursors:
                self.active_cursor, self.inactive_cursor = zone_cursors[zone]
            elif data:
                active, inactive, axis_points = data
                self._active_axis_points = axis_points
                self.active_cursor = self._get_cursor(active)
                self.inactive_cursor = self._get_cursor(inactive)

    def motion_notify_cb(self, tdw, event):
        if not self.in_drag:
            self._update_zone_and_cursor(event.x, event.y)
            tdw.set_override_cursor(self.inactive_cursor)
        return super(SymmetryEditMode, self).motion_notify_cb(tdw, event)

    def drag_start_cb(self, tdw, event):
        tdw.renderer.defer_hq_rendering(10)
        if self._zone in (_EditZone.MOVE_AXIS, _EditZone.MOVE_CENTER):
            self._drag_start_pos = tdw.doc.layer_stack.symmetry_center
            if self._zone == _EditZone.MOVE_AXIS:
                p1, p2 = self._active_axis_points
                # Calculate how the pixel offsets (display coordinates) relate
                # to the symmetry center offsets (model coordinates).
                # Sloppy way to do it, but it isn't done often.
                offs = 1000.0
                dx, dy = tdw.model_to_display(*p1)
                # Horizontal and vertical display offsets -> model coordinates
                xrefx, xrefy = tdw.display_to_model(dx - offs, dy)
                yrefx, yrefy = tdw.display_to_model(dx, dy + offs)
                # Display x offsets -> model x,y offsets
                xc, yc = lib.alg.nearest_point_on_line(p1, p2, (xrefx, xrefy))
                xx, xy = (xc - xrefx) / offs, (yc - xrefy) / offs
                # Display y offsets -> model x,y offsets
                xc, yc = lib.alg.nearest_point_on_line(p1, p2, (yrefx, yrefy))
                yx, yy = (yrefx - xc) / offs, (yrefy - yc) / offs
                self._drag_factors = xx, xy, yx, yy
        else:
            self._update_zone_and_cursor(self.start_x, self.start_y)
        return super(SymmetryEditMode, self).drag_start_cb(tdw, event)

    def drag_update_cb(self, tdw, event, ev_x, ev_y, dx, dy):
        zone = self._zone
        if zone == _EditZone.MOVE_CENTER:
            self._queue_movement(zone, (ev_x, ev_y, tdw))
        elif zone == _EditZone.MOVE_AXIS:
            self._queue_movement(
                zone, (ev_x - self.start_x, ev_y - self.start_y, tdw))

    def _queue_movement(self, zone, args):
        self._move_item = (zone, args)
        if not self._move_timeout_id:
            self._move_timeout_id = GLib.timeout_add(
                interval=16.66,  # 60 fps cap
                function=self._do_move,
            )

    def _do_move(self):
        if self._move_item:
            zone, args = self._move_item
            self._move_item = None
            if zone == _EditZone.MOVE_AXIS:
                dx, dy, tdw = args
                self._move_axis(dx, dy, tdw.doc.layer_stack)
            elif zone == _EditZone.MOVE_CENTER:
                x, y, tdw = args
                self._move_center(x, y, tdw)
        self._move_timeout_id = None

    def _move_center(self, x, y, tdw):
        xm, ym = tdw.display_to_model(x, y)
        tdw.doc.layer_stack.symmetry_center = (xm, ym)

    def _move_axis(self, dx_full, dy_full, stack):
        xs, ys = self._drag_start_pos
        xx, xy, yx, yy = self._drag_factors
        xm = round(xs + (dx_full * xx + dy_full * yx))
        ym = round(ys + (dx_full * xy + dy_full * yy))
        new_pos = xm, ym
        if self._drag_prev_pos != new_pos:
            self._drag_prev_pos = new_pos
            stack.symmetry_center = new_pos

    def drag_stop_cb(self, tdw):
        if self._move_item and not self._move_timeout_id:
            self._do_move()
        tdw.renderer.defer_hq_rendering(0)
        return super(SymmetryEditMode, self).drag_stop_cb(tdw)
Example #13
0
    def _init_ui(self):

        # Layout grid
        row = 0
        grid = Gtk.Grid()
        grid.set_border_width(gui.widgets.SPACING_CRAMPED)
        grid.set_row_spacing(gui.widgets.SPACING_CRAMPED)
        grid.set_column_spacing(gui.widgets.SPACING_CRAMPED)
        self.add(grid)

        row += 1
        label = Gtk.Label(label=self._ALPHA_LABEL_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        grid.attach(label, 0, row, 1, 1)
        scale = InputSlider()
        scale.set_range(0, 1)
        scale.set_round_digits(1)
        scale.set_draw_value(False)
        line_alpha = self.app.preferences.get(_ALPHA_PREFS_KEY, _DEFAULT_ALPHA)
        scale.set_value(line_alpha)
        scale.set_hexpand(True)
        scale.set_vexpand(False)
        scale.scale.connect("value-changed", self._scale_value_changed_cb)
        grid.attach(scale, 1, row, 1, 1)

        row += 1
        store = Gtk.ListStore(int, str)
        rootstack = self.app.doc.model.layer_stack
        for _type in lib.tiledsurface.SYMMETRY_TYPES:
            store.append([_type, lib.tiledsurface.SYMMETRY_STRINGS[_type]])
        self._symmetry_type_combo = Gtk.ComboBox()
        self._symmetry_type_combo.set_model(store)
        self._symmetry_type_combo.set_active(rootstack.symmetry_type)
        self._symmetry_type_combo.set_hexpand(True)
        cell = Gtk.CellRendererText()
        self._symmetry_type_combo.pack_start(cell, True)
        self._symmetry_type_combo.add_attribute(cell, "text", 1)
        self._type_cb_id = self._symmetry_type_combo.connect(
            'changed',
            self._symmetry_type_combo_changed_cb
        )
        label = Gtk.Label(label=self._SYMMETRY_TYPE_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        grid.attach(label, 0, row, 1, 1)
        grid.attach(self._symmetry_type_combo, 1, row, 1, 1)

        row += 1
        label = Gtk.Label(label=self._SYMMETRY_ROT_LINES_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        self._axis_sym_lines_entry = Gtk.SpinButton(
            adjustment=self._axis_symmetry_lines,
            climb_rate=0.25
        )
        self._update_num_lines_sensitivity(rootstack.symmetry_type)
        grid.attach(label, 0, row, 1, 1)
        grid.attach(self._axis_sym_lines_entry, 1, row, 1, 1)

        row += 1
        label = Gtk.Label(label=self._POSITION_LABEL_X_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        entry = Gtk.SpinButton(
            adjustment=self._axis_pos_adj_x,
            climb_rate=0.25,
            digits=0
        )
        entry.set_hexpand(True)
        entry.set_vexpand(False)
        grid.attach(label, 0, row, 1, 1)
        grid.attach(entry, 1, row, 1, 1)

        row += 1
        label = Gtk.Label(label=self._POSITION_LABEL_Y_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        entry = Gtk.SpinButton(
            adjustment=self._axis_pos_adj_y,
            climb_rate=0.25,
            digits=0
        )
        entry.set_hexpand(True)
        entry.set_vexpand(False)
        grid.attach(label, 0, row, 1, 1)
        grid.attach(entry, 1, row, 1, 1)

        row += 1
        label = Gtk.Label()
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        self._angle_label = label
        self._update_angle_label()
        grid.attach(label, 0, row, 1, 1)
        scale = InputSlider(self._axis_angle)
        scale.set_draw_value(False)
        scale.set_hexpand(True)
        scale.set_vexpand(False)
        grid.attach(scale, 1, row, 1, 1)

        row += 1
        button = Gtk.CheckButton()
        toggle_action = self.app.find_action("SymmetryActive")
        button.set_related_action(toggle_action)
        button.set_label(C_(
            "symmetry axis options panel: axis active checkbox",
            u'Enabled',
        ))
        button.set_hexpand(True)
        button.set_vexpand(False)
        grid.attach(button, 1, row, 2, 1)
Example #14
0
class SymmetryEditOptionsWidget (Gtk.Alignment):

    _POSITION_LABEL_X_TEXT = C_(
        "symmetry axis options panel: labels",
        u"X Position:",
    )
    _POSITION_LABEL_Y_TEXT = C_(
        "symmetry axis options panel: labels",
        u"Y Position:",
    )
    _ANGLE_LABEL_TEXT = C_(
        "symmetry axis options panel: labels",
        u"Angle: %.2f°",
    )
    _POSITION_BUTTON_TEXT_INACTIVE = C_(
        "symmetry axis options panel: position button: no axis pos.",
        u"None",
    )
    _ALPHA_LABEL_TEXT = C_(
        "symmetry axis options panel: labels",
        u"Alpha:",
    )
    _SYMMETRY_TYPE_TEXT = C_(
        "symmetry axis options panel: labels",
        u"Symmetry Type:",
    )
    _SYMMETRY_ROT_LINES_TEXT = C_(
        "symmetry axis options panel: labels",
        u"Rotational lines:",
    )

    def __init__(self):
        super(SymmetryEditOptionsWidget, self).__init__(
            xalign=0.5,
            yalign=0.5,
            xscale=1.0,
            yscale=1.0,
        )
        self._symmetry_type_combo = None
        self._axis_sym_lines_entry = None
        from gui.application import get_app
        self.app = get_app()
        rootstack = self.app.doc.model.layer_stack
        x, y = rootstack.symmetry_center

        def pos_adj(start_val):
            return Gtk.Adjustment(
                value=start_val,
                upper=32000,
                lower=-32000,
                step_increment=1,
                page_increment=100,
            )

        self._axis_pos_adj_x = pos_adj(x)
        self._xpos_cb_id = self._axis_pos_adj_x.connect(
            'value-changed',
            self._axis_pos_adj_x_changed,
        )
        self._axis_pos_adj_y = pos_adj(y)
        self._ypos_cb_id = self._axis_pos_adj_y.connect(
            'value-changed',
            self._axis_pos_adj_y_changed,
        )
        self._axis_angle = Gtk.Adjustment(
            value=rootstack.symmetry_angle,
            upper=180,
            lower=0,
            step_increment=1,
            page_increment=15,
        )
        self._angle_cb_id = self._axis_angle.connect(
            "value-changed", self._angle_value_changed)
        self._axis_symmetry_lines = Gtk.Adjustment(
            value=rootstack.symmetry_lines,
            upper=50,
            lower=2,
            step_increment=1,
            page_increment=3,
        )
        self._lines_cb_id = self._axis_symmetry_lines.connect(
            'value-changed',
            self._axis_rot_symmetry_lines_changed,
        )

        self._init_ui()
        rootstack.symmetry_state_changed += self._symmetry_state_changed_cb

    def _init_ui(self):

        # Layout grid
        row = 0
        grid = Gtk.Grid()
        grid.set_border_width(gui.widgets.SPACING_CRAMPED)
        grid.set_row_spacing(gui.widgets.SPACING_CRAMPED)
        grid.set_column_spacing(gui.widgets.SPACING_CRAMPED)
        self.add(grid)

        row += 1
        label = Gtk.Label(label=self._ALPHA_LABEL_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        grid.attach(label, 0, row, 1, 1)
        scale = InputSlider()
        scale.set_range(0, 1)
        scale.set_round_digits(1)
        scale.set_draw_value(False)
        line_alpha = self.app.preferences.get(_ALPHA_PREFS_KEY, _DEFAULT_ALPHA)
        scale.set_value(line_alpha)
        scale.set_hexpand(True)
        scale.set_vexpand(False)
        scale.scale.connect("value-changed", self._scale_value_changed_cb)
        grid.attach(scale, 1, row, 1, 1)

        row += 1
        store = Gtk.ListStore(int, str)
        rootstack = self.app.doc.model.layer_stack
        for _type in lib.tiledsurface.SYMMETRY_TYPES:
            store.append([_type, lib.tiledsurface.SYMMETRY_STRINGS[_type]])
        self._symmetry_type_combo = Gtk.ComboBox()
        self._symmetry_type_combo.set_model(store)
        self._symmetry_type_combo.set_active(rootstack.symmetry_type)
        self._symmetry_type_combo.set_hexpand(True)
        cell = Gtk.CellRendererText()
        self._symmetry_type_combo.pack_start(cell, True)
        self._symmetry_type_combo.add_attribute(cell, "text", 1)
        self._type_cb_id = self._symmetry_type_combo.connect(
            'changed',
            self._symmetry_type_combo_changed_cb
        )
        label = Gtk.Label(label=self._SYMMETRY_TYPE_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        grid.attach(label, 0, row, 1, 1)
        grid.attach(self._symmetry_type_combo, 1, row, 1, 1)

        row += 1
        label = Gtk.Label(label=self._SYMMETRY_ROT_LINES_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        self._axis_sym_lines_entry = Gtk.SpinButton(
            adjustment=self._axis_symmetry_lines,
            climb_rate=0.25
        )
        self._update_num_lines_sensitivity(rootstack.symmetry_type)
        grid.attach(label, 0, row, 1, 1)
        grid.attach(self._axis_sym_lines_entry, 1, row, 1, 1)

        row += 1
        label = Gtk.Label(label=self._POSITION_LABEL_X_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        entry = Gtk.SpinButton(
            adjustment=self._axis_pos_adj_x,
            climb_rate=0.25,
            digits=0
        )
        entry.set_hexpand(True)
        entry.set_vexpand(False)
        grid.attach(label, 0, row, 1, 1)
        grid.attach(entry, 1, row, 1, 1)

        row += 1
        label = Gtk.Label(label=self._POSITION_LABEL_Y_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        entry = Gtk.SpinButton(
            adjustment=self._axis_pos_adj_y,
            climb_rate=0.25,
            digits=0
        )
        entry.set_hexpand(True)
        entry.set_vexpand(False)
        grid.attach(label, 0, row, 1, 1)
        grid.attach(entry, 1, row, 1, 1)

        row += 1
        label = Gtk.Label()
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        self._angle_label = label
        self._update_angle_label()
        grid.attach(label, 0, row, 1, 1)
        scale = InputSlider(self._axis_angle)
        scale.set_draw_value(False)
        scale.set_hexpand(True)
        scale.set_vexpand(False)
        grid.attach(scale, 1, row, 1, 1)

        row += 1
        button = Gtk.CheckButton()
        toggle_action = self.app.find_action("SymmetryActive")
        button.set_related_action(toggle_action)
        button.set_label(C_(
            "symmetry axis options panel: axis active checkbox",
            u'Enabled',
        ))
        button.set_hexpand(True)
        button.set_vexpand(False)
        grid.attach(button, 1, row, 2, 1)

    def _update_angle_label(self):
        self._angle_label.set_text(
            self._ANGLE_LABEL_TEXT % self._axis_angle.get_value()
        )

    def _symmetry_state_changed_cb(
            self, stack, active, center, sym_type, sym_lines, sym_angle):

        if center:
            cx, cy = center
            with self._axis_pos_adj_x.handler_block(self._xpos_cb_id):
                self._axis_pos_adj_x.set_value(cx)
            with self._axis_pos_adj_y.handler_block(self._ypos_cb_id):
                self._axis_pos_adj_y.set_value(cy)
        if sym_type is not None:
            with self._symmetry_type_combo.handler_block(self._type_cb_id):
                self._symmetry_type_combo.set_active(sym_type)
            self._update_num_lines_sensitivity(sym_type)
        if sym_lines is not None:
            with self._axis_symmetry_lines.handler_block(self._lines_cb_id):
                self._axis_symmetry_lines.set_value(sym_lines)
        if sym_angle is not None:
            with self._axis_angle.handler_block(self._angle_cb_id):
                self._axis_angle.set_value(sym_angle)
            self._update_angle_label()

    def _update_num_lines_sensitivity(self, sym_type):
        self._axis_sym_lines_entry.set_sensitive(
            sym_type in (SymmetryRotational, SymmetrySnowflake)
        )

    def _axis_pos_adj_x_changed(self, adj):
        self.app.doc.model.layer_stack.symmetry_x = int(adj.get_value())

    def _axis_rot_symmetry_lines_changed(self, adj):
        self.app.doc.model.layer_stack.symmetry_lines = int(adj.get_value())

    def _axis_pos_adj_y_changed(self, adj):
        self.app.doc.model.layer_stack.symmetry_y = int(adj.get_value())

    def _angle_value_changed(self, adj):
        self._update_angle_label()
        self.app.doc.model.layer_stack.symmetry_angle = adj.get_value()

    def _symmetry_type_combo_changed_cb(self, combo):
        sym_type = combo.get_model()[combo.get_active()][0]
        self.app.doc.model.layer_stack.symmetry_type = sym_type

    def _scale_value_changed_cb(self, alpha_scale):
        self.app.preferences[_ALPHA_PREFS_KEY] = alpha_scale.get_value()
        for overlay in self._symmetry_overlays():
            overlay.set_line_alpha(alpha_scale.get_value())

    @staticmethod
    def _symmetry_overlays():
        for tdw in gui.tileddrawwidget.TiledDrawWidget.get_visible_tdws():
            for ov in tdw.display_overlays:
                if isinstance(ov, SymmetryOverlay):
                    yield ov
Example #15
0
 def get_usage(self):
     return C_(
         "symmetry axis edit mode: mode description (tooltips)",
         u"Adjust the painting symmetry axis.",
     )
Example #16
0
class ModeOptionsTool(workspace.SizedVBoxToolWidget):
    """Dockable panel showing options for the current mode

    This panel has a title and an icon reflecting the current mode, and
    displays its options widget if it has one: define an object method named
    ``get_options_widget()`` returning an arbitrary GTK widget. Singletons work
    well here, and are encouraged. ``get_options_widget()`` can also return
    `None` if the mode is a temporary mode which has no sensible options. In
    this case, any widget already displayed will not be replaced, which is
    particularly appropriate for modes which only persist for the length of
    time the mouse button is held, and stack on top of other modes.
    """

    ## Class constants

    SIZED_VBOX_NATURAL_HEIGHT = workspace.TOOL_WIDGET_NATURAL_HEIGHT_SHORT

    tool_widget_icon_name = "mypaint-options-symbolic"
    tool_widget_title = C_(
        "options panel: tab tooltip: title",
        "Tool Options",
    )
    tool_widget_description = C_(
        "options panel: tab tooltip: description",
        "Specialized settings for the current editing tool",
    )

    __gtype_name__ = 'MyPaintModeOptionsTool'

    OPTIONS_MARKUP = C_(
        "options panel: header",
        "<b>{mode_name}</b>",
    )
    NO_OPTIONS_MARKUP = C_(
        "options panel: body",
        "<i>No options available</i>",
    )

    ## Method defs

    def __init__(self):
        """Construct, and connect internal signals & callbacks"""
        workspace.SizedVBoxToolWidget.__init__(self)
        from application import get_app
        self._app = get_app()
        self._app.doc.modes.changed += self._modestack_changed_cb
        self.set_border_width(3)
        self.set_spacing(6)
        # Placeholder in case a mode has no options
        label = Gtk.Label()
        label.set_markup(self.NO_OPTIONS_MARKUP)
        self._no_options_label = label
        # Container for an options widget exposed by the current mode
        self._mode_icon = Gtk.Image()
        label = Gtk.Label()
        label.set_text("<options-label>")
        self._options_label = label
        label.set_alignment(0.0, 0.5)
        label_hbox = Gtk.HBox()
        label_hbox.set_spacing(3)
        label_hbox.set_border_width(3)
        label_hbox.pack_start(self._mode_icon, False, False, 0)
        label_hbox.pack_start(self._options_label, True, True, 0)
        align = Gtk.Alignment.new(0.5, 0.5, 1.0, 1.0)
        align.set_padding(0, 0, 0, 0)
        align.set_border_width(3)
        self._options_bin = align
        self.pack_start(label_hbox, False, False, 0)
        self.pack_start(align, True, True, 0)
        self.connect("show", lambda *a: self._update_ui())
        # Fallback
        self._update_ui_with_options_widget(
            self._no_options_label,
            self.tool_widget_title,
            self.tool_widget_icon_name,
        )

    def _modestack_changed_cb(self, modestack, old, new):
        """Update the UI when the mode changes"""
        self._update_ui()

    def _update_ui(self):
        """Update the UI to show the options widget of the current mode"""
        mode = self._app.doc.modes.top
        self._update_ui_for_mode(mode)

    def _update_ui_for_mode(self, mode):
        # Get the new options widget
        try:
            get_options_widget = mode.get_options_widget
        except AttributeError:
            get_options_widget = None
        if get_options_widget:
            new_options = get_options_widget()
        else:
            new_options = self._no_options_label
        if not new_options:
            # Leave existing widget as-is, even if it's the default.
            # XXX maybe we should be doing something stack-based here?
            return
        icon_name = mode.get_icon_name()
        name = mode.get_name()
        self._update_ui_with_options_widget(new_options, name, icon_name)

    def _update_ui_with_options_widget(self, new_options, name, icon_name):
        old_options = self._options_bin.get_child()
        logger.debug("name: %r, icon name: %r", name, icon_name)
        if name:
            markup = self.OPTIONS_MARKUP.format(
                mode_name=lib.xml.escape(name), )
            self._options_label.set_markup(markup)
        if icon_name:
            self._mode_icon.set_from_icon_name(
                icon_name,
                Gtk.IconSize.SMALL_TOOLBAR,
            )
        # Options widget: only update if there's a change
        if new_options is not old_options:
            if old_options:
                old_options.hide()
                self._options_bin.remove(old_options)
            self._options_bin.add(new_options)
            new_options.show()
        self._options_bin.show_all()
Example #17
0
 def get_name(cls):
     return C_(
         "symmetry axis edit mode: mode name (tooltips)",
         u"Edit Symmetry Axis",
     )
Example #18
0
class SymmetryEditMode (gui.mode.ScrollableModeMixin, gui.mode.DragMode):
    """Tool/mode for editing the axis of symmetry used when painting"""

    ## Class-level config

    ACTION_NAME = 'SymmetryEditMode'

    pointer_behavior = gui.mode.Behavior.EDIT_OBJECTS
    scroll_behavior = gui.mode.Behavior.CHANGE_VIEW

    # These will be overridden on enter()
    inactive_cursor = None
    active_cursor = None

    unmodified_persist = True
    permitted_switch_actions = set([
        'ShowPopupMenu',
        'RotateViewMode',
        'ZoomViewMode',
        'PanViewMode',
    ])

    _GRAB_SENSITIVITY = 8  # pixels

    # Statusbar stuff
    _STATUSBAR_CONTEXT = 'symmetry-mode'
    _STATUSBAR_CREATE_AXIS_MSG = C_(
        "symmetry axis edit mode: instructions shown in statusbar",
        u"Place axis",
    )
    _STATUSBAR_MOVE_AXIS_MSG = C_(
        "symmetry axis edit mode: instructions shown in statusbar",
        u"Move axis",
    )
    _STATUSBAR_DELETE_AXIS_MSG = C_(
        "symmetry axis edit mode: instructions shown in statusbar",
        u"Remove axis",
    )

    # Options widget singleton
    _OPTIONS_WIDGET = None

    ## Info strings

    @classmethod
    def get_name(cls):
        return C_(
            "symmetry axis edit mode: mode name (tooltips)",
            u"Edit Symmetry Axis",
        )

    def get_usage(self):
        return C_(
            "symmetry axis edit mode: mode description (tooltips)",
            u"Adjust the painting symmetry axis.",
        )

    ## Initization and mode interface

    def __init__(self, **kwds):
        """Initialize."""
        super(SymmetryEditMode, self).__init__(**kwds)
        from application import get_app
        app = get_app()
        self.app = app
        statusbar_cid = app.statusbar.get_context_id(self._STATUSBAR_CONTEXT)
        self._statusbar_context_id = statusbar_cid
        self._drag_start_axis = None
        self._drag_start_model_x = None
        self.zone = _EditZone.UNKNOWN
        self._last_msg_zone = None
        self._click_info = None
        self.button_pos = None
        self._entered_before = False
        self.line_alphafrac = 0.0

    def enter(self, doc, **kwds):
        """Enter the mode"""
        super(SymmetryEditMode, self).enter(doc, **kwds)
        # Initialize/fetch cursors
        mkcursor = lambda name: doc.app.cursors.get_action_cursor(
            self.ACTION_NAME,
            name,
        )
        self._move_cursors = {}
        self.cursor_remove = mkcursor(gui.cursor.Name.ARROW)
        self.cursor_add = mkcursor(gui.cursor.Name.ADD)
        self.cursor_normal = mkcursor(gui.cursor.Name.ARROW)
        # Turn on the axis, if it happens to be off right now
        if not self._entered_before:
            action = self.app.find_action("SymmetryActive")
            action.set_active(True)
            self._entered_before = True

    def _update_statusbar(self):
        if self.in_drag:
            return
        if self._last_msg_zone == self.zone:
            return
        statusbar = self.app.statusbar
        statusbar_cid = self._statusbar_context_id
        statusbar.remove_all(statusbar_cid)
        msgs = {
            _EditZone.CREATE_AXIS: self._STATUSBAR_CREATE_AXIS_MSG,
            _EditZone.MOVE_AXIS: self._STATUSBAR_MOVE_AXIS_MSG,
            _EditZone.DELETE_AXIS: self._STATUSBAR_DELETE_AXIS_MSG,
        }
        msg = msgs.get(self.zone, None)
        if msg:
            statusbar.push(statusbar_cid, msg)
            self._last_msg_zone = self.zone

    def get_options_widget(self):
        """Get the (class singleton) options widget"""
        cls = self.__class__
        if cls._OPTIONS_WIDGET is None:
            widget = SymmetryEditOptionsWidget()
            cls._OPTIONS_WIDGET = widget
        return cls._OPTIONS_WIDGET

    ## Events and internals

    def button_press_cb(self, tdw, event):
        if self.zone in (_EditZone.CREATE_AXIS, _EditZone.DELETE_AXIS):
            button = event.button
            if button == 1 and event.type == Gdk.EventType.BUTTON_PRESS:
                self._click_info = (button, self.zone)
                return False
        return super(SymmetryEditMode, self).button_press_cb(tdw, event)

    def button_release_cb(self, tdw, event):
        if self._click_info is not None:
            button0, zone0 = self._click_info
            if event.button == button0:
                if self.zone == zone0:
                    model = tdw.doc
                    layer_stack = model.layer_stack
                    if zone0 == _EditZone.DELETE_AXIS:
                        layer_stack.symmetry_active = False
                    elif zone0 == _EditZone.CREATE_AXIS:
                        x, y = tdw.display_to_model(event.x, event.y)
                        layer_stack.symmetry_axis = x
                        layer_stack.symmetry_active = True
                self._click_info = None
                self._update_zone_and_cursor(tdw, event.x, event.y)
                return False
        return super(SymmetryEditMode, self).button_release_cb(tdw, event)

    def _update_zone_and_cursor(self, tdw, x, y):
        """Update UI & some internal zone flags from pointer position

        :param tdw: canvas widget
        :param x: cursor x position
        :param y: cursor y position

        See also: `SymmetryOverlay`.

        """
        if self.in_drag:
            return
        old_zone = self.zone
        new_zone = None
        new_alphafrac = self.line_alphafrac
        xm, ym = tdw.display_to_model(x, y)
        model = tdw.doc
        layer_stack = model.layer_stack
        axis = layer_stack.symmetry_axis
        if not layer_stack.symmetry_active:
            self.active_cursor = self.cursor_add
            self.inactive_cursor = self.cursor_add
            new_zone = _EditZone.CREATE_AXIS

        # Button hits.
        # NOTE: the position is calculated by the related overlay,
        # in its paint() method.
        if new_zone is None and self.button_pos:
            bx, by = self.button_pos
            d = math.hypot(bx-x, by-y)
            if d <= gui.style.FLOATING_BUTTON_RADIUS:
                self.active_cursor = self.cursor_remove
                self.inactive_cursor = self.cursor_remove
                new_zone = _EditZone.DELETE_AXIS

        if new_zone is None:
            move_cursor_name, perp_dist = tdw.get_move_cursor_name_for_edge(
                (x, y),
                (axis, 0),
                (axis, 1000),
                tolerance=self._GRAB_SENSITIVITY,
                finite=False,
            )
            if move_cursor_name:
                move_cursor = self._move_cursors.get(move_cursor_name)
                if not move_cursor:
                    move_cursor = self.doc.app.cursors.get_action_cursor(
                        self.ACTION_NAME,
                        move_cursor_name,
                    )
                    self._move_cursors[move_cursor_name] = move_cursor
                self.active_cursor = move_cursor
                self.inactive_cursor = move_cursor
                new_zone = _EditZone.MOVE_AXIS
            dfrac = lib.helpers.clamp(
                perp_dist / (10.0 * self._GRAB_SENSITIVITY),
                0.0, 1.0,
            )
            new_alphafrac = 1.0 - dfrac

        if new_zone is None:
            new_zone = _EditZone.UNKNOWN
            self.active_cursor = self.cursor_normal
            self.inactive_cursor = self.cursor_normal

        if new_zone != old_zone:
            self.zone = new_zone
            self._update_statusbar()
            tdw.queue_draw()
        elif new_alphafrac != self.line_alphafrac:
            tdw.queue_draw()
            self.line_alphafrac = new_alphafrac

    def motion_notify_cb(self, tdw, event):
        if not self.in_drag:
            self._update_zone_and_cursor(tdw, event.x, event.y)
            tdw.set_override_cursor(self.inactive_cursor)
        return super(SymmetryEditMode, self).motion_notify_cb(tdw, event)

    def drag_start_cb(self, tdw, event):
        model = tdw.doc
        layer_stack = model.layer_stack
        self._update_zone_and_cursor(tdw, event.x, event.y)
        if self.zone == _EditZone.MOVE_AXIS:
            x0, y0 = self.start_x, self.start_y
            self._drag_start_axis = int(round(layer_stack.symmetry_axis))
            x0_m, y0_m = tdw.display_to_model(x0, y0)
            self._drag_start_model_x = x0_m
        return super(SymmetryEditMode, self).drag_start_cb(tdw, event)

    def drag_update_cb(self, tdw, event, dx, dy):
        if self.zone == _EditZone.MOVE_AXIS:
            x_m, y_m = tdw.display_to_model(event.x, event.y)
            axis = self._drag_start_axis + x_m - self._drag_start_model_x
            axis = int(round(axis))
            if axis != self._drag_start_axis:
                model = tdw.doc
                layer_stack = model.layer_stack
                layer_stack.symmetry_axis = axis
        return super(SymmetryEditMode, self).drag_update_cb(tdw, event, dx, dy)

    def drag_stop_cb(self, tdw):
        if self.zone == _EditZone.MOVE_AXIS:
            tdw.queue_draw()
        return super(SymmetryEditMode, self).drag_stop_cb(tdw)
Example #19
0
    def confirm_destructive_action(self,
                                   title=None,
                                   confirm=None,
                                   offer_save=True):
        """Asks the user to confirm an action that might lose work.

        :param unicode title: Short question to ask the user.
        :param unicode confirm: Imperative verb for the "do it" button.
        :param bool offer_save: Set False to turn off the save checkbox.
        :rtype: bool
        :returns: True if the user allows the destructive action

        Phrase the title question tersely.
        In English/source, use title case for it, and with a question mark.
        Good examples are “Really Quit?”,
        or “Delete Everything?”.
        The title should always tell the user
        what destructive action is about to take place.
        If it is not specified, a default title is used.

        Use a single, specific, imperative verb for the confirm string.
        It should reflect the title question.
        This is used for the primary confirmation button, if specified.
        See the GNOME HIG for further guidelines on what to use here.

        This method doesn't bother asking
        if there's less than a handful of seconds of unsaved work.
        By default, that's 1 second.
        The build-time and runtime debugging flags
        make this period longer
        to allow more convenient development and testing.

        Ref: https://developer.gnome.org/hig/stable/dialogs.html.en

        """
        if title is None:
            title = C_(
                "Destructive action confirm dialog: "
                "fallback title (normally overridden)", "Really Continue?")

        # Get an accurate assesment of how much change is unsaved.
        self.doc.model.sync_pending_changes()
        t = self.doc.model.unsaved_painting_time

        # This used to be 30, but see https://gna.org/bugs/?17955
        # Then 8 by default, but Twitter users hate that too.
        t_bother = 1
        if mypaintlib.heavy_debug:
            t_bother += 7
        if os.environ.get("MYPAINT_DEBUG", False):
            t_bother += 7
        logger.debug("Destructive action don't-bother period is %ds", t_bother)
        if t < t_bother:
            return True

        # Custom response codes.
        # The default ones are all negative ints.
        continue_response_code = 1

        # Dialog setup.
        d = Gtk.MessageDialog(
            title=title,
            parent=self.app.drawWindow,
            type=Gtk.MessageType.QUESTION,
            flags=Gtk.DialogFlags.MODAL,
        )

        # Translated strings for things
        cancel_btn_text = C_(
            "Destructive action confirm dialog: cancel button",
            u"_Cancel",
        )
        save_to_scraps_first_text = C_(
            "Destructive action confirm dialog: save checkbox",
            u"_Save to Scraps first",
        )
        if not confirm:
            continue_btn_text = C_(
                "Destructive action confirm dialog: "
                "fallback continue button (normally overridden)",
                u"Co_ntinue",
            )
        else:
            continue_btn_text = confirm

        # Button setup. Cancel first, continue at end.
        d.add_button(cancel_btn_text, Gtk.ResponseType.CANCEL)
        d.add_button(continue_btn_text, continue_response_code)

        # Explanatory message.
        if self.filename:
            file_basename = os.path.basename(self.filename)
        else:
            file_basename = None
        warning_msg_tmpl = C_(
            "Destructive action confirm dialog: warning message",
            u"You risk losing {abbreviated_time} of unsaved painting. ")
        markup_tmpl = warning_msg_tmpl
        d.set_markup(
            markup_tmpl.format(
                abbreviated_time=lib.xml.escape(
                    helpers.fmt_time_period_abbr(t)),
                current_file_name=lib.xml.escape(file_basename),
            ))

        # Checkbox for saving
        if offer_save:
            save1st_text = save_to_scraps_first_text
            save1st_cb = Gtk.CheckButton.new_with_mnemonic(save1st_text)
            save1st_cb.set_hexpand(False)
            save1st_cb.set_halign(Gtk.Align.END)
            save1st_cb.set_vexpand(False)
            save1st_cb.set_margin_top(12)
            save1st_cb.set_margin_bottom(12)
            save1st_cb.set_margin_start(12)
            save1st_cb.set_margin_end(12)
            save1st_cb.set_can_focus(False)  # set back again in show handler
            d.connect(
                "show",
                self._destructive_action_dialog_show_cb,
                save1st_cb,
            )
            save1st_cb.connect(
                "toggled",
                self._destructive_action_dialog_save1st_toggled_cb,
                d,
            )
            vbox = d.get_content_area()
            vbox.set_spacing(0)
            vbox.set_margin_top(12)
            vbox.pack_start(save1st_cb, False, True, 0)

        # Get a response and handle it.
        d.set_default_response(Gtk.ResponseType.CANCEL)
        response_code = d.run()
        d.destroy()
        if response_code == continue_response_code:
            logger.debug("Destructive action confirmed")
            if offer_save and save1st_cb.get_active():
                logger.info("Saving current canvas as a new scrap")
                self.save_scrap_cb(None)
            return True
        else:
            logger.debug("Destructive action cancelled")
            return False
Example #20
0
class LayerBase (Renderable):
    """Base class defining the layer API

    Layers support the Renderable interface, and are rendered with the
    "render_*()" methods of their root layer stack.

    Layers are minimally aware of the tree structure they reside in, in
    that they contain a reference to the root of their tree for
    signalling purposes.  Updates to the tree structure and to layers'
    graphical contents are announced via the RootLayerStack object
    representing the base of the tree.

    """

    ## Class constants

    #: Forms the default name, may be suffixed per lib.naming consts.
    DEFAULT_NAME = C_(
        "layer default names",
        u"Layer",
    )

    #: A string for the layer type.
    TYPE_DESCRIPTION = None

    PERMITTED_MODES = set(STANDARD_MODES)
    INITIAL_MODE = DEFAULT_MODE

    ## Construction, loading, other lifecycle stuff

    def __init__(self, name=None, **kwargs):
        """Construct a new layer

        :param name: The name for the new layer.
        :param **kwargs: Ignored.

        All layer subclasses must permit construction without
        parameters.
        """
        super(LayerBase, self).__init__()
        # Defaults for the notifiable properties
        self._opacity = 1.0
        self._name = name
        self._visible = True
        self._locked = False
        self._mode = self.INITIAL_MODE
        self._group_ref = None
        self._root_ref = None
        self._thumbnail = None
        #: True if the layer was marked as selected when loaded.
        self.initially_selected = False

    @classmethod
    def new_from_openraster(cls, orazip, elem, cache_dir, progress,
                            root, x=0, y=0, **kwargs):
        """Reads and returns a layer from an OpenRaster zipfile

        This implementation just creates a new instance of its class and
        calls `load_from_openraster()` on it. This should suffice for
        all subclasses which support parameterless construction.
        """

        layer = cls()
        layer.load_from_openraster(
            orazip,
            elem,
            cache_dir,
            progress,
            x=x, y=y,
            **kwargs
        )
        return layer

    @classmethod
    def new_from_openraster_dir(cls, oradir, elem, cache_dir, progress,
                                root, x=0, y=0, **kwargs):
        """Reads and returns a layer from an OpenRaster-like folder

        This implementation just creates a new instance of its class and
        calls `load_from_openraster_dir()` on it. This should suffice
        for all subclasses which support parameterless construction.

        """
        layer = cls()
        layer.load_from_openraster_dir(
            oradir,
            elem,
            cache_dir,
            progress,
            x=x, y=y,
            **kwargs
        )
        return layer

    def load_from_openraster(self, orazip, elem, cache_dir, progress,
                             x=0, y=0, **kwargs):
        """Loads layer data from an open OpenRaster zipfile

        :param orazip: An OpenRaster zipfile, opened for extracting
        :type orazip: zipfile.ZipFile
        :param elem: <layer/> or <stack/> element to load (stack.xml)
        :type elem: xml.etree.ElementTree.Element
        :param cache_dir: Cache root dir for this document
        :param progress: Provides feedback to the user.
        :type progress: lib.feedback.Progress or None
        :param x: X offset of the top-left point for image data
        :param y: Y offset of the top-left point for image data
        :param **kwargs: Extensibility

        The base implementation loads the common layer flags from a `<layer/>`
        or `<stack/>` element, but does nothing more than that. Loading layer
        data from the zipfile or recursing into stack contents is deferred to
        subclasses.
        """
        self._load_common_flags_from_ora_elem(elem)

    def load_from_openraster_dir(self, oradir, elem, cache_dir, progress,
                                 x=0, y=0, **kwargs):
        """Loads layer data from an OpenRaster-style folder.

        Parameters are the same as for load_from_openraster, with the
        following exception (replacing ``orazip``):

        :param unicode/str oradir: Folder with a .ORA-like tree structure.

        """
        self._load_common_flags_from_ora_elem(elem)

    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)

    def __deepcopy__(self, memo):
        """Returns an independent copy of the layer, for Duplicate Layer

        >>> from copy import deepcopy
        >>> orig = _StubLayerBase()
        >>> dup = deepcopy(orig)

        Everything about the returned layer must be a completely
        independent copy of the original layer.  If the copy can be
        worked on, working on it must leave the original unaffected.
        This base implementation can be reused/extended by subclasses if
        they support zero-argument construction. It will use the derived
        class's snapshotting implementation (see `save_snapshot()` and
        `load_snapshot()`) to populate the copy.
        """
        layer = self.__class__()
        layer.load_snapshot(self.save_snapshot())
        return layer

    def clear(self):
        """Clears the layer"""
        pass

    ## Properties

    @property
    def group(self):
        """The group of the current layer.

        Returns None if the layer is not in a group.

        >>> from . import group
        >>> outer = group.LayerStack()
        >>> inner = group.LayerStack()
        >>> scribble = _StubLayerBase()
        >>> outer.append(inner)
        >>> inner.append(scribble)
        >>> outer.group is None
        True
        >>> inner.group == outer
        True
        >>> scribble.group == inner
        True
        """
        if self._group_ref is not None:
            return self._group_ref()
        return None

    @group.setter
    def group(self, group):
        if group is None:
            self._group_ref = None
        else:
            self._group_ref = weakref.ref(group)

    @property
    def root(self):
        """The root of the layer tree structure

        Only RootLayerStack instances or None are permitted.
        You won't normally need to adjust this unless you're doing
        something fancy: it's automatically maintained by intermediate
        and root `LayerStack` elements in the tree whenever layers are
        added or removed from a rooted tree structure.

        >>> from . import tree
        >>> root = tree.RootLayerStack(doc=None)
        >>> layer = _StubLayerBase()
        >>> root.append(layer)
        >>> layer.root                 #doctest: +ELLIPSIS
        <RootLayerStack...>
        >>> layer.root is root
        True

        """
        if self._root_ref is not None:
            return self._root_ref()
        return None

    @root.setter
    def root(self, newroot):
        if newroot is None:
            self._root_ref = None
        else:
            self._root_ref = weakref.ref(newroot)

    @property
    def opacity(self):
        """Opacity multiplier for the layer.

        Values must permit conversion to a `float` in [0, 1].
        Changing this property issues ``layer_properties_changed`` and
        appropriate ``layer_content_changed`` notifications via the root
        layer stack if the layer is within a tree structure.

        Layers with a `mode` of `PASS_THROUGH_MODE` have immutable
        opacities: the value is always 100%. This restriction only
        applies to `LayerStack`s - i.e. layer groups - because those are
        the only kinds of layer which can be put into pass-through mode.
        """
        return self._opacity

    @opacity.setter
    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)

    @property
    def name(self):
        """The layer's name, for display purposes

        Values must permit conversion to a unicode string.  If the
        layer is part of a tree structure, ``layer_properties_changed``
        notifications will be issued via the root layer stack. In
        addition, assigned names may be corrected to be unique within
        the tree.
        """
        return self._name

    @name.setter
    def name(self, name):
        if name is not None:
            name = unicode(name)
        else:
            name = self.DEFAULT_NAME
        oldname = self._name
        self._name = name
        root = self.root
        if root is not None:
            self._name = root.get_unique_name(self)
        if self._name != oldname:
            self._properties_changed(["name"])

    @property
    def visible(self):
        """Whether the layer has a visible effect on its backdrop.

        Some layer modes normally have an effect even if the calculated
        alpha of a pixel is zero. This switch turns that off too.

        Values must permit conversion to a `bool`.
        Changing this property issues ``layer_properties_changed`` and
        appropriate ``layer_content_changed`` notifications via the root
        layer stack if the layer is within a tree structure.
        """
        return self._visible

    @visible.setter
    def visible(self, visible):
        visible = bool(visible)
        if visible == self._visible:
            return
        self._visible = visible
        self._properties_changed(["visible"])
        # Toggling the visibility flag always causes the mode to stop
        # or start having its normal effect. Need the full redraw bbox
        # so that outlying empty tiles will be updated properly.
        bbox = tuple(self.get_full_redraw_bbox())
        self._content_changed(*bbox)

    @property
    def branch_visible(self):
        """Check whether the layer's branch is visible.

        Returns True if the layer's group and all of its parents are visible,
        False otherwise.

        Returns True if the layer is not in a group.

        >>> from . import group
        >>> outer = group.LayerStack()
        >>> inner = group.LayerStack()
        >>> scribble = _StubLayerBase()
        >>> outer.append(inner)
        >>> inner.append(scribble)
        >>> outer.branch_visible
        True
        >>> inner.branch_visible
        True
        >>> scribble.branch_visible
        True
        >>> outer.visible = False
        >>> outer.branch_visible
        True
        >>> inner.branch_visible
        False
        >>> scribble.branch_visible
        False
        """
        group = self.group
        if group is None:
            return True

        return group.visible and group.branch_visible

    @property
    def locked(self):
        """Whether the layer is locked (immutable).

        Values must permit conversion to a `bool`.
        Changing this property issues `layer_properties_changed` via the
        root layer stack if the layer is within a tree structure.

        """
        return self._locked

    @locked.setter
    def locked(self, locked):
        locked = bool(locked)
        if locked != self._locked:
            self._locked = locked
            self._properties_changed(["locked"])

    @property
    def branch_locked(self):
        """Check whether the layer's branch is locked.

        Returns True if the layer's group or at least one of its parents
        is locked, False otherwise.

        Returns False if the layer is not in a group.

        >>> from . import group
        >>> outer = group.LayerStack()
        >>> inner = group.LayerStack()
        >>> scribble = _StubLayerBase()
        >>> outer.append(inner)
        >>> inner.append(scribble)
        >>> outer.branch_locked
        False
        >>> inner.branch_locked
        False
        >>> scribble.branch_locked
        False
        >>> outer.locked = True
        >>> outer.branch_locked
        False
        >>> inner.branch_locked
        True
        >>> scribble.branch_locked
        True
        """
        group = self.group
        if group is None:
            return False

        return group.locked or group.branch_locked

    @property
    def mode(self):
        """How this layer combines with its backdrop.

        Values must permit conversion to an int, and must be permitted
        for the mode's class.

        Changing this property issues ``layer_properties_changed`` and
        appropriate ``layer_content_changed`` notifications via the root
        layer stack if the layer is within a tree structure.

        In addition to the modes supported by the base implementation,
        layer groups permit `lib.modes.PASS_THROUGH_MODE`, an
        additional mode where group contents are rendered as if their
        group were not present. Setting the mode to this value also
        sets the opacity to 100%.

        For layer groups, "Normal" mode implies group isolation
        internally. These semantics differ from those of OpenRaster and
        the W3C, but saving and loading applies the appropriate
        transformation.

        See also: PERMITTED_MODES.

        """
        return self._mode

    @mode.setter
    def mode(self, mode):
        mode = int(mode)
        if mode not in self.PERMITTED_MODES:
            mode = DEFAULT_MODE
        if mode == self._mode:
            return
        # Forcing the opacity for layer groups here allows a redraw to
        # be subsumed. Only layer groups permit PASS_THROUGH_MODE.
        propchanges = []
        if mode == PASS_THROUGH_MODE:
            self._opacity = 1.0
            propchanges.append("opacity")
        # When changing the mode, the before and after states may have
        # different treatments of outlying empty tiles. Need the full
        # redraw bboxes of both states to ensure correct redraws.
        redraws = [self.get_full_redraw_bbox()]
        self._mode = mode
        redraws.append(self.get_full_redraw_bbox())
        self._content_changed(*tuple(combine_redraws(redraws)))
        propchanges.append("mode")
        self._properties_changed(propchanges)

    ## Notifications

    def _content_changed(self, *args):
        """Notifies the root's content observers

        If this layer's root stack is defined, i.e. if it is part of a
        tree structure, the root's `layer_content_changed()` event
        method will be invoked with this layer and the supplied
        arguments. This reflects a region of pixels in the document
        changing.
        """
        root = self.root
        if root is not None:
            root.layer_content_changed(self, *args)

    def _properties_changed(self, properties):
        """Notifies the root's layer properties observers

        If this layer's root stack is defined, i.e. if it is part of a
        tree structure, the root's `layer_properties_changed()` event
        method will be invoked with the layer and the supplied
        arguments. This reflects details about the layer like its name
        or its locked status changing.
        """
        root = self.root
        if root is not None:
            root._notify_layer_properties_changed(self, set(properties))

    ## Info methods

    def get_icon_name(self):
        """The name of the icon to display for the layer

        Ideally symbolic. A value of `None` means that no icon should be
        displayed.
        """
        return None

    @property
    def effective_opacity(self):
        """The opacity used when rendering a layer: zero if invisible

        This must match the appearance produced by the layer's
        Renderable.get_render_ops() implementation when it is called
        with no explicit "layers" specification. The base class's
        effective opacity is zero because the base get_render_ops() is
        unimplemented.

        """
        return 0.0

    def get_alpha(self, x, y, radius):
        """Gets the average alpha within a certain radius at a point

        :param x: model X coordinate
        :param y: model Y coordinate
        :param radius: radius over which to average
        :rtype: float

        The return value is not affected by the layer opacity, effective or
        otherwise. This is used by `Document.pick_layer()` and friends to test
        whether there's anything significant present at a particular point.
        The default alpha at a point is zero.
        """
        return 0.0

    def get_bbox(self):
        """Returns the inherent (data) bounding box of the layer

        :rtype: lib.helpers.Rect

        The returned rectangle is generally tile-aligned, but isn't
        required to be. In this base implementation, the returned bbox
        is a zero-size default Rect, which is also how a full redraw is
        signalled. Subclasses should override this with a better
        implementation.

        The data bounding box is used for certain classes of redraws.
        See also get_full_redraw_bbox().

        """
        return helpers.Rect()

    def get_full_redraw_bbox(self):
        """Gets the full update notification bounding box of the layer

        :rtype: lib.helpers.Rect

        This is the appropriate bounding box for redraws if a layer-wide
        property like visibility or combining mode changes.

        Normally this is the layer's inherent data bounding box, which
        allows the GUI to skip outlying empty tiles when redrawing the
        layer stack.  If instead the layer's compositing mode dictates
        that a calculated pixel alpha of zero would affect the backdrop
        regardless - something that's true of certain masking modes -
        then the returned bbox is a zero-size rectangle, which is the
        signal for a full redraw.

        See also get_bbox().

        """
        if self.mode in MODES_EFFECTIVE_AT_ZERO_ALPHA:
            return helpers.Rect()
        else:
            return self.get_bbox()

    def is_empty(self):
        """Tests whether the surface is empty

        Always true in the base implementation.
        """
        return True

    def get_paintable(self):
        """True if this layer currently accepts painting brushstrokes

        Always false in the base implementation.
        """
        return False

    def get_fillable(self):
        """True if this layer currently accepts flood fill

        Always false in the base implementation.
        """
        return False

    def get_stroke_info_at(self, x, y):
        """Return the brushstroke at a given point

        :param x: X coordinate to pick from, in model space.
        :param y: Y coordinate to pick from, in model space.
        :rtype: lib.strokemap.StrokeShape or None

        Returns None for the base class.
        """
        return None

    def get_last_stroke_info(self):
        """Return the most recently painted stroke

        :rtype lib.strokemap.StrokeShape or None

        Returns None for the base class.
        """
        return None

    def get_mode_normalizable(self):
        """True if this layer can be normalized"""
        unsupported = set(MODES_EFFECTIVE_AT_ZERO_ALPHA)
        # Normalizing would have to make an infinite number of tiles
        unsupported.update(MODES_DECREASING_BACKDROP_ALPHA)
        # Normal mode cannot decrease the bg's alpha
        return self.mode not in unsupported

    def get_trimmable(self):
        """True if this layer currently accepts trim()"""
        return False

    def has_interesting_name(self):
        """True if the layer looks as if it has a user-assigned name

        Interesting means non-blank, and not the default name or a
        numbered version of it. This is used when merging layers: Merge
        Down is used on temporary layers a lot, and those probably have
        boring names.
        """
        name = self._name
        if name is None or name.strip() == '':
            return False
        if name == self.DEFAULT_NAME:
            return False
        match = lib.naming.UNIQUE_NAME_REGEX.match(name)
        if match is not None:
            base = unicode(match.group("name"))
            if base == self.DEFAULT_NAME:
                return False
        return True

    ## Flood fill

    def flood_fill(self, x, y, color, bbox, tolerance, dst_layer=None):
        """Fills a point on the surface with a color

        See PaintingLayer.flood_fill() for parameters and semantics.
        The base implementation does nothing.

        """
        pass

    ## Rendering

    def get_tile_coords(self):
        """Returns all data tiles in this layer

        :returns: All tiles with data
        :rtype: sequence

        This method should return a sequence listing the coordinates for
        all tiles with data in this layer.

        It is used when computing layer merges.  Tile coordinates must
        be returned as ``(tx, ty)`` pairs.

        The base implementation returns an empty sequence.
        """
        return []

    ## Translation

    def get_move(self, x, y):
        """Get a translation/move object for this layer

        :param x: Model X position of the start of the move
        :param y: Model X position of the start of the move
        :returns: A move object
        """
        raise NotImplementedError

    def translate(self, dx, dy):
        """Translate a layer non-interactively

        :param dx: Horizontal offset in model coordinates
        :param dy: Vertical offset in model coordinates
        :returns: full redraw bboxes for the move: ``[before, after]``
        :rtype: list

        The base implementation uses `get_move()` and the object it returns.
        """
        update_bboxes = [self.get_full_redraw_bbox()]
        move = self.get_move(0, 0)
        move.update(dx, dy)
        move.process(n=-1)
        move.cleanup()
        update_bboxes.append(self.get_full_redraw_bbox())
        return update_bboxes

    ## Standard stuff

    def __repr__(self):
        """Simplified repr() of a layer"""
        if self.name:
            return "<%s %r>" % (self.__class__.__name__, self.name)
        else:
            return "<%s>" % (self.__class__.__name__)

    def __nonzero__(self):
        """Layers are never false in Py2."""
        return self.__bool__()

    def __bool__(self):
        """Layers are never false in Py3.

        >>> sample = _StubLayerBase()
        >>> bool(sample)
        True

        """
        return True

    def __eq__(self, layer):
        """Two layers are only equal if they are the same object

        This is meaningful during layer repositions in the GUI, where
        shallow copies are used.
        """
        return self is layer

    def __hash__(self):
        """Return a hash for the layer (identity only)"""
        return id(self)

    ## Saving

    def save_as_png(self, filename, *rect, **kwargs):
        """Save to a named PNG file

        :param filename: filename to save to
        :param *rect: rectangle to save, as a 4-tuple
        :param **kwargs: passthrough opts for underlying implementations
        :rtype: Gdk.Pixbuf

        The base implementation does nothing.
        """
        pass

    def save_to_openraster(self, orazip, tmpdir, path,
                           canvas_bbox, frame_bbox, **kwargs):
        """Saves the layer's data into an open OpenRaster ZipFile

        :param orazip: a `zipfile.ZipFile` open for write
        :param tmpdir: path to a temp dir, removed after the save
        :param path: Unique path of the layer, for encoding in filenames
        :type path: tuple of ints
        :param canvas_bbox: Bounding box of all layers, absolute coords
        :type canvas_bbox: tuple
        :param frame_bbox: Bounding box of the image being saved
        :type frame_bbox: tuple
        :param **kwargs: Keyword args used by the save implementation
        :returns: element describing data written
        :rtype: xml.etree.ElementTree.Element

        There are three bounding boxes which need to considered. The
        inherent bbox of the layer as returned by `get_bbox()` is always
        tile aligned and refers to absolute model coordinates, as is
        `canvas_bbox`.

        All of the above bbox's coordinates are defined relative to the
        canvas origin. However, when saving, the data written must be
        translated so that `frame_bbox`'s top left corner defines the
        origin (0, 0), of the saved OpenRaster file. The width and
        height of `frame_bbox` determine the saved image's dimensions.

        More than one file may be written to the zipfile. The etree
        element returned should describe everything that was written.

        Paths must be unique sequences of ints, but are not necessarily
        valid RootLayerStack paths. It's faked for the normally
        unaddressable background layer right now, for example.
        """
        raise NotImplementedError

    def _get_stackxml_element(self, tag, x=None, y=None):
        """Internal: get a basic etree Element for .ora saving"""

        elem = ET.Element(tag)
        attrs = elem.attrib
        if self.name:
            attrs["name"] = str(self.name)
        if x is not None:
            attrs["x"] = str(x)
        if y is not None:
            attrs["y"] = str(y)
        attrs["opacity"] = str(self.opacity)
        if self.initially_selected:
            attrs["selected"] = "true"
        if self.locked:
            attrs["edit-locked"] = "true"
        if self.visible:
            attrs["visibility"] = "visible"
        else:
            attrs["visibility"] = "hidden"
        # NOTE: This *will* be wrong for the PASS_THROUGH_MODE case.
        # NOTE: LayerStack will need to override this attr.
        mode_info = lib.mypaintlib.combine_mode_get_info(self.mode)
        if mode_info is not None:
            compop = mode_info.get("name")
            if compop is not None:
                attrs["composite-op"] = str(compop)
        return elem

    ## Painting symmetry axis

    def set_symmetry_state(self, active, center_x, center_y,
                           symmetry_type, rot_symmetry_lines):
        """Set the surface's painting symmetry axis and active flag.

        :param bool active: Whether painting should be symmetrical.
        :param int center_x: X coord of the axis of symmetry.
        :param int center_y: Y coord of the axis of symmetry.
        :param int symmetry_type: symmetry type that will be applied if active
        :param int rot_symmetry_lines: number of rotational
            symmetry lines for angle dependent symmetry modes.

        The symmetry axis is only meaningful to paintable layers.
        Received strokes are reflected along the line ``x=center_x``
        when symmetrical painting is active.

        This method is used by RootLayerStack only,
        propagating a central shared flag and value to all layers.

        The base implementation does nothing.
        """
        pass

    ## Snapshot

    def save_snapshot(self):
        """Snapshots the state of the layer, for undo purposes

        The returned data should be considered opaque, useful only as a
        memento to be restored with load_snapshot().
        """
        return LayerBaseSnapshot(self)

    def load_snapshot(self, sshot):
        """Restores the layer from snapshot data"""
        sshot.restore_to_layer(self)

    ## Thumbnails

    @property
    def thumbnail(self):
        """The layer's cached preview thumbnail.

        :rtype: GdkPixbuf.Pixbuf or None

        Thumbnail pixbufs are always 256x256 pixels, and correspond to
        the data bounding box of the layer only.

        See also: render_thumbnail().

        """
        return self._thumbnail

    def update_thumbnail(self):
        """Safely updates the cached preview thumbnail.

        This method updates self.thumbnail using render_thumbnail() and
        the data bounding box, and eats any NotImplementedErrors.

        This is used by the layer stack to keep the preview thumbnail up
        to date. It is called automatically after layer data is changed
        and stable for a bit, so there is normally no need to call it in
        client code.

        """
        try:
            self._thumbnail = self.render_thumbnail(
                self.get_bbox(),
                alpha=True,
            )
        except NotImplementedError:
            self._thumbnail = None

    def render_thumbnail(self, bbox, **options):
        """Renders a 256x256 thumb of the layer in an arbitrary bbox.

        :param tuple bbox: Bounding box to make a thumbnail of.
        :param **options: Passed to RootLayerStack.render_layer_preview().
        :rtype: GtkPixbuf or None

        Use the thumbnail property if you just want a reasonably
        up-to-date preview thumbnail for a single layer.

        See also: RootLayerStack.render_layer_preview().

        """
        root = self.root
        if root is None:
            return None
        return root.render_layer_preview(self, bbox=bbox, **options)

    ## Trimming

    def trim(self, rect):
        """Trim the layer to a rectangle, discarding data outside it

        :param rect: A trimming rectangle in model coordinates
        :type rect: tuple (x, y, w, h)

        The base implementation does nothing.
        """
        pass
Example #21
0
    def _call_doc_load_method(self, method, arg, argdesc, is_import):
        """Internal: common GUI aspects of loading or importing files.

        Calls a document model loader method (on lib.document.Document)
        with the given argument. Catches common loading exceptions and
        shows appropriate error messages.

        """
        prefs = self.app.preferences
        display_colorspace_setting = prefs["display.colorspace"]
        statusbar = self.app.statusbar
        statusbar_cid = self._statusbar_context_id
        statusbar.remove_all(statusbar_cid)

        if is_import:
            message = C_(
                "file handling: import layers: during file loads (statusbar)",
                u"Importing layers from {files_summary}…").format(
                    files_summary=argdesc, )
        else:
            message = C_("file handling: open: during load (statusbar)",
                         u"Loading “{file_basename}”…").format(
                             file_basename=argdesc, )
        statusbar.push(statusbar_cid, message)
        try:
            method(
                arg,
                feedback_cb=self.gtk_main_tick,
                convert_to_srgb=(display_colorspace_setting == "srgb"),
            )
        except (FileHandlingError, AllocationError, MemoryError) as e:
            statusbar.remove_all(statusbar_cid)
            if is_import:
                self.app.show_transient_message(
                    C_(
                        "file handling: import layers failed (statusbar)",
                        u"Could not load {files_summary}.",
                    ).format(files_summary=argdesc, ))
            else:
                self.app.show_transient_message(
                    C_(
                        "file handling: open failed (statusbar)",
                        u"Could not load “{file_basename}”.",
                    ).format(file_basename=argdesc, ))
            self.app.message_dialog(unicode(e), type=Gtk.MessageType.ERROR)
            return False
        else:
            statusbar.remove_all(statusbar_cid)
            if is_import:
                self.app.show_transient_message(
                    C_(
                        "file handling: import layers success (statusbar)",
                        u"Imported layers from {files_summary}.",
                    ).format(files_summary=argdesc, ))
            else:
                self.app.show_transient_message(
                    C_(
                        "file handling: open success (statusbar)",
                        u"Loaded “{file_basename}”.",
                    ).format(file_basename=argdesc, ))
            return True
Example #22
0
 def picking_status_text(self):
     """The statusbar text to use during the grab."""
     return C_(
         "context picker: statusbar text during grab",
         u"Pick brushstroke settings, stroke color, and layer…",
     )
Example #23
0
    def _save_doc_to_file(self,
                          filename,
                          doc,
                          export=False,
                          statusmsg=True,
                          **options):
        """Saves a document to one or more files

        :param filename: The base filename to save
        :param gui.document.Document doc: Controller for the document to save
        :param bool export: True if exporting
        :param **options: Pass-through options

        This method handles logging, statusbar messages,
        and alerting the user to when the save failed.

        See also: `lib.document.Document.save()`.
        """
        thumbnail_pixbuf = None
        prefs = self.app.preferences
        display_colorspace_setting = prefs["display.colorspace"]
        options['save_srgb_chunks'] = (display_colorspace_setting == "srgb")
        if statusmsg:
            statusbar = self.app.statusbar
            statusbar_cid = self._statusbar_context_id
            statusbar.remove_all(statusbar_cid)
            file_basename = os.path.basename(filename)
            if export:
                during_tmpl = C_("file handling: during export (statusbar)",
                                 u"Exporting to “{file_basename}”…")
            else:
                during_tmpl = C_("file handling: during save (statusbar)",
                                 u"Saving “{file_basename}”…")
            statusbar.push(statusbar_cid,
                           during_tmpl.format(file_basename=file_basename, ))
        try:
            x, y, w, h = doc.model.get_bbox()
            if w == 0 and h == 0:
                w, h = tiledsurface.N, tiledsurface.N
                # TODO: Add support for other sizes
            thumbnail_pixbuf = doc.model.save(filename,
                                              feedback_cb=self.gtk_main_tick,
                                              **options)
            self.lastsavefailed = False
        except (FileHandlingError, AllocationError, MemoryError) as e:
            if statusmsg:
                statusbar.remove_all(statusbar_cid)
                if export:
                    failed_tmpl = C_(
                        "file handling: export failure (statusbar)",
                        u"Failed to export to “{file_basename}”.",
                    )
                else:
                    failed_tmpl = C_(
                        "file handling: save failure (statusbar)",
                        u"Failed to save “{file_basename}”.",
                    )
                self.app.show_transient_message(
                    failed_tmpl.format(file_basename=file_basename, ))
            self.lastsavefailed = True
            self.app.message_dialog(unicode(e), type=Gtk.MessageType.ERROR)
        else:
            if statusmsg:
                statusbar.remove_all(statusbar_cid)
            file_location = os.path.abspath(filename)
            multifile_info = ''
            if "multifile" in options:
                multifile_info = " (basis; used multiple .XXX.ext names)"
            if not export:
                logger.info('Saved to %r%s', file_location, multifile_info)
            else:
                logger.info('Exported to %r%s', file_location, multifile_info)
            if statusmsg:
                if export:
                    success_tmpl = C_(
                        "file handling: export success (statusbar)",
                        u"Exported to “{file_basename}” successfully.",
                    )
                else:
                    success_tmpl = C_(
                        "file handling: save success (statusbar)",
                        u"Saved “{file_basename}” successfully.",
                    )
                self.app.show_transient_message(
                    success_tmpl.format(file_basename=file_basename, ))
        return thumbnail_pixbuf
Example #24
0
from lib.color import HCYColor
from lib.color import HSVColor
import gui.uicolor
from .util import clamp
from lib.palette import Palette
import lib.alg as geom
from .paletteview import palette_load_via_dialog
from .paletteview import palette_save_via_dialog
from lib.gettext import C_

from lib.pycompat import xrange

PREFS_MASK_KEY = "colors.hcywheel.mask.gamuts"
PREFS_ACTIVE_KEY = "colors.hcywheel.mask.active"
MASK_EDITOR_HELP_URI = C_(
    "Online help pages", u"https://github.com/mypaint/mypaint/wiki/"
    u"v1.2-HCY-Wheel-and-Gamut-Mask-Editor")


class MaskableWheelMixin(object):
    """Provides wheel widgets with maskable areas.

    For use with implementations of `HueSaturationWheelAdjusterMixin`.
    Concrete implementations can be masked so that they ignore clicks outside
    certain color areas. If the mask is active, clicks inside the mask
    shapes are treated as normal, but clicks outside them are remapped to a
    point on the nearest edge of the nearest shape. This can be useful for
    artists who wish to plan the color gamut of their artwork in advance.

    http://gurneyjourney.blogspot.com/2011/09/part-1-gamut-masking-method.html
    http://gurneyjourney.blogspot.com/2008/01/color-wheel-masking-part-1.html
Example #25
0
from lib.layer.data import BackgroundLayer
from lib.meta import Compatibility, PREREL, MYPAINT_VERSION
from lib.modes import MODE_STRINGS, set_default_mode
from lib.mypaintlib import CombineNormal, CombineSpectralWGM
from lib.mypaintlib import combine_mode_get_info
from lib.gettext import C_

logger = getLogger(__name__)

FILE_WARNINGS = {
    Compatibility.INCOMPATIBLE: 'ui.file_compat_warning_severe',
    Compatibility.PARTIALLY: 'ui.file_compat_warning_mild',
}

_FILE_OPEN_OPTIONS = [
    ('', C_("File Load Compat Options", "Based on file")),
    (C1X, C_("Prefs Dialog|Compatibility", "1.x")),
    (C2X, C_("Prefs Dialog|Compatibility", "2.x")),
]

FILE_WARNING_MSGS = {
    Compatibility.INCOMPATIBLE: C_(
        "file compatibility warning",
        # TRANSLATORS: This is probably a rare warning, and it will not
        # TRANSLATORS: really be shown at all before the release of 3.0
        u"“{filename}” was saved with <b>MyPaint {new_version}</b>."
        " It may be <b>incompatible</b> with <b>MyPaint {current_version}</b>."
        "\n\n"
        "Editing this file with this version of MyPaint is not guaranteed"
        " to work, and may even result in crashes."
        "\n\n"
Example #26
0
 def get_properties_description(cls):
     return C_(
         "HCY Wheel color adjuster page: properties tooltip.",
         u"Set gamut mask.",
     )
Example #27
0
def incompatible_ora_warning_dialog(
        comp_type, prerel, filename, target_version, app):
    # Skip the dialog if the user has disabled the warning
    # for this level of incompatibility
    warn = app.preferences.get(FILE_WARNINGS[comp_type], True)
    if not warn:
        return True

    # Toggle allowing users to disable future warnings directly
    # in the dialog, this is configurable in the settings too.
    # The checkbutton code is pretty much copied from the filehandling
    # save-to-scrap checkbutton; a lot of duplication.
    skip_warning_text = C_(
        "Version compat warning toggle",
        u"Don't show this warning again"
    )
    skip_warning_button = Gtk.CheckButton.new()
    skip_warning_button.set_label(skip_warning_text)
    skip_warning_button.set_hexpand(False)
    skip_warning_button.set_vexpand(False)
    skip_warning_button.set_halign(Gtk.Align.END)
    skip_warning_button.set_margin_top(12)
    skip_warning_button.set_margin_bottom(12)
    skip_warning_button.set_margin_start(12)
    skip_warning_button.set_margin_end(12)
    skip_warning_button.set_can_focus(False)

    def skip_warning_toggled(checkbut):
        app.preferences[FILE_WARNINGS[comp_type]] = not checkbut.get_active()
        app.preferences_window.compat_preferences.update_ui()
    skip_warning_button.connect("toggled", skip_warning_toggled)

    def_msg = "Invalid key, report this! key={key}".format(key=comp_type)
    msg_markup = FILE_WARNING_MSGS.get(comp_type, def_msg).format(
        filename=filename,
        new_version=target_version,
        current_version=MYPAINT_VERSION
    ) + "\n\n" + OPEN_ANYWAY
    d = Gtk.MessageDialog(
        transient_for=app.drawWindow,
        buttons=Gtk.ButtonsType.NONE,
        modal=True,
        message_type=Gtk.MessageType.WARNING,
    )
    d.set_markup(msg_markup)

    vbox = d.get_content_area()
    vbox.set_spacing(0)
    vbox.set_margin_top(12)
    vbox.pack_start(skip_warning_button, False, True, 0)

    d.add_button(Gtk.STOCK_NO, Gtk.ResponseType.REJECT)
    d.add_button(Gtk.STOCK_YES, Gtk.ResponseType.ACCEPT)
    d.set_default_response(Gtk.ResponseType.REJECT)

    # Without this, the check button takes initial focus
    def show_checkbut(*args):
        skip_warning_button.show()
        skip_warning_button.set_can_focus(True)
    d.connect("show", show_checkbut)

    response = d.run()
    d.destroy()
    return response == Gtk.ResponseType.ACCEPT
Example #28
0
 def get_page_description(cls):
     return C_(
         "HCY Wheel color adjuster page: description for tooltips etc.",
         u"Set the color using cylindrical hue/chroma/luma space. "
         u"The circular slices are equiluminant.",
     )
Example #29
0
class CompatFileBehavior(config.CompatFileBehaviorConfig):
    """ Holds data and functions related to per-file choice of compat mode
    """
    _CFBC = config.CompatFileBehaviorConfig
    _OPTIONS = [
        _CFBC.ALWAYS_1X,
        _CFBC.ALWAYS_2X,
        _CFBC.UNLESS_PIGMENT_LAYER_1X,
    ]
    _LABELS = {
        _CFBC.ALWAYS_1X: (
            C_(
                "Prefs Dialog|Compatibility",
                # TRANSLATORS: One of the options for the
                # TRANSLATORS: "When Not Specified in File"
                # TRANSLATORS: compatibility setting.
                "Always open in 1.x mode"
            )
        ),
        _CFBC.ALWAYS_2X: (
            C_(
                "Prefs Dialog|Compatibility",
                # TRANSLATORS: One of the options for the
                # TRANSLATORS: "When Not Specified in File"
                # TRANSLATORS: compatibility setting.
                "Always open in 2.x mode"
            )
        ),
        _CFBC.UNLESS_PIGMENT_LAYER_1X: (
            C_(
                "Prefs Dialog|Compatibility",
                # TRANSLATORS: One of the options for the
                # TRANSLATORS: "When Not Specified in File"
                # TRANSLATORS: compatibility setting.
                "Open in 1.x mode unless file contains pigment layers"
            )
        ),
    }

    def __init__(self, combobox, prefs):
        self.combo = combobox
        self.prefs = prefs
        options_store = Gtk.ListStore()
        options_store.set_column_types((str, str))
        for option in self._OPTIONS:
            options_store.append((option, self._LABELS[option]))
        combobox.set_model(options_store)

        cell = Gtk.CellRendererText()
        combobox.pack_start(cell, True)
        combobox.add_attribute(cell, 'text', 1)
        self.update_ui()
        combobox.connect('changed', self.changed_cb)

    def update_ui(self):
        self.combo.set_active_id(self.prefs[self.SETTING])

    def changed_cb(self, combo):
        active_id = self.combo.get_active_id()
        self.prefs[self.SETTING] = active_id

    @staticmethod
    def get_compat_mode(setting, root_elem, default):
        """ Get the compat mode to use for a file

        The decision is based on the given file behavior setting
        and the layer stack xml.
        """
        # If more options are added, rewrite to use separate classes.
        if setting == CompatFileBehavior.ALWAYS_1X:
            return C1X
        elif setting == CompatFileBehavior.ALWAYS_2X:
            return C2X
        elif setting == CompatFileBehavior.UNLESS_PIGMENT_LAYER_1X:
            if has_pigment_layers(root_elem):
                logger.info("Pigment layer found!")
                return C2X
            else:
                return C1X
        else:
            msg = "Unknown file compat setting: {setting}, using default mode."
            logger.warning(msg.format(setting=setting))
            return default
Example #30
0
    def __templates(self):
        Y = 0.5
        H = 1 - 0.05
        # Reusable shapes...
        atmos_triad = [(H, 0.95, Y), ((H + 0.275) % 1, 0.55, Y),
                       ((1 + H - 0.275) % 1, 0.55, Y)]

        def __coffin(h):
            # Hexagonal coffin shape with the foot end at the centre
            # of the wheel.
            shape = []
            shape.append(((h + 0.25) % 1, 0.03, Y))
            shape.append(((h + 1 - 0.25) % 1, 0.03, Y))
            shape.append(((h + 0.01) % 1, 0.95, Y))
            shape.append(((h + 1 - 0.01) % 1, 0.95, Y))
            shape.append(((h + 0.04) % 1, 0.70, Y))
            shape.append(((h + 1 - 0.04) % 1, 0.70, Y))
            return shape

        def __complement_blob(h):
            # Small pentagonal blob at the given hue, used for an organic-
            # looking dab of a complementary hue.
            shape = []
            shape.append(((h + 0.015) % 1, 0.94, Y))
            shape.append(((h + 0.985) % 1, 0.94, Y))
            shape.append(((h + 0.035) % 1, 0.71, Y))
            shape.append(((h + 0.965) % 1, 0.71, Y))
            shape.append(((h) % 1, 0.54, Y))
            return shape

        templates = []
        templates.append(
            (C_(
                "HCY Gamut Mask template name",
                u"Atmospheric Triad",
            ),
             C_(
                 "HCY Gamut Mask template description",
                 "Moody and subjective, defined by one dominant primary "
                 "and two primaries which are less intense.",
             ), [deepcopy(atmos_triad)]))
        templates.append(
            (C_(
                "HCY Gamut Mask template name",
                u"Shifted Triad",
            ),
             C_(
                 "HCY Gamut Mask template description",
                 u"Weighted more strongly towards the dominant color.",
             ), [[(H, 0.95, Y), ((H + 0.35) % 1, 0.4, Y),
                  ((1 + H - 0.35) % 1, 0.4, Y)]]))
        templates.append((C_(
            "HCY Gamut Mask template name",
            u"Complementary",
        ),
                          C_(
                              "HCY Gamut Mask template description",
                              u"Contrasting opposites, "
                              u"balanced by having central neutrals "
                              u"between them on the color wheel.",
                          ), [[((H + 0.005) % 1, 0.9, Y),
                               ((H + 0.995) % 1, 0.9, Y),
                               ((H + 0.250) % 1, 0.1, Y),
                               ((H + 0.750) % 1, 0.1, Y),
                               ((H + 0.505) % 1, 0.9, Y),
                               ((H + 0.495) % 1, 0.9, Y)]]))
        templates.append(
            (C_(
                "HCY Gamut Mask template name",
                u"Mood and Accent",
            ),
             C_(
                 "HCY Gamut Mask template description",
                 u"One main range of colors, "
                 u"with a complementary accent for "
                 u"variation and highlights.",
             ), [deepcopy(atmos_triad),
                 __complement_blob(H + 0.5)]))
        templates.append(
            (C_(
                "HCY Gamut Mask template name",
                u"Split Complementary",
            ),
             C_(
                 "HCY Gamut Mask template description",
                 u"Two analogous colors and a complement to them, "
                 u"with no secondary colors between them.",
             ), [__coffin(H + 0.5),
                 __coffin(1 + H - 0.1),
                 __coffin(H + 0.1)]))
        return templates
Example #31
0
    def _layer_description_markup(layer):
        """GMarkup text description of a layer, used in the list."""
        name_markup = None
        description = None

        if layer is None:
            name_markup = escape(lib.layer.PlaceholderLayer.DEFAULT_NAME)
            description = C_(
                "Layers: description: no layer (\"never happens\" condition!)",
                u"?layer",
            )
        elif layer.name is None:
            name_markup = escape(layer.DEFAULT_NAME)
        else:
            name_markup = escape(layer.name)

        if layer is not None:
            desc_parts = []
            if isinstance(layer, lib.layer.LayerStack):
                name_markup = "<i>{}</i>".format(name_markup)

            # Mode (if it's interesting)
            if layer.mode in lib.modes.MODE_STRINGS:
                if layer.mode != lib.modes.DEFAULT_MODE:
                    s, d = lib.modes.MODE_STRINGS[layer.mode]
                    desc_parts.append(s)
            else:
                desc_parts.append(C_(
                    "Layers: description parts: unknown mode (fallback str!)",
                    u"?mode",
                ))

            # Visibility and opacity (if interesting)
            if not layer.visible:
                desc_parts.append(C_(
                    "Layers: description parts: layer hidden",
                    u"Hidden",
                ))
            elif layer.opacity < 1.0:
                desc_parts.append(C_(
                    "Layers: description parts: opacity percentage",
                    u"%d%% opaque" % (round(layer.opacity * 100),)
                ))

            # Locked flag (locked is interesting)
            if layer.locked:
                desc_parts.append(C_(
                    "Layers dockable: description parts: layer locked flag",
                    u"Locked",
                ))

            # Description of the layer's type.
            # Currently always used, for visual rhythm reasons, but it goes
            # on the end since it's perhaps the least interesting info.
            if layer.TYPE_DESCRIPTION is not None:
                desc_parts.append(layer.TYPE_DESCRIPTION)
            else:
                desc_parts.append(C_(
                    "Layers: description parts: unknown type (fallback str!)",
                    u"?type",
                ))

            # Stitch it all together
            if desc_parts:
                description = C_(
                    "Layers dockable: description parts joiner text",
                    u", ",
                ).join(desc_parts)
            else:
                description = None

        if description is None:
            markup_template = C_(
                "Layers dockable: markup for a layer with no description",
                u"{layer_name}",
            )
        else:
            markup_template = C_(
                "Layers dockable: markup for a layer with a description",
                '<span size="smaller">{layer_name}\n'
                '<span size="smaller" alpha="50%">{layer_description}</span>'
                '</span>'
            )

        markup = markup_template.format(
            layer_name=name_markup,
            layer_description=escape(description),
        )
        return markup
Example #32
0
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

"""Stuff for making names and keeping them unique."""

import re

from lib.gettext import C_
from lib.pycompat import unicode

# TRANSLATORS: UNIQUE_NAME_TEMPLATE. Must match its regex.
# TRANSLATORS: Leave this untranslated if you are unsure.
# TRANSLATORS: Change only if your lang *REQUIRES* a different order or
# TRANSLATORS: if raw digits and a space aren't enough.
UNIQUE_NAME_TEMPLATE = C_(
    "unique names: serial number needed: template",
    u'{name} {number}',
)

# TRANSLATORS: UNIQUE_NAME_REGEX. Must match its template.
# TRANSLATORS: Leave this untranslated if you are unsure.
UNIQUE_NAME_REGEX = re.compile(C_(
    "unique names: regex matching a string with a serial number",
    u'^(?P<name>.*?)\\s+(?P<number>\\d+)$',
))


def make_unique_name(name, existing, start=1, always_number=None):
    """Ensures that a name is unique.

    :param unicode name: Name to be made unique.
    :param existing: An existing list or set of names.
Example #33
0
)
_TASK_SUPPORT = C_(
    "About dialog: credits: tasks: user support",
    u"support"
)
_TASK_OUTREACH = C_(
    "About dialog: credits: tasks: outreach (social media, ads?)",
    u"outreach"
)
_TASK_COMMUNITY = C_(
    "About dialog: credits: tasks: running or building a community",
    u"community"
)

_TASK_COMMA = C_(
    "About dialog: credits: tasks: joiner punctuation",
    u", ",
)

# List contributors in order of their appearance.
# The author's name is always written in their native script,
# and is not marked for translation. It may also have:
# transcriptions (Latin, English-ish) in brackets following, and/or
# a quoted ’nym in Latin script.
# For <given(s)> <surname(s)> combinations,
# a quoted publicly-known alias may go after the given name.

# TODO: Simplify/unify how the dialog is built.
#  - This should really be built from a giant matrix.
#  - Each task type should determine a tab of the about dialog
#  - Contributors will still appear on multiple tabs,
#     - but that'd be automatic now
Example #34
0
import os
import platform

from gi.repository import Gtk
from gi.repository import GdkPixbuf
from gi.repository import GLib
import cairo

from lib.gettext import C_
import lib.meta
from lib.xml import escape

## Program-related string constants

COPYRIGHT_STRING = C_(
    "About dialog: copyright statement", u"Copyright (C) 2005-2016\n"
    u"Martin Renold and the MyPaint Development Team")
WEBSITE_URI = "http://mypaint.org"
LICENSE_SUMMARY = C_(
    "About dialog: license summary",
    u"This program is free software; you can redistribute it and/or modify "
    u"it under the terms of the GNU General Public License as published by "
    u"the Free Software Foundation; either version 2 of the License, or "
    u"(at your option) any later version.\n"
    u"\n"
    u"This program is distributed in the hope that it will be useful, "
    u"but WITHOUT ANY WARRANTY. See the COPYING file for more details.")

## Credits-related string constants

# Strings for specific tasks, all translated
Example #35
0
    def _init_ui(self):
        app = self.app

        # Dialog for showing and editing the axis value directly
        buttons = (Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)
        dialog = gui.windowing.Dialog(
            app, C_(
                "symmetry axis options panel: axis position dialog: window title",
                u"Axis Position",
            ),
            app.drawWindow,
            buttons=buttons,
        )
        dialog.connect('response', self._axis_pos_dialog_response_cb)
        grid = Gtk.Grid()
        grid.set_border_width(gui.widgets.SPACING_LOOSE)
        grid.set_column_spacing(gui.widgets.SPACING)
        grid.set_row_spacing(gui.widgets.SPACING)
        label = Gtk.Label(self._POSITION_LABEL_TEXT)
        label.set_hexpand(False)
        label.set_vexpand(False)
        grid.attach(label, 0, 0, 1, 1)
        entry = Gtk.SpinButton(
            adjustment=self._axis_pos_adj,
            climb_rate=0.25,
            digits=0
        )
        entry.set_hexpand(True)
        entry.set_vexpand(False)
        grid.attach(entry, 1, 0, 1, 1)
        dialog_content_box = dialog.get_content_area()
        dialog_content_box.pack_start(grid, True, True, 0)
        self._axis_pos_dialog = dialog

        # Layout grid
        row = 0
        grid = Gtk.Grid()
        grid.set_border_width(gui.widgets.SPACING_CRAMPED)
        grid.set_row_spacing(gui.widgets.SPACING_CRAMPED)
        grid.set_column_spacing(gui.widgets.SPACING_CRAMPED)
        self.add(grid)

        row += 1
        label = Gtk.Label(self._ALPHA_LABEL_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        grid.attach(label, 0, row, 1, 1)
        scale = Gtk.Scale.new_with_range(
            orientation = Gtk.Orientation.HORIZONTAL,
            min = 0,
            max = 1,
            step = 0.1,
        )
        scale.set_draw_value(False)
        line_alpha = self.app.preferences.get(_ALPHA_PREFS_KEY, _DEFAULT_ALPHA)
        scale.set_value(line_alpha)
        scale.set_hexpand(True)
        scale.set_vexpand(False)
        scale.connect("value-changed", self._scale_value_changed_cb)
        grid.attach(scale, 1, row, 1, 1)

        row += 1
        label = Gtk.Label(self._POSITION_LABEL_TEXT)
        label.set_hexpand(False)
        label.set_halign(Gtk.Align.START)
        button = Gtk.Button(self._POSITION_BUTTON_TEXT_INACTIVE)
        button.set_vexpand(False)
        button.connect("clicked", self._axis_pos_button_clicked_cb)
        button.set_hexpand(True)
        button.set_vexpand(False)
        grid.attach(label, 0, row, 1, 1)
        grid.attach(button, 1, row, 1, 1)
        self._axis_pos_button = button

        row += 1
        button = Gtk.CheckButton()
        toggle_action = self.app.find_action("SymmetryActive")
        button.set_related_action(toggle_action)
        button.set_label(C_(
            "symmetry axis options panel: axis active checkbox",
            u'Enabled',
        ))
        button.set_hexpand(True)
        button.set_vexpand(False)
        grid.attach(button, 1, row, 2, 1)
        self._axis_active_button = button
Example #36
0
    def rename_button_clicked_cb(self, button):
        """Rename the current brush; user is prompted for a new name"""
        bm = self.app.brushmanager
        src_brush = bm.selected_brush
        if not src_brush.name:
            dialogs.error(
                self,
                C_(
                    'brush settings editor: rename brush: error message',
                    'No brush selected!',
                ))
            return

        src_name_pp = src_brush.name.replace('_', ' ')
        dst_name = dialogs.ask_for_name(
            self,
            C_(
                "brush settings editor: rename brush: dialog title",
                "Rename Brush",
            ),
            src_name_pp,
        )
        if not dst_name:
            return
        dst_name = dst_name.replace(' ', '_')
        # ensure we don't overwrite an existing brush by accident
        dst_deleted = None
        for group, brushes in bm.groups.iteritems():
            for b2 in brushes:
                if b2.name == dst_name:
                    if group == brushmanager.DELETED_BRUSH_GROUP:
                        dst_deleted = b2
                    else:
                        msg = C_(
                            'brush settings editor: '
                            'rename brush: error message',
                            'A brush with this name already exists!',
                        )
                        dialogs.error(self, msg)
                        return

        logger.info("Renaming brush %r -> %r", src_brush.name, dst_name)
        if dst_deleted:
            deleted_group = brushmanager.DELETED_BRUSH_GROUP
            deleted_brushes = bm.get_group_brushes(deleted_group)
            deleted_brushes.remove(dst_deleted)
            bm.brushes_changed(deleted_brushes)

        # save src as dst
        src_name = src_brush.name
        src_brush.name = dst_name
        src_brush.save()
        src_brush.name = src_name
        # load dst
        dst_brush = brushmanager.ManagedBrush(bm, dst_name, persistent=True)
        dst_brush.load()

        # Replace src with dst, but keep src in the deleted list if it
        # is a stock brush
        self._delete_brush(src_brush, replacement=dst_brush)

        bm.select_brush(dst_brush)
Example #37
0
def setup_locale_combobox(locale_combo):
    # Set up locales liststore
    locale_liststore = Gtk.ListStore()
    default_loc = C_(
        "Language preferences menu - default option",
        # TRANSLATORS: This option means that MyPaint will try to use
        # TRANSLATORS: the language the system tells it to use.
        "System Language")
    # Default value - use the system locale
    locale_liststore.set_column_types((str, str, str))
    locale_liststore.append((None, default_loc, default_loc))

    loc_names = lib.localecodes.LOCALE_DICT
    # Base language - US english
    base_locale = "en_US"
    locale_liststore.append((base_locale, loc_names[base_locale][0], ""))

    # Separator
    locale_liststore.append((None, None, None))

    supported_locales = lib.config.supported_locales

    def tuplify(loc):
        if loc in loc_names:
            name_en, name_native = loc_names[loc]
            return loc, name_en, name_native
        else:
            logger.warning("Locale name not found: (%s)", loc)
            return loc, loc, loc

    # Sort alphabetically on english name of language
    for i in sorted(map(tuplify, supported_locales), key=lambda a: a[1]):
        locale_liststore.append(i)

    def sep_func(model, it):
        return model[it][1] is None

    def render_language_names(_, name_cell, model, it):
        locale, lang_en, lang_nat = model[it][:3]
        # Mark default with bold font
        if locale is None:
            name_cell.set_property("markup",
                                   "<b>{lang_en}</b>".format(lang_en=lang_en))
        # If a language does not have its native spelling
        # available, only show its name in english.
        elif lang_en == lang_nat or lang_nat == "":
            name_cell.set_property("text", lang_en)
        else:
            name_cell.set_property(
                "text",
                C_(
                    "Prefs Dialog|View|Interface - menu entries",
                    # TRANSLATORS: lang_en is the english name of the language
                    # TRANSLATORS: lang_nat is the native name of the language
                    # TRANSLATORS: in that _same language_.
                    # TRANSLATORS: This can just be copied most of the time.
                    "{lang_en} - ({lang_nat})").format(lang_en=lang_en,
                                                       lang_nat=lang_nat))

    # Remove the existing cell renderer
    locale_combo.clear()
    locale_combo.set_row_separator_func(sep_func)

    cell = Gtk.CellRendererText()
    locale_combo.pack_start(cell, True)
    locale_combo.set_cell_data_func(cell, render_language_names)
    locale_combo.set_model(locale_liststore)
Example #38
0
    def _save_doc_to_file(self, filename, doc, export=False, statusmsg=True,
                          **options):
        """Saves a document to one or more files

        :param filename: The base filename to save
        :param gui.document.Document doc: Controller for the document to save
        :param bool export: True if exporting
        :param **options: Pass-through options

        This method handles logging, statusbar messages,
        and alerting the user to when the save failed.

        See also: `lib.document.Document.save()`.
        """
        thumbnail_pixbuf = None
        prefs = self.app.preferences
        display_colorspace_setting = prefs["display.colorspace"]
        options['save_srgb_chunks'] = (display_colorspace_setting == "srgb")
        if statusmsg:
            statusbar = self.app.statusbar
            statusbar_cid = self._statusbar_context_id
            statusbar.remove_all(statusbar_cid)
            file_basename = os.path.basename(filename)
            if export:
                during_tmpl = C_(
                    "file handling: during export (statusbar)",
                    u"Exporting to “{file_basename}”…"
                )
            else:
                during_tmpl = C_(
                    "file handling: during save (statusbar)",
                    u"Saving “{file_basename}”…"
                )
            statusbar.push(statusbar_cid, during_tmpl.format(
                file_basename = file_basename,
            ))
        try:
            x, y, w, h = doc.model.get_bbox()
            if w == 0 and h == 0:
                w, h = tiledsurface.N, tiledsurface.N
                # TODO: Add support for other sizes
            thumbnail_pixbuf = doc.model.save(
                filename,
                feedback_cb=self.gtk_main_tick,
                **options
            )
            self.lastsavefailed = False
        except (FileHandlingError, AllocationError, MemoryError) as e:
            if statusmsg:
                statusbar.remove_all(statusbar_cid)
                if export:
                    failed_tmpl = C_(
                        "file handling: export failure (statusbar)",
                        u"Failed to export to “{file_basename}”.",
                    )
                else:
                    failed_tmpl = C_(
                        "file handling: save failure (statusbar)",
                        u"Failed to save “{file_basename}”.",
                    )
                self.app.show_transient_message(failed_tmpl.format(
                    file_basename = file_basename,
                ))
            self.lastsavefailed = True
            self.app.message_dialog(unicode(e), type=Gtk.MessageType.ERROR)
        else:
            if statusmsg:
                statusbar.remove_all(statusbar_cid)
            file_location = os.path.abspath(filename)
            multifile_info = ''
            if "multifile" in options:
                multifile_info = " (basis; used multiple .XXX.ext names)"
            if not export:
                logger.info('Saved to %r%s', file_location, multifile_info)
            else:
                logger.info('Exported to %r%s', file_location, multifile_info)
            if statusmsg:
                if export:
                    success_tmpl = C_(
                        "file handling: export success (statusbar)",
                        u"Exported to “{file_basename}” successfully.",
                    )
                else:
                    success_tmpl = C_(
                        "file handling: save success (statusbar)",
                        u"Saved “{file_basename}” successfully.",
                    )
                self.app.show_transient_message(success_tmpl.format(
                    file_basename = file_basename,
                ))
        return thumbnail_pixbuf
Example #39
0
)
_TASK_SUPPORT = C_(
    "About dialog: credits: tasks: user support",
    u"support"
)
_TASK_OUTREACH = C_(
    "About dialog: credits: tasks: outreach (social media, ads?)",
    u"outreach"
)
_TASK_COMMUNITY = C_(
    "About dialog: credits: tasks: running or building a community",
    u"community"
)

_TASK_COMMA = C_(
    "About dialog: credits: tasks: joiner punctuation",
    u", ",
)

# List contributors in order of their appearance.
# The author's name is always written in their native script,
# and is not marked for translation. It may also have:
# transcriptions (Latin, English-ish) in brackets following, and/or
# a quoted ’nym in Latin script.
# For <given(s)> <surname(s)> combinations,
# a quoted publicly-known alias may go after the given name.

_AUTHOR_CREDITS = [
    u"Martin Renold (%s)" % _TASK_PROGRAMMING,
    u"Yves Combe (%s)" % _TASK_PORTING,
    u"Popolon (%s)" % _TASK_PROGRAMMING,
    u"Clement Skau (%s)" % _TASK_PROGRAMMING,