def _quote_device_name(device_name): """Converts a device name to something safely storable on the disk Quotes an arbitrary device name for use as the basename of a device-specific brush. >>> result = _quote_device_name(u'Heavy Metal Umlaut D\u00ebvice') >>> result == 'Heavy+Metal+Umlaut+D%C3%ABvice' True >>> type(result) == type(u'') True >>> result = _quote_device_name(u'unsafe/device\\\\name') >>> result == 'unsafe%2Fdevice%5Cname' True >>> type(result) == type(u'') True Hopefully this is OK for Windows, UNIX and Mac OS X names. """ device_name = unicode(device_name) if PY3: quoted = urllib.parse.quote_plus( device_name, safe='', encoding="utf-8", ) else: u8bytes = device_name.encode("utf-8") quoted = urllib.quote_plus(u8bytes, safe='') return unicode(quoted)
def update_title(self, tool_widget_titles): """Update the title from a list of strings""" titles = [unicode(s) for s in tool_widget_titles] workspace = self.stack.workspace if workspace is not None: title_sep = unicode(workspace.floating_window_title_separator) title = title_sep.join(titles) title_suffix = unicode(workspace.floating_window_title_suffix) if title_suffix: title += unicode(title_suffix) logger.debug(u"Renamed floating window title to \"%s\"", title) self.set_title(title)
def message_dialog(self, text, type=Gtk.MessageType.INFO, flags=0, secondary_text=None, long_text=None, title=None, investigate_dir=None, investigate_str=None): """Utility function to show a message/information dialog""" d = Gtk.MessageDialog( parent=self.drawWindow, flags=flags, type=type, buttons=[], ) # Auxiliary actions first... if investigate_dir and os.path.isdir(investigate_dir): if not investigate_str: tmpl = _(u"Open Folder “{folder_basename}”…") investigate_str = tmpl.format( folder_basename=os.path.basename(investigate_dir), ) d.add_button(investigate_str, -1) # ... so that the main actions end up in the bottom-right of the # dialog (reversed for rtl scripts), where the eye ends up # naturally at the end of the flow. d.add_button(_("OK"), Gtk.ResponseType.OK) markup = lib.xml.escape(unicode(text)) d.set_markup(markup) if title is not None: d.set_title(unicode(title)) if secondary_text is not None: secondary_markup = lib.xml.escape(unicode(secondary_text)) d.format_secondary_markup(secondary_markup) if long_text is not None: buf = Gtk.TextBuffer() buf.set_text(unicode(long_text)) tv = Gtk.TextView.new_with_buffer(buf) tv.show() tv.set_editable(False) tv.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) scrolls = Gtk.ScrolledWindow() scrolls.show() scrolls.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) scrolls.add(tv) scrolls.set_size_request(-1, 300) scrolls.set_shadow_type(Gtk.ShadowType.IN) d.get_message_area().pack_start(scrolls, True, True, 0) response = d.run() d.destroy() if response == -1: lib.fileutils.startfile(investigate_dir, "open")
def _tool_widget_get_title(widget): """Returns the title to use for a tool-widget. :param widget: a tool widget :type widget: Gtk.Widget :rtype: unicode """ for attr in ("tool_widget_title", "__gtype_name__"): title = getattr(widget, attr, None) if title is not None: return unicode(title) return unicode(widget.__class__.__name__)
def _device_name_uuid(device_name): """Return UUID5 string for a given device name >>> result = _device_name_uuid(u'Wacom Intuos5 touch S Pen stylus') >>> result == u'e97830e9-f9f9-50a5-8fff-68bead1a7021' True >>> type(result) == type(u'') True """ if not PY3: device_name = unicode(device_name).encode('utf-8') return unicode(uuid.uuid5(_DEVICE_NAME_NAMESPACE, device_name))
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 button_press_displayname(button, mods, shorten=False): """Converts a button number & modifier mask to a localized unicode string. """ button = int(button) mods = int(mods) if button <= 0: return None mods = Gdk.ModifierType(mods) modif_label = Gtk.accelerator_get_label(0, mods) modif_label = unicode(modif_label) separator = "" if modif_label: separator = u"+" # TRANSLATORS: "Button" refers to a mouse button # TRANSLATORS: It is part of a button map label. mouse_button_label = _("Button") if shorten: # TRANSLATORS: abbreviated "Button <number>" for forms like "Alt+Btn1" mouse_button_label = _("Btn") return "{modifiers}{plus}{btn}{button_number}".format( modifiers=modif_label, plus=separator, btn=mouse_button_label, button_number=button, )
def expanduser_unicode(s): """Expands a ~/ on the front of a unicode path, where meaningful. :param s: path to expand, coercable to unicode :returns: The expanded path :rtype: unicode This doesn't do anything on the Windows platform other than coerce its argument to unicode. On other platforms, it converts a "~" component on the front of a relative path to the user's absolute home, like os.expanduser(). Certain workarounds for OS and filesystem encoding issues are implemented here too. """ s = unicode(s) # The sys.getfilesystemencoding() on Win32 (mbcs) is for encode # only, and isn't roundtrippable. Luckily ~ is not meaningful on # Windows, and MyPaint uses a better default for the scrap prefix on # the Windows platform anyway. if sys.platform == "win32": return s # expanduser() doesn't handle non-ascii characters in environment variables # https://gna.org/bugs/index.php?17111 s = s.encode(sys.getfilesystemencoding()) s = os.path.expanduser(s) s = s.decode(sys.getfilesystemencoding()) return s
def fetch_brush_for_device(self, device_name): """Fetches the brush associated with an input device.""" if not device_name: return None if device_name not in self._brush_by_device: self._brush_by_device[device_name] = None names = ( _device_name_uuid(device_name), _quote_device_name(device_name), # for backward compatibility ) for name in names: path = os.path.join(self.user_brushpath, _DEVBRUSH_NAME_PREFIX + name + '.myb') if not os.path.isfile(path): continue try: b = ManagedBrush(self, unicode(_DEVBRUSH_NAME_PREFIX + name), persistent=True) except IOError as e: logger.warn("%r: %r (ignored)", name, e) else: self._brush_by_device[device_name] = b break assert device_name in self._brush_by_device return self._brush_by_device[device_name]
def add_new_view(self, name=None): """Adds a new named view capturing the currently visible layers. :param unicode name: Base name for a new named view, or None. :rtype: _View :returns: the added view. If name=None or name="" is passed, the new view will be named uniquely after NEW_VIEW_IDENT. The None value is reserved for representing the default working view. """ if name is None or name == "": name = NEW_VIEW_IDENT name = unicode(name) # All currently visible layers are tagged as visible in the new view. view = _View(name) for path, layer in self._stack.walk(): if layer.visible: vset = self._get_vset_for_layer(layer) vset.add(view) self._views.add(view) self.view_names_changed() self.activate_view(view) return view
def load_from_openraster_dir(self, oradir, elem, cache_dir, progress, x=0, y=0, **kwargs): """Loads layer flags and data from an OpenRaster-style dir""" if elem.tag != "stack": raise lib.layer.error.LoadingFailed("<stack/> expected") super(LayerStack, self).load_from_openraster_dir( oradir, elem, cache_dir, progress, x=x, y=y, **kwargs ) self.clear() x += int(elem.attrib.get("x", 0)) y += int(elem.attrib.get("y", 0)) # Convert normal+nonisolated to the internal pass-thru mode isolated_flag = unicode(elem.attrib.get("isolation", "auto")) is_pass_through = (self.mode == DEFAULT_MODE and self.opacity == 1.0 and (isolated_flag.lower() == "auto")) if is_pass_through: self.mode = PASS_THROUGH_MODE # Delegate loading of child layers for child_elem in elem.findall("./*"): assert child_elem is not elem self._load_child_layer_from_oradir( oradir, child_elem, cache_dir, progress, x=x, y=y, **kwargs )
def message_dialog(self, text, type=Gtk.MessageType.INFO, flags=0, secondary_text=None, long_text=None, title=None, investigate_dir=None, investigate_str=None): """Utility function to show a message/information dialog""" d = Gtk.MessageDialog( parent=self.drawWindow, flags=flags, type=type, buttons=[], ) # Auxiliary actions first... if investigate_dir and os.path.isdir(investigate_dir): if not investigate_str: tmpl = _(u"Open Folder “{folder_basename}”…") investigate_str = tmpl.format( folder_basename = os.path.basename(investigate_dir), ) d.add_button(investigate_str, -1) # ... so that the main actions end up in the bottom-right of the # dialog (reversed for rtl scripts), where the eye ends up # naturally at the end of the flow. d.add_button(_("OK"), Gtk.ResponseType.OK) markup = lib.xml.escape(unicode(text)) d.set_markup(markup) if title is not None: d.set_title(unicode(title)) if secondary_text is not None: secondary_markup = lib.xml.escape(unicode(secondary_text)) d.format_secondary_markup(secondary_markup) if long_text is not None: buf = Gtk.TextBuffer() buf.set_text(unicode(long_text)) tv = Gtk.TextView.new_with_buffer(buf) tv.show() tv.set_editable(False) tv.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) scrolls = Gtk.ScrolledWindow() scrolls.show() scrolls.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) scrolls.add(tv) scrolls.set_size_request(-1, 300) scrolls.set_shadow_type(Gtk.ShadowType.IN) d.get_message_area().pack_start(scrolls, True, True, 0) response = d.run() d.destroy() if response == -1: lib.fileutils.startfile(investigate_dir, "open")
def set_string_property(self, name, value): assert name in STRING_VALUE_SETTINGS if value is None: self.settings.pop(name, None) else: assert isinstance(value, str) or isinstance(value, unicode) self.settings[name] = unicode(value) for f in self.observers: f(set([name]))
def name(self, name): if name is not None: name = unicode(name) else: name = self.DEFAULT_NAME oldname = self._name self._name = name root = self.root if root is not None: self._name = root.get_unique_name(self) if self._name != oldname: self._properties_changed(["name"])
def _load_common_flags_from_ora_elem(self, elem): attrs = elem.attrib self.name = unicode(attrs.get('name', '')) compop = str(attrs.get('composite-op', '')) self.mode = ORA_MODES_BY_OPNAME.get(compop, lib.modes.default_mode()) self.opacity = helpers.clamp(float(attrs.get('opacity', '1.0')), 0.0, 1.0) visible = attrs.get('visibility', 'visible').lower() self.visible = (visible != "hidden") locked = attrs.get("edit-locked", 'false').lower() self.locked = lib.xml.xsd2bool(locked) selected = attrs.get("selected", 'false').lower() self.initially_selected = lib.xml.xsd2bool(selected)
def _load_common_flags_from_ora_elem(self, elem): attrs = elem.attrib self.name = unicode(attrs.get('name', '')) compop = str(attrs.get('composite-op', '')) self.mode = ORA_MODES_BY_OPNAME.get(compop, DEFAULT_MODE) self.opacity = helpers.clamp(float(attrs.get('opacity', '1.0')), 0.0, 1.0) visible = attrs.get('visibility', 'visible').lower() self.visible = (visible != "hidden") locked = attrs.get("edit-locked", 'false').lower() self.locked = lib.xml.xsd2bool(locked) selected = attrs.get("selected", 'false').lower() self.initially_selected = lib.xml.xsd2bool(selected)
def show_transient_message(self, text, seconds=5): """Display a brief, impermanent status message""" context_id = self._transient_msg_context_id self.statusbar.remove_all(context_id) self.statusbar.push(context_id, unicode(text)) timeout_id = self._transient_msg_remove_timeout_id if timeout_id is not None: GLib.source_remove(timeout_id) timeout_id = GLib.timeout_add_seconds( interval=seconds, function=self._transient_msg_remove_timer_cb, ) self._transient_msg_remove_timeout_id = timeout_id
def load_from_openraster(self, orazip, elem, cache_dir, progress, x=0, y=0, **kwargs): """Load this layer from an open .ora file""" if elem.tag != "stack": raise lib.layer.error.LoadingFailed("<stack/> expected") if not progress: progress = lib.feedback.Progress() progress.items = 1 + len(list(elem.findall("./*"))) # Item 1: supercall super(LayerStack, self).load_from_openraster(orazip, elem, cache_dir, progress.open(), x=x, y=y, **kwargs) self.clear() x += int(elem.attrib.get("x", 0)) y += int(elem.attrib.get("y", 0)) # The only combination which can result in a non-isolated mode # under the OpenRaster and W3C definition. Represented # internally with a special mode to make the UI prettier. isolated_flag = unicode(elem.attrib.get("isolation", "auto")) # TODO: Check if this applies to CombineSpectralWGM as well is_pass_through = (self.mode == lib.mypaintlib.CombineNormal and self.opacity == 1.0 and (isolated_flag.lower() == "auto")) if is_pass_through: self.mode = PASS_THROUGH_MODE # Items 2..n: child elements. # Document order is the same as _layers, bottom layer to top. for child_elem in elem.findall("./*"): assert child_elem is not elem self._load_child_layer_from_orazip(orazip, child_elem, cache_dir, progress.open(), x=x, y=y, **kwargs) progress.close()
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 brushinfo_quote(string): """Quote a string for serialisation of brushes. >>> brushinfo_quote(u'foo') == b'foo' True >>> brushinfo_quote(u'foo/bar blah') == b'foo%2Fbar%20blah' True >>> expected = b'Have%20a%20nice%20day%20%E2%98%BA' >>> brushinfo_quote(u'Have a nice day \u263A') == expected True """ string = unicode(string) u8bytes = string.encode("utf-8") return url_quote(u8bytes, safe='').encode("ascii")
def _init_default_brushkeys_and_history(self): """Assign sensible defaults for brushkeys and history. Operates by filling in the gaps after `_init_unordered_groups()` has had a chance to populate the two lists. """ # Try the default startup group first. default_group = self.groups.get(_DEFAULT_STARTUP_GROUP, None) # Otherwise, use the biggest group to minimise the chance # of repetition. if default_group is None: groups_by_len = sorted( (len(g), n, g) for n, g in self.groups.items()) _len, _name, default_group = groups_by_len[-1] # Populate blank entries. for i in xrange(_NUM_BRUSHKEYS): if self.contexts[i] is None: idx = (i + 9) % 10 # keyboard order c_name = unicode('context%02d') % i c = ManagedBrush(self, name=c_name, persistent=False) group_idx = idx % len(default_group) b = default_group[group_idx] b.clone_into(c, c_name) self.contexts[i] = c for i in xrange(_BRUSH_HISTORY_SIZE): if self.history[i] is None: h_name = unicode('%s%d') % (_BRUSH_HISTORY_NAME_PREFIX, i) h = ManagedBrush(self, name=h_name, persistent=False) group_i = i % len(default_group) b = default_group[group_i] b.clone_into(h, h_name) self.history[i] = h
def brushinfo_unquote(quoted): """Unquote a serialised string value from a brush field. >>> brushinfo_unquote(b"foo") == u'foo' True >>> brushinfo_unquote(b"foo%2fbar%20blah") == u'foo/bar blah' True >>> expected = u'Have a nice day \u263A' >>> brushinfo_unquote(b'Have%20a%20nice%20day%20%E2%98%BA') == expected True """ if not isinstance(quoted, bytes): raise ValueError("Cann") u8bytes = url_unquote(quoted) return unicode(u8bytes.decode("utf-8"))
def make_unique_name(name, existing, start=1, always_number=None): """Ensures that a name is unique. :param unicode name: Name to be made unique. :param existing: An existing list or set of names. :type existing: anything supporting ``in`` :param int start: starting number for numbering. :param unicode always_number: always number if name is this value. :returns: A unique name. :rtype: unicode >>> existing = set([u"abc 1", u"abc 2", u"abc"]) >>> expected = u'abc 3' >>> make_unique_name(u"abc", existing) == expected True >>> expected not in existing True >>> make_unique_name(u"abc 1", existing) == expected # still True Sometimes you may want a serial number every time if the given name is some specific value, normally a default. This allows your first item to be, for example, "Widget 1", not "Widget". >>> x1 = u'xyz 1' >>> make_unique_name(u"xyz", {}, start=1, always_number=u"xyz") == x1 True >>> x2 = u'xyz 2' >>> make_unique_name(u"xyz", {}, start=2, always_number=u"xyz") == x2 True """ name = unicode(name) match = UNIQUE_NAME_REGEX.match(name) if match: base = match.group("name") num = int(match.group("number")) else: base = name num = max(0, int(start)) force_numbering = (name == always_number) while (name in existing) or force_numbering: name = UNIQUE_NAME_TEMPLATE.format(name=base, number=num) num += 1 force_numbering = False return name
def brushinfo_unquote(quoted): """Unquote a serialised string value from a brush field. >>> f = str if PY3 else bytes >>> brushinfo_unquote(f("foo")) == u'foo' True >>> brushinfo_unquote(f("foo%2fbar%20blah")) == u'foo/bar blah' True >>> expected = u'Have a nice day \u263A' >>> brushinfo_unquote(f('Have%20a%20nice%20day%20%E2%98%BA')) == expected True """ if PY3: return unquote(quoted) else: return unicode(unquote(quoted).decode("utf-8"))
def load_from_openraster(self, orazip, elem, cache_dir, progress, x=0, y=0, **kwargs): """Load this layer from an open .ora file""" if elem.tag != "stack": raise lib.layer.error.LoadingFailed("<stack/> expected") if not progress: progress = lib.feedback.Progress() progress.items = 1 + len(list(elem.findall("./*"))) # Item 1: supercall super(LayerStack, self).load_from_openraster( orazip, elem, cache_dir, progress.open(), x=x, y=y, **kwargs ) self.clear() x += int(elem.attrib.get("x", 0)) y += int(elem.attrib.get("y", 0)) # The only combination which can result in a non-isolated mode # under the OpenRaster and W3C definition. Represented # internally with a special mode to make the UI prettier. isolated_flag = unicode(elem.attrib.get("isolation", "auto")) is_pass_through = (self.mode == DEFAULT_MODE and self.opacity == 1.0 and (isolated_flag.lower() == "auto")) if is_pass_through: self.mode = PASS_THROUGH_MODE # Items 2..n: child elements. # Document order is the same as _layers, bottom layer to top. for child_elem in elem.findall("./*"): assert child_elem is not elem self._load_child_layer_from_orazip( orazip, child_elem, cache_dir, progress.open(), x=x, y=y, **kwargs ) progress.close()
def store_brush_for_device(self, device_name, managed_brush): """Records a brush as associated with an input device. :param device_name: name of an input device :type device_name: str :param managed_brush: the brush to associate :type managed_brush: MnagedBrush Normally the brush will be cloned first, since it will be given a new name. However, if the brush has a 'name' attribute of None, it will *not* be cloned and just modified in place and stored. """ brush = managed_brush if brush.name is not None: brush = brush.clone() brush.name = unicode(_DEVBRUSH_NAME_PREFIX + _device_name_uuid(device_name)) self._brush_by_device[device_name] = brush
def has_interesting_name(self): """True if the layer looks as if it has a user-assigned name Interesting means non-blank, and not the default name or a numbered version of it. This is used when merging layers: Merge Down is used on temporary layers a lot, and those probably have boring names. """ name = self._name if name is None or name.strip() == '': return False if name == self.DEFAULT_NAME: return False match = lib.naming.UNIQUE_NAME_REGEX.match(name) if match is not None: base = unicode(match.group("name")) if base == self.DEFAULT_NAME: return False return True
def casefold(s): """Converts a unicode string into a case-insensitively comparable form. Forward-compat marker for things that should be .casefold() in Python 3, but which need to be .lower() in Python2. :param str s: The string to convert. :rtype: str :returns: The converted string. >>> casefold("Xyz") == u'xyz' True """ if sys.version_info <= (3, 0, 0): s = unicode(s) return s.lower() else: s = str(s) return s.casefold()
def button_press_displayname(button, mods, shorten = False): """Converts a button number & modifier mask to a localized unicode string. """ button = int(button) mods = int(mods) if button <= 0: return None mods = Gdk.ModifierType(mods) modif_label = Gtk.accelerator_get_label(0, mods) modif_label = unicode(modif_label) separator = "" if modif_label: separator = u"+" mouse_button_label = _("Button") if shorten: # TRANSLATORS: abbreviated "Button <number>" for forms like "Alt+Btn1" mouse_button_label = _("Btn") return "{modifiers}{plus}{btn}{button_number}".format( modifiers=modif_label, plus=separator, btn=mouse_button_label, button_number=button, )
def name(self, value): self._name = unicode(value)
def safename(s, fragment=False): """Returns a safe filename based on its unicode-string argument. Returns a safe filename or filename fragment based on an arbitrary string. The name generated in this way should be good for all OSes. Slashes, colons and other special characters will be stripped. The string will have its leading and trailing whitespace trimmed. :param unicode s: The string to convert. :param bool fragment: Name will never be used as a complete file name. Normally, extra checks are applied that assume the returned name will be used for a complete file basename, including extension. If the "fragment" parameter is True, these additional safety conversions will be ignored, and its is the caller's responsibility to make the name safe. Appending other safe fragments or an extension will make the combined name safe again. >>> safename("test 1/4") == u'test 1_4' True >>> (safename("test 2/4 with a \022 and a trailing space ") ... == u'test 2_4 with a _ and a trailing space') True >>> safename("lpt3") == u'_lpt3' True >>> safename("lpt3", fragment=True) == u'lpt3' True Note that fragments can be blank. Whole names cannot. A completely blank name is treated like the reserved words. >>> safename(" ", fragment=True) == u'' True >>> safename(" ", fragment=False) == u'_' True """ # A little cleanup first s = unicode(s) s = unicodedata.normalize("NFKC", s) s = s.strip() # Strip characters that break interop. forbidden = [ord(c) for c in u"\\|/:*?\"<>"] forbidden += list(range(31)) # control chars table = {n: ord("_") for n in forbidden} s = s.translate(table) if not fragment: # Certain whole-filenames are reserved on Windows. reserved = lib.helpers.casefold(u""" NUL CON PRN AUX COM1 COM2 COM3 COM4 COM5 COM6 COM7 COM8 COM9 LPT1 LPT2 LPT3 LPT4 LPT5 LPT6 LPT7 LPT8 LPT9 """).strip().split() # A blank name is invalid for all systems. reserved += [u""] if lib.helpers.casefold(s) in reserved: s = "_" + s # Windows file names cannot end with a dot or a space. # Spaces are already handled. if s.endswith("."): s = s + "_" return s
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 __init__(self, name, locked=True, **kwargs): super(_View, self).__init__() self._name = unicode(name) self._locked = bool(locked)
def _test(): """Test the custom model in an ad-hoc GUI window""" from lib.document import Document from lib.layer import PaintingLayer, LayerStack doc_model = Document() root = doc_model.layer_stack root.clear() layer_info = [ ((0,), LayerStack(name="Layer 0")), ((0, 0), PaintingLayer(name="Layer 0:0")), ((0, 1), PaintingLayer(name="Layer 0:1")), ((0, 2), LayerStack(name="Layer 0:2")), ((0, 2, 0), PaintingLayer(name="Layer 0:2:0")), ((0, 2, 1), PaintingLayer(name="Layer 0:2:1")), ((0, 3), PaintingLayer(name="Layer 0:3")), ((1,), LayerStack(name="Layer 1")), ((1, 0), PaintingLayer(name="Layer 1:0")), ((1, 1), PaintingLayer(name="Layer 1:1")), ((1, 2), LayerStack(name="Layer 1:2")), ((1, 2, 0), PaintingLayer(name="Layer 1:2:0")), ((1, 2, 1), PaintingLayer(name="Layer 1:2:1")), ((1, 2, 2), PaintingLayer(name="Layer 1:2:2")), ((1, 2, 3), PaintingLayer(name="Layer 1:2:3")), ((1, 3), PaintingLayer(name="Layer 1:3")), ((1, 4), PaintingLayer(name="Layer 1:4")), ((1, 5), PaintingLayer(name="Layer 1:5")), ((1, 6), PaintingLayer(name="Layer 1:6")), ((2,), PaintingLayer(name="Layer 2")), ((3,), PaintingLayer(name="Layer 3")), ((4,), PaintingLayer(name="Layer 4")), ((5,), PaintingLayer(name="Layer 5")), ((6,), LayerStack(name="Layer 6")), ((6, 0), PaintingLayer(name="Layer 6:0")), ((6, 1), PaintingLayer(name="Layer 6:1")), ((6, 2), PaintingLayer(name="Layer 6:2")), ((6, 3), PaintingLayer(name="Layer 6:3")), ((6, 4), PaintingLayer(name="Layer 6:4")), ((6, 5), PaintingLayer(name="Layer 6:5")), ((7,), PaintingLayer(name="Layer 7")), ] for path, layer in layer_info: root.deepinsert(path, layer) root.set_current_path([4]) icon_theme = Gtk.IconTheme.get_default() icon_theme.append_search_path("./desktop/icons") view = RootStackTreeView(doc_model) view_scroll = Gtk.ScrolledWindow() view_scroll.set_shadow_type(Gtk.ShadowType.ETCHED_IN) scroll_pol = Gtk.PolicyType.AUTOMATIC view_scroll.set_policy(scroll_pol, scroll_pol) view_scroll.add(view) view_scroll.set_size_request(-1, 100) win = Gtk.Window() win.set_title(unicode(__package__)) win.connect("destroy", Gtk.main_quit) win.add(view_scroll) win.set_default_size(300, 500) win.show_all() Gtk.main()
def freedesktop_thumbnail(filename, pixbuf=None, force=False): """Fetch or (re-)generate the thumbnail in $XDG_CACHE_HOME/thumbnails. If there is no thumbnail for the specified filename, a new thumbnail will be generated and stored according to the FDO spec. A thumbnail will also get regenerated if the file modification times of thumbnail and original image do not match. :param GdkPixbuf.Pixbuf pixbuf: Thumbnail to save, optional. :param bool force: Force rengeneration (skip mtime checks). :returns: the large (256x256) thumbnail, or None. :rtype: GdkPixbuf.Pixbuf When pixbuf is given, it will be scaled and used as thumbnail instead of reading the file itself. In this case the file is still accessed to get its mtime, so this method must not be called if the file is still open. >>> icon = "desktop/icons/hicolor/512x512/apps/mypaint.png" >>> p1 = freedesktop_thumbnail(icon, force=True) >>> isinstance(p1, GdkPixbuf.Pixbuf) True >>> p2 = freedesktop_thumbnail(icon) >>> isinstance(p2, GdkPixbuf.Pixbuf) True >>> p2.to_string() == p1.to_string() True >>> p2.get_width() == p2.get_height() == 256 True """ uri = lib.glib.filename_to_uri(os.path.abspath(filename)) logger.debug("thumb: uri=%r", uri) if not isinstance(uri, bytes): uri = uri.encode("utf-8") file_hash = hashlib.md5(uri).hexdigest() cache_dir = lib.glib.get_user_cache_dir() base_directory = os.path.join(cache_dir, u'thumbnails') directory = os.path.join(base_directory, u'normal') tb_filename_normal = os.path.join(directory, file_hash) + u'.png' if not os.path.exists(directory): os.makedirs(directory, 0o700) directory = os.path.join(base_directory, u'large') tb_filename_large = os.path.join(directory, file_hash) + u'.png' if not os.path.exists(directory): os.makedirs(directory, 0o700) file_mtime = str(int(os.stat(filename).st_mtime)) save_thumbnail = True if filename.lower().endswith(u'.ora'): # don't bother with normal (128x128) thumbnails when we can # get a large one (256x256) from the file in an instant acceptable_tb_filenames = [tb_filename_large] else: # prefer the large thumbnail, but accept the normal one if # available, for the sake of performance acceptable_tb_filenames = [tb_filename_large, tb_filename_normal] # Use the largest stored thumbnail that isn't obsolete, # Unless one was passed in, # or regeneration is being forced. for fn in acceptable_tb_filenames: if pixbuf or force or (not os.path.isfile(fn)): continue try: pixbuf = lib.pixbuf.load_from_file(fn) except Exception as e: logger.warning( u"thumb: cache file %r looks corrupt (%r). " u"It will be regenerated.", fn, unicode(e), ) pixbuf = None else: assert pixbuf is not None if file_mtime == pixbuf.get_option("tEXt::Thumb::MTime"): save_thumbnail = False break else: pixbuf = None # Try to load a pixbuf from the file, if we still need one. if not pixbuf: pixbuf = get_pixbuf(filename) # Update the fd.o thumbs cache. if pixbuf: pixbuf = scale_proportionally(pixbuf, 256, 256) if save_thumbnail: png_opts = {"tEXt::Thumb::MTime": file_mtime, "tEXt::Thumb::URI": uri} logger.debug("thumb: png_opts=%r", png_opts) lib.pixbuf.save( pixbuf, tb_filename_large, type='png', **png_opts ) logger.debug("thumb: saved large (256x256) thumbnail to %r", tb_filename_large) # save normal size too, in case some implementations don't # bother with large thumbnails pixbuf_normal = scale_proportionally(pixbuf, 128, 128) lib.pixbuf.save( pixbuf_normal, tb_filename_normal, type='png', **png_opts ) logger.debug("thumb: saved normal (128x128) thumbnail to %r", tb_filename_normal) # Return the 256x256 scaled version. return pixbuf
def get_string_property(self, name): value = self.settings.get(name, None) if value is None: return None return unicode(value)
def run(self, startup=False): """Show and run the dialog, and possibly resume an autosave. :param bool startup: indicates that MyPaint is starting up. """ # Don't run at startup if asked not to. if startup: if not self.check_at_startup: return # Only run if there are autosaves which can be recovered. autosaves = self._reload_liststore() if not autosaves: if not startup: cache_root = lib.document.get_app_cache_root() self._app.message_dialog( _(u"No backups were found in the cache."), title=_(u"No Available Backups"), investigate_dir=cache_root, investigate_str=_(u"Open the Cache Folder…"), message_type=Gtk.MessageType.ERROR, ) return doc = self._app.doc # Get the user to pick an autosave to recover autosave = None error = None try: self._dialog.set_transient_for(self._app.drawWindow) self._dialog.show_all() result = self._dialog.run() if result == self._RESPONSE_CONTINUE: autosave = self._get_selected_autosave() if autosave is not None: logger.info("Recovering %r...", autosave) try: doc.model.resume_from_autosave(autosave.path) except lib.errors.FileHandlingError as e: error = e else: doc.reset_view(True, True, True) finally: self._dialog.hide() # If an error was detected, tell the user about it. # They'll be given a new working doc & cache automatically. if error: self._app.message_dialog( unicode(error), title=_(u"Backup Recovery Failed"), investigate_dir=error.investigate_dir, investigate_str=_(u"Open the Backup’s Folder…"), message_type=Gtk.MessageType.ERROR, ) # If it loaded OK, get the user to save the recovered file ASAP. elif autosave: fh = self._app.filehandler fh.set_filename(None) lastmod = autosave.last_modified strftime_tmpl = "%Y-%m-%d %H%M%S" sugg_name_tmpl = _(u"Recovered file from {iso_datetime}.ora") sugg_name = sugg_name_tmpl.format( iso_datetime=lastmod.strftime(strftime_tmpl), ) fh.save_as_dialog(fh.save_file, suggested_filename=sugg_name)
def _test(): """Test the custom model in an ad-hoc GUI window""" from lib.layer import PaintingLayer, LayerStack doc_model = Document() root = doc_model.layer_stack root.clear() layer_info = [ ((0, ), LayerStack(name="Layer 0")), ((0, 0), PaintingLayer(name="Layer 0:0")), ((0, 1), PaintingLayer(name="Layer 0:1")), ((0, 2), LayerStack(name="Layer 0:2")), ((0, 2, 0), PaintingLayer(name="Layer 0:2:0")), ((0, 2, 1), PaintingLayer(name="Layer 0:2:1")), ((0, 3), PaintingLayer(name="Layer 0:3")), ((1, ), LayerStack(name="Layer 1")), ((1, 0), PaintingLayer(name="Layer 1:0")), ((1, 1), PaintingLayer(name="Layer 1:1")), ((1, 2), LayerStack(name="Layer 1:2")), ((1, 2, 0), PaintingLayer(name="Layer 1:2:0")), ((1, 2, 1), PaintingLayer(name="Layer 1:2:1")), ((1, 2, 2), PaintingLayer(name="Layer 1:2:2")), ((1, 2, 3), PaintingLayer(name="Layer 1:2:3")), ((1, 3), PaintingLayer(name="Layer 1:3")), ((1, 4), PaintingLayer(name="Layer 1:4")), ((1, 5), PaintingLayer(name="Layer 1:5")), ((1, 6), PaintingLayer(name="Layer 1:6")), ((2, ), PaintingLayer(name="Layer 2")), ((3, ), PaintingLayer(name="Layer 3")), ((4, ), PaintingLayer(name="Layer 4")), ((5, ), PaintingLayer(name="Layer 5")), ((6, ), LayerStack(name="Layer 6")), ((6, 0), PaintingLayer(name="Layer 6:0")), ((6, 1), PaintingLayer(name="Layer 6:1")), ((6, 2), PaintingLayer(name="Layer 6:2")), ((6, 3), PaintingLayer(name="Layer 6:3")), ((6, 4), PaintingLayer(name="Layer 6:4")), ((6, 5), PaintingLayer(name="Layer 6:5")), ((7, ), PaintingLayer(name="Layer 7")), ] for path, layer in layer_info: root.deepinsert(path, layer) root.set_current_path([4]) icon_theme = Gtk.IconTheme.get_default() icon_theme.append_search_path("./desktop/icons") view = RootStackTreeView(doc_model) view_scroll = Gtk.ScrolledWindow() view_scroll.set_shadow_type(Gtk.ShadowType.ETCHED_IN) scroll_pol = Gtk.PolicyType.AUTOMATIC view_scroll.set_policy(scroll_pol, scroll_pol) view_scroll.add(view) view_scroll.set_size_request(-1, 100) win = Gtk.Window() win.set_title(unicode(__package__)) win.connect("destroy", Gtk.main_quit) win.add(view_scroll) win.set_default_size(300, 500) win.show_all() Gtk.main()
def load_background(filename, bloatmax=BLOAT_MAX_SIZE): """Load a pixbuf, testing it for suitability as a background :param str filename: Full path to the filename to load. :param int bloatmax: Repeat up to this size :rtype: tuple The returned tuple is a pair ``(PIXBUF, ERRORS)``, where ``ERRORS`` is a list of localized strings describing the errors encountered, and ``PIXBUF`` contains the loaded background pixbuf. If there were errors, ``PIXBUF`` is None. The MyPaint rendering engine can only manage background layers which fit into its tile structure. Formerly, only background images with dimensions which were exact multiples of the tile size were permitted. We have a couple of workarounds now: * "Bloating" the background by repetition (pixel-perfect) * Scaling the image down to fit (distorts the image) """ filename_display = _filename_to_display(filename) load_errors = [] try: pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) except Exception as ex: logger.error("Failed to load background %r: %s", filename, ex) msg = unicode(_( 'Gdk-Pixbuf couldn\'t load "{filename}", and reported "{error}"' )) load_errors.append(msg.format( filename=filename_display, error=repr(ex), )) return (None, load_errors) # Validity check w, h = pixbuf.get_width(), pixbuf.get_height() if w == 0 or h == 0: msg = unicode(_("{filename} has zero size (w={w}, h={h})")) load_errors.append(msg.format( filename=filename_display, w=w, h=h, )) return (None, load_errors) # Flatten if pixbuf.get_has_alpha(): logger.warning( "%r has an alpha channel, which should be removed manually", filename, ) new_pixbuf = new_blank_pixbuf((0, 0, 0), w, h) pixbuf.composite( dest=new_pixbuf, dest_x=0, dest_y=0, dest_width=w, dest_height=h, offset_x=0, offset_y=0, scale_x=1.0, scale_y=1.0, interp_type=GdkPixbuf.InterpType.NEAREST, overall_alpha=255, ) pixbuf = new_pixbuf logger.debug( "Flattened %s by compositing it onto a black backdrop", filename, ) # Attempt to fit the image into our grid. exact_fit = ((w % N, h % N) == (0, 0)) if not exact_fit: logger.warning( "%r (%dx%d) does not fit the %dx%d tile grid exactly", filename, w, h, N, N, ) repeats_x = _best_nrepeats_for_scaling(w, bloatmax) repeats_y = _best_nrepeats_for_scaling(h, bloatmax) if repeats_x > 1 or repeats_y > 1: logger.info( "Tiling %r to %dx%d (was: %dx%d, repeats: %d vert, %d horiz)", filename, w * repeats_x, h * repeats_y, w, h, repeats_x, repeats_y, ) pixbuf = _tile_pixbuf(pixbuf, repeats_x, repeats_y) w, h = pixbuf.get_width(), pixbuf.get_height() if (w % N != 0) or (h % N != 0): orig_w, orig_h = w, h w = max(1, w // N) * N h = max(1, h // N) * N logger.info( "Scaling %r to %dx%d (was: %dx%d)", filename, w, h, orig_w, orig_h, ) pixbuf = pixbuf.scale_simple( dest_width=w, dest_height=h, interp_type=GdkPixbuf.InterpType.BILINEAR, ) assert (w % N == 0) and (h % N == 0) if load_errors: pixbuf = None return pixbuf, load_errors
def load_background(filename, bloatmax=BLOAT_MAX_SIZE): """Load a pixbuf, testing it for suitability as a background :param str filename: Full path to the filename to load. :param int bloatmax: Repeat up to this size :rtype: tuple The returned tuple is a pair ``(PIXBUF, ERRORS)``, where ``ERRORS`` is a list of localized strings describing the errors encountered, and ``PIXBUF`` contains the loaded background pixbuf. If there were errors, ``PIXBUF`` is None. The MyPaint rendering engine can only manage background layers which fit into its tile structure. Formerly, only background images with dimensions which were exact multiples of the tile size were permitted. We have a couple of workarounds now: * "Bloating" the background by repetition (pixel-perfect) * Scaling the image down to fit (distorts the image) """ filename_display = _filename_to_display(filename) load_errors = [] try: pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) except Exception as ex: logger.error("Failed to load background %r: %s", filename, ex) msg = unicode( _('Gdk-Pixbuf couldn\'t load "{filename}", and reported "{error}"') ) load_errors.append( msg.format( filename=filename_display, error=repr(ex), )) return (None, load_errors) # Validity check w, h = pixbuf.get_width(), pixbuf.get_height() if w == 0 or h == 0: msg = unicode(_("{filename} has zero size (w={w}, h={h})")) load_errors.append(msg.format( filename=filename_display, w=w, h=h, )) return (None, load_errors) # Flatten if pixbuf.get_has_alpha(): logger.warning( "%r has an alpha channel, which should be removed manually", filename, ) new_pixbuf = new_blank_pixbuf((0, 0, 0), w, h) pixbuf.composite( dest=new_pixbuf, dest_x=0, dest_y=0, dest_width=w, dest_height=h, offset_x=0, offset_y=0, scale_x=1.0, scale_y=1.0, interp_type=GdkPixbuf.InterpType.NEAREST, overall_alpha=255, ) pixbuf = new_pixbuf logger.debug( "Flattened %s by compositing it onto a black backdrop", filename, ) # Attempt to fit the image into our grid. exact_fit = ((w % N, h % N) == (0, 0)) if not exact_fit: logger.warning( "%r (%dx%d) does not fit the %dx%d tile grid exactly", filename, w, h, N, N, ) repeats_x = _best_nrepeats_for_scaling(w, bloatmax) repeats_y = _best_nrepeats_for_scaling(h, bloatmax) if repeats_x > 1 or repeats_y > 1: logger.info( "Tiling %r to %dx%d (was: %dx%d, repeats: %d vert, %d horiz)", filename, w * repeats_x, h * repeats_y, w, h, repeats_x, repeats_y, ) pixbuf = _tile_pixbuf(pixbuf, repeats_x, repeats_y) w, h = pixbuf.get_width(), pixbuf.get_height() if (w % N != 0) or (h % N != 0): orig_w, orig_h = w, h w = max(1, w // N) * N h = max(1, h // N) * N logger.info( "Scaling %r to %dx%d (was: %dx%d)", filename, w, h, orig_w, orig_h, ) pixbuf = pixbuf.scale_simple( dest_width=w, dest_height=h, interp_type=GdkPixbuf.InterpType.BILINEAR, ) assert (w % N == 0) and (h % N == 0) if load_errors: pixbuf = None return pixbuf, load_errors
def run(self, startup=False): """Show and run the dialog, and possibly resume an autosave. :param bool startup: indicates that MyPaint is starting up. """ # Don't run at startup if asked not to. if startup: if not self.check_at_startup: return # Only run if there are autosaves which can be recovered. autosaves = self._reload_liststore() if not autosaves: if not startup: cache_root = lib.document.get_app_cache_root() self._app.message_dialog( _(u"No backups were found in the cache."), title = _(u"No Available Backups"), type = Gtk.MessageType.ERROR, investigate_dir = cache_root, investigate_str = _(u"Open the Cache Folder…") ) return doc = self._app.doc # Get the user to pick an autosave to recover autosave = None error = None try: self._dialog.set_transient_for(self._app.drawWindow) self._dialog.show_all() result = self._dialog.run() if result == self._RESPONSE_CONTINUE: autosave = self._get_selected_autosave() if autosave is not None: logger.info("Recovering %r...", autosave) try: doc.model.resume_from_autosave(autosave.path) except lib.errors.FileHandlingError as e: error = e else: doc.reset_view(True, True, True) finally: self._dialog.set_transient_for(None) self._dialog.hide() # If an error was detected, tell the user about it. # They'll be given a new working doc & cache automatically. if error: self._app.message_dialog( unicode(error), title = _(u"Backup Recovery Failed"), type = Gtk.MessageType.ERROR, investigate_dir = error.investigate_dir, investigate_str = _(u"Open the Backup’s Folder…") ) # If it loaded OK, get the user to save the recovered file ASAP. elif autosave: fh = self._app.filehandler fh.set_filename(None) lastmod = autosave.last_modified strftime_tmpl = "%Y-%m-%d %H%M%S" sugg_name_tmpl = _(u"Recovered file from {iso_datetime}.ora") sugg_name = sugg_name_tmpl.format( iso_datetime = lastmod.strftime(strftime_tmpl), ) fh.save_as_dialog(fh.save_file, suggested_filename=sugg_name)