Exemplo n.º 1
0
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)
Exemplo n.º 2
0
 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)
Exemplo n.º 3
0
 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")
Exemplo n.º 4
0
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__)
Exemplo n.º 5
0
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))
Exemplo n.º 6
0
    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))
Exemplo n.º 7
0
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,
    )
Exemplo n.º 8
0
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
Exemplo n.º 9
0
    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]
Exemplo n.º 10
0
    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
Exemplo n.º 11
0
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
Exemplo n.º 12
0
 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
         )
Exemplo n.º 13
0
    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
Exemplo n.º 14
0
    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))
Exemplo n.º 15
0
 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")
Exemplo n.º 16
0
 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]))
Exemplo n.º 17
0
 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]))
Exemplo n.º 18
0
 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"])
Exemplo n.º 19
0
 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"])
Exemplo n.º 20
0
 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)
Exemplo n.º 21
0
 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)
Exemplo n.º 22
0
 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
Exemplo n.º 23
0
 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
Exemplo n.º 24
0
    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()
Exemplo n.º 25
0
 def _copy_color_in(self, col, name=None):
     if col is self._EMPTY_SLOT_ITEM or col is None:
         result = self._EMPTY_SLOT_ITEM
     else:
         if name is None:
             try:
                 name = col.__name
             except AttributeError:
                 pass
         if name is not None:
             name = unicode(name)
         result = RGBColor(color=col)
         result.__name = name
     return result
Exemplo n.º 26
0
 def _copy_color_in(self, col, name=None):
     if col is self._EMPTY_SLOT_ITEM or col is None:
         result = self._EMPTY_SLOT_ITEM
     else:
         if name is None:
             try:
                 name = col.__name
             except AttributeError:
                 pass
         if name is not None:
             name = unicode(name)
         result = RGBColor(color=col)
         result.__name = name
     return result
Exemplo n.º 27
0
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")
Exemplo n.º 28
0
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")
Exemplo n.º 29
0
    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
Exemplo n.º 30
0
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"))
Exemplo n.º 31
0
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"))
Exemplo n.º 32
0
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
Exemplo n.º 33
0
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"))
Exemplo n.º 34
0
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
Exemplo n.º 35
0
    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()
Exemplo n.º 36
0
    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
Exemplo n.º 37
0
    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
Exemplo n.º 38
0
    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
Exemplo n.º 39
0
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()
Exemplo n.º 40
0
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()
Exemplo n.º 41
0
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,
    )
Exemplo n.º 42
0
 def name(self, value):
     self._name = unicode(value)
Exemplo n.º 43
0
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
Exemplo n.º 44
0
 def set_name(self, name):
     """Sets the palette's name."""
     if name is not None:
         name = unicode(name)
     self._name = name
     self.info_changed()
Exemplo n.º 45
0
 def __init__(self, name, locked=True, **kwargs):
     super(_View, self).__init__()
     self._name = unicode(name)
     self._locked = bool(locked)
Exemplo n.º 46
0
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()
Exemplo n.º 47
0
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
Exemplo n.º 48
0
 def get_string_property(self, name):
     value = self.settings.get(name, None)
     if value is None:
         return None
     return unicode(value)
Exemplo n.º 49
0
    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)
Exemplo n.º 50
0
 def set_name(self, name):
     """Sets the palette's name."""
     if name is not None:
         name = unicode(name)
     self._name = name
     self.info_changed()
Exemplo n.º 51
0
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()
Exemplo n.º 52
0
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
Exemplo n.º 53
0
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
Exemplo n.º 54
0
 def get_string_property(self, name):
     value = self.settings.get(name, None)
     if value is None:
         return None
     return unicode(value)
Exemplo n.º 55
0
    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)