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
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 _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)
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)
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)
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)
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)
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()
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
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])
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])
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()
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()
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()
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 __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)
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 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
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
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()
def get_color_for_bar_amount(self, amt): col = RGBColor(color=self.get_managed_color()) col.b = amt return col
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
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
# 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]
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
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()
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()
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
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()
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()
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")
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 _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)
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)