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)
def load(self, filehandle, silent=False): """Load contents from a file handle containing a GIMP palette. If the 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()
def load(self, filehandle, silent=False): """Load contents from a file handle containing a GIMP palette. If the 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()
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 _drag_data_received_cb(self, widget, context, x, y, selection, info, t): if "application/x-color" not in context.targets: return False color = RGBColor.new_from_drag_data(selection.data) target_index = self.get_index_at_pos(x, y) source_widget = context.get_source_widget() if self is source_widget: # Move/copy assert self._current_index is not None self._palette.move(self._current_index, target_index) else: if target_index is None: # Append if the drop wasn't over a swatch target_index = len(self._palette) else: # Insert before populated swatches, or overwrite empties if self._palette.get_color(target_index) is None: self._palette.pop(target_index) self._palette.insert(target_index, color) self._call_palette_observers() self.update_layout() self.queue_draw() self._drag_insertion_index = None context.finish(True, True, t) self.set_managed_color(color) self.set_current_index(target_index)
def drag_data_received_cb(self, widget, context, x, y, selection, info, t): if "application/x-color" not in map(str, context.list_targets()): return False data = selection.get_data() data_type = selection.get_data_type() fmt = selection.get_format() logger.debug("drag-data-received: got type=%r", data_type) logger.debug("drag-data-received: got fmt=%r", fmt) logger.debug("drag-data-received: got data=%r len=%r", data, len(data)) color = RGBColor.new_from_drag_data(data) target_index = self.get_index_at_pos(x, y) mgr = self.get_color_manager() if Gtk.drag_get_source_widget(context) is self: # Move/copy current_index = mgr.palette.match_position logger.debug("Move/copy %r -> %r", current_index, target_index) assert current_index is not None mgr.palette.reposition(current_index, target_index) else: if target_index is None: # Append if the drop wasn't over a swatch target_index = len(mgr.palette) else: # Insert before populated swatches, or overwrite empties if mgr.palette.get_color(target_index) is None: mgr.palette.pop(target_index) mgr.palette.insert(target_index, color) self.queue_draw() self._drag_insertion_index = None context.finish(True, True, t) self.set_managed_color(color) mgr.palette.set_match_position(target_index)
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 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) if gui.pygtkcompat.USE_GTK3: pixbuf = gdk.pixbuf_get_from_window(win, x, y, w, h) else: screen = win.get_screen() colormap = screen.get_system_colormap() pixbuf = gdk.Pixbuf(gdk.COLORSPACE_RGB, False, 8, w, h) pixbuf = pixbuf.get_from_drawable(win, colormap, x, y, 0, 0, w, h) if pixbuf is None: errcol = RGBColor(1, 0, 0) print "warning: failed to get pixbuf from screen; returning", errcol return errcol return RGBColor.new_from_pixbuf_average(pixbuf)
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)
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 _paint_palette_layout(self, cr): if self._palette is None: return state = self.get_state() style = self.get_style() dx, dy = self.get_painting_offset() bg_col = RGBColor.new_from_gdk_color(style.bg[state]) self._palette.render(cr, rows=self._rows, columns=self._columns, swatch_size=self._swatch_size, bg_color=bg_col, offset_x=dx, offset_y=dy, rtl=False)
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
def _paint_palette_layout(self, cr): if self._palette is None: return state = self.get_state_flags() style = self.get_style_context() bg_rgba = style.get_background_color(state) bg_col = RGBColor.new_from_gdk_rgba(bg_rgba) dx, dy = self.get_painting_offset() self._palette.render(cr, rows=self._rows, columns=self._columns, swatch_size=self._swatch_size, bg_color=bg_col, offset_x=dx, offset_y=dy, rtl=False)
def _paint_palette_layout(self, cr): mgr = self.get_color_manager() if mgr.palette is None: return state = self.get_state_flags() style = self.get_style_context() bg_rgba = style.get_background_color(state) bg_col = RGBColor.new_from_gdk_rgba(bg_rgba) dx, dy = self.get_painting_offset() _palette_render(mgr.palette, cr, rows=self._rows, columns=self._columns, swatch_size=self._swatch_size, bg_color=bg_col, offset_x=dx, offset_y=dy, rtl=False)
def _draw_cb(self, widget, cr): if self._palette is None: return alloc = widget.get_allocation() w, h = alloc.width, alloc.height s_max = 16 # min(w, h) s_min = 4 ncolumns = self._palette.get_columns() ncolors = len(self._palette) if ncolors == 0: return if not ncolumns == 0: s = w / ncolumns s = clamp(s, s_min, s_max) s = int(s) if s * ncolumns > w: ncolumns = 0 if ncolumns == 0: s = math.sqrt(float(w * h) / ncolors) s = clamp(s, s_min, s_max) s = int(s) ncolumns = max(1, int(w / s)) nrows = int(ncolors // ncolumns) if ncolors % ncolumns != 0: nrows += 1 nrows = max(1, nrows) dx, dy = 0, 0 if nrows * s < h: dy = int(h - nrows * s) / 2 if ncolumns * s < w: dx = int(w - ncolumns * s) / 2 state = self.get_state_flags() style = self.get_style_context() bg_rgba = style.get_background_color(state) bg_color = RGBColor.new_from_gdk_rgba(bg_rgba) _palette_render(self._palette, cr, rows=nrows, columns=ncolumns, swatch_size=s, bg_color=bg_color, offset_x=dx, offset_y=dy, rtl=False)
def get_color_in_window(win, x, y, size=3): """Attempts to get the color from a position within a GDK window. """ win_x, win_y, win_w, win_h = win.get_geometry() 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) print "warning: failed to get pixbuf from screen; returning", errcol return errcol return RGBColor.new_from_pixbuf_average(pixbuf)
def _draw_cb(self, widget, cr): if self._palette is None: return alloc = widget.get_allocation() w, h = alloc.width, alloc.height s_max = 16 # min(w, h) s_min = 4 ncolumns = self._palette.get_columns() ncolors = len(self._palette) if ncolors == 0: return if not ncolumns == 0: s = w / ncolumns s = clamp(s, s_min, s_max) s = int(s) if s*ncolumns > w: ncolumns = 0 if ncolumns == 0: s = math.sqrt(float(w*h) / ncolors) s = clamp(s, s_min, s_max) s = int(s) ncolumns = max(1, int(w / s)) nrows = int(ncolors // ncolumns) if ncolors % ncolumns != 0: nrows += 1 nrows = max(1, nrows) dx, dy = 0, 0 if nrows*s < h: dy = int(h - nrows*s) / 2 if ncolumns*s < w: dx = int(w - ncolumns*s) / 2 state = self.get_state_flags() style = self.get_style_context() bg_rgba = style.get_background_color(state) bg_color = RGBColor.new_from_gdk_rgba(bg_rgba) _palette_render(self._palette, cr, rows=nrows, columns=ncolumns, swatch_size=s, bg_color=bg_color, offset_x=dx, offset_y=dy, rtl=False)
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 (uicolor.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: fp = open(filename, "r") self.load(fp, silent=True) fp.close() 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 <Palette colors=16, columns=3, name=u'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. If the 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() def save(self, filehandle): """Saves the palette to an open file handle. The file handle is not flushed, and is left open after the write. """ 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 __nonzero__(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) ## 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: 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: UIColor :returns: the color newly matched, if the match position has changed :rtype: 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() ] 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 (u"<Palette colors=%d, columns=%d, name=%s>" % (len(self._colors), self._columns, repr(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(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
def __init__(self, parent, target_color_manager): flags = Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT Gtk.Dialog.__init__(self, _("Palette Editor"), parent, flags, (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)) self.set_position(Gtk.WindowPosition.MOUSE) assert isinstance(target_color_manager, ColorManager) #: ColorManager containing the palette to be edited. self._target_color_manager = target_color_manager view = PaletteView() view.set_size_request(400, 300) view.grid.show_matched_color = True view.grid.can_select_empty = True self._view = view #: The working ColorManager, holding a working copy of the palette #: to be edited. self._mgr = ColorManager(prefs={}, datapath=target_color_manager.get_data_path()) self._mgr.set_color(RGBColor(1, 1, 1)) view.set_color_manager(self._mgr) # Action buttons, positiopned down the right hand side action_bbox = Gtk.VButtonBox() load_btn = self._load_button = Gtk.Button(stock=Gtk.STOCK_OPEN) save_btn = self._save_button = Gtk.Button(stock=Gtk.STOCK_SAVE) add_btn = self._add_button = Gtk.Button(stock=Gtk.STOCK_ADD) remove_btn = self._remove_button = Gtk.Button(stock=Gtk.STOCK_REMOVE) clear_btn = self._clear_button = Gtk.Button(stock=Gtk.STOCK_CLEAR) action_bbox.pack_start(load_btn) action_bbox.pack_start(save_btn) action_bbox.pack_start(add_btn) action_bbox.pack_start(remove_btn) action_bbox.pack_start(clear_btn) action_bbox.set_layout(Gtk.ButtonBoxStyle.START) load_btn.connect("clicked", self._load_btn_clicked) save_btn.connect("clicked", self._save_btn_clicked) remove_btn.connect("clicked", self._remove_btn_clicked) add_btn.connect("clicked", self._add_btn_clicked) clear_btn.connect("clicked", self._clear_btn_clicked) load_btn.set_tooltip_text(_("Load from a GIMP palette file")) save_btn.set_tooltip_text(_("Save to a GIMP palette file")) add_btn.set_tooltip_text(_("Add a new empty swatch")) remove_btn.set_tooltip_text(_("Remove the current swatch")) clear_btn.set_tooltip_text(_("Remove all swatches")) # Button initial state and subsequent updates remove_btn.set_sensitive(False) self._mgr.palette.match_changed += self._palette_match_changed_cb self._mgr.palette.info_changed += self._palette_changed_cb self._mgr.palette.sequence_changed += self._palette_changed_cb self._mgr.palette.color_changed += self._palette_changed_cb # Palette name and number of entries palette_details_hbox = Gtk.HBox() palette_name_label = Gtk.Label(_("Name:")) palette_name_label.set_tooltip_text(_("Name or description for" " this palette")) palette_name_entry = Gtk.Entry() palette_name_entry.connect("changed", self._palette_name_changed_cb) self._palette_name_entry = palette_name_entry self._columns_adj = Gtk.Adjustment( value=0, lower=0, upper=99, step_incr=1, page_incr=1, page_size=0 ) self._columns_adj.connect("value-changed", self._columns_changed_cb) columns_label = Gtk.Label(_("Columns:")) columns_label.set_tooltip_text(_("Number of columns")) columns_label.set_tooltip_text(_("Number of columns")) columns_spinbutton = Gtk.SpinButton( adjustment=self._columns_adj, climb_rate=1.5, digits=0 ) palette_details_hbox.set_spacing(0) palette_details_hbox.set_border_width(0) palette_details_hbox.pack_start(palette_name_label, False, False, 0) palette_details_hbox.pack_start(palette_name_entry, True, True, 6) palette_details_hbox.pack_start(columns_label, False, False, 6) palette_details_hbox.pack_start(columns_spinbutton, False, False, 0) color_name_hbox = Gtk.HBox() color_name_label = Gtk.Label(_("Color name:")) color_name_label.set_tooltip_text(_("Current color's name")) color_name_entry = Gtk.Entry() color_name_entry.connect("changed", self._color_name_changed_cb) color_name_entry.set_sensitive(False) self._color_name_entry = color_name_entry color_name_hbox.set_spacing(6) color_name_hbox.pack_start(color_name_label, False, False, 0) color_name_hbox.pack_start(color_name_entry, True, True, 0) palette_vbox = Gtk.VBox() palette_vbox.set_spacing(12) palette_vbox.pack_start(palette_details_hbox, False, False) palette_vbox.pack_start(view, True, True) palette_vbox.pack_start(color_name_hbox, False, False) # Dialog contents # Main edit area to the left, buttons to the right hbox = Gtk.HBox() hbox.set_spacing(12) hbox.pack_start(palette_vbox, True, True) hbox.pack_start(action_bbox, False, False) hbox.set_border_width(12) self.vbox.pack_start(hbox, True, True) # Dialog vbox contents must be shown separately for w in self.vbox: w.show_all() self.connect("response", self._response_cb) self.connect("show", self._show_cb)
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