OSDN Git Service

refactor brush/groups file handling
authorMartin Renold <martinxyz@gmx.ch>
Sat, 12 Dec 2009 17:37:25 +0000 (18:37 +0100)
committerMartin Renold <martinxyz@gmx.ch>
Sun, 13 Dec 2009 14:56:16 +0000 (15:56 +0100)
Mostly just moving code around and renaming members.

Move all brushes and group filehandling into brushmanager.py.
Set up observers for group/brush changes instead of updating the
GUI widgets directly all over the place.

Including some minor bugfixes that have become trivial now.

12 files changed:
gui/application.py
gui/brushcreationwidget.py [new file with mode: 0644]
gui/brushmanager.py [new file with mode: 0644]
gui/brushselectionwindow.py
gui/brushsettingswindow.py
gui/dialogs.py [new file with mode: 0644]
gui/drawwindow.py
gui/functionwindow.py
lib/brush.py
lib/document.py
lib/stroke.py
tests/test_mypaintlib.py

index 31b8b3e..5d057c9 100644 (file)
@@ -7,11 +7,11 @@
 # (at your option) any later version.
 
 import os
+from os.path import join
 import gtk, gobject
 gdk = gtk.gdk
 from lib import brush
-import filehandling, keyboard
-from brushselectionwindow import DEFAULT_BRUSH_GROUP
+import filehandling, keyboard, brushmanager
 
 class Application: # singleton
     """
@@ -23,11 +23,15 @@ class Application: # singleton
         self.confpath = confpath
         self.datapath = datapath
 
+        if not os.path.isdir(self.confpath):
+            os.mkdir(self.confpath)
+            print 'Created', self.confpath
+
         self.ui_manager = gtk.UIManager()
 
         # if we are not installed, use the the icons from the source
         theme = gtk.icon_theme_get_default()
-        themedir_src = os.path.join(self.datapath, 'desktop/icons')
+        themedir_src = join(self.datapath, 'desktop/icons')
         theme.prepend_search_path(themedir_src)
         if not theme.has_icon('mypaint'):
             print 'Warning: Where have all my icons gone?'
@@ -36,22 +40,19 @@ class Application: # singleton
 
         gdk.set_program_class('MyPaint')
 
-        self.pixmaps = PixbufDirectory(os.path.join(self.datapath, 'pixmaps'))
+        self.pixmaps = PixbufDirectory(join(self.datapath, 'pixmaps'))
         self.cursor_color_picker = gdk.Cursor(gdk.display_get_default(), self.pixmaps.cursor_color_picker, 1, 30)
 
-        self.user_brushpath = os.path.join(self.confpath, 'brushes')
-        self.stock_brushpath = os.path.join(self.datapath, 'brushes')
-
-        if not os.path.isdir(self.confpath):
-            os.mkdir(self.confpath)
-            print 'Created', self.confpath
-        if not os.path.isdir(self.user_brushpath):
-            os.mkdir(self.user_brushpath)
+        # unmanaged main brush; always the same instance (we can attach settings_observers)
+        # this brush is where temporary changes (color, size...) happen
+        self.brush = brush.Brush()
 
-        self.init_brushes()
+        self.brushmanager = brushmanager.BrushManager(join(datapath, 'brushes'), join(confpath, 'brushes'))
         self.kbm = keyboard.KeyboardManager()
         self.filehandler = filehandling.FileHandler(self)
 
+        self.brushmanager.selected_brush_observers.append(self.brush_selected_cb)
+
         self.window_names = '''
         drawWindow
         brushSettingsWindow
@@ -70,16 +71,10 @@ class Application: # singleton
                 window.connect("realize", set_hint)
             self.load_window_position(name, window)
 
-        self.brushSelectionWindow.disable_selection_callback = False # FIXME: huh?
         self.kbm.start_listening()
         self.filehandler.doc = self.drawWindow.doc
         self.filehandler.filename = None
-        gtk.accel_map_load(os.path.join(self.confpath, 'accelmap.conf'))
-
-        # TODO: remember last selected brush, and select one at frist startup
-        #if self.brushes:
-        #    self.select_brush(self.brushes[0])
-        self.brush.set_color_hsv((0, 0, 0))
+        gtk.accel_map_load(join(self.confpath, 'accelmap.conf'))
 
         def at_application_start(*trash):
             if filenames:
@@ -89,137 +84,22 @@ class Application: # singleton
 
         gobject.idle_add(at_application_start)
 
-    def init_brushes(self):
-        self.brush = brush.Brush(self)
-        self.selected_brush = self.brush
-        self.selected_brush_observers = []
-        self.contexts = []
-        for i in range(10):
-            c = brush.Brush(self)
-            c.name = 'context%02d' % i
-            self.contexts.append(c)
-        self.selected_context = None
-
-        brush_by_name = {}
-        def get_brush(name):
-            if name not in brush_by_name:
-                b = brush.Brush(self)
-                b.load(name)
-                brush_by_name[name] = b
-            return brush_by_name[name]
-
-        # maybe this should be save/loaded too?
-        self.brush_by_device = {}
-
-        def read_groups(filename):
-            groups = {}
-            if os.path.exists(filename):
-                curr_group = DEFAULT_BRUSH_GROUP
-                groups[curr_group] = []
-                for line in open(filename):
-                    name = line.strip()
-                    if name.startswith('#'):
-                        continue
-                    if name.startswith('Group: '):
-                        curr_group = unicode(name[7:], 'utf-8')
-                        if curr_group not in groups:
-                            groups[curr_group] = []
-                        continue
-                    try:
-                        b = get_brush(name)
-                    except IOError, e:
-                        print e, '(removed from group)'
-                        continue
-                    if b in groups[curr_group]:
-                        print filename + ': Warning: brush appears twice in the same group, ignored'
-                        continue
-                    groups[curr_group].append(b)
-            return groups
-
-        # tree-way-merge of brush groups (for upgrading)
-        base  = read_groups(os.path.join(self.user_brushpath,  'order_default.conf'))
-        our   = read_groups(os.path.join(self.user_brushpath,  'order.conf'))
-        their = read_groups(os.path.join(self.stock_brushpath, 'order.conf'))
-
-        if base == their:
-            self.brushgroups = our
-        else:
-            print 'Merging upstream brush changes into your collection.'
-            groups = set(base).union(our).union(their)
-            for group in groups:
-                # treat the non-existing groups as if empty
-                base_brushes = base.setdefault(group, [])
-                our_brushes = our.setdefault(group, [])
-                their_brushes = their.setdefault(group, [])
-                # add new brushes
-                insert_index = 0
-                for b in their_brushes:
-                    if b in our_brushes:
-                        insert_index = our_brushes.index(b) + 1
-                    else:
-                        if b in their_brushes:
-                            our_brushes.insert(insert_index, b)
-                            insert_index += 1
-                # remove deleted brushes
-                for b in base_brushes:
-                    if b not in their_brushes and b in our_brushes:
-                        our_brushes.remove(b)
-                # remove empty groups
-                if not our_brushes:
-                    del our[group]
-            # finish
-            self.brushgroups = our
-            self.save_brushorder()
-            data = open(os.path.join(self.stock_brushpath, 'order.conf')).read()
-            open(os.path.join(self.user_brushpath,  'order_default.conf'), 'w').write(data)
-                
-        # handle brushes that are in the brush directory, but not in any group
-        def listbrushes(path):
-            return [filename[:-4] for filename in os.listdir(path) if filename.endswith('.myb')]
-        for name in listbrushes(self.stock_brushpath) + listbrushes(self.user_brushpath):
-            b = get_brush(name)
-            if name.startswith('context'):
-                i = int(name[-2:])
-                self.contexts[i] = b
-                continue
-            if not [True for group in our.itervalues() if b in group]:
-                self.brushgroups.setdefault(DEFAULT_BRUSH_GROUP, [])
-                self.brushgroups[DEFAULT_BRUSH_GROUP].insert(0, b)
-
-        # clean up legacy stuff
-        fn = os.path.join(self.user_brushpath, 'deleted.conf')
-        if os.path.exists(fn):
-            os.path.remove(fn)
-
-    def save_brushorder(self):
-        f = open(os.path.join(self.user_brushpath, 'order.conf'), 'w')
-        f.write('# this file saves brush groups and order\n')
-        for group, brushes in self.brushgroups.iteritems():
-            f.write('Group: %s\n' % group.encode('utf-8'))
-            for b in brushes:
-                f.write(b.name + '\n')
-        f.close()
-
-    def select_brush(self, brush):
-        assert brush is not self.brush # self.brush never gets exchanged
-        self.selected_brush = brush
-        if brush is not None:
+    def brush_selected_cb(self, brush):
+        assert brush is not self.brush
+        if brush:
             self.brush.copy_settings_from(brush)
 
-        for callback in self.selected_brush_observers:
-            callback(brush)
-
     def hide_window_cb(self, window, event):
         # used by some of the windows
         window.hide()
         return True
 
     def save_gui_config(self):
-        gtk.accel_map_save(os.path.join(self.confpath, 'accelmap.conf'))
+        gtk.accel_map_save(join(self.confpath, 'accelmap.conf'))
         self.save_window_positions()
         
     def save_window_positions(self):
-        f = open(os.path.join(self.confpath, 'windowpos.conf'), 'w')
+        f = open(join(self.confpath, 'windowpos.conf'), 'w')
         f.write('# name visible x y width height\n')
         for name in self.window_names:
             window = self.__dict__[name]
@@ -232,7 +112,7 @@ class Application: # singleton
 
     def load_window_position(self, name, window):
         try:
-            for line in open(os.path.join(self.confpath, 'windowpos.conf')):
+            for line in open(join(self.confpath, 'windowpos.conf')):
                 if line.startswith(name):
                     parts = line.split()
                     visible = parts[1] == 'True'
@@ -265,6 +145,6 @@ class PixbufDirectory:
 
     def __getattr__(self, name):
         if name not in self.cache:
-            pixbuf = gdk.pixbuf_new_from_file(os.path.join(self.dirname, name + '.png'))
+            pixbuf = gdk.pixbuf_new_from_file(join(self.dirname, name + '.png'))
             self.cache[name] = pixbuf
         return self.cache[name]
diff --git a/gui/brushcreationwidget.py b/gui/brushcreationwidget.py
new file mode 100644 (file)
index 0000000..ded1947
--- /dev/null
@@ -0,0 +1,187 @@
+# This file is part of MyPaint.
+# Copyright (C) 2009 by Martin Renold <martinxyz@gmx.ch>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+import gtk, pango
+gdk = gtk.gdk
+from lib import brush, document
+import tileddrawwidget, brushmanager, dialogs
+from gettext import gettext as _
+
+class Widget(gtk.HBox):
+    def __init__(self, app):
+        gtk.HBox.__init__(self)
+        self.app = app
+        self.bm = app.brushmanager
+
+        self.set_border_width(8)
+
+        left_vbox = gtk.VBox()
+        right_vbox = gtk.VBox()
+        self.pack_start(left_vbox, expand=False, fill=False)
+        self.pack_end(right_vbox, expand=False, fill=False)
+
+        #expanded part, left side
+        doc = document.Document()
+        self.tdw = tileddrawwidget.TiledDrawWidget(doc)
+        self.tdw.set_size_request(brushmanager.preview_w, brushmanager.preview_h)
+        left_vbox.pack_start(self.tdw, expand=False, fill=False)
+
+        b = gtk.Button(_('Clear'))
+        def clear_cb(window):
+            self.tdw.doc.clear_layer()
+        b.connect('clicked', clear_cb)
+        left_vbox.pack_start(b, expand=False, padding=5)
+
+        #expanded part, right side
+        l = self.brush_name_label = gtk.Label()
+        l.set_justify(gtk.JUSTIFY_LEFT)
+        l.set_text(_('(no name)'))
+        right_vbox.pack_start(l, expand=False)
+
+        right_vbox_buttons = [
+        (_('add as new'), self.create_brush_cb),
+        (_('rename...'), self.rename_brush_cb),
+        (_('remove...'), self.delete_brush_cb),
+        (_('settings...'), self.brush_settings_cb),
+        (_('save settings'), self.update_settings_cb),
+        (_('save preview'), self.update_preview_cb),
+        ]
+
+        for title, clicked_cb in right_vbox_buttons:
+            b = gtk.Button(title)
+            b.connect('clicked', clicked_cb)
+            right_vbox.pack_start(b, expand=False)
+
+        self.app.brushmanager.selected_brush_observers.append(self.brush_selected_cb)
+        self.app.brush.settings_observers.append(self.brush_modified_cb)
+
+
+    def set_preview_pixbuf(self, pixbuf):
+        if pixbuf is None:
+            self.tdw.doc.clear()
+        else:
+            self.tdw.doc.load_from_pixbuf(pixbuf)
+
+    def get_preview_pixbuf(self):
+        pixbuf = self.tdw.doc.render_as_pixbuf(0, 0, brushmanager.preview_w, brushmanager.preview_h)
+        return pixbuf
+
+    def brush_settings_cb(self, window):
+        w = self.app.brushSettingsWindow
+        w.show_all() # might be for the first time
+        w.present()
+
+    def create_brush_cb(self, window):
+        b = brushmanager.ManagedBrush(self.bm)
+        b.copy_settings_from(self.app.brush)
+        b.preview = self.get_preview_pixbuf()
+        b.save()
+
+        if self.bm.active_groups:
+            group = self.bm.active_groups[0]
+        else:
+            group = brushmanager.DEFAULT_BRUSH_GROUP
+
+        brushes = self.bm.get_group_brushes(group, make_active=True)
+        brushes.insert(0, b)
+        for f in self.bm.brushes_observers: f(brushes)
+
+        self.bm.select_brush(b)
+
+    def rename_brush_cb(self, window):
+        b = self.bm.selected_brush
+        if not b.name:
+            display = gdk.display_get_default()
+            display.beep()
+            return
+
+        name = dialogs.ask_for_name(self, _("Rename Brush"), b.name.replace('_', ' '))
+        if not name:
+            return
+        name = name.replace(' ', '_')
+        print 'renaming brush', repr(b.name), '-->', repr(name)
+        # ensure we don't overwrite an existing brush by accident
+        for group, brushes in self.bm.groups.iteritems():
+            if group == brushmanager.DELETED_BRUSH_GROUP:
+                continue
+            for b2 in brushes:
+                if b2.name == name:
+                    dialogs.error(self, _('A brush with this name already exists!'))
+                    return
+        success = b.delete_from_disk()
+        old_name = b.name
+        b.name = name
+        b.save()
+        if not success:
+            # we are renaming a stock brush
+            # we can't delete the original; instead we put it away so it doesn't reappear
+            old_brush = brushmanager.ManagedBrush(self.bm)
+            old_brush.load(old_name)
+            deleted_brushes = self.bm.get_group_brushes(DELETED_BRUSH_GROUP)
+            deleted_brushes.insert(0, old_brush)
+            for f in self.bm.brushes_observers: f(deleted_brushes)
+
+        self.bm.select_brush(b)
+
+    def update_preview_cb(self, window):
+        pixbuf = self.get_preview_pixbuf()
+        b = self.app.selected_brush
+        if not b.name:
+            # no brush selected
+            display = gdk.display_get_default()
+            display.beep()
+            return
+        b.preview = pixbuf
+        b.save()
+        for brushes in self.bm.groups.itervalues():
+            if b in brushes:
+                for f in self.bm.brushes_observers: f(brushes)
+
+    def update_settings_cb(self, window):
+        b = self.bm.selected_brush
+        if not b.name:
+            # no brush selected
+            display = gdk.display_get_default()
+            display.beep()
+            return
+        b.copy_settings_from(self.app.brush)
+        b.save()
+
+    def delete_brush_cb(self, window):
+        b = self.bm.selected_brush
+        if not b.name:
+            display = gdk.display_get_default()
+            display.beep()
+            return
+        if not dialogs.confirm(self, _("Really delete brush from disk?")):
+            return
+
+        self.bm.select_brush(None)
+
+        for brushes in self.bm.groups.itervalues():
+            if b in brushes:
+                brushes.remove(b)
+                for f in self.bm.brushes_observers: f(brushes)
+
+        if not b.delete_from_disk():
+            # stock brush can't be deleted
+            deleted_brushes = self.bm.get_group_brushes(brushmanager.DELETED_BRUSH_GROUP)
+            deleted_brushes.insert(0, b)
+            for f in self.bm.brushes_observers: f(deleted_brushes)
+
+    def brush_selected_cb(self, brush):
+        name = brush.name
+        if name is None:
+            name = _('(no name)')
+        else:
+            name = name.replace('_', ' ')
+        self.brush_name_label.set_text(name)
+
+    def brush_modified_cb(self):
+        self.tdw.doc.set_brush(self.app.brush)
+
diff --git a/gui/brushmanager.py b/gui/brushmanager.py
new file mode 100644 (file)
index 0000000..50e3431
--- /dev/null
@@ -0,0 +1,300 @@
+# This file is part of MyPaint.
+# Copyright (C) 2009 by Martin Renold <martinxyz@gmx.ch>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+"""
+This module does file management for brushes and brush groups.
+"""
+
+from lib import brush
+from gtk import gdk # only for gdk.pixbuf
+import os
+
+# not translatable for now (this string is saved into a file and would screw up between language switches)
+DEFAULT_BRUSH_GROUP = 'default'
+DELETED_BRUSH_GROUP = 'deleted'
+
+preview_w = 128
+preview_h = 128
+
+class BrushManager:
+    def __init__(self, stock_brushpath, user_brushpath):
+        self.stock_brushpath = stock_brushpath
+        self.user_brushpath = user_brushpath
+
+        self.selected_brush = ManagedBrush(self)
+        self.groups = []
+        self.contexts = []
+        self.brush_by_device = {} # should be save/loaded too?
+
+        self.selected_brush_observers = []
+        self.groups_observers = [] # for both self.groups and self.active_groups
+        self.brushes_observers = [] # for all bruslists inside groups
+
+        if not os.path.isdir(self.user_brushpath):
+            os.mkdir(self.user_brushpath)
+        self.load_groups()
+
+        # TODO: load from config instead
+        self.groups.setdefault(DEFAULT_BRUSH_GROUP, [])
+        self.active_groups = [DEFAULT_BRUSH_GROUP]
+
+        self.brushes_observers.append(self.brushes_modified_cb)
+
+    def load_groups(self):
+        for i in range(10):
+            c = ManagedBrush(self)
+            c.name = 'context%02d' % i
+            self.contexts.append(c)
+        self.selected_context = None
+
+        brush_by_name = {}
+        def get_brush(name):
+            if name not in brush_by_name:
+                b = ManagedBrush(self)
+                b.load(name)
+                brush_by_name[name] = b
+            return brush_by_name[name]
+
+        def read_groups(filename):
+            groups = {}
+            if os.path.exists(filename):
+                curr_group = DEFAULT_BRUSH_GROUP
+                groups[curr_group] = []
+                for line in open(filename):
+
+                    name = line.strip()
+                    if name.startswith('#'):
+                        continue
+                    if name.startswith('Group: '):
+                        curr_group = unicode(name[7:], 'utf-8')
+                        if curr_group not in groups:
+                            groups[curr_group] = []
+                        continue
+                    try:
+                        b = get_brush(name)
+                    except IOError, e:
+                        print e, '(removed from group)'
+                        continue
+                    if b in groups[curr_group]:
+                        print filename + ': Warning: brush appears twice in the same group, ignored'
+                        continue
+                    groups[curr_group].append(b)
+            return groups
+
+        # tree-way-merge of brush groups (for upgrading)
+        base  = read_groups(os.path.join(self.user_brushpath,  'order_default.conf'))
+        our   = read_groups(os.path.join(self.user_brushpath,  'order.conf'))
+        their = read_groups(os.path.join(self.stock_brushpath, 'order.conf'))
+
+        if base == their:
+            self.groups = our
+        else:
+            print 'Merging upstream brush changes into your collection.'
+            groups = set(base).union(our).union(their)
+            for group in groups:
+                # treat the non-existing groups as if empty
+                base_brushes = base.setdefault(group, [])
+                our_brushes = our.setdefault(group, [])
+                their_brushes = their.setdefault(group, [])
+                # add new brushes
+                insert_index = 0
+                for b in their_brushes:
+                    if b in our_brushes:
+                        insert_index = our_brushes.index(b) + 1
+                    else:
+                        if b in their_brushes:
+                            our_brushes.insert(insert_index, b)
+                            insert_index += 1
+                # remove deleted brushes
+                for b in base_brushes:
+                    if b not in their_brushes and b in our_brushes:
+                        our_brushes.remove(b)
+                # remove empty groups
+                if not our_brushes:
+                    del our[group]
+            # finish
+            self.groups = our
+            self.save_brushorder()
+            data = open(os.path.join(self.stock_brushpath, 'order.conf')).read()
+            open(os.path.join(self.user_brushpath,  'order_default.conf'), 'w').write(data)
+
+        # check for brushes that are in the brush directory, but not in any group
+        def listbrushes(path):
+            return [filename[:-4] for filename in os.listdir(path) if filename.endswith('.myb')]
+        for name in listbrushes(self.stock_brushpath) + listbrushes(self.user_brushpath):
+            b = get_brush(name)
+            if name.startswith('context'):
+                i = int(name[-2:])
+                self.contexts[i] = b
+                continue
+            if not [True for group in our.itervalues() if b in group]:
+                brushes = self.groups.setdefault(DEFAULT_BRUSH_GROUP, [])
+                brushes.insert(0, b)
+
+        # clean up legacy stuff
+        fn = os.path.join(self.user_brushpath, 'deleted.conf')
+        if os.path.exists(fn):
+            os.path.remove(fn)
+
+    def brushes_modified_cb(self, brushes):
+        self.save_brushorder()
+
+    def save_brushorder(self):
+        f = open(os.path.join(self.user_brushpath, 'order.conf'), 'w')
+        f.write('# this file saves brush groups and order\n')
+        for group, brushes in self.groups.iteritems():
+            f.write('Group: %s\n' % group.encode('utf-8'))
+            for b in brushes:
+                f.write(b.name + '\n')
+        f.close()
+
+    def select_brush(self, brush):
+        if brush is None:
+            brush = ManagedBrush(self)
+        assert isinstance(brush, ManagedBrush)
+        self.selected_brush = brush
+        for callback in self.selected_brush_observers:
+            callback(brush)
+
+    def get_group_brushes(self, group, make_active=False):
+        if group not in self.groups:
+            brushes = []
+            self.groups[group] = brushes
+            for f in self.groups_observers: f()
+        if make_active and group not in self.active_groups:
+            self.active_groups.insert(0, group)
+            for f in self.groups_observers: f()
+        return self.groups[group]
+
+    def create_group(self, new_group, make_active=True):
+        return self.get_group_brushes(new_group, make_active)
+
+    def rename_group(self, old_group, new_group):
+        was_active = (old_group in self.active_groups)
+        brushes = self.create_group(new_group, make_active=was_active)
+        brushes += self.groups[old_group]
+        self.delete_group(old_group)
+
+    def delete_group(self, group):
+        homeless_brushes = self.groups[group]
+        del self.groups[group]
+        if group in self.active_groups:
+            self.active_groups.remove(group)
+
+        for brushes in self.groups.itervalues():
+            for b2 in brushes:
+                if b2 in homeless_brushes:
+                    homeless_brushes.remove(b2)
+
+        if homeless_brushes:
+            deleted_brushes = self.get_group_brushes(DELETED_BRUSH_GROUP)
+            for b in homeless_brushes:
+                deleted_brushes.insert(0, b)
+            for f in self.brushes_observers: f(deleted_brushes)
+        for f in self.brushes_observers: f(homeless_brushes)
+        for f in self.groups_observers: f()
+
+
+class ManagedBrush(brush.Brush):
+    def __init__(self, brushmanager):
+        brush.Brush.__init__(self)
+        self.bm = brushmanager
+        self.preview = None
+        self.name = None
+        self.preview_changed = True
+
+        self.settings_mtime = None
+        self.preview_mtime = None
+
+    def get_fileprefix(self, saving=False):
+        prefix = 'b'
+        if os.path.realpath(self.bm.user_brushpath) == os.path.realpath(self.bm.stock_brushpath):
+            # working directly on brush collection, use different prefix
+            prefix = 's'
+
+        if not self.name:
+            i = 0
+            while 1:
+                self.name = '%s%03d' % (prefix, i)
+                a = os.path.join(self.bm.user_brushpath,self.name + '.myb')
+                b = os.path.join(self.bm.stock_brushpath,self.name + '.myb')
+                if not os.path.isfile(a) and not os.path.isfile(b):
+                    break
+                i += 1
+        prefix = os.path.join(self.bm.user_brushpath, self.name)
+        if saving: 
+            return prefix
+        if not os.path.isfile(prefix + '.myb'):
+            prefix = os.path.join(self.bm.stock_brushpath,self.name)
+        if not os.path.isfile(prefix + '.myb'):
+            raise IOError, 'brush "' + self.name + '" not found'
+        return prefix
+
+    def delete_from_disk(self):
+        prefix = os.path.join(self.bm.user_brushpath, self.name)
+        if os.path.isfile(prefix + '.myb'):
+            os.remove(prefix + '_prev.png')
+            os.remove(prefix + '.myb')
+            self.preview_changed = True # need to recreate when saving
+            return True
+        # stock brush cannot be deleted
+        return False
+
+    def remember_mtimes(self):
+        prefix = self.get_fileprefix()
+        self.preview_mtime = os.path.getmtime(prefix + '_prev.png')
+        self.settings_mtime = os.path.getmtime(prefix + '.myb')
+
+    def has_changed_on_disk(self):
+        prefix = self.get_fileprefix()
+        if self.preview_mtime != os.path.getmtime(prefix + '_prev.png'): return True
+        if self.settings_mtime != os.path.getmtime(prefix + '.myb'): return True
+        return False
+
+    def save(self):
+        prefix = self.get_fileprefix(saving=True)
+        if self.preview_changed:
+            self.preview.save(prefix + '_prev.png', 'png')
+            self.preview_changed = False
+        open(prefix + '.myb', 'w').write(self.save_to_string())
+        self.remember_mtimes()
+
+    def load(self, name):
+        self.name = name
+        prefix = self.get_fileprefix()
+
+        filename = prefix + '_prev.png'
+        pixbuf = gdk.pixbuf_new_from_file(filename)
+        self.preview = pixbuf
+
+        if prefix.startswith(self.bm.user_brushpath):
+            self.preview_changed = False
+        else:
+            # for saving, create the preview file even if not changed
+            self.preview_changed = True
+
+        filename = prefix + '.myb'
+        errors = self.load_from_string(open(filename).read())
+        if errors:
+            print '%s:' % filename
+            for line, reason in errors:
+                print line
+                print '==>', reason
+            print
+
+        self.remember_mtimes()
+
+    def reload_if_changed(self):
+        if self.settings_mtime is None: return
+        if self.preview_mtime is None: return
+        if not self.name: return
+        if not self.has_changed_on_disk(): return False
+        print 'Brush "' + self.name + '" has changed on disk, reloading it.'
+        self.load(self.name)
+        return True
+
index 74f3ba9..6e604d9 100644 (file)
@@ -9,20 +9,14 @@
 "select brush window"
 import gtk, pango
 gdk = gtk.gdk
-from lib import brush, document
-import tileddrawwidget, pixbuflist
+from lib import document
+import pixbuflist, brushcreationwidget, dialogs, brushmanager
 from gettext import gettext as _
 
-# not translatable for now (this string is saved into a file and would screw up between language switches)
-DEFAULT_BRUSH_GROUP = 'default'
-DELETED_BRUSH_GROUP = 'deleted'
-
 class Window(gtk.Window):
     def __init__(self, app):
         gtk.Window.__init__(self)
         self.app = app
-        self.app.selected_brush_observers.append(self.brush_selected_cb)
-        self.app.brush.settings_observers.append(self.brush_modified_cb)
         self.app.kbm.add_window(self)
         self.last_selected_brush = None
 
@@ -43,6 +37,7 @@ class Window(gtk.Window):
         #self.connect('configure-event', self.on_configure)
         expander = self.expander = gtk.Expander(label=_('Edit'))
         expander.set_expanded(False)
+        expander.add(brushcreationwidget.Widget(app))
 
         vbox.pack_start(self.groupselector, expand=False)
         vbox.pack_start(gtk.HSeparator(), expand=False)
@@ -50,248 +45,36 @@ class Window(gtk.Window):
         vbox.pack_start(gtk.HSeparator(), expand=False)
         vbox.pack_start(expander, expand=False, fill=False)
 
-        #expanded part
-        vbox2 = gtk.VBox()
-        hbox = gtk.HBox()
-        hbox.set_border_width(8)
-        vbox2.pack_start(hbox, expand=True)
-        update_button = gtk.Button(stock=gtk.STOCK_REFRESH) # FIXME: remove?
-        update_button.connect('clicked', self.update_cb)
-        vbox2.pack_start(update_button, expand=False)
-        expander.add(vbox2)
-
-        left_vbox = gtk.VBox()
-        right_vbox = gtk.VBox()
-        hbox.pack_start(left_vbox, expand=False, fill=False)
-        hbox.pack_end(right_vbox, expand=False, fill=False)
-
-        #expanded part, left side
-        doc = document.Document()
-        self.tdw = tileddrawwidget.TiledDrawWidget(doc)
-        self.tdw.set_size_request(brush.preview_w, brush.preview_h)
-        left_vbox.pack_start(self.tdw, expand=False, fill=False)
-
-        b = gtk.Button(_('Clear'))
-        def clear_cb(window):
-            self.tdw.doc.clear_layer()
-        b.connect('clicked', clear_cb)
-        left_vbox.pack_start(b, expand=False, padding=5)
-
-        #expanded part, right side
-        l = self.brush_name_label = gtk.Label()
-        l.set_justify(gtk.JUSTIFY_LEFT)
-        l.set_text(_('(no name)'))
-        right_vbox.pack_start(l, expand=False)
-
-        right_vbox_buttons = [
-        (_('add as new'), self.create_brush_cb),
-        (_('rename...'), self.rename_brush_cb),
-        (_('remove...'), self.delete_brush_cb),
-        (_('settings...'), self.brush_settings_cb),
-        (_('save settings'), self.update_settings_cb),
-        (_('save preview'), self.update_preview_cb),
-        ]
-
-        for title, clicked_cb in right_vbox_buttons:
-            b = gtk.Button(title)
-            b.connect('clicked', clicked_cb)
-            right_vbox.pack_start(b, expand=False)
-
-    def set_preview_pixbuf(self, pixbuf):
-        if pixbuf is None:
-            self.tdw.doc.clear()
-        else:
-            self.tdw.doc.load_from_pixbuf(pixbuf)
-
-    def get_preview_pixbuf(self):
-        pixbuf = self.tdw.doc.render_as_pixbuf(0, 0, brush.preview_w, brush.preview_h)
-        return pixbuf
-
-    def brush_settings_cb(self, window):
-        w = self.app.brushSettingsWindow
-        w.show_all() # might be for the first time
-        w.present()
-
-    def create_brush_cb(self, window):
-        b = brush.Brush(self.app)
-        if self.app.brush:
-            b.copy_settings_from(self.app.brush)
-        b.preview = self.get_preview_pixbuf()
-        b.save()
-        active_groups = self.brushgroups.active_groups
-        if active_groups:
-            group = active_groups[0]
-        else:
-            group = DEFAULT_BRUSH_GROUP
-        if group not in active_groups:
-            active_groups.insert(0, group)
-        self.app.brushgroups.setdefault(group, []) # create default group if needed
-        self.app.brushgroups[group].insert(0, b)
-        self.app.save_brushorder()
-        self.brushgroups.update()
-        self.groupselector.queue_draw()
-        self.app.select_brush(b)
-
-    def rename_brush_cb(self, window):
-        b = self.app.selected_brush
-        if b is None or not b.name:
-            display = gtk.gdk.display_get_default()
-            display.beep()
-            return
-
-        name = ask_for_name(self, _("Rename Brush"), b.name.replace('_', ' '))
-        if not name:
-            return
-        name = name.replace(' ', '_')
-        print 'renaming brush', repr(b.name), '-->', repr(name)
-        # ensure we don't overwrite an existing brush by accident
-        for groupname, brushes in self.app.brushgroups.iteritems():
-            if groupname == DELETED_BRUSH_GROUP:
-                continue
-            for b2 in brushes:
-                if b2.name == name:
-                    print 'Target already exists!'
-                    display = gtk.gdk.display_get_default()
-                    display.beep()
-                    return
-        success = b.delete_from_disk()
-        old_name = b.name
-        b.name = name
-        b.save()
-        if not success:
-            # we are renaming a stock brush
-            # we can't delete the original; instead we put it away so it doesn't reappear
-            old_brush = brush.Brush(self.app)
-            old_brush.load(old_name)
-            deleted_brushes = self.app.brushgroups.setdefault(DELETED_BRUSH_GROUP, [])
-            deleted_brushes.insert(0, old_brush)
-
-        self.app.select_brush(b)
-        self.app.save_brushorder()
-        self.brushgroups.update()
-
-    def update_preview_cb(self, window):
-        pixbuf = self.get_preview_pixbuf()
-        b = self.app.selected_brush
-        if b is None or not b.name:
-            # no brush selected
-            display = gtk.gdk.display_get_default()
-            display.beep()
-            return
-        b.preview = pixbuf
-        b.save()
-        self.brushgroups.update()
-
-    def update_settings_cb(self, window):
-        b = self.app.selected_brush
-        if b is None or not b.name:
-            # no brush selected
-            display = gtk.gdk.display_get_default()
-            display.beep()
-            return
-        b.copy_settings_from(self.app.brush)
-        b.save()
-
-    def delete_brush_cb(self, window):
-        # XXXX brushgroup update? Better idea: make "deleted" group mandatory! (and undeletable)
-        b = self.app.selected_brush
-        if b is None or not b.name: return
-        if not run_confirm_dialog(self, _("Really delete brush from disk?")):
-            return
-
-        self.app.select_brush(None)
-
-        for brushes in self.app.brushgroups.itervalues():
-            if b in brushes:
-                brushes.remove(b)
-        if not b.delete_from_disk():
-            # stock brush can't be deleted
-            deleted_brushes = self.app.brushgroups.setdefault(DELETED_BRUSH_GROUP, [])
-            deleted_brushes.insert(0, b)
-
-        self.app.save_brushorder()
-        self.brushgroups.update()
-
-    def brush_selected_cb(self, brush):
-        if brush is None: return
-        name = brush.name
-        if name is None:
-            name = _('(no name)')
-        else:
-            name = name.replace('_', ' ')
-        self.brush_name_label.set_text(name)
-
-    def update_brush_preview(self, brush):
-        self.set_preview_pixbuf(brush.preview)
-        self.last_selected_brush = brush
-
-    def brush_modified_cb(self):
-        self.tdw.doc.set_brush(self.app.brush)
-
-    def update_cb(self, button):
-        callbacks = self.app.selected_brush_observers
-        self.app.selected_brush_observers = callbacks
-        self.brushgroups.update()
-
-def ask_for_name(window, title, default):
-    d = gtk.Dialog(title,
-                   window,
-                   gtk.DIALOG_MODAL,
-                   (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
-                    gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
-
-    hbox = gtk.HBox()
-    d.vbox.pack_start(hbox)
-    hbox.pack_start(gtk.Label(_('Name')))
-
-    d.e = e = gtk.Entry()
-    e.set_text(default)
-    e.select_region(0, len(default))
-    def responseToDialog(entry, dialog, response):  
-        dialog.response(response)  
-    e.connect("activate", responseToDialog, d, gtk.RESPONSE_ACCEPT)  
-
-    hbox.pack_start(e)
-    d.vbox.show_all()
-    if d.run() == gtk.RESPONSE_ACCEPT:
-        result = d.e.get_text()
-    else:
-        result = None
-    d.destroy()
-    return result
-
-def run_confirm_dialog(window, title):
-    d = gtk.Dialog(title,
-         window,
-         gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
-         (gtk.STOCK_YES, gtk.RESPONSE_ACCEPT,
-          gtk.STOCK_NO, gtk.RESPONSE_REJECT))
-    response = d.run()
-    d.destroy()
-    return response == gtk.RESPONSE_ACCEPT
 
 class BrushList(pixbuflist.PixbufList):
     def __init__(self, app, win, groupname, grouplist):
         self.app = app
+        self.bm = app.brushmanager
         self.win = win
         self.group = groupname
         self.grouplist = grouplist
-        self.brushes = self.app.brushgroups[self.group]
+        self.brushes = self.bm.groups[self.group]
         pixbuflist.PixbufList.__init__(self, self.brushes, 48, 48,
                                        namefunc = lambda x: x.name,
                                        pixbuffunc = lambda x: x.preview)
+        self.bm.brushes_observers.append(self.brushes_modified_cb)
+
+    def brushes_modified_cb(self, brushes):
+        if brushes is self.brushes:
+            self.update()
+
     def remove_brush(self, brush):
         self.brushes.remove(brush)
-        self.update()
+        for f in self.bm.brushes_observers: f(self.brushes)
 
     def insert_brush(self, idx, brush):
         self.brushes.insert(idx, brush)
-        self.update()
+        for f in self.bm.brushes_observers: f(self.brushes)
 
     def on_drag_data(self, copy, source_widget, brush_name, target_idx):
         assert source_widget, 'cannot handle drag data from another app'
         b, = [b for b in source_widget.brushes if b.name == brush_name]
-        if source_widget is self:                  # If brush dragged from same widget
+        if source_widget is self:
             copy = False
         else:
             if b in self.brushes:
@@ -299,9 +82,8 @@ class BrushList(pixbuflist.PixbufList):
                 return True
         if not copy:
             source_widget.remove_brush(b)
-        self.grouplist.active_group = self.group # hm... could use some self.select() method somewhere?
         self.insert_brush(target_idx, b)
-        self.app.save_brushorder()
+        for f in self.bm.brushes_observers: f(self.brushes)
         return True
 
     def on_select(self, brush):
@@ -313,19 +95,21 @@ class BrushList(pixbuflist.PixbufList):
         changed = brush.reload_if_changed()
         if changed:
             self.update()
-        self.app.select_brush(brush)
-        #self.grouplist.set_active_group(self.group, brush)
+        self.bm.select_brush(brush)
 
 class BrushGroupsList(gtk.VBox):
     def __init__(self, app, window):
         gtk.VBox.__init__(self)
         self.app = app
+        self.bm = app.brushmanager
         self.parent_window = window
-        self.app.brushgroups.setdefault(DEFAULT_BRUSH_GROUP, [])
-        self.active_groups = [DEFAULT_BRUSH_GROUP]
         self.group_widgets = {}
         self.update()
-        self.app.selected_brush_observers.append(self.brush_selected_cb)
+        self.bm.selected_brush_observers.append(self.brush_selected_cb)
+        self.bm.groups_observers.append(self.brushes_modified_cb)
+
+    def brushes_modified_cb(self):
+        self.update()
 
     def update(self):
         old_widgets = self.group_widgets
@@ -333,7 +117,7 @@ class BrushGroupsList(gtk.VBox):
 
         self.foreach(self.remove)
 
-        for group in self.active_groups:
+        for group in self.bm.active_groups:
             if group in old_widgets:
                 w = old_widgets[group]
             else:
@@ -357,10 +141,10 @@ class GroupSelector(gtk.DrawingArea):
         gtk.DrawingArea.__init__(self)
 
         self.app = app
+        self.bm = app.brushmanager
+        self.bm.groups_observers.append(self.active_groups_changed_cb)
         self.brushgroups = brushgroups
 
-        self.active_groups = brushgroups.active_groups
-
         self.connect("expose-event", self.expose_cb)
         self.connect("button-press-event", self.button_press_cb)
        self.set_events(gdk.EXPOSURE_MASK |
@@ -371,6 +155,9 @@ class GroupSelector(gtk.DrawingArea):
         self.idx2group = {}
         self.layout = None
 
+    def active_groups_changed_cb(self):
+        self.queue_draw()
+
     def expose_cb(self, widget, event):
         cr = self.window.cairo_create()
         width, height = self.window.get_size()
@@ -387,7 +174,7 @@ class GroupSelector(gtk.DrawingArea):
         #attr = pango.AttrList()
         #attr.insert(pango.AttrBackground(0x5555, 0x5555, 0xffff, 5, 7))
 
-        all_groups = list(sorted(self.app.brushgroups.keys()))
+        all_groups = list(sorted(self.bm.groups.keys()))
 
         idx = 0
         text = ''
@@ -398,7 +185,7 @@ class GroupSelector(gtk.DrawingArea):
             for c in s:
                 self.idx2group[idx] = group
                 idx += 1
-            if group in self.active_groups:
+            if group in self.bm.active_groups:
                 text += '<b>' + group + '</b>'
             else:
                 text += group
@@ -428,10 +215,10 @@ class GroupSelector(gtk.DrawingArea):
         if event.button == 1:
             if not group:
                 return
-            if group in self.active_groups:
-                self.active_groups.remove(group)
+            if group in self.bm.active_groups:
+                self.bm.active_groups.remove(group)
             else:
-                self.active_groups.insert(0, group)
+                self.bm.active_groups.insert(0, group)
             self.brushgroups.update()
             self.queue_draw()
         elif event.button == 3:
@@ -441,10 +228,10 @@ class GroupSelector(gtk.DrawingArea):
     def context_menu(self, group):
         m = gtk.Menu()
         menu = []
-        menu = [ (_("New group..."), self.create_group) ]
+        menu = [ (_("New group..."), self.create_group_cb) ]
         if group:
-            menu += [ (_("Rename group..."), self.rename_group),
-                      (_("Delete group..."), self.delete_group)]
+            menu += [ (_("Rename group..."), self.rename_group_cb),
+                      (_("Delete group..."), self.delete_group_cb)]
         for label, callback in menu:
             mi = gtk.MenuItem(label)
             mi.connect('activate', callback, group)
@@ -452,42 +239,18 @@ class GroupSelector(gtk.DrawingArea):
         m.show_all()
         return m
 
-    def create_group(self, w, group):
-        new_group = ask_for_name(self.get_toplevel(), _('Create group'), '')
-        if new_group and new_group not in self.app.brushgroups:
-            self.app.brushgroups[new_group] = []
-            self.app.save_brushorder()
-            self.active_groups.insert(0, new_group)
-            self.brushgroups.update()
-            self.queue_draw()
-
-    def rename_group(self, w, old_group):
-        new_group = ask_for_name(self.get_toplevel(), _('Rename group'), old_group)
-        if new_group and new_group not in self.app.brushgroups:
-            self.app.brushgroups[new_group] = self.app.brushgroups[old_group]
-            del self.app.brushgroups[old_group]
-            self.app.save_brushorder()
-            self.brushgroups.update()
-            self.queue_draw()
-
-    def delete_group(self,w, group):
-        if run_confirm_dialog(self.get_toplevel(), _('Delete group %s') % group):
-            homeless_brushes = self.app.brushgroups[group]
-            del self.app.brushgroups[group]
-            if group in self.active_groups:
-                self.active_groups.remove(group)
-
-            for brushes in self.app.brushgroups.itervalues():
-                for b2 in brushes:
-                    if b2 in homeless_brushes:
-                        homeless_brushes.remove(b2)
-
-            # if the user has deleted the "deleted" group, we recreate it...?
-            deleted_brushes = self.app.brushgroups.setdefault(DELETED_BRUSH_GROUP, [])
-            for b in homeless_brushes:
-                deleted_brushes.insert(0, b)
-
-            self.app.save_brushorder()
-            self.brushgroups.update()
-            self.queue_draw()
-
+    def create_group_cb(self, w, group):
+        new_group = dialogs.ask_for_name(self.get_toplevel(), _('Create group'), '')
+        if new_group:
+            self.bm.create_group(new_group)
+
+    def rename_group_cb(self, w, old_group):
+        new_group = dialogs.ask_for_name(self.get_toplevel(), _('Rename group'), old_group)
+        # TODO: complain if target exists 
+        if new_group and new_group not in self.bm.groups:
+            self.bm.rename_group(old_group, new_group)
+
+    def delete_group_cb(self, w, group):
+        # TODO: complain if group == DELETED_BRUSH_GROUP
+        if dialogs.confirm(self.get_toplevel(), _('Delete group %s') % group):
+            self.bm.delete_group(group)
index 149c3e6..eccb888 100644 (file)
@@ -17,7 +17,7 @@ class Window(gtk.Window):
     def __init__(self, app):
         gtk.Window.__init__(self)
         self.app = app
-        self.app.selected_brush_observers.append(self.brush_selected_cb)
+        self.app.brushmanager.selected_brush_observers.append(self.brush_selected_cb)
         self.app.kbm.add_window(self)
 
         self.set_title(_('Brush settings'))
diff --git a/gui/dialogs.py b/gui/dialogs.py
new file mode 100644 (file)
index 0000000..df5a4ba
--- /dev/null
@@ -0,0 +1,60 @@
+# This file is part of MyPaint.
+# Copyright (C) 2009 by Martin Renold <martinxyz@gmx.ch>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+import gtk
+from gettext import gettext as _
+
+def confirm(window, title):
+    if not isinstance(window, gtk.Window):
+        window = window.get_toplevel()
+    d = gtk.Dialog(title,
+         window,
+         gtk.DIALOG_MODAL,
+         (gtk.STOCK_YES, gtk.RESPONSE_ACCEPT,
+          gtk.STOCK_NO, gtk.RESPONSE_REJECT))
+    response = d.run()
+    d.destroy()
+    return response == gtk.RESPONSE_ACCEPT
+
+
+def ask_for_name(window, title, default):
+    if not isinstance(window, gtk.Window):
+        window = window.get_toplevel()
+    d = gtk.Dialog(title,
+                   window,
+                   gtk.DIALOG_MODAL,
+                   (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
+                    gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
+
+    hbox = gtk.HBox()
+    d.vbox.pack_start(hbox)
+    hbox.pack_start(gtk.Label(_('Name')))
+
+    d.e = e = gtk.Entry()
+    e.set_text(default)
+    e.select_region(0, len(default))
+    def responseToDialog(entry, dialog, response):  
+        dialog.response(response)  
+    e.connect("activate", responseToDialog, d, gtk.RESPONSE_ACCEPT)  
+
+    hbox.pack_start(e)
+    d.vbox.show_all()
+    if d.run() == gtk.RESPONSE_ACCEPT:
+        result = d.e.get_text()
+    else:
+        result = None
+    d.destroy()
+    return result
+
+def error(window, message):
+    if not isinstance(window, gtk.Window):
+        window = window.get_toplevel()
+    d = gtk.MessageDialog(window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, message)
+    d.run()
+    d.destroy()
+
index dcdbcf2..11067c2 100644 (file)
@@ -448,7 +448,7 @@ class Window(gtk.Window):
 
                 if si:
                     self.app.brush.load_from_string(si.brush_string)
-                    self.app.select_brush(None)
+                    self.app.brushmanager.select_brush(None)
                     self.si = si # FIXME: should be a method parameter?
                     self.strokeblink_state.activate(action)
                 return
@@ -551,12 +551,13 @@ class Window(gtk.Window):
 
         print 'device change:', new_device.name, new_device.source
 
-        self.app.brush_by_device[old_device.name] = (self.app.selected_brush, self.app.brush.save_to_string())
+        bm = self.app.brushmanager
+        bm.brush_by_device[old_device.name] = (self.app.selected_brush, self.app.brush.save_to_string())
 
-        if new_device.name in self.app.brush_by_device:
-            brush_to_select, brush_settings = self.app.brush_by_device[new_device.name]
+        if new_device.name in bm.brush_by_device:
+            brush_to_select, brush_settings = bm.brush_by_device[new_device.name]
             # mark as selected in brushlist
-            self.app.select_brush(brush_to_select)
+            bm.select_brush(brush_to_select)
             # restore modifications (radius / color change the user made)
             self.app.brush.load_from_string(brush_settings)
         else:
@@ -672,8 +673,9 @@ class Window(gtk.Window):
     def context_cb(self, action):
         name = action.get_name()
         store = False
+        bm = self.app.brushmanager
         if name == 'ContextStore':
-            context = self.app.selected_context
+            context = bm.selected_context
             if not context:
                 print 'No context was selected, ignoring store command.'
                 return
@@ -683,8 +685,8 @@ class Window(gtk.Window):
                 store = True
                 name = name[:-1]
             i = int(name[-2:])
-            context = self.app.contexts[i]
-        self.app.selected_context = context
+            context = bm.contexts[i]
+        bm.selected_context = context
         if store:
             context.copy_settings_from(self.app.brush)
             preview = self.app.brushSelectionWindow.get_preview_pixbuf()
@@ -694,7 +696,7 @@ class Window(gtk.Window):
             # restore (but keep color)
             color = self.app.brush.get_color_hsv()
             context.set_color_hsv(color)
-            self.app.select_brush(context)
+            bm.select_brush(context)
             self.app.brushSelectionWindow.set_preview_pixbuf(context.preview)
 
     def about_cb(self, action):
index 087fb6b..92d7594 100644 (file)
@@ -15,7 +15,7 @@ class Window(gtk.Window):
     def __init__(self, app, setting, adj):
         gtk.Window.__init__(self)
         self.app = app
-        self.app.selected_brush_observers.append(self.brush_selected_cb)
+        self.app.brushmanager.selected_brush_observers.append(self.brush_selected_cb)
 
         self.set_title(setting.name)
         self.connect('delete-event', self.app.hide_window_cb)
index 19872c9..3e802f4 100644 (file)
@@ -6,17 +6,10 @@
 # the Free Software Foundation; either version 2 of the License, or
 # (at your option) any later version.
 
-"interface to MyBrush; hiding some C implementation details"
-# FIXME: bad file name, saying nothing about what's in here
-# FIXME: should split brush_lowlevel into its own gtk-independent module
 import mypaintlib
 from brushlib import brushsettings
-import gtk, os
 import helpers
 
-preview_w = 128
-preview_h = 128
-
 current_brushfile_version = 2
 
 # points = [(x1, y1), (x2, y2), ...] (at least two points, or None)
@@ -125,7 +118,7 @@ class Setting:
             points = [(x, func(y)) for x, y in points]
             self.set_points(i, points)
 
-class Brush_Lowlevel(mypaintlib.Brush):
+class Brush(mypaintlib.Brush):
     def __init__(self):
         mypaintlib.Brush.__init__(self)
         self.settings_observers = []
@@ -248,102 +241,3 @@ class Brush_Lowlevel(mypaintlib.Brush):
     def is_eraser(self):
         return self.setting_by_cname('eraser').base_value > 0.9
 
-class Brush(Brush_Lowlevel):
-    def __init__(self, app):
-        Brush_Lowlevel.__init__(self)
-        self.app = app
-        self.preview = None
-        self.name = None
-        self.preview_changed = True
-
-        self.settings_mtime = None
-        self.preview_mtime = None
-
-    def get_fileprefix(self, saving=False):
-        prefix = 'b'
-        if os.path.realpath(self.app.user_brushpath) == os.path.realpath(self.app.stock_brushpath):
-        #if os.path.samefile(self.app.user_brushpath, self.app.stock_brushpath):
-            # working directly on brush collection, use different prefix
-            prefix = 's'
-
-        if not self.name:
-            i = 0
-            while 1:
-                self.name = '%s%03d' % (prefix, i)
-                a = os.path.join(self.app.user_brushpath,self.name + '.myb')
-                b = os.path.join(self.app.stock_brushpath,self.name + '.myb')
-                if not os.path.isfile(a) and not os.path.isfile(b):
-                    break
-                i += 1
-        prefix = os.path.join(self.app.user_brushpath, self.name)
-        if saving: 
-            return prefix
-        if not os.path.isfile(prefix + '.myb'):
-            prefix = os.path.join(self.app.stock_brushpath,self.name)
-        if not os.path.isfile(prefix + '.myb'):
-            raise IOError, 'brush "' + self.name + '" not found'
-        return prefix
-
-    def delete_from_disk(self):
-        prefix = os.path.join(self.app.user_brushpath, self.name)
-        if os.path.isfile(prefix + '.myb'):
-            os.remove(prefix + '_prev.png')
-            os.remove(prefix + '.myb')
-            self.preview_changed = True # need to recreate when saving
-            return True
-        # stock brush cannot be deleted
-        return False
-
-    def remember_mtimes(self):
-        prefix = self.get_fileprefix()
-        self.preview_mtime = os.path.getmtime(prefix + '_prev.png')
-        self.settings_mtime = os.path.getmtime(prefix + '.myb')
-
-    def has_changed_on_disk(self):
-        prefix = self.get_fileprefix()
-        if self.preview_mtime != os.path.getmtime(prefix + '_prev.png'): return True
-        if self.settings_mtime != os.path.getmtime(prefix + '.myb'): return True
-        return False
-
-    def save(self):
-        prefix = self.get_fileprefix(saving=True)
-        if self.preview_changed:
-            self.preview.save(prefix + '_prev.png', 'png')
-            self.preview_changed = False
-        open(prefix + '.myb', 'w').write(self.save_to_string())
-        self.remember_mtimes()
-
-    def load(self, name):
-        self.name = name
-        prefix = self.get_fileprefix()
-
-        filename = prefix + '_prev.png'
-        pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
-        self.preview = pixbuf
-
-        if prefix.startswith(self.app.user_brushpath):
-            self.preview_changed = False
-        else:
-            # for saving, create the preview file even if not changed
-            self.preview_changed = True
-
-        filename = prefix + '.myb'
-        errors = self.load_from_string(open(filename).read())
-        if errors:
-            print '%s:' % filename
-            for line, reason in errors:
-                print line
-                print '==>', reason
-            print
-
-        self.remember_mtimes()
-
-    def reload_if_changed(self):
-        if self.settings_mtime is None: return
-        if self.preview_mtime is None: return
-        if not self.name: return
-        if not self.has_changed_on_disk(): return False
-        print 'Brush "' + self.name + '" has changed on disk, reloading it.'
-        self.load(self.name)
-        return True
-
index 8570f0c..a60432d 100644 (file)
@@ -14,7 +14,7 @@ import gobject, numpy
 
 import helpers, tiledsurface, pixbufsurface, backgroundsurface, mypaintlib
 import command, stroke, layer
-import brush # FIXME: the brush module depends on gtk and everything, but we only need brush_lowlevel
+import brush
 N = tiledsurface.N
 
 class SaveLoadError(Exception):
@@ -39,7 +39,7 @@ class Document():
     #   (split_stroke)
 
     def __init__(self):
-        self.brush = brush.Brush_Lowlevel()
+        self.brush = brush.Brush()
         self.stroke = None
         self.canvas_observers = []
         self.stroke_observers = [] # callback arguments: stroke, brush (brush is a temporary read-only convenience object)
@@ -103,7 +103,7 @@ class Document():
         self.split_stroke()
         # TODO: undo last stroke if it was very short... (but not at document level?)
         real_brush = self.brush
-        self.brush = brush.Brush_Lowlevel()
+        self.brush = brush.Brush()
         self.brush.copy_settings_from(real_brush)
 
         duration = 3.0
index 4d49370..c0acdaa 100644 (file)
@@ -66,7 +66,7 @@ class Stroke:
     def render(self, surface):
         assert self.finished
 
-        b = brush.Brush_Lowlevel()
+        b = brush.Brush()
         b.load_from_string(self.brush_settings) # OPTIMIZE: check if this is a performance bottleneck
 
         states = numpy.fromstring(self.brush_state, dtype='float32')
index cc991f7..b044faf 100755 (executable)
@@ -24,7 +24,7 @@ def directPaint():
 def brushPaint():
 
     s = tiledsurface.Surface()
-    b = brush.Brush_Lowlevel()
+    b = brush.Brush()
     #b.load_from_string(open('../brushes/s006.myb').read())
     b.load_from_string(open('../brushes/charcoal.myb').read())
 
@@ -108,9 +108,9 @@ def pngs_equal(a, b, exact=True):
     return equal
 
 def docPaint():
-    b1 = brush.Brush_Lowlevel()
+    b1 = brush.Brush()
     b1.load_from_string(open('../brushes/s008.myb').read())
-    b2 = brush.Brush_Lowlevel()
+    b2 = brush.Brush()
     b2.load_from_string(open('../brushes/redbrush.myb').read())
     b2.set_color_hsv((0.3, 0.4, 0.35))