1 # This file is part of MyPaint.
2 # Copyright (C) 2009 by Martin Renold <martinxyz@gmx.ch>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
10 This module does file management for brushes and brush groups.
15 from gtk import gdk # only for gdk.pixbuf
16 from gettext import gettext as _
18 from os.path import basename
21 from lib.brush import BrushInfo
22 from warnings import warn
27 DEFAULT_STARTUP_GROUP = 'set#2' # Suggestion only
28 DEFAULT_BRUSH = 'deevad/artpen' # TODO: phase out and use heuristics?
29 DEFAULT_ERASER = 'deevad/stick' # TODO: ---------------"--------------
30 FOUND_BRUSHES_GROUP = 'lost&found'
31 DELETED_BRUSH_GROUP = 'deleted'
32 FAVORITES_BRUSH_GROUP = 'favorites'
33 DEVBRUSH_NAME_PREFIX = "devbrush_"
34 BRUSH_HISTORY_NAME_PREFIX = "history_"
35 BRUSH_HISTORY_SIZE = 5
38 def devbrush_quote(device_name, prefix=DEVBRUSH_NAME_PREFIX):
40 Quotes an arbitrary device name for use as the basename of a
41 device-specific brush.
43 >>> devbrush_quote(u'Heavy Metal Umlaut D\u00ebvice')
44 'devbrush_Heavy+Metal+Umlaut+D%C3%ABvice'
45 >>> devbrush_quote(u'unsafe/device\u005Cname') # U+005C == backslash
46 'devbrush_unsafe%2Fdevice%5Cname'
48 Hopefully this is OK for Windows, UNIX and Mac OS X names.
50 device_name = unicode(device_name)
51 u8bytes = device_name.encode("utf-8")
52 quoted = urllib.quote_plus(u8bytes, safe='')
53 return prefix + quoted
55 def devbrush_unquote(devbrush_name, prefix=DEVBRUSH_NAME_PREFIX):
57 Unquotes the basename of a devbrush for use when matching device names.
59 >>> expected = "My sister was bitten by a m\u00f8\u00f8se..."
60 >>> quoted = 'devbrush_My+sister+was+bitten+by+a+m%5Cu00f8%5Cu00f8se...'
61 >>> devbrush_unquote(quoted) == expected
64 devbrush_name = str(devbrush_name)
65 assert devbrush_name.startswith(prefix)
66 quoted = devbrush_name[len(prefix):]
67 u8bytes = urllib.unquote_plus(quoted)
68 return unicode(u8bytes.decode("utf-8"))
70 def translate_group_name(name):
71 d = {FOUND_BRUSHES_GROUP: _('Lost & Found'),
72 DELETED_BRUSH_GROUP: _('Deleted'),
73 FAVORITES_BRUSH_GROUP: _('Favorites'),
75 'classic': _('Classic'),
81 'experimental': _('Experimental'),
83 return d.get(name, name)
85 def parse_order_conf(file_content):
86 # parse order.conf file returing a dict like this:
87 # {'group1' : ['brush1', 'brush2'], 'group2' : ['brush3']}
89 curr_group = FOUND_BRUSHES_GROUP
90 lines = file_content.replace('\r', '\n').split('\n')
92 name = line.strip().decode('utf-8')
93 if name.startswith('#') or not name:
95 if name.startswith('Group: '):
97 if curr_group not in groups:
98 groups[curr_group] = []
100 groups.setdefault(curr_group, [])
101 if name in groups[curr_group]:
102 print name + ': Warning: brush appears twice in the same group, ignored'
104 groups[curr_group].append(name)
108 def __init__(self, stock_brushpath, user_brushpath, app):
109 self.stock_brushpath = stock_brushpath
110 self.user_brushpath = user_brushpath
113 self.selected_brush = None
116 self.active_groups = []
117 self.loaded_groups = []
118 self.brush_by_device = {} # should be save/loaded too?
119 self.selected_context = None
121 self.selected_brush_observers = []
122 self.groups_observers = [] # for both self.groups and self.active_groups
123 self.brushes_observers = [] # for all brushlists inside groups
125 if not os.path.isdir(self.user_brushpath):
126 os.mkdir(self.user_brushpath)
129 # Retreive which groups were last open, or default to a nice/sane set.
130 last_active_groups = self.app.preferences['brushmanager.selected_groups']
131 if not last_active_groups:
132 if DEFAULT_STARTUP_GROUP in self.groups:
133 last_active_groups = [DEFAULT_STARTUP_GROUP]
135 group_names = self.groups.keys()
137 last_active_groups = [group_names[0]]
139 last_active_groups = []
140 for group in reversed(last_active_groups):
141 if group in self.groups:
142 brushes = self.get_group_brushes(group, make_active=True)
144 self.brushes_observers.append(self.brushes_modified_cb)
146 self.app.doc.input_stroke_ended_observers.append(self.input_stroke_ended_cb)
148 def select_initial_brush(self):
150 # If we recorded which devbrush was last in use, restore it and assume
151 # that most of the time the user will continue to work with the same
152 # brush and its settings.
153 last_used_devbrush = self.app.preferences.get('devbrush.last_used', None)
154 initial_brush = self.brush_by_device.get(last_used_devbrush, None)
155 # Otherwise, initialise from the old selected_brush setting
156 if initial_brush is None:
157 last_active_name = self.app.preferences['brushmanager.selected_brush']
158 if last_active_name is not None:
159 initial_brush = self.get_brush_by_name(last_active_name)
161 if initial_brush is None:
162 initial_brush = self.get_default_brush()
163 self.select_brush(initial_brush)
165 def get_matching_brush(self, name=None, keywords=None,
166 favored_group=DEFAULT_STARTUP_GROUP,
167 fallback_eraser=0.0):
168 """Gets a brush robustly by name, by partial name, or a default.
170 If a brush named `name` exists, use that. Otherwise search though all
171 groups, `favored_group` first, for brushes with any of `keywords`
172 in their name. If that fails, construct a new default brush and use
173 a given value for its 'eraser' property.
176 brush = self.get_brush_by_name(name)
177 if brush is not None:
179 if keywords is not None:
180 group_names = self.groups.keys()
182 if favored_group in self.groups:
183 group_names.remove(favored_group)
184 group_names.insert(0, favored_group)
185 for group_name in group_names:
186 for brush in self.groups[group_name]:
187 for keyword in keywords:
188 if keyword in brush.name:
191 name = 'fallback-default'
192 if fallback_eraser != 0.0:
194 brush = ManagedBrush(self, name)
195 brush.brushinfo.set_base_value("eraser", fallback_eraser)
199 def get_default_brush(self):
200 """Returns a suitable default drawing brush."""
201 return self.get_matching_brush(name=DEFAULT_BRUSH,
202 keywords=["pencil", "charcoal", "sketch"])
205 def get_default_eraser(self):
206 """Returns a suitable default eraser brush."""
207 return self.get_matching_brush(name=DEFAULT_ERASER,
208 keywords=["eraser", "kneaded", "smudge"],
213 def load_groups(self):
214 self.contexts = [None for i in xrange(NUM_BRUSHKEYS)]
215 self.history = [None for i in xrange(BRUSH_HISTORY_SIZE)]
219 if name not in brush_by_name:
220 b = ManagedBrush(self, name, persistent=True)
221 brush_by_name[name] = b
222 return brush_by_name[name]
224 def read_groups(filename):
226 if os.path.exists(filename):
227 groups = parse_order_conf(open(filename).read())
228 # replace brush names with ManagedBrush instances
229 for group, names in groups.items():
235 print e, '(removed from group)'
238 groups[group] = brushes
241 # tree-way-merge of brush groups (for upgrading)
242 base = read_groups(os.path.join(self.user_brushpath, 'order_default.conf'))
243 our = read_groups(os.path.join(self.user_brushpath, 'order.conf'))
244 their = read_groups(os.path.join(self.stock_brushpath, 'order.conf'))
247 # order.conf missing, restore stock order even if order_default.conf exists
253 print 'Merging upstream brush changes into your collection.'
254 groups = set(base).union(our).union(their)
256 # treat the non-existing groups as if empty
257 base_brushes = base.setdefault(group, [])
258 our_brushes = our.setdefault(group, [])
259 their_brushes = their.setdefault(group, [])
262 for b in their_brushes:
264 insert_index = our_brushes.index(b) + 1
266 if b not in base_brushes:
267 our_brushes.insert(insert_index, b)
269 # remove deleted brushes
270 for b in base_brushes:
271 if b not in their_brushes and b in our_brushes:
272 our_brushes.remove(b)
273 # remove empty groups (except for the favorites)
274 if not our_brushes and group != FAVORITES_BRUSH_GROUP:
278 self.save_brushorder()
279 data = open(os.path.join(self.stock_brushpath, 'order.conf')).read()
280 open(os.path.join(self.user_brushpath, 'order_default.conf'), 'w').write(data)
282 # check for brushes that are in the brush directory, but not in any group
284 def listbrushes(path):
285 # Return a list of brush names relative to path, using
286 # slashes for subirectories on all platforms.
289 assert isinstance(path, unicode) # make sure we get unicode filenames
290 for name in os.listdir(path):
291 assert isinstance(name, unicode)
292 if name.endswith('.myb'):
294 elif os.path.isdir(path+name):
295 for name2 in listbrushes(path+name):
296 l.append(name + '/' + name2)
299 # Distinguish between brushes in the brushlist and those that are not;
300 # handle lost-and-found ones.
301 for name in listbrushes(self.stock_brushpath) + listbrushes(self.user_brushpath):
303 b.in_brushlist = True
304 if name.startswith('context'):
307 b.load_settings(retain_parent=True)
308 b.in_brushlist = False
309 elif name.startswith(DEVBRUSH_NAME_PREFIX):
310 device_name = devbrush_unquote(name)
311 self.brush_by_device[device_name] = b
312 b.load_settings(retain_parent=True)
313 b.in_brushlist = False
314 elif name.startswith(BRUSH_HISTORY_NAME_PREFIX):
315 i_str = name.replace(BRUSH_HISTORY_NAME_PREFIX, '')
318 b.load_settings(retain_parent=True)
319 b.in_brushlist = False
321 if not [True for group in our.itervalues() if b in group]:
322 brushes = self.groups.setdefault(FOUND_BRUSHES_GROUP, [])
325 # Sensible defaults for brushkeys and history: clone the first few
326 # brushes from a normal group if we need to and if we can.
327 # Try the default startup group first.
328 default_group = self.groups.get(DEFAULT_STARTUP_GROUP, None)
330 # Otherwise, use the biggest group to minimise the chance
332 if default_group is None:
333 groups_by_len = [(len(g),n,g) for n,g in self.groups.items()]
335 _len, _name, default_group = groups_by_len[-1]
337 # Populate blank entries.
338 for i in xrange(NUM_BRUSHKEYS):
339 if self.contexts[i] is None:
340 idx = (i+9) % 10 # keyboard order
341 c_name = unicode('context%02d') % i
342 c = ManagedBrush(self, name=c_name, persistent=False)
343 group_idx = idx % len(default_group)
344 b = default_group[group_idx]
345 b.clone_into(c, c_name)
347 for i in xrange(BRUSH_HISTORY_SIZE):
348 if self.history[i] is None:
349 h_name = unicode('%s%d') % (BRUSH_HISTORY_NAME_PREFIX, i)
350 h = ManagedBrush(self, name=h_name, persistent=False)
351 group_i = i % len(default_group)
352 b = default_group[group_i]
353 b.clone_into(h, h_name)
356 # clean up legacy stuff
357 fn = os.path.join(self.user_brushpath, 'deleted.conf')
358 if os.path.exists(fn):
361 def import_brushpack(self, path, window):
362 zip = zipfile.ZipFile(path)
363 names = zip.namelist()
364 # zipfile does utf-8 decoding on its own; this is just to make
365 # sure we have only unicode objects as brush names.
366 names = [s.decode('utf-8') for s in names]
369 if 'readme.txt' in names:
370 readme = zip.read('readme.txt')
372 assert 'order.conf' in names, 'invalid brushpack: order.conf missing'
373 groups = parse_order_conf(zip.read('order.conf'))
376 for brushes in groups.itervalues():
377 for brush in brushes:
378 if brush not in new_brushes:
379 new_brushes.append(brush)
380 print len(new_brushes), 'different brushes found in order.conf of brushpack'
382 # Validate file content. The names in order.conf and the
383 # brushes found in the zip must match. This should catch
384 # encoding screwups, everything should be an unicode object.
385 for brush in new_brushes:
386 assert brush + '.myb' in names, 'invalid brushpack: brush %r in order.conf does not exist in zip' % brush
388 if name.endswith('.myb'):
390 assert brush in new_brushes, 'invalid brushpack: brush %r exists in zip, but not in order.conf' % brush
393 answer = dialogs.confirm_brushpack_import(basename(path), window, readme)
394 if answer == gtk.RESPONSE_REJECT:
401 for groupname, brushes in groups.iteritems():
402 managed_brushes = self.get_group_brushes(groupname)
403 self.set_active_groups([groupname])
405 answer = dialogs.confirm_rewrite_group(
406 window, translate_group_name(groupname), translate_group_name(DELETED_BRUSH_GROUP))
407 if answer == dialogs.CANCEL:
409 elif answer == dialogs.OVERWRITE_THIS:
410 self.delete_group(groupname)
411 elif answer == dialogs.DONT_OVERWRITE_THIS:
413 old_groupname = groupname
414 while groupname in self.groups:
416 groupname = old_groupname + '#%d' % i
417 managed_brushes = self.get_group_brushes(groupname, make_active=True)
419 final_groups.append(groupname)
421 for brushname in brushes:
422 # extract the brush from the zip
423 assert (brushname + '.myb') in zip.namelist()
424 # Support for utf-8 ZIP filenames that don't have the utf-8 bit set.
425 brushname_utf8 = brushname.encode('utf-8')
427 myb_data = zip.read(brushname + '.myb')
429 myb_data = zip.read(brushname_utf8 + '.myb')
431 preview_data = zip.read(brushname + '_prev.png')
433 preview_data = zip.read(brushname_utf8 + '_prev.png')
434 # in case we have imported that brush already in a previous group, but decided to rename it
435 if brushname in renamed_brushes:
436 brushname = renamed_brushes[brushname]
437 # possibly ask how to import the brush file (if we didn't already)
438 b = self.get_brush_by_name(brushname)
439 if brushname in new_brushes:
440 new_brushes.remove(brushname)
442 existing_preview_pixbuf = b.preview
444 answer = dialogs.confirm_rewrite_brush(window, brushname, existing_preview_pixbuf, preview_data)
445 if answer == dialogs.CANCEL:
447 elif answer == dialogs.OVERWRITE_ALL:
450 elif answer == dialogs.OVERWRITE_THIS:
453 elif answer == dialogs.DONT_OVERWRITE_THIS:
456 elif answer == dialogs.DONT_OVERWRITE_ANYTHING:
459 # find a new name (if requested)
460 brushname_old = brushname
462 while not do_overwrite and b:
464 brushname = brushname_old + '#%d' % i
465 renamed_brushes[brushname_old] = brushname
466 b = self.get_brush_by_name(brushname)
469 b = ManagedBrush(self, brushname)
471 # write to disk and reload brush (if overwritten)
472 prefix = b.get_fileprefix(saving=True)
473 myb_f = open(prefix + '.myb', 'w')
474 myb_f.write(myb_data)
476 preview_f = open(prefix + '_prev.png', 'wb')
477 preview_f.write(preview_data)
480 b.in_brushlist = True
481 # finally, add it to the group
482 if b not in managed_brushes:
483 managed_brushes.append(b)
484 for f in self.brushes_observers: f(managed_brushes)
486 if DELETED_BRUSH_GROUP in self.groups:
487 # remove deleted brushes that are in some group again
488 self.delete_group(DELETED_BRUSH_GROUP)
489 self.set_active_groups(final_groups)
491 def export_group(self, group, filename):
492 zip = zipfile.ZipFile(filename, mode='w')
493 brushes = self.get_group_brushes(group)
494 order_conf = 'Group: %s\n' % group.encode('utf-8')
495 for brush in brushes:
496 prefix = brush.get_fileprefix()
497 zip.write(prefix + '.myb', brush.name + '.myb')
498 zip.write(prefix + '_prev.png', brush.name + '_prev.png')
499 order_conf += brush.name.encode('utf-8') + '\n'
500 zip.writestr('order.conf', order_conf)
503 def get_brush_by_name(self, name):
504 # slow method, should not be called too often
505 # FIXME: speed up, use a dict.
506 for group, brushes in self.groups.iteritems():
511 def brushes_modified_cb(self, brushes):
512 self.save_brushorder()
514 def save_brushorder(self):
515 f = open(os.path.join(self.user_brushpath, 'order.conf'), 'w')
516 f.write('# this file saves brush groups and order\n')
517 for group, brushes in self.groups.iteritems():
518 f.write('Group: %s\n' % group.encode('utf-8'))
520 f.write(b.name.encode('utf-8') + '\n')
524 def input_stroke_ended_cb(self, *junk):
525 """Update brush history at the end of an input stroke.
528 b_parent = b.get_string_property("parent_brush_name")
529 for i, h in enumerate(self.history):
530 h_parent = h.brushinfo.get_string_property("parent_brush_name")
531 # Possibly we should use a tighter equality check than this, but
532 # then we'd need icons showing modifications from the parent.
533 if b_parent == h_parent:
536 h = ManagedBrush(self, name=None, persistent=False)
537 h.brushinfo = b.clone()
538 h.preview = self.selected_brush.preview
539 self.history.append(h)
540 while len(self.history) > BRUSH_HISTORY_SIZE:
542 for i, h in enumerate(self.history):
543 h.name = u"%s%d" % (BRUSH_HISTORY_NAME_PREFIX, i)
546 def select_brush(self, brush):
547 """Selects a ManagedBrush, highlights it, & updates the live brush."""
549 brush = self.get_default_brush()
550 if brush.persistent and not brush.settings_loaded: # XXX refactor
551 brush.load_settings()
553 brushinfo = brush.brushinfo
554 if not brush.in_brushlist:
555 # select parent brush instead, but keep brushinfo
556 brush = self.get_parent_brush(brush=brush)
558 # no parent, select an empty brush instead
559 brush = ManagedBrush(self)
561 self.selected_brush = brush
562 self.app.preferences['brushmanager.selected_brush'] = brush.name
563 # Take care of updating the live brush, amongst other things
564 for callback in self.selected_brush_observers:
565 callback(brush, brushinfo)
568 def get_parent_brush(self, brush=None, brushinfo=None):
569 """Gets the parent `ManagedBrush` for a brush or a `BrushInfo`.
571 if brush is not None:
572 if brush.persistent and not brush.settings_loaded: # XXX refactor
573 brush.load_settings()
574 brushinfo = brush.brushinfo
575 if brushinfo is None:
576 raise RuntimeError, "One of `brush` or `brushinfo` must be defined."
577 parent_name = brushinfo.get_string_property("parent_brush_name")
578 if parent_name is None:
581 parent_brush = self.get_brush_by_name(parent_name)
582 if parent_brush is None:
584 if parent_brush.persistent and not parent_brush.settings_loaded: # XXX refactor
585 parent_brush.load_settings()
589 def clone_selected_brush(self, name):
591 Creates a new ManagedBrush based on the selected
592 brush in the brushlist and the currently active lib.brush.
594 clone = ManagedBrush(self, name, persistent=False)
595 clone.brushinfo = self.app.brush.clone()
596 clone.preview = self.selected_brush.preview
597 parent = self.selected_brush.name
598 clone.brushinfo.set_string_property("parent_brush_name", parent)
601 def store_brush_for_device(self, device_name, managed_brush):
603 Records an existing ManagedBrush as associated with a given input device.
605 Normally the brush will be cloned first, since it will be given a new
606 name. However, if the ManagedBrush has a 'name' attribute of None, it
607 will *not* be cloned and just modified in place and stored.
609 brush = managed_brush
610 if brush.name is not None:
611 brush = brush.clone()
612 brush.name = unicode(devbrush_quote(device_name))
613 self.brush_by_device[device_name] = brush
615 def fetch_brush_for_device(self, device_name):
617 Fetches the brush associated with a particular input device name.
619 devbrush_name = devbrush_quote(device_name)
620 brush = self.brush_by_device.get(device_name, None)
623 def save_brushes_for_devices(self):
624 for device_name, devbrush in self.brush_by_device.iteritems():
627 def save_brush_history(self):
628 for brush in self.history:
631 def set_active_groups(self, groups):
632 """Set active groups."""
633 self.active_groups = groups
634 self.app.preferences['brushmanager.selected_groups'] = groups
635 for f in self.groups_observers: f()
637 def get_group_brushes(self, group, make_active=False):
638 if group not in self.groups:
640 self.groups[group] = brushes
641 for f in self.groups_observers: f()
642 self.save_brushorder()
643 if make_active and group not in self.active_groups:
644 self.set_active_groups([group] + self.active_groups)
645 return self.groups[group]
647 def create_group(self, new_group, make_active=True):
648 return self.get_group_brushes(new_group, make_active)
650 def rename_group(self, old_group, new_group):
651 was_active = (old_group in self.active_groups)
652 brushes = self.create_group(new_group, make_active=was_active)
653 brushes += self.groups[old_group]
654 self.delete_group(old_group)
656 def delete_group(self, group):
657 homeless_brushes = self.groups[group]
658 del self.groups[group]
659 if group in self.active_groups:
660 self.active_groups.remove(group)
662 for brushes in self.groups.itervalues():
664 if b2 in homeless_brushes:
665 homeless_brushes.remove(b2)
668 deleted_brushes = self.get_group_brushes(DELETED_BRUSH_GROUP)
669 for b in homeless_brushes:
670 deleted_brushes.insert(0, b)
671 for f in self.brushes_observers: f(deleted_brushes)
672 for f in self.brushes_observers: f(homeless_brushes)
673 for f in self.groups_observers: f()
674 self.save_brushorder()
677 class ManagedBrush(object):
678 '''Represents a brush, but cannot be selected or painted with directly.'''
679 def __init__(self, brushmanager, name=None, persistent=False):
680 self.bm = brushmanager
683 self.brushinfo = BrushInfo()
684 self.persistent = persistent #: If True this brush is stored in the filesystem.
685 self.settings_loaded = False #: If True this brush is fully initialized, ready to paint with.
686 self.in_brushlist = False #: Set to True if this brush is known to be in the brushlist
688 self.settings_mtime = None
689 self.preview_mtime = None
692 # we load the files later, but throw an exception now if they don't exist
693 self.get_fileprefix()
695 # load preview pixbuf on demand
696 def get_preview(self):
697 if self._preview is None and self.name:
699 if self._preview is None:
700 self.preview = gdk.Pixbuf(gdk.COLORSPACE_RGB, False, 8, preview_w, preview_h)
701 self.preview.fill(0xffffffff) # white
703 def set_preview(self, pixbuf):
704 self._preview = pixbuf
705 preview = property(get_preview, set_preview)
707 def get_display_name(self):
708 """Gets a displayable name for the brush.
710 if self.in_brushlist:
713 if self.persistent and not self.settings_loaded: # XXX refactor
715 dname = self.brushinfo.get_string_property("parent_brush_name")
717 return _("Unknown Brush")
718 return dname.replace("_", " ")
721 def get_fileprefix(self, saving=False):
723 if os.path.realpath(self.bm.user_brushpath) == os.path.realpath(self.bm.stock_brushpath):
724 # working directly on brush collection, use different prefix
730 self.name = u'%s%03d' % (prefix, i)
731 a = os.path.join(self.bm.user_brushpath, self.name + '.myb')
732 b = os.path.join(self.bm.stock_brushpath, self.name + '.myb')
733 if not os.path.isfile(a) and not os.path.isfile(b):
736 assert isinstance(self.name, unicode)
737 prefix = os.path.join(self.bm.user_brushpath, self.name)
740 d = os.path.dirname(prefix)
741 if not os.path.isdir(d):
744 if not os.path.isfile(prefix + '.myb'):
745 prefix = os.path.join(self.bm.stock_brushpath, self.name)
746 if not os.path.isfile(prefix + '.myb'):
747 raise IOError, 'brush "' + self.name + '" not found'
750 def clone(self, name):
751 "Creates a new brush with all the settings of this brush, assigning it a new name"
752 clone = ManagedBrush(self.bm)
753 self.clone_into(clone, name=name)
756 def clone_into(self, target, name):
757 "Copies all brush settings into another brush, giving it a new name"
758 if not self.settings_loaded: # XXX refactor
760 target.brushinfo = self.brushinfo.clone()
761 if self.in_brushlist:
762 target.brushinfo.set_string_property("parent_brush_name", self.name)
763 target.preview = self.preview
766 def delete_from_disk(self):
767 prefix = os.path.join(self.bm.user_brushpath, self.name)
768 if os.path.isfile(prefix + '.myb'):
769 os.remove(prefix + '_prev.png')
770 os.remove(prefix + '.myb')
774 return True # success
776 return False # partial success, this brush was hiding a stock brush with the same name
777 # stock brush cannot be deleted
780 def remember_mtimes(self):
781 prefix = self.get_fileprefix()
782 self.preview_mtime = os.path.getmtime(prefix + '_prev.png')
783 self.settings_mtime = os.path.getmtime(prefix + '.myb')
785 def has_changed_on_disk(self):
786 prefix = self.get_fileprefix()
787 if self.preview_mtime != os.path.getmtime(prefix + '_prev.png'): return True
788 if self.settings_mtime != os.path.getmtime(prefix + '.myb'): return True
792 prefix = self.get_fileprefix(saving=True)
793 self.preview.save(prefix + '_prev.png', 'png')
794 brushinfo = self.brushinfo.clone()
795 open(prefix + '.myb', 'w').write(brushinfo.save_to_string())
796 self.remember_mtimes()
798 def load(self, retain_parent=False):
799 """Loads the brush's preview and settings from disk."""
800 if self.name is None:
801 warn("Attempt to load an unnamed brush, don't do that.",
805 self.load_settings(retain_parent)
807 def _load_preview(self):
808 """Loads the brush preview as pixbuf into the brush."""
810 prefix = self.get_fileprefix()
812 filename = prefix + '_prev.png'
813 pixbuf = gdk.pixbuf_new_from_file(filename)
814 self._preview = pixbuf
815 self.remember_mtimes()
817 def load_settings(self, retain_parent=False):
818 """Loads the brush settings/dynamics from disk."""
819 prefix = self.get_fileprefix()
820 filename = prefix + '.myb'
821 brushinfo_str = open(filename).read()
822 self.brushinfo.load_from_string(brushinfo_str)
823 self.remember_mtimes()
824 self.settings_loaded = True
825 if not retain_parent:
826 self.brushinfo.set_string_property("parent_brush_name", None)
827 self.persistent = True
829 def reload_if_changed(self):
830 if self.settings_mtime is None: return
831 if self.preview_mtime is None: return
832 if not self.name: return
833 if not self.has_changed_on_disk(): return False
834 print 'Brush "' + self.name + '" has changed on disk, reloading it.'
839 if self.brushinfo.settings:
840 return "<ManagedBrush %s p=%s>" % (self.name, self.brushinfo.get_string_property("parent_brush_name"))
842 return "<ManagedBrush %s (settings not loaded yet)>" % self.name
845 if __name__ == '__main__':