Example #1
0
    def __init__(self, prefs, datapath):
        """Initialises with default colors and an empty adjuster list.

        :param prefs: Prefs dict for saving settings.
        :param datapath: Base path for saving palettes and masks.

        """
        super(ColorManager, self).__init__()

        # Defaults
        self._color = None  #: Currently edited color, a UIColor object
        self._hist = []  #: List of previous colors, most recent last
        self._palette = None  #: Current working palette
        self._adjusters = weakref.WeakSet()  #: The set of registered adjusters
        #: Cursor for pickers
        # FIXME: Use Gdk.Cursor.new_for_display()
        self._picker_cursor = Gdk.Cursor.new(Gdk.CursorType.CROSSHAIR)
        self._datapath = datapath  #: Base path for saving palettes and masks
        self._hue_distorts = None  #: Hue-remapping table for color wheels
        self._prefs = prefs  #: Shared preferences dictionary

        # Build the history. Last item is most recent.
        hist_hex = list(prefs.get(PREFS_KEY_COLOR_HISTORY, []))
        hist_hex = self._DEFAULT_HIST + hist_hex
        self._hist = [RGBColor.new_from_hex_str(s) for s in hist_hex]
        self._trim_hist()

        # Restore current color, or use the most recent color.
        col_hex = prefs.get(PREFS_KEY_CURRENT_COLOR, None)
        if col_hex is None:
            col_hex = hist_hex[-1]
        self._color = RGBColor.new_from_hex_str(col_hex)

        # Initialize angle distort table
        wheel_type = prefs.get(PREFS_KEY_WHEEL_TYPE, self._DEFAULT_WHEEL_TYPE)
        distorts_table = self._HUE_DISTORTION_TABLES[wheel_type]
        self._hue_distorts = distorts_table

        # Initialize working palette
        palette_dict = prefs.get(PREFS_PALETTE_DICT_KEY, None)
        if palette_dict is not None:
            palette = Palette.new_from_simple_dict(palette_dict)
        else:
            datapath = self.get_data_path()
            palettes_dir = os.path.join(datapath, DATAPATH_PALETTES_SUBDIR)
            default = os.path.join(palettes_dir, DEFAULT_PALETTE_FILE)
            palette = Palette(filename=default)
        self._palette = palette

        # Capture updates to the working palette
        palette.info_changed += self._palette_changed_cb
        palette.match_changed += self._palette_changed_cb
        palette.sequence_changed += self._palette_changed_cb
        palette.color_changed += self._palette_changed_cb
Example #2
0
 def _copy_color_in(self, col, name=None):
     if col is self._EMPTY_SLOT_ITEM or col is None:
         result = self._EMPTY_SLOT_ITEM
     else:
         if name is None:
             try:
                 name = col.__name
             except AttributeError:
                 pass
         if name is not None:
             name = unicode(name)
         result = RGBColor(color=col)
         result.__name = name
     return result
Example #3
0
 def _copy_color_in(self, col, name=None):
     if col is self._EMPTY_SLOT_ITEM or col is None:
         result = self._EMPTY_SLOT_ITEM
     else:
         if name is None:
             try:
                 name = col.__name
             except AttributeError:
                 pass
         if name is not None:
             name = unicode(name)
         result = RGBColor(color=col)
         result.__name = name
     return result
Example #4
0
def _get_color_in_window(win, x, y, size=3):
    """Attempts to get the color from a position within a GDK window"""
    # GTK2 used to return a bitdepth as well.
    # The C API docs for GTK3 3.14.5 don't mention that,
    # but its GI wrapper seems to attempt backwards compatibility.
    # Um, yeah.
    # Just write code that won't break when they fix it.
    geom_tuple = win.get_geometry()
    win_x, win_y, win_w, win_h = geom_tuple[0:4]
    # Rectangle to sample
    x = int(max(0, x - size/2))
    y = int(max(0, y - size/2))
    w = int(min(size, win_w - x))
    h = int(min(size, win_h - y))
    if w <= 0 or h <= 0:
        return RGBColor(0, 0, 0)
    # The call below can take over 20ms,
    # and it depends on the window size.
    # It must be causing a full-window pixmap copy somewhere.
    pixbuf = Gdk.pixbuf_get_from_window(win, x, y, w, h)
    if pixbuf is None:
        errcol = RGBColor(1, 0, 0)
        logger.warning(
            "Failed to get pixbuf from screen; returning "
            "error indicator color %r",
            errcol,
        )
        return errcol
    return RGBColor.new_from_pixbuf_average(pixbuf)
Example #5
0
 def render_as_icon(self, cr, size):
     """Renders as an icon into a Cairo context.
     """
     # Strategy: construct tmp R,G,B sliders with a color that shows off
     # their primary a bit. Render carefully (might need special handling
     # for the 16px size).
     from adjbases import ColorManager
     mgr = ColorManager(prefs={}, datapath=".")
     mgr.set_color(RGBColor(0.3, 0.3, 0.4))
     adjs = [RGBRedSlider(), RGBGreenSlider(), RGBBlueSlider()]
     for adj in adjs:
         adj.set_color_manager(mgr)
     if size <= 16:
         cr.save()
         for adj in adjs:
             adj.BORDER_WIDTH = 1
             adj.render_background_cb(cr, wd=16, ht=5)
             cr.translate(0, 5)
         cr.restore()
     else:
         cr.save()
         bar_ht = int(size // 3)
         offset = int((size - bar_ht*3) // 2)
         cr.translate(0, offset)
         for adj in adjs:
             adj.BORDER_WIDTH = max(2, int(size // 16))
             adj.render_background_cb(cr, wd=size, ht=bar_ht)
             cr.translate(0, bar_ht)
         cr.restore()
     for adj in adjs:
         adj.set_color_manager(None)
Example #6
0
 def render_as_icon(self, cr, size):
     """Renders as an icon into a Cairo context.
     """
     # Strategy: construct tmp R,G,B sliders with a color that shows off
     # their primary a bit. Render carefully (might need special handling
     # for the 16px size).
     from .adjbases import ColorManager
     mgr = ColorManager(prefs={}, datapath=".")
     mgr.set_color(RGBColor(0.3, 0.3, 0.4))
     ring_adj = _HSVSquareOuterRing(self)
     ring_adj.set_color_manager(mgr)
     square_adj = _HSVSquareInnerSquare(self)
     square_adj.set_color_manager(mgr)
     if size <= 16:
         cr.save()
         ring_adj.render_background_cb(cr, wd=16, ht=16)
         cr.translate(-6, -6)
         square_adj.render_background_cb(cr, wd=12, ht=12)
         cr.restore()
     else:
         cr.save()
         square_offset = int(size / 5.0 * 1.6)
         square_dim = int(size * 0.64)
         ring_adj.render_background_cb(cr, wd=size, ht=size)
         # do minor rounding adjustments for hsvsquare icons at this size
         if size == 24:
             cr.translate(-1, -1)
             square_dim += 1
         cr.translate(-square_offset, -square_offset)
         square_adj.render_background_cb(cr, wd=square_dim, ht=square_dim)
         cr.restore()
     ring_adj.set_color_manager(None)
     square_adj.set_color_manager(None)
Example #7
0
def get_color_in_window(win, x, y, size=3):
    """Attempts to get the color from a position within a GDK window.
    """

    # [GTK3] GDK2 and GDK3 return different tuples: no bitdepth in GDK3
    # We use GTK3 now but for some reason users still get different results,
    # see https://gna.org/bugs/?20791
    geom_tuple = win.get_geometry()
    win_x, win_y, win_w, win_h = geom_tuple[0:4]

    x = int(max(0, x - size/2))
    y = int(max(0, y - size/2))
    w = int(min(size, win_w - x))
    h = int(min(size, win_h - y))
    if w <= 0 or h <= 0:
        return RGBColor(0, 0, 0)
    # The call below can take over 20ms, and it depends on the window size!
    # It must be causing a full-window pixmap copy somewhere.
    pixbuf = gdk.pixbuf_get_from_window(win, x, y, w, h)
    if pixbuf is None:
        errcol = RGBColor(1, 0, 0)
        logger.warning("Failed to get pixbuf from screen; returning "
                       "error indicator color %r", errcol)
        return errcol
    return RGBColor.new_from_pixbuf_average(pixbuf)
class RGBColorRange(ThreeValueColorRange):
    type = "RGB"
    base_color = RGBColor(0, 0, 0)
    references = {
        "red": (0, RGBRedSlider),
        "green": (1, RGBGreenSlider),
        "blue": (2, RGBBlueSlider),
    }

    def in_range(self, color: color.UIColor) -> (bool, float, float):
        return super().in_range(color.get_rgb())

    @property
    def r(self):
        return self._get_val_mean(0)

    @property
    def g(self):
        return self._get_val_mean(1)

    @property
    def b(self):
        return self._get_val_mean(2)

    def center_color(self) -> color.UIColor:
        return color.RGBColor(self.r, self.g, self.b)
Example #9
0
def from_drag_data(bytes):
    """Construct from drag+dropped bytes of type application/x-color.

    The data format is 8 bytes, RRGGBBAA, with assumed native endianness.
    Alpha is ignored.
    """
    r, g, b, a = [float(h)/0xffff for h in struct.unpack("=HHHH", bytes)]
    return RGBColor(r, g, b)
Example #10
0
def render_round_floating_color_chip(cr, x, y, color, radius):
    """Draw a round color chip with a slight drop shadow

    Currently used for dismiss/delete buttons and control points.
    The button's style is similar to that used for the paint chips
    in the dockable palette panel.

    """
    x = round(float(x))
    y = round(float(y))

    cr.save()
    cr.set_dash([], 0)
    cr.set_line_width(0)

    base_col = RGBColor(color=color)
    hi_col = _get_paint_chip_highlight(base_col)

    xoffs = gui.style.DROP_SHADOW_X_OFFSET
    yoffs = gui.style.DROP_SHADOW_Y_OFFSET
    blur = gui.style.DROP_SHADOW_BLUR
    alpha = gui.style.DROP_SHADOW_ALPHA
    drop_shadow = cairo.RadialGradient(
        x + xoffs,
        y + yoffs,
        radius,
        x + xoffs,
        y + yoffs,
        radius + blur,
    )
    drop_shadow.add_color_stop_rgba(0, 0, 0, 0, alpha)
    drop_shadow.add_color_stop_rgba(1, 0, 0, 0, 0.0)
    cr.arc(x + xoffs, y + yoffs, radius + blur + 1, 0, 2 * math.pi)
    cr.set_source(drop_shadow)
    cr.fill()

    cr.arc(x, y, radius, 0, 2 * math.pi)
    cr.set_source_rgb(*base_col.get_rgb())
    cr.fill_preserve()
    cr.clip_preserve()

    cr.set_source_rgb(*hi_col.get_rgb())
    cr.set_line_width(2)
    cr.stroke()

    cr.restore()
Example #11
0
 def get_color_for_bar_amount(self, amt):
     # CCT range from 1904-25000
     # below 1904 is out of sRGB gamut
     # power function to put 6500k near middle
     cct = amt**2 * 23096 + 1904
     rgb = CCT_to_RGB(cct)
     col = RGBColor(rgb=rgb)
     return col
Example #12
0
def from_gdk_rgba(gdk_rgba):
    """Construct a new UIColor from a `Gdk.RGBA` (omitting alpha)

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

    """
    rgbflt = (gdk_rgba.red, gdk_rgba.green, gdk_rgba.blue)
    return RGBColor(*[clamp(c, 0., 1.) for c in rgbflt])
Example #13
0
def from_gdk_color(gdk_color):
    """Construct a new UIColor from a Gdk.Color.

    >>> from_gdk_color(Gdk.Color(0.0000, 0x8000, 0xffff))
    <RGBColor r=0.0000, g=0.5000, b=1.0000>

    """
    rgb16 = (gdk_color.red, gdk_color.green, gdk_color.blue)
    return RGBColor(*[float(c)/65535 for c in rgb16])
Example #14
0
def render_round_floating_color_chip(cr, x, y, color, radius, z=2):
    """Draw a round color chip with a slight drop shadow

    :param cairo.Context cr: Context in which to draw.
    :param float x: X coordinate of the center pixel.
    :param float y: Y coordinate of the center pixel.
    :param lib.color.UIColor color: Color for the chip.
    :param float radius: Circle radius, in pixels.
    :param int z: Simulated height of the object above the canvas.

    Currently used for accept/dismiss/delete buttons and control points
    on the painting canvas, in certain modes.

    The button's style is similar to that used for the paint chips in
    the dockable palette panel. As used here with drop shadows to
    indicate that the blob can be interacted with, the style is similar
    to Google's Material Design approach. This style adds a subtle edge
    highlight in a brighter variant of "color", which seems to help
    address adjacent color interactions.

    """
    x = round(float(x))
    y = round(float(y))
    radius = round(radius)

    cr.save()
    cr.set_dash([], 0)
    cr.set_line_width(0)

    base_col = RGBColor(color=color)
    hi_col = _get_paint_chip_highlight(base_col)

    cr.arc(x, y, radius + 0, 0, 2 * math.pi)
    cr.set_line_width(2)
    render_drop_shadow(cr, z=z)

    cr.set_source_rgb(*base_col.get_rgb())
    cr.fill_preserve()
    cr.clip_preserve()

    cr.set_source_rgb(*hi_col.get_rgb())
    cr.stroke()

    cr.restore()
Example #15
0
def render_round_floating_color_chip(cr, x, y, color, radius, z=2):
    """Draw a round color chip with a slight drop shadow

    :param cairo.Context cr: Context in which to draw.
    :param float x: X coordinate of the center pixel.
    :param float y: Y coordinate of the center pixel.
    :param lib.color.UIColor color: Color for the chip.
    :param float radius: Circle radius, in pixels.
    :param int z: Simulated height of the object above the canvas.

    Currently used for accept/dismiss/delete buttons and control points
    on the painting canvas, in certain modes.

    The button's style is similar to that used for the paint chips in
    the dockable palette panel. As used here with drop shadows to
    indicate that the blob can be interacted with, the style is similar
    to Google's Material Design approach. This style adds a subtle edge
    highlight in a brighter variant of "color", which seems to help
    address adjacent color interactions.

    """
    x = round(float(x))
    y = round(float(y))
    radius = round(radius)

    cr.save()
    cr.set_dash([], 0)
    cr.set_line_width(0)

    base_col = RGBColor(color=color)
    hi_col = _get_paint_chip_highlight(base_col)

    cr.arc(x, y, radius+0, 0, 2*math.pi)
    cr.set_line_width(2)
    render_drop_shadow(cr, z=z)

    cr.set_source_rgb(*base_col.get_rgb())
    cr.fill_preserve()
    cr.clip_preserve()

    cr.set_source_rgb(*hi_col.get_rgb())
    cr.stroke()

    cr.restore()
Example #16
0
def render_round_floating_color_chip(cr, x, y, color, radius):
    """Draw a round color chip with a slight drop shadow

    Currently used for dismiss/delete buttons and control points.
    The button's style is similar to that used for the paint chips
    in the dockable palette panel.

    """
    x = round(float(x))
    y = round(float(y))

    cr.save()
    cr.set_dash([], 0)
    cr.set_line_width(0)

    base_col = RGBColor(color=color)
    hi_col = _get_paint_chip_highlight(base_col)

    xoffs = gui.style.DROP_SHADOW_X_OFFSET
    yoffs = gui.style.DROP_SHADOW_Y_OFFSET
    blur = gui.style.DROP_SHADOW_BLUR
    alpha = gui.style.DROP_SHADOW_ALPHA
    drop_shadow = cairo.RadialGradient(
        x+xoffs, y+yoffs, radius,
        x+xoffs, y+yoffs, radius + blur,
    )
    drop_shadow.add_color_stop_rgba(0, 0, 0, 0, alpha)
    drop_shadow.add_color_stop_rgba(1, 0, 0, 0, 0.0)
    cr.arc(x+xoffs, y+yoffs, radius + blur + 1, 0, 2*math.pi)
    cr.set_source(drop_shadow)
    cr.fill()

    cr.arc(x, y, radius, 0, 2*math.pi)
    cr.set_source_rgb(*base_col.get_rgb())
    cr.fill_preserve()
    cr.clip_preserve()

    cr.set_source_rgb(*hi_col.get_rgb())
    cr.set_line_width(2)
    cr.stroke()

    cr.restore()
Example #17
0
    def get_color_by_name(self, name):
        """Looks up the first color with the given name.

          >>> pltt = Palette()
          >>> pltt.append(RGBColor(1,0,1), "Magenta")
          >>> pltt.get_color_by_name("Magenta")
          <RGBColor r=1.0000, g=0.0000, b=1.0000>

        """
        for col in self:
            if col.__name == name:
                return RGBColor(color=col)
Example #18
0
 def __init__(self, color=None):
     """Initialize with a color (default is black"""
     Gtk.AspectFrame.__init__(self, xalign=0.5, yalign=0.5,
                              ratio=1.0, obey_child=False)
     self.set_shadow_type(Gtk.ShadowType.IN)
     self.drawingarea = Gtk.DrawingArea()
     self.add(self.drawingarea)
     if color is None:
         color = RGBColor(0, 0, 0)
     self._color = color
     self.drawingarea.set_size_request(8, 8)
     self.drawingarea.connect("draw", self._draw_cb)
Example #19
0
 def new_from_simple_dict(class_, simple):
     """Constructs and returns a palette from the simple dict form."""
     pal = class_()
     pal.set_name(simple.get("name", None))
     pal.set_columns(simple.get("columns", None))
     for entry in simple.get("entries", []):
         if entry is None:
             pal.append(None)
         else:
             s, name = entry
             col = RGBColor.new_from_hex_str(s)
             pal.append(col, name)
     return pal
Example #20
0
 def new_from_simple_dict(cls, simple):
     """Constructs and returns a palette from the simple dict form."""
     pal = cls()
     pal.set_name(simple.get("name", None))
     pal.set_columns(simple.get("columns", None))
     for entry in simple.get("entries", []):
         if entry is None:
             pal.append(None)
         else:
             s, name = entry
             col = RGBColor.new_from_hex_str(s)
             pal.append(col, name)
     return pal
Example #21
0
def ask_for_color(title, color=None, previous_color=None, parent=None):
    """Returns a color chosen by the user via a modal dialog.

    The dialog is a standard `Gtk.ColorSelectionDialog`.
    The returned value may be `None`,
    which means that the user pressed Cancel in the dialog.

    """
    if color is None:
        color = RGBColor(0.5, 0.5, 0.5)
    if previous_color is None:
        previous_color = RGBColor(0.5, 0.5, 0.5)
    dialog = Gtk.ColorSelectionDialog(title)
    sel = dialog.get_color_selection()
    sel.set_current_color(uicolor.to_gdk_color(color))
    sel.set_previous_color(uicolor.to_gdk_color(previous_color))
    dialog.set_position(Gtk.WindowPosition.MOUSE)
    dialog.set_modal(True)
    dialog.set_resizable(False)
    if parent is not None:
        dialog.set_transient_for(parent)
    # GNOME likes to darken the main window
    # when it is set as the transient-for parent window.
    # The setting is "Attached Modal Dialogs", which defaultss to ON.
    # See https://github.com/mypaint/mypaint/issues/325 .
    # This is unhelpful for art programs,
    # but advertising the dialog
    # as a utility window restores sensible behaviour.
    dialog.set_type_hint(Gdk.WindowTypeHint.UTILITY)
    dialog.set_default_response(Gtk.ResponseType.OK)
    response_id = dialog.run()
    result = None
    if response_id == Gtk.ResponseType.OK:
        col_gdk = sel.get_current_color()
        result = uicolor.from_gdk_color(col_gdk)
    dialog.destroy()
    return result
Example #22
0
    def get_bar_amount_for_color(self, col):
        col = HCYColor(color=col)
        return col.y

    def get_background_validity(self):
        col = HCYColor(color=self.get_managed_color())
        return int(col.h * 1000), int(col.c * 1000)


if __name__ == '__main__':
    import os
    import sys
    from adjbases import ColorManager
    mgr = ColorManager(prefs={}, datapath=".")
    cs_adj = ComponentSlidersAdjusterPage()
    cs_adj.set_color_manager(mgr)
    cs_adj.set_managed_color(RGBColor(0.3, 0.6, 0.7))
    if len(sys.argv) > 1:
        icon_name = cs_adj.get_page_icon_name()
        for dir_name in sys.argv[1:]:
            cs_adj.save_icon_tree(dir_name, icon_name)
    else:
        # Interactive test
        window = Gtk.Window()
        window.add(cs_adj.get_page_widget())
        window.set_title(os.path.basename(sys.argv[0]))
        window.connect("destroy", lambda *a: Gtk.main_quit())
        window.show_all()
        Gtk.main()
Example #23
0
 def get_color_for_bar_amount(self, amt):
     col = RGBColor(color=self.get_managed_color())
     col.b = amt
     return col
Example #24
0
 def _copy_color_out(self, col):
     if col is self._EMPTY_SLOT_ITEM:
         return None
     result = RGBColor(color=col)
     result.__name = col.__name
     return result
Example #25
0
class Palette (object):
    """A flat list of color swatches, compatible with the GIMP

    As a (sideways-compatible) extension to the GIMP's format, MyPaint supports
    empty slots in the palette. These slots are represented by pure black
    swatches with the name ``__NONE__``.

    Palette objects expose the position within the palette of a current color
    match, which can be declared to be approximate or exact. This is used for
    highlighting the user concept of the "current color" in the GUI.

    Palette objects can be serialized in the GIMP's file format (the regular
    `unicode()` function on a Palette will do this too), or converted to and
    from a simpler JSON-ready representation for storing in the MyPaint prefs.
    Support for loading and saving via modal dialogs is defined here too.

    """

    ## Class-level constants
    _EMPTY_SLOT_ITEM = RGBColor(-1, -1, -1)
    _EMPTY_SLOT_NAME = "__NONE__"

    ## Construction, loading and saving

    def __init__(self, filehandle=None, filename=None, colors=None):
        """Instantiate, from a file or a sequence of colors

        :param filehandle: Filehandle to load.
        :param filename: Name of a file to load.
        :param colors: Iterable sequence of colors (lib.color.UIColor).

        The constructor arguments are mutually exclusive.  With no args
        specified, you get an empty palette.

          >>> Palette()
          <Palette colors=0, columns=0, name=None>

        Palettes can be generated from interpolations, which is handy for
        testing, at least.

          >>> cols = RGBColor(1,1,0).interpolate(RGBColor(1,0,1), 10)
          >>> Palette(colors=cols)
          <Palette colors=10, columns=0, name=None>

        """
        super(Palette, self).__init__()

        #: Number of columns. 0 means "natural flow"
        self._columns = 0
        #: List of named colors
        self._colors = []
        #: Name of the palette as a Unicode string, or None
        self._name = None
        #: Current position in the palette. None=no match; integer=index.
        self._match_position = None
        #: True if the current match is approximate
        self._match_is_approx = False

        # Clear and initialize
        self.clear(silent=True)
        if colors:
            for col in colors:
                col = self._copy_color_in(col)
                self._colors.append(col)
        elif filehandle:
            self.load(filehandle, silent=True)
        elif filename:
            with open(filename, "r") as fp:
                self.load(fp, silent=True)

    def clear(self, silent=False):
        """Resets the palette to its initial state.

          >>> grey16 = RGBColor(1,1,1).interpolate(RGBColor(0,0,0), 16)
          >>> p = Palette(colors=grey16)
          >>> p.name = "Greyscale"
          >>> p.columns = 3
          >>> p                               # doctest: +ELLIPSIS
          <Palette colors=16, columns=3, name=...'Greyscale'>
          >>> p.clear()
          >>> p
          <Palette colors=0, columns=0, name=None>

        Fires the `info_changed()`, `sequence_changed()`, and `match_changed()`
        events, unless the `silent` parameter tests true.
        """
        self._colors = []
        self._columns = 0
        self._name = None
        self._match_position = None
        self._match_is_approx = False
        if not silent:
            self.info_changed()
            self.sequence_changed()
            self.match_changed()

    def load(self, filehandle, silent=False):
        """Load contents from a file handle containing a GIMP palette.

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

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

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

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

    def save(self, filehandle):
        """Saves the palette to an open file handle.

        :param filehandle: File-like object (.write suffices)

        >>> from lib.pycompat import PY3
        >>> if PY3:
        ...     from io import StringIO
        ... else:
        ...     from cStringIO import StringIO
        >>> fp = StringIO()
        >>> cols = RGBColor(1,.7,0).interpolate(RGBColor(.1,.1,.5), 16)
        >>> pal = Palette(colors=cols)
        >>> pal.save(fp)
        >>> fp.getvalue() == unicode(pal)
        True

        The file handle is not flushed, and is left open after the
        write.

        >>> fp.flush()
        >>> fp.close()

        """
        filehandle.write(unicode(self))

    def update(self, other):
        """Updates all details of this palette from another palette.

        Fires the `info_changed()`, `sequence_changed()`, and `match_changed()`
        events.
        """
        self.clear(silent=True)
        for col in other._colors:
            col = self._copy_color_in(col)
            self._colors.append(col)
        self._name = other._name
        self._columns = other._columns
        self.info_changed()
        self.sequence_changed()
        self.match_changed()

    ## Palette size and metadata

    def get_columns(self):
        """Get the number of columns (0 means unspecified)."""
        return self._columns

    def set_columns(self, n):
        """Set the number of columns (0 means unspecified)."""
        self._columns = int(n)
        self.info_changed()

    def get_name(self):
        """Gets the palette's name."""
        return self._name

    def set_name(self, name):
        """Sets the palette's name."""
        if name is not None:
            name = unicode(name)
        self._name = name
        self.info_changed()

    def __bool__(self):
        """Palettes never test false, regardless of their length.

        >>> p = Palette()
        >>> bool(p)
        True

        """
        return True

    def __len__(self):
        """Palette length is the number of color slots within it."""
        return len(self._colors)

    ## PY2/PY3 compat

    __nonzero__ = __bool__

    ## Match position marker

    def get_match_position(self):
        """Return the position of the current match (int ot None)"""
        return self._match_position

    def set_match_position(self, i):
        """Sets the position of the current match (int or None)

        Fires `match_changed()` if the value is changed."""
        if i is not None:
            i = int(i)
            if i < 0 or i >= len(self):
                i = None
        if i != self._match_position:
            self._match_position = i
            self.match_changed()

    def get_match_is_approx(self):
        """Returns whether the current match is approximate."""
        return self._match_is_approx

    def set_match_is_approx(self, approx):
        """Sets whether the current match is approximate

        Fires match_changed() if the boolean value changes."""
        approx = bool(approx)
        if approx != self._match_is_approx:
            self._match_is_approx = approx
            self.match_changed()

    def match_color(self, col, exact=False, order=None):
        """Moves the match position to the color closest to the argument.

        :param col: The color to match.
        :type col: lib.color.UIColor
        :param exact: Only consider exact matches, and not near-exact or
                approximate matches.
        :type exact: bool
        :param order: a search order to use. Default is outwards from the
                match position, or in order if the match is unset.
        :type order: sequence or iterator of integer color indices.
        :returns: Whether the match succeeded.
        :rtype: bool

        By default, the matching algorithm favours exact or near-exact matches
        which are close to the current position. If the current position is
        unset, this search starts at 0. If there are no exact or near-exact
        matches, a looser approximate match will be used, again favouring
        matches with nearby positions.

          >>> red2blue = RGBColor(1, 0, 0).interpolate(RGBColor(0, 1, 1), 5)
          >>> p = Palette(colors=red2blue)
          >>> p.match_color(RGBColor(0.45, 0.45, 0.45))
          True
          >>> p.match_position
          2
          >>> p.match_is_approx
          True
          >>> p[p.match_position]
          <RGBColor r=0.5000, g=0.5000, b=0.5000>
          >>> p.match_color(RGBColor(0.5, 0.5, 0.5))
          True
          >>> p.match_is_approx
          False
          >>> p.match_color(RGBColor(0.45, 0.45, 0.45), exact=True)
          False
          >>> p.match_color(RGBColor(0.5, 0.5, 0.5), exact=True)
          True

        Fires the ``match_changed()`` event when changes happen.
        """
        if order is not None:
            search_order = order
        elif self.match_position is not None:
            search_order = _outwards_from(len(self), self.match_position)
        else:
            search_order = xrange(len(self))
        bestmatch_i = None
        bestmatch_d = None
        is_approx = True
        for i in search_order:
            c = self._colors[i]
            if c is self._EMPTY_SLOT_ITEM:
                continue
            # Closest exact or near-exact match by index distance (according to
            # the search_order). Considering near-exact matches as equivalent
            # to exact matches improves the feel of PaletteNext and
            # PalettePrev.
            if exact:
                if c == col:
                    bestmatch_i = i
                    is_approx = False
                    break
            else:
                d = _color_distance(col, c)
                if c == col or d < 0.06:
                    bestmatch_i = i
                    is_approx = False
                    break
                if bestmatch_d is None or d < bestmatch_d:
                    bestmatch_i = i
                    bestmatch_d = d
            # Measuring over a blend into solid equiluminant 0-chroma
            # grey for the orange #DA5D2E with an opaque but feathered
            # brush made huge, and picking just inside the point where the
            # palette widget begins to call it approximate:
            #
            # 0.05 is a difference only discernible (to me) by tilting LCD
            # 0.066 to 0.075 appears slightly greyer for large areas
            # 0.1 and above is very clearly distinct

        # If there are no exact or near-exact matches, choose the most similar
        # color anywhere in the palette.
        if bestmatch_i is not None:
            self._match_position = bestmatch_i
            self._match_is_approx = is_approx
            self.match_changed()
            return True
        return False

    def move_match_position(self, direction, refcol):
        """Move the match position in steps, matching first if neeeded.

        :param direction: Direction for moving, positive or negative
        :type direction: int:, ``1`` or ``-1``
        :param refcol: Reference color, used for initial mathing when needed.
        :type refcol: lib.color.UIColor
        :returns: the color newly matched, if the match position has changed
        :rtype: lib.color.UIColor, or None

        Invoking this method when there's no current match position will select
        the color that's closest to the reference color, just like
        `match_color()`

        >>> greys = RGBColor(1,1,1).interpolate(RGBColor(0,0,0), 16)
        >>> pal = Palette(colors=greys)
        >>> refcol = RGBColor(0.5, 0.55, 0.45)
        >>> pal.move_match_position(-1, refcol)
        >>> pal.match_position
        7
        >>> pal.match_is_approx
        True

        When the current match is defined, but only an approximate match, this
        method converts it to an exact match but does not change its position.

          >>> pal.move_match_position(-1, refcol) is None
          False
          >>> pal.match_position
          7
          >>> pal.match_is_approx
          False

        When the match is initially exact, its position is stepped in the
        direction indicated, either by +1 or -1. Blank palette entries are
        skipped.

          >>> pal.move_match_position(-1, refcol) is None
          False
          >>> pal.match_position
          6
          >>> pal.match_is_approx
          False

        Fires ``match_position_changed()`` and ``match_is_approx_changed()`` as
        appropriate.  The return value is the newly matched color whenever this
        method produces a new exact match.

        """
        # Normalize direction
        direction = int(direction)
        if direction < 0:
            direction = -1
        elif direction > 0:
            direction = 1
        else:
            return None
        # If nothing is selected, pick the closest match without changing
        # the managed color.
        old_pos = self._match_position
        if old_pos is None:
            self.match_color(refcol)
            return None
        # Otherwise, refine the match, or step it in the requested direction.
        new_pos = None
        if self._match_is_approx:
            # Make an existing approximate match concrete.
            new_pos = old_pos
        else:
            # Index reflects a close or identical match.
            # Seek in the requested direction, skipping empty entries.
            pos = old_pos
            assert direction != 0
            pos += direction
            while pos < len(self._colors) and pos >= 0:
                if self._colors[pos] is not self._EMPTY_SLOT_ITEM:
                    new_pos = pos
                    break
                pos += direction
        # Update the palette index and the managed color.
        result = None
        if new_pos is not None:
            col = self._colors[new_pos]
            if col is not self._EMPTY_SLOT_ITEM:
                result = self._copy_color_out(col)
            self.set_match_position(new_pos)
            self.set_match_is_approx(False)
        return result

    ## Property-style access for setters and getters

    columns = property(get_columns, set_columns)
    name = property(get_name, set_name)
    match_position = property(get_match_position, set_match_position)
    match_is_approx = property(get_match_is_approx, set_match_is_approx)

    ## Color access

    def _copy_color_out(self, col):
        if col is self._EMPTY_SLOT_ITEM:
            return None
        result = RGBColor(color=col)
        result.__name = col.__name
        return result

    def _copy_color_in(self, col, name=None):
        if col is self._EMPTY_SLOT_ITEM or col is None:
            result = self._EMPTY_SLOT_ITEM
        else:
            if name is None:
                try:
                    name = col.__name
                except AttributeError:
                    pass
            if name is not None:
                name = unicode(name)
            result = RGBColor(color=col)
            result.__name = name
        return result

    def append(self, col, name=None, unique=False, match=False):
        """Appends a color, optionally setting a name for it.

        :param col: The color to append.
        :param name: Name of the color to insert.
        :param unique: If true, don't append if the color already exists
                in the palette. Only exact matches count.
        :param match: If true, set the match position to the
                appropriate palette entry.
        """
        col = self._copy_color_in(col, name)
        if unique:
            # Find the final exact match, if one is present
            for i in xrange(len(self._colors)-1, -1, -1):
                if col == self._colors[i]:
                    if match:
                        self._match_position = i
                        self._match_is_approx = False
                        self.match_changed()
                    return
        # Append new color, and select it if requested
        end_i = len(self._colors)
        self._colors.append(col)
        if match:
            self._match_position = end_i
            self._match_is_approx = False
            self.match_changed()
        self.sequence_changed()

    def insert(self, i, col, name=None):
        """Inserts a color, setting an optional name for it.

        :param i: Target index. `None` indicates appending a color.
        :param col: Color to insert. `None` indicates an empty slot.
        :param name: Name of the color to insert.

          >>> grey16 = RGBColor(1, 1, 1).interpolate(RGBColor(0, 0, 0), 16)
          >>> p = Palette(colors=grey16)
          >>> p.insert(5, RGBColor(1, 0, 0), name="red")
          >>> p
          <Palette colors=17, columns=0, name=None>
          >>> p[5]
          <RGBColor r=1.0000, g=0.0000, b=0.0000>

        Fires the `sequence_changed()` event. If the match position changes as
        a result, `match_changed()` is fired too.

        """
        col = self._copy_color_in(col, name)
        if i is None:
            self._colors.append(col)
        else:
            self._colors.insert(i, col)
            if self.match_position is not None:
                if self.match_position >= i:
                    self.match_position += 1
        self.sequence_changed()

    def reposition(self, src_i, targ_i):
        """Moves a color, or copies it to empty slots, or moves it the end.

        :param src_i: Source color index.
        :param targ_i: Source color index, or None to indicate the end.

        This operation performs a copy if the target is an empty slot, and a
        remove followed by an insert if the target slot contains a color.

          >>> grey16 = RGBColor(1, 1, 1).interpolate(RGBColor(0, 0, 0), 16)
          >>> p = Palette(colors=grey16)
          >>> p[5] = None           # creates an empty slot
          >>> p.match_position = 8
          >>> p[5] == p[0]
          False
          >>> p.reposition(0, 5)
          >>> p[5] == p[0]
          True
          >>> p.match_position
          8
          >>> p[5] = RGBColor(1, 0, 0)
          >>> p.reposition(14, 5)
          >>> p.match_position     # continues pointing to the same color
          9
          >>> len(p)       # repositioning doesn't change the length
          16

        Fires the `color_changed()` event for copies to empty slots, or
        `sequence_changed()` for moves. If the match position changes as a
        result, `match_changed()` is fired too.

        """
        assert src_i is not None
        if src_i == targ_i:
            return
        try:
            col = self._colors[src_i]
            assert col is not None  # just in case we change the internal repr
        except IndexError:
            return

        # Special case: just copy if the target is an empty slot
        match_pos = self.match_position
        if targ_i is not None:
            targ = self._colors[targ_i]
            if targ is self._EMPTY_SLOT_ITEM:
                self._colors[targ_i] = self._copy_color_in(col)
                self.color_changed(targ_i)
                # Copying from the matched color moves the match postion.
                # Copying to the match position clears the match.
                if match_pos == src_i:
                    self.match_position = targ_i
                elif match_pos == targ_i:
                    self.match_position = None
                return

        # Normal case. Remove...
        self._colors.pop(src_i)
        moving_match = False
        updated_match = False
        if match_pos is not None:
            # Moving rightwards. Adjust for the pop().
            if targ_i is not None and targ_i > src_i:
                targ_i -= 1
            # Similar logic for the match position, but allow it to follow
            # the move if it started at the src position.
            if match_pos == src_i:
                match_pos = None
                moving_match = True
                updated_match = True
            elif match_pos > src_i:
                match_pos -= 1
                updated_match = True
        # ... then append or insert.
        if targ_i is None:
            self._colors.append(col)
            if moving_match:
                match_pos = len(self._colors) - 1
                updated_match = True
        else:
            self._colors.insert(targ_i, col)
            if match_pos is not None:
                if moving_match:
                    match_pos = targ_i
                    updated_match = True
                elif match_pos >= targ_i:
                    match_pos += 1
                    updated_match = True
        # Announce changes
        self.sequence_changed()
        if updated_match:
            self.match_position = match_pos
            self.match_changed()

    def pop(self, i):
        """Removes a color, returning it.

        Fires the `match_changed()` event if the match index changes as a
        result of the removal, and `sequence_changed()` if a color was removed,
        prior to its return.
        """
        i = int(i)
        try:
            col = self._colors.pop(i)
        except IndexError:
            return
        if self.match_position == i:
            self.match_position = None
        elif self.match_position > i:
            self.match_position -= 1
        self.sequence_changed()
        return self._copy_color_out(col)

    def get_color(self, i):
        """Looks up a color by its list index."""
        if i is None:
            return None
        try:
            col = self._colors[i]
            return self._copy_color_out(col)
        except IndexError:
            return None

    def __getitem__(self, i):
        return self.get_color(i)

    def __setitem__(self, i, col):
        self._colors[i] = self._copy_color_in(col, None)
        self.color_changed(i)

    ## Color name access

    def get_color_name(self, i):
        """Looks up a color's name by its list index."""
        try:
            col = self._colors[i]
        except IndexError:
            return
        if col is self._EMPTY_SLOT_ITEM:
            return
        return col.__name

    def set_color_name(self, i, name):
        """Sets a color's name by its list index."""
        try:
            col = self._colors[i]
        except IndexError:
            return
        if col is self._EMPTY_SLOT_ITEM:
            return
        col.__name = name
        self.color_changed(i)

    def get_color_by_name(self, name):
        """Looks up the first color with the given name.

          >>> pltt = Palette()
          >>> pltt.append(RGBColor(1,0,1), "Magenta")
          >>> pltt.get_color_by_name("Magenta")
          <RGBColor r=1.0000, g=0.0000, b=1.0000>

        """
        for col in self:
            if col.__name == name:
                return RGBColor(color=col)

    def __iter__(self):
        return self.iter_colors()

    def iter_colors(self):
        """Iterates across the palette's colors."""
        for col in self._colors:
            if col is self._EMPTY_SLOT_ITEM:
                yield None
            else:
                yield col

    ## Observable events

    @event
    def info_changed(self):
        """Event: palette name, or number of columns was changed."""

    @event
    def match_changed(self):
        """Event: either match position or match_is_approx was updated."""

    @event
    def sequence_changed(self):
        """Event: the color ordering or palette length was changed."""

    @event
    def color_changed(self, i):
        """Event: the color in the given slot, or its name, was modified."""

    ## Dumping and cloning

    def __unicode__(self):
        result = u"GIMP Palette\n"
        if self._name is not None:
            result += u"Name: %s\n" % self._name
        if self._columns > 0:
            result += u"Columns: %d\n" % self._columns
        result += u"#\n"
        for col in self._colors:
            if col is self._EMPTY_SLOT_ITEM:
                col_name = self._EMPTY_SLOT_NAME
                r = g = b = 0
            else:
                col_name = col.__name
                r, g, b = [clamp(int(c*0xff), 0, 0xff) for c in col.get_rgb()]
            if col_name is None:
                result += u"%d %d %d\n" % (r, g, b)
            else:
                result += u"%d %d %d    %s\n" % (r, g, b, col_name)
        return result

    def __copy__(self):
        clone = Palette()
        clone.set_name(self.get_name())
        clone.set_columns(self.get_columns())
        for col in self._colors:
            if col is self._EMPTY_SLOT_ITEM:
                clone.append(None)
            else:
                clone.append(copy(col), col.__name)
        return clone

    def __deepcopy__(self, memo):
        return self.__copy__()

    def __repr__(self):
        return "<Palette colors=%d, columns=%d, name=%r>" % (
            len(self._colors),
            self._columns,
            self._name,
        )

    ## Conversion to/from simple dict representation

    def to_simple_dict(self):
        """Converts the palette to a simple dict form used in the prefs."""
        simple = {}
        simple["name"] = self.get_name()
        simple["columns"] = self.get_columns()
        entries = []
        for col in self.iter_colors():
            if col is None:
                entries.append(None)
            else:
                name = col.__name
                entries.append((col.to_hex_str(), name))
        simple["entries"] = entries
        return simple

    @classmethod
    def new_from_simple_dict(cls, simple):
        """Constructs and returns a palette from the simple dict form."""
        pal = cls()
        pal.set_name(simple.get("name", None))
        pal.set_columns(simple.get("columns", None))
        for entry in simple.get("entries", []):
            if entry is None:
                pal.append(None)
            else:
                s, name = entry
                col = RGBColor.new_from_hex_str(s)
                pal.append(col, name)
        return pal
Example #26
0
 def get_color_for_bar_amount(self, amt):
     col = RGBColor(color=self.get_managed_color())
     col.b = amt
     return col
Example #27
0
# Transient on-canvas information, intended to be read quickly.
# Used for fading textual info or vanishing positional markers.
# Need to be high-contrast, and clear. Black and white is good.

TRANSIENT_INFO_BG_RGBA = (0, 0, 0, 0.666)  #: Transient text bg / outline
TRANSIENT_INFO_RGBA = (1, 1, 1, 1)  #: Transient text / marker


# Editable on-screen items.
# Used for editable handles on things like the document frame,
# when it's being edited.
# It's a good idea to use this and a user-tuneable alpha if the item
# is to be shown on screen permanently, in modes other than the object's
# own edit mode.

EDITABLE_ITEM_COLOR = RGBColor.new_from_hex_str("#ECF0F1")


# Active/dragging state for editable items.

ACTIVE_ITEM_COLOR = RGBColor.new_from_hex_str("#F1C40F")


# Prelight color (for complex modes, when there needs to be a distinction)

PRELIT_ITEM_COLOR = tuple(
        ACTIVE_ITEM_COLOR.interpolate(EDITABLE_ITEM_COLOR, 3)
    )[1]

Example #28
0
 def _unflatten_mask(flat_mask):
     mask = []
     for shape_flat in flat_mask:
         shape_colors = [RGBColor.new_from_hex_str(s) for s in shape_flat]
         mask.append(shape_colors)
     return mask
Example #29
0
 def _unflatten_mask(flat_mask):
     mask = []
     for shape_flat in flat_mask:
         shape_colors = [RGBColor.new_from_hex_str(s) for s in shape_flat]
         mask.append(shape_colors)
     return mask
Example #30
0
        eff_ht = ht - 2 * b
        x = b + f1_amt * eff_wd
        y = b + f2_amt * eff_ht
        return x, y

    def paint_foreground_cb(self, cr, wd, ht):
        x, y = self.get_position_for_color(self.get_managed_color())
        draw_marker_circle(cr, x, y)


if __name__ == '__main__':
    import os
    import sys
    from .adjbases import ColorManager
    mgr = ColorManager(prefs={}, datapath='.')
    cube = HSVSquarePage()
    cube.set_color_manager(mgr)
    mgr.set_color(RGBColor(0.3, 0.6, 0.7))
    if len(sys.argv) > 1:
        icon_name = cube.get_page_icon_name()
        for dir_name in sys.argv[1:]:
            cube.save_icon_tree(dir_name, icon_name)
    else:
        # Interactive test
        window = Gtk.Window()
        window.add(cube.get_page_widget())
        window.set_title(os.path.basename(sys.argv[0]))
        window.connect("destroy", lambda *a: Gtk.main_quit())
        window.show_all()
        Gtk.main()
Example #31
0
 def get_managed_color(self):
     """Gets the managed color. Convenience method for use by subclasses.
     """
     if self.color_manager is None:
         return RGBColor(color=self._DEFAULT_COLOR)
     return self.color_manager.get_color()
Example #32
0
class ColorAdjuster(object):
    """Base class for any object which can manipulate a shared `UIColor`.

    Color adjusters are used for changing one or more elements of a color.
    Several are bound to a central `ColorManager`, and broadcast
    changes to it.

    """

    ## Constants

    _DEFAULT_COLOR = RGBColor(0.55, 0.55, 0.55)

    ## Central ColorManager instance (accessors)

    def set_color_manager(self, manager):
        """Sets the shared color adjustment manager this adjuster points to.
        """
        if manager is not None:
            if self in manager.get_adjusters():
                return
        existing = self.get_color_manager()
        if existing is not None:
            existing.remove_adjuster(self)
        self.__manager = manager
        if self.__manager is not None:
            self.__manager.add_adjuster(self)

    def get_color_manager(self):
        """Gets the shared color adjustment manager.
        """
        try:
            return self.__manager
        except AttributeError:
            self.__manager = None
            return None

    color_manager = property(get_color_manager, set_color_manager)

    ## Access to the central managed UIColor (convenience methods)

    def get_managed_color(self):
        """Gets the managed color. Convenience method for use by subclasses.
        """
        if self.color_manager is None:
            return RGBColor(color=self._DEFAULT_COLOR)
        return self.color_manager.get_color()

    def set_managed_color(self, color):
        """Sets the managed color. Convenience method for use by subclasses.
        """
        if self.color_manager is None:
            return
        if color is not None:
            self.color_manager.set_color(color)

    managed_color = property(get_managed_color, set_managed_color)

    ## Central shared prefs access (convenience methods)

    def get_prefs(self):
        if self.color_manager is not None:
            return self.color_manager.get_prefs()
        return {}

    ## Update notification

    def color_updated(self):
        """Called by the manager when the shared `UIColor` changes.
        """
        pass

    def color_history_updated(self):
        """Called by the manager when the color usage history changes.
        """
        pass
Example #33
0
    def render_background_cb(self, cr, wd, ht, icon_border=None):
        """Renders the offscreen bg, for `ColorAdjusterWidget` impls.
        """
        cr.save()

        ref_grey = self.color_at_normalized_polar_pos(0, 0)

        border = icon_border
        if border is None:
            border = self.BORDER_WIDTH
        radius = self.get_radius(wd, ht, border)

        steps = self.HUE_SLICES
        sat_slices = self.SAT_SLICES
        sat_gamma = self.SAT_GAMMA

        # Move to the centre
        cx, cy = self.get_center(wd, ht)
        cr.translate(cx, cy)

        # Clip, for a slight speedup
        cr.arc(0, 0, radius + border, 0, 2 * math.pi)
        cr.clip()

        # Tangoesque outer border
        cr.set_line_width(self.OUTLINE_WIDTH)
        cr.arc(0, 0, radius, 0, 2 * math.pi)
        cr.set_source_rgba(*self.OUTLINE_RGBA)
        cr.stroke()

        # Each slice in turn
        cr.save()
        cr.set_line_width(1.0)
        cr.set_line_join(cairo.LINE_JOIN_ROUND)
        step_angle = 2.0 * math.pi / steps
        mgr = self.get_color_manager()
        for ih in xrange(steps + 1):  # overshoot by 1, no solid bit for final
            h = float(ih) / steps
            if mgr:
                h = mgr.undistort_hue(h)
            edge_col = self.color_at_normalized_polar_pos(1.0, h)
            rgb = edge_col.get_rgb()
            if ih > 0:
                # Backwards gradient
                cr.arc_negative(0, 0, radius, 0, -step_angle)
                x, y = cr.get_current_point()
                cr.line_to(0, 0)
                cr.close_path()
                lg = cairo.LinearGradient(radius, 0, float(x + radius) / 2, y)
                lg.add_color_stop_rgba(0, rgb[0], rgb[1], rgb[2], 1.0)
                lg.add_color_stop_rgba(1, rgb[0], rgb[1], rgb[2], 0.0)
                cr.set_source(lg)
                cr.fill()
            if ih < steps:
                # Forward solid
                cr.arc(0, 0, radius, 0, step_angle)
                x, y = cr.get_current_point()
                cr.line_to(0, 0)
                cr.close_path()
                cr.set_source_rgb(*rgb)
                cr.stroke_preserve()
                cr.fill()
            cr.rotate(step_angle)
        cr.restore()

        # Cheeky approximation of the right desaturation gradients
        rg = cairo.RadialGradient(0, 0, 0, 0, 0, radius)
        add_distance_fade_stops(rg, ref_grey.get_rgb(),
                                nstops=sat_slices,
                                gamma=1.0 / sat_gamma)
        cr.set_source(rg)
        cr.arc(0, 0, radius, 0, 2 * math.pi)
        cr.fill()

        # Tangoesque inner border
        cr.set_source_rgba(*self.EDGE_HIGHLIGHT_RGBA)
        cr.set_line_width(self.EDGE_HIGHLIGHT_WIDTH)
        cr.arc(0, 0, radius, 0, 2 * math.pi)
        cr.stroke()

        # Some small notches on the disc edge for pure colors
        if wd > 75 or ht > 75:
            cr.save()
            cr.arc(0, 0, radius + self.EDGE_HIGHLIGHT_WIDTH, 0, 2 * math.pi)
            cr.clip()
            pure_cols = [
                RGBColor(1, 0, 0), RGBColor(1, 1, 0), RGBColor(0, 1, 0),
                RGBColor(0, 1, 1), RGBColor(0, 0, 1), RGBColor(1, 0, 1),
            ]
            for col in pure_cols:
                x, y = self.get_pos_for_color(col)
                x = int(x) - cx
                y = int(y) - cy
                cr.set_source_rgba(*self.EDGE_HIGHLIGHT_RGBA)
                cr.arc(
                    x + 0.5, y + 0.5,
                    1.0 + self.EDGE_HIGHLIGHT_WIDTH,
                    0, 2 * math.pi,
                )
                cr.fill()
                cr.set_source_rgba(*self.OUTLINE_RGBA)
                cr.arc(
                    x + 0.5, y + 0.5,
                    self.EDGE_HIGHLIGHT_WIDTH,
                    0, 2 * math.pi,
                )
                cr.fill()
            cr.restore()

        cr.restore()
Example #34
0
    def load(self, filehandle, silent=False):
        """Load contents from a file handle containing a GIMP palette.

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

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

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

        """
        comment_line_re = re.compile(r'^#')
        field_line_re = re.compile(r'^(\w+)\s*:\s*(.*)$')
        color_line_re = re.compile(r'^(\d+)\s+(\d+)\s+(\d+)\s*(?:\b(.*))$')
        fp = filehandle
        self.clear(silent=True)   # method fires events itself
        line = fp.readline()
        if line.strip() != "GIMP Palette":
            raise RuntimeError("Not a valid GIMP Palette")
        header_done = False
        line_num = 0
        for line in fp:
            line = line.strip()
            line_num += 1
            if line == '':
                continue
            if comment_line_re.match(line):
                continue
            if not header_done:
                match = field_line_re.match(line)
                if match:
                    key, value = match.groups()
                    key = key.lower()
                    if key == 'name':
                        self._name = value.strip()
                    elif key == 'columns':
                        self._columns = int(value)
                    else:
                        logger.warning("Unknown 'key:value' pair %r", line)
                    continue
                else:
                    header_done = True
            match = color_line_re.match(line)
            if not match:
                logger.warning("Expected 'R G B [Name]', not %r", line)
                continue
            r, g, b, col_name = match.groups()
            col_name = col_name.strip()
            r = float(clamp(int(r), 0, 0xff))/0xff
            g = float(clamp(int(g), 0, 0xff))/0xff
            b = float(clamp(int(b), 0, 0xff))/0xff
            if r == g == b == 0 and col_name == self._EMPTY_SLOT_NAME:
                self.append(None)
            else:
                col = RGBColor(r, g, b)
                col.__name = col_name
                self._colors.append(col)
        if not silent:
            self.info_changed()
            self.sequence_changed()
            self.match_changed()
Example #35
0
DROP_SHADOW_BLUR = 2.0
DROP_SHADOW_X_OFFSET = 0.25
DROP_SHADOW_Y_OFFSET = 1.0
# These are only used for otherwise flat editable or draggable objects.

## Colors for additonal on-canvas information

# Transient on-canvas information, intended to be read quickly.
# Used for fading textual info or vanishing positional markers.
# Need to be high-contrast, and clear. Black and white is good.

TRANSIENT_INFO_BG_RGBA = (0, 0, 0, 0.666)  #: Transient text bg / outline
TRANSIENT_INFO_RGBA = (1, 1, 1, 1)  #: Transient text / marker

# Passive position markers.
# Used for inactive but permanent marks that convey useful information,
# like the symmetry axis while it's not being edited.
# These need to be non-distracting.

PASSIVE_ITEM_COLOR = RGBColor.new_from_hex_str("#BDC3C7")

# Editable on-screen items.
# Used for editable handles on things like the document frame,
# when it's being edited.

EDITABLE_ITEM_COLOR = RGBColor.new_from_hex_str("#ECF0F1")

# Prelit/active/dragging state for editable icons

ACTIVE_ITEM_COLOR = RGBColor.new_from_hex_str("#F1C40F")
Example #36
0
 def _copy_color_out(self, col):
     if col is self._EMPTY_SLOT_ITEM:
         return None
     result = RGBColor(color=col)
     result.__name = col.__name
     return result
Example #37
0
    def load(self, filehandle, silent=False):
        """Load contents from a file handle containing a GIMP palette.

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

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

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

        """
        comment_line_re = re.compile(r'^#')
        field_line_re = re.compile(r'^(\w+)\s*:\s*(.*)$')
        color_line_re = re.compile(r'^(\d+)\s+(\d+)\s+(\d+)\s*(?:\b(.*))$')
        fp = filehandle
        self.clear(silent=True)   # method fires events itself
        line = fp.readline()
        if line.strip() != "GIMP Palette":
            raise RuntimeError("Not a valid GIMP Palette")
        header_done = False
        line_num = 0
        for line in fp:
            line = line.strip()
            line_num += 1
            if line == '':
                continue
            if comment_line_re.match(line):
                continue
            if not header_done:
                match = field_line_re.match(line)
                if match:
                    key, value = match.groups()
                    key = key.lower()
                    if key == 'name':
                        self._name = value.strip()
                    elif key == 'columns':
                        self._columns = int(value)
                    else:
                        logger.warning("Unknown 'key:value' pair %r", line)
                    continue
                else:
                    header_done = True
            match = color_line_re.match(line)
            if not match:
                logger.warning("Expected 'R G B [Name]', not %r", line)
                continue
            r, g, b, col_name = match.groups()
            col_name = col_name.strip()
            r = clamp(int(r), 0, 0xff) / 0xff
            g = clamp(int(g), 0, 0xff) / 0xff
            b = clamp(int(b), 0, 0xff) / 0xff
            if r == g == b == 0 and col_name == self._EMPTY_SLOT_NAME:
                self.append(None)
            else:
                col = RGBColor(r, g, b)
                col.__name = col_name
                self._colors.append(col)
        if not silent:
            self.info_changed()
            self.sequence_changed()
            self.match_changed()
Example #38
0
    def _init_ui(self):
        # Dialog for editing dimensions (width, height, DPI)
        app = self.app
        buttons = (Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)
        self._size_dialog = windowing.Dialog(
            app, _("Frame Size"), app.drawWindow,
            buttons=buttons
        )
        unit = _('px')

        height_label = self._new_key_label(_('Height:'))
        width_label = self._new_key_label(_('Width:'))
        dpi_label1 = self._new_key_label(_('Resolution:'))

        dpi_label2 = Gtk.Label(label=_('DPI'))
        dpi_label2.set_alignment(0.0, 0.5)
        dpi_label2.set_hexpand(False)
        dpi_label2.set_vexpand(False)
        dpi_label2.set_tooltip_text(
            _("Dots Per Inch (really Pixels Per Inch)")
        )

        color_label = Gtk.Label(label=_('Color:'))
        color_label.set_alignment(0.0, 0.5)

        height_entry = Gtk.SpinButton(
            adjustment=self.height_adj,
            climb_rate=0.25,
            digits=0
        )
        height_entry.set_vexpand(False)
        height_entry.set_hexpand(True)
        self.height_adj.set_spin_button(height_entry)

        width_entry = Gtk.SpinButton(
            adjustment=self.width_adj,
            climb_rate=0.25,
            digits=0
        )
        width_entry.set_vexpand(False)
        width_entry.set_hexpand(True)
        self.width_adj.set_spin_button(width_entry)

        dpi_entry = Gtk.SpinButton(
            adjustment=self.dpi_adj,
            climb_rate=0.0,
            digits=0
        )
        dpi_entry.set_vexpand(False)
        dpi_entry.set_hexpand(True)

        color_button = Gtk.ColorButton()
        color_rgba = self.app.preferences.get("frame.color_rgba")
        color_rgba = [min(max(c, 0), 1) for c in color_rgba]
        color_gdk = uicolor.to_gdk_color(RGBColor(*color_rgba[0:3]))
        color_alpha = int(65535 * color_rgba[3])
        color_button.set_color(color_gdk)
        color_button.set_use_alpha(True)
        color_button.set_alpha(color_alpha)
        color_button.set_title(_("Frame Color"))
        color_button.connect("color-set", self._color_set_cb)
        color_align = Gtk.Alignment.new(0, 0.5, 0, 0)
        color_align.add(color_button)

        size_grid = Gtk.Grid()
        size_grid.set_border_width(12)

        size_grid.set_row_spacing(6)
        size_grid.set_column_spacing(6)

        unit_combobox = Gtk.ComboBoxText()
        for unit in UnitAdjustment.CONVERT_UNITS.keys():
            unit_combobox.append_text(unit)
        for i, key in enumerate(UnitAdjustment.CONVERT_UNITS):
            if key == _('px'):
                unit_combobox.set_active(i)
        unit_combobox.connect('changed', self.on_unit_changed)
        unit_combobox.set_hexpand(False)
        unit_combobox.set_vexpand(False)
        self._unit_combobox = unit_combobox

        row = 0
        label = self._new_header_label(_("<b>Frame dimensions</b>"))
        label.set_margin_top(0)
        size_grid.attach(label, 0, row, 3, 1)

        row += 1
        size_grid.attach(width_label, 0, row, 1, 1)
        size_grid.attach(width_entry, 1, row, 1, 1)
        size_grid.attach(unit_combobox, 2, row, 1, 1)

        row += 1
        size_grid.attach(height_label, 0, row, 1, 1)
        size_grid.attach(height_entry, 1, row, 1, 1)

        row += 1
        label = self._new_header_label(_("<b>Pixel density</b>"))
        size_grid.attach(label, 0, row, 3, 1)

        row += 1
        size_grid.attach(dpi_label1, 0, row, 1, 1)
        size_grid.attach(dpi_entry, 1, row, 1, 1)
        size_grid.attach(dpi_label2, 2, row, 1, 1)

        # Options panel UI
        opts_table = Gtk.Table(3, 3)
        opts_table.set_border_width(3)
        xopts = Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND
        yopts = Gtk.AttachOptions.FILL
        xpad = ypad = 3

        row = 0
        size_button = Gtk.Button(label="<size-summary>")
        self._size_button = size_button
        size_button.connect("clicked", self._size_button_clicked_cb)
        opts_table.attach(size_button, 0, 2, row, row+1,
                          xopts, yopts, xpad, ypad)

        row += 1
        opts_table.attach(color_label, 0, 1, row, row+1,
                          xopts, yopts, xpad, ypad)
        opts_table.attach(color_align, 1, 2, row, row+1,
                          xopts, yopts, xpad, ypad)

        crop_layer_button = Gtk.Button(label=_('Set Frame to Layer'))
        crop_layer_button.set_tooltip_text(_("Set frame to the extents of "
                                             "the current layer"))
        crop_document_button = Gtk.Button(label=_('Set Frame to Document'))
        crop_document_button.set_tooltip_text(_("Set frame to the combination "
                                                "of all layers"))
        crop_layer_button.connect('clicked', self.crop_frame_cb,
                                  'CropFrameToLayer')
        crop_document_button.connect('clicked', self.crop_frame_cb,
                                     'CropFrameToDocument')

        trim_button = Gtk.Button()
        trim_action = self.app.find_action("TrimLayer")
        trim_button.set_related_action(trim_action)
        trim_button.set_label(_('Trim Layer to Frame'))
        trim_button.set_tooltip_text(_("Trim parts of the current layer "
                                       "which lie outside the frame"))

        self.enable_button = Gtk.CheckButton()
        frame_toggle_action = self.app.find_action("FrameToggle")
        self.enable_button.set_related_action(frame_toggle_action)
        self.enable_button.set_label(_('Enabled'))

        row += 1
        opts_table.attach(self.enable_button, 1, 2, row, row+1,
                          xopts, yopts, xpad, ypad)

        row += 1
        opts_table.attach(crop_layer_button, 0, 2, row, row+1,
                          xopts, yopts, xpad, ypad)

        row += 1
        opts_table.attach(crop_document_button, 0, 2, row, row+1,
                          xopts, yopts, xpad, ypad)

        row += 1
        opts_table.attach(trim_button, 0, 2, row, row+1,
                          xopts, yopts, xpad, ypad)

        content_area = self._size_dialog.get_content_area()
        content_area.pack_start(size_grid, True, True, 0)

        self._size_dialog.connect('response', self._size_dialog_response_cb)

        self.add(opts_table)
Example #39
0
    def _init_ui(self):

        height_label = self._new_key_label(_('Height:'))
        width_label = self._new_key_label(_('Width:'))
        dpi_label1 = self._new_key_label(_('Resolution:'))

        dpi_label2 = self._new_key_label(_('DPI'))
        dpi_label2.set_tooltip_text(
            _("Dots Per Inch (really Pixels Per Inch)"))

        color_label = self._new_key_label(_('Color:'))

        height_entry = Gtk.SpinButton(adjustment=self.height_adj,
                                      climb_rate=0.25,
                                      digits=0)
        height_entry.set_vexpand(False)
        height_entry.set_hexpand(True)
        self.height_adj.set_spin_button(height_entry)

        width_entry = Gtk.SpinButton(adjustment=self.width_adj,
                                     climb_rate=0.25,
                                     digits=0)
        width_entry.set_vexpand(False)
        width_entry.set_hexpand(True)
        self.width_adj.set_spin_button(width_entry)

        dpi_entry = Gtk.SpinButton(adjustment=self.dpi_adj,
                                   climb_rate=0.0,
                                   digits=0)
        dpi_entry.set_vexpand(False)
        dpi_entry.set_hexpand(True)

        color_button = Gtk.ColorButton()
        color_rgba = self.app.preferences.get("frame.color_rgba")
        color_rgba = [min(max(c, 0), 1) for c in color_rgba]
        color_gdk = uicolor.to_gdk_color(RGBColor(*color_rgba[0:3]))
        color_alpha = int(65535 * color_rgba[3])
        color_button.set_color(color_gdk)
        color_button.set_use_alpha(True)
        color_button.set_alpha(color_alpha)
        color_button.set_title(_("Frame Color"))
        color_button.connect("color-set", self._color_set_cb)

        unit_combobox = Gtk.ComboBoxText()
        for unit in sorted(UnitAdjustment.CONVERT_UNITS.keys()):
            unit_combobox.append_text(_Unit.STRINGS[unit])
        unit_combobox.set_active(_Unit.PX)
        unit_combobox.connect('changed', self.on_unit_changed)
        unit_combobox.set_hexpand(False)
        unit_combobox.set_vexpand(False)
        self._unit_combobox = unit_combobox

        # Options panel UI
        self.set_border_width(3)
        self.set_row_spacing(6)
        self.set_column_spacing(6)

        row = 0

        self.enable_button = Gtk.CheckButton()
        frame_toggle_action = self.app.find_action("FrameToggle")
        self.enable_button.set_related_action(frame_toggle_action)
        self.enable_button.set_label(_('Enabled'))
        self.attach(self.enable_button, 0, row, 3, 1)

        row += 1
        label = self._new_header_label(_("<b>Frame dimensions</b>"))
        self.attach(label, 0, row, 3, 1)

        row += 1
        self.attach(width_entry, 1, row, 1, 1)
        self.attach(unit_combobox, 2, row, 1, 1)
        self.attach(width_label, 0, row, 1, 1)

        row += 1
        self.attach(height_label, 0, row, 1, 1)
        self.attach(height_entry, 1, row, 1, 1)

        row += 1
        self.attach(dpi_label1, 0, row, 1, 1)
        self.attach(dpi_entry, 1, row, 1, 1)
        self.attach(dpi_label2, 2, row, 1, 1)

        row += 1
        self.attach(color_label, 0, row, 1, 1)
        self.attach(color_button, 1, row, 3, 1)

        crop_layer_button = Gtk.Button(label=_('Set Frame to Layer'))
        crop_layer_button.set_tooltip_text(
            _("Set frame to the extents of "
              "the current layer"))
        crop_document_button = Gtk.Button(label=_('Set Frame to Document'))
        crop_document_button.set_tooltip_text(
            _("Set frame to the combination "
              "of all layers"))
        crop_layer_button.connect('clicked', self.crop_frame_cb,
                                  'CropFrameToLayer')
        crop_document_button.connect('clicked', self.crop_frame_cb,
                                     'CropFrameToDocument')

        trim_button = Gtk.Button()
        trim_action = self.app.find_action("TrimLayer")
        trim_button.set_related_action(trim_action)
        trim_button.set_label(_('Trim Layer to Frame'))
        trim_button.set_tooltip_text(
            _("Trim parts of the current layer "
              "which lie outside the frame"))

        row += 1
        self.attach(crop_layer_button, 0, row, 3, 1)

        row += 1
        self.attach(crop_document_button, 0, row, 3, 1)

        row += 1
        self.attach(trim_button, 0, row, 3, 1)