diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/Base/const.py sketch1/Sketch/Base/const.py
--- sketch.orig/Sketch/Base/const.py	2005-03-19 18:48:56.000000000 +0100
+++ sketch1/Sketch/Base/const.py	2005-03-19 20:27:08.000000000 +0100
@@ -83,6 +83,11 @@
 REMOVED = "REMOVED"
 META_CHANGED = "META_CHANGED"
 
+# REQUESTS
+SET_COLOR = 'SET_COLOR'
+CLEAR_REQUEST = 'CLEAR_REQUEST'
+REQUEST_COLOR = 'REQUEST_COLOR'
+
 #
 #       Scripting Access
 #
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/canvas.py sketch1/Sketch/UI/canvas.py
--- sketch.orig/Sketch/UI/canvas.py	2005-03-19 18:48:56.000000000 +0100
+++ sketch1/Sketch/UI/canvas.py	2005-03-19 20:27:08.000000000 +0100
@@ -509,11 +509,12 @@
     def set_window_cursor(self, cursor):
         if cursor != self.last_cursor:
             self.last_cursor = cursor
-            if type(cursor) == IntType:
+            if type(cursor) is gtk.gdk.CursorType:
                 cursor = gtk.gdk.Cursor(cursor)
                 # else, assume cursor is a cursor object
             self.window.set_cursor(cursor)
-
+            
+             
     #
     #
     #
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/colorpanel.py sketch1/Sketch/UI/colorpanel.py
--- sketch.orig/Sketch/UI/colorpanel.py	1970-01-01 01:00:00.000000000 +0100
+++ sketch1/Sketch/UI/colorpanel.py	2005-03-19 20:44:16.000000000 +0100
@@ -0,0 +1,378 @@
+import gtk
+ 
+from colorsys import rgb_to_hsv, hsv_to_rgb
+
+from Sketch import _, const, Editor
+from Sketch.Base import Publisher
+from Sketch.const import CHANGED
+from Sketch.Graphics import CreateRGBColor, SolidPattern
+
+from skpanel import Card, MultiPanel
+from command import make_command
+
+import dnd
+from rendermisc import PatternSample, ImageRenderer
+
+# XXX Currently not needed. Will be used in memo
+class PreviewFactory(PatternSample, ImageRenderer):
+    pass
+
+class ColorHandler(Publisher):
+
+    # the lock blocks the response of the widget when they are changed    
+    locked = 0
+    memo = None
+    def __init__(self, name, set_method, get_method):
+        self.name = name
+        self._set_method = set_method
+        self._get_method = get_method
+        
+    def lock(self):
+        self.locked = 1
+        
+    def unlock(self):
+        self.locked = 0
+
+    def set_color(self, color):
+        if self.locked: return
+
+        self.lock()
+        self._set_method(color)
+        self.unlock()
+        
+    def changed(self, *args):
+        """called when one of the assigned widgets is changed"""
+
+        if self.locked: return
+
+        color = self._get_method()
+        memo = color.red, color.green, color.blue
+
+        # avoid multiple notifications
+        if self.memo == memo:
+            return
+        
+        self.lock()
+
+        self.issue(CHANGED, color)
+        self.unlock()
+        self.memo = memo
+        
+class SketchColorSelection(gtk.VBox, Publisher):
+
+    def __init__(self):
+        gtk.VBox.__init__(self)
+
+        self.colorsel = gtk.ColorSelection()
+        self.notebook = gtk.Notebook()
+
+        self.steel_widgets()
+        self.build_wheel()
+        self.build_RGB()
+        self.build_CMY()
+        self.build_HSV()
+        self.build_BW()
+        
+        name = gtk.HBox()
+        l = gtk.Label(_("Color name:"))
+        name.add(l)
+        name.add(self.hexentry)
+        
+        self.add(self.notebook)
+        self.add(self.sample)
+        self.add(name)
+        self.show_all()
+        self.changed(self.get_wheel())
+        #self.connect("destroy", self.destroyed)
+
+    def changed(self, color):
+        self.issue(const.CHANGED, color)
+                
+    def _create_memo(self, context):
+        color, opacity = self.get()
+        memo = ColorMemo(self, context, color, opacity)
+        memo.show_all()
+        memo.show()
+        
+    def steel_widgets(self):
+        colorsel = self.colorsel
+        h = colorsel.get_children()[0]
+        v1, v2 = h.get_children()
+        hsv, sample = v1.get_children()
+
+        self.wheel = hsv
+        v1.remove(hsv)
+        
+        t, f = v2.get_children()
+
+        self.sample = sample
+        v1.remove(sample)
+        
+        hexentry, l = t.get_children()[:2]
+        self.hexentry = hexentry
+        t.remove(hexentry)
+
+    def build_wheel(self):
+        handler = ColorHandler("wheel", self.set_wheel, self.get_wheel)
+        self.Subscribe(CHANGED, handler.set_color)
+        handler.Subscribe(CHANGED, self.changed)
+        self.wheel.connect("changed", handler.changed)
+
+        page = gtk.Alignment(xalign=0.5, yalign=0.5)
+        page.add(self.wheel)
+        l = gtk.Label(_("Wheel"))
+        self.notebook.append_page(page, l)
+        page.show_all()
+        
+    def set_wheel(self, color):
+        self.colorsel.set_current_color(color)
+
+    def get_wheel(self):
+        return self.colorsel.get_current_color()
+            
+    def build_BW(self):
+        handler = ColorHandler("BW", self.set_BW, self.get_BW)
+        self.Subscribe(CHANGED, handler.set_color)
+        handler.Subscribe(CHANGED, self.changed)
+
+        page = gtk.Alignment(yalign=0.5, yscale = 0.5, xscale=1)
+        table = gtk.Table(2, 1)
+        l = gtk.Label(_("White:"))
+        l.set_alignment(1, 0.5)
+        l.set_padding(10, 2)
+        table.attach(l, 0, 1, 0, 1)
+        self.bw = gtk.Adjustment(0.0, 0.0, 100.0, 1.0, 1.0, 1.0)
+        s = gtk.SpinButton(self.bw, digits=1)
+        s.connect("changed", handler.changed)
+        table.attach(s, 1, 2, 0, 1)
+        page.add(table)
+        l = gtk.Label(_("BW"))        
+        self.notebook.append_page(page, l)
+
+    def get_BW(self):
+        val=int(0.5+self.bw.get_value()*65535./100.)
+        return gtk.gdk.Color(val, val, val)
+
+    def set_BW(self, color):
+        value = (color.red+color.green+color.blue)*100./(3*65535.)
+        self.bw.set_value(value)
+
+    def build_RGB(self):
+        handler = ColorHandler("RGB", self.set_RGB, self.get_RGB)
+        self.Subscribe(CHANGED, handler.set_color)
+        handler.Subscribe(CHANGED, self.changed)
+
+        page = gtk.Alignment(yalign=0.5, yscale = 0.5, xscale=1)
+
+        self.red = gtk.Adjustment(0.0, 0.0, 255, 1.0, 1.0, 1.0)
+        self.green = gtk.Adjustment(0.0, 0.0, 255, 1.0, 1.0, 1.0)
+        self.blue = gtk.Adjustment(0.0, 0.0, 255, 1.0, 1.0, 1.0)
+
+        table = gtk.Table(2, 3)
+        
+        row = 0
+        for text, adj in ((_("Red:"), self.red),
+                          (_("Green:"), self.green),
+                          (_("Blue:"), self.blue)):
+            l = gtk.Label(text)
+            l.set_alignment(1, 0.5)
+            l.set_padding(10, 2)
+            table.attach(l, 0, 1, row, row+1)
+
+            s = gtk.SpinButton(adj, digits=0)
+            s.connect("changed", handler.changed)
+            table.attach(s, 1, 2, row, row+1)
+            row = row+1
+            
+        table.show_all()
+        page.add(table)
+        l = gtk.Label(_("RGB"))
+        self.notebook.append_page(page, l)
+
+    def get_RGB(self):
+        r=int(0.5+self.red.get_value()*65535./255.)
+        g=int(0.5+self.green.get_value()*65535./255.)
+        b=int(0.5+self.blue.get_value()*65535./255.)
+        return gtk.gdk.Color(r, g, b)
+            
+    def set_RGB(self, color):
+        f = 255./65535.
+        self.red.set_value(f*color.red)
+        self.green.set_value(f*color.green)
+        self.blue.set_value(f*color.blue)
+
+    def build_CMY(self):
+        handler = ColorHandler("CMY", self.set_CMY, self.get_CMY)
+        self.Subscribe(CHANGED, handler.set_color)
+        handler.Subscribe(CHANGED, self.changed)
+
+        page = gtk.Alignment(yalign=0.5, yscale = 0.5, xscale=1)
+
+        self.cyan = gtk.Adjustment(0.0, 0.0, 100.0, 1.0, 1.0, 1.0)
+        self.magenta = gtk.Adjustment(0.0, 0.0, 100.0, 1.0, 1.0, 1.0)
+        self.yellow = gtk.Adjustment(0.0, 0.0, 100.0, 1.0, 1.0, 1.0)
+
+        table = gtk.Table(2, 3)
+        
+        row = 0
+        for text, adj in ((_("Cyan:"), self.cyan),
+                          (_("Magenta:"), self.magenta),
+                          (_("Yellow:"), self.yellow)):
+            l = gtk.Label(text)
+            l.set_alignment(1, 0.5)
+            l.set_padding(10, 2)
+            table.attach(l, 0, 1, row, row+1)
+
+            s = gtk.SpinButton(adj, digits=1)
+            s.connect("changed", handler.changed)
+            table.attach(s, 1, 2, row, row+1)
+            row = row+1
+            
+        table.show_all()
+        page.add(table)
+        l = gtk.Label(_("CMY"))
+        self.notebook.append_page(page, l)
+
+    def get_CMY(self, *args):
+        r = 100-int(0.5+self.cyan.get_value()*65535./100.)
+        g = 100-int(0.5+self.magenta.get_value()*65535./100.)
+        b = 100-int(0.5+self.yellow.get_value()*65535./100.)        
+        return gtk.gdk.Color(r, g, b)
+        
+    def set_CMY(self, color):
+        f = 100./65535.
+        self.cyan.set_value(100-f*color.red)
+        self.magenta.set_value(100-f*color.green)
+        self.yellow.set_value(100-f*color.blue)
+
+    def build_HSV(self):
+        handler = ColorHandler("HSV", self.set_HSV, self.get_HSV)
+        self.Subscribe(CHANGED, handler.set_color)
+        handler.Subscribe(CHANGED, self.changed)
+
+        page = gtk.Alignment(yalign=0.5, yscale = 0.5, xscale=1)
+
+        self.hue = gtk.Adjustment(0.0, 0.0, 100.0, 1.0, 1.0, 1.0)
+        self.saturation = gtk.Adjustment(0.0, 0.0, 100.0, 1.0, 1.0, 1.0)
+        self.brightness = gtk.Adjustment(0.0, 0.0, 100.0, 1.0, 1.0, 1.0)
+
+        table = gtk.Table(2, 3)
+
+        row = 0
+        for text, adj in ((_("Hue:"), self.hue),
+                          (_("Saturation:"), self.saturation),
+                          (_("Brightness:"), self.brightness)):
+            l = gtk.Label(text)
+            l.set_alignment(1, 0.5)
+            l.set_padding(10, 2)
+            table.attach(l, 0, 1, row, row+1)
+
+            s = gtk.SpinButton(adj, digits=1)
+            s.connect("changed", handler.changed)
+            table.attach(s, 1, 2, row, row+1)
+            row = row+1
+            
+        page.add(table)
+        table.show_all()
+        l = gtk.Label(_("HSV"))
+        self.notebook.append_page(page, l)
+
+    def get_HSV(self):
+        f = 1./101.
+        h = f*self.hue.get_value()
+        
+        s = f*self.saturation.get_value()
+        v = f*self.brightness.get_value()
+        r, g, b = map(lambda x:int(x*65535), hsv_to_rgb(h, s, v))
+        return gtk.gdk.Color(r, g, b)
+        
+    def set_HSV(self, color):
+        f = 1/65536.
+        h, s, v = rgb_to_hsv(f*color.red, f*color.green, f*color.blue)
+        f = 101
+        self.hue.set_value(f*h)
+        self.saturation.set_value(f*s)
+        self.brightness.set_value(f*v)
+        
+    def set_current_color(self, color):        
+        cmap = self.get_colormap()
+        r, g, b = color
+        gdk_color = cmap.alloc_color(int(round(0xFFFF * r)),
+                                     int(round(0xFFFF * g)),
+                                     int(round(0xFFFF * b)))
+        self.colorsel.set_current_color(gdk_color)
+                    
+    def set_previous_color(self, color):
+        cmap = self.get_colormap()
+        r, g, b = color
+        gdk_color = cmap.alloc_color(int(round(0xFFFF * r)),
+                                     int(round(0xFFFF * g)),
+                                     int(round(0xFFFF * b)))
+        self.colorsel.set_previous_color(gdk_color)
+
+    def get(self):
+        gdk_color = self.colorsel.get_current_color()
+        #opacity = self.colorsel.get_current_alpha()/65535.
+        color = CreateRGBColor(gdk_color.red / 65536.0,
+                               gdk_color.green / 65536.0,
+                               gdk_color.blue / 65536.0)
+        return color
+
+
+class ColorCard(Card):
+    def __init__(self, application):        
+        Card.__init__(self, application, _("Color"))
+        
+    def build_card(self):
+        self.colorsel = SketchColorSelection()
+        self.colorsel.Subscribe(CHANGED, self.color_changed)
+        self.pack_start(self.colorsel)
+        self.show_all()
+
+    def set_current_color(self, color):        
+        self.colorsel.set_current_color(color)
+                    
+    def set_previous_color(self, color):
+        self.colorsel.set_previous_color(color)
+
+    def color_changed(self, *args):
+        requesthandler = self.context.application.requesthandler
+        requesthandler.SetColor(self.colorsel.get())
+        
+class ColorPanel(MultiPanel):
+    multiple = 1
+    def __init__(self, context):        
+        MultiPanel.__init__(self, context, _("Color"))
+        requesthandler = self.context.application.requesthandler
+        requesthandler.AddColorPanel()
+        requesthandler.Subscribe(const.REQUEST_COLOR,
+                                 self.color_requested)
+
+    def destroyed(self, *args):
+        requesthandler = self.context.application.requesthandler
+        requesthandler.RemoveColorPanel()
+        requesthandler.Unsubscribe(const.REQUEST_COLOR,
+                                   self.color_requested)
+        MultiPanel.destroyed(self)
+        
+    def build_window(self):
+        MultiPanel.build_window(self)
+
+        self.build_menu()
+
+        color_card = ColorCard(self.context.application)
+        self.attach_card(color_card)
+        self.color_card = color_card
+        
+    def build_menu(self):        
+        items = ('close_panel',
+                 'duplicate_panel')            
+        #menudata = Editor.BuildMenuTree('<colorpanelmenu>', '', items)
+        import tmpmenu
+        menudata = tmpmenu.BuildMenuTree('<colorpanelmenu>', '', items)
+        self.set_menudata(menudata)
+        
+    def color_requested(self, color):
+        self.Raise()
+        self.color_card.set_previous_color(color)
+
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/command.py sketch1/Sketch/UI/command.py
--- sketch.orig/Sketch/UI/command.py	2005-03-19 18:48:56.000000000 +0100
+++ sketch1/Sketch/UI/command.py	2005-03-19 20:27:08.000000000 +0100
@@ -16,8 +16,19 @@
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 from Sketch.Editor import Command
+from types import TupleType
 
 class WindowCommand(Command):
 
     def Sensitive(self, context):
         return context.window is not None and Command.Sensitive(self, context)
+
+def make_command(label, func = None, args = (), sensitive = None):
+    # XXX extend options
+    if type(label) == TupleType:
+            return apply(make_command, label)
+
+    if label is None:
+        return None
+    return Command("", label, func, args, sensitive=sensitive)
+    
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/contextproxy.py sketch1/Sketch/UI/contextproxy.py
--- sketch.orig/Sketch/UI/contextproxy.py	1970-01-01 01:00:00.000000000 +0100
+++ sketch1/Sketch/UI/contextproxy.py	2005-03-19 20:27:08.000000000 +0100
@@ -0,0 +1,69 @@
+
+from Sketch.Base import Publisher
+
+
+class ContextProxy(Publisher):
+    """A generic class to create special contexts.
+    """
+
+    # list of own signals
+    channels = ()
+
+    # the parent (usually an application context)
+    parent = None
+    
+    def __init__(self, parent=None):
+
+        # Channels which are forwarded from the parent. The channal
+        # name is translated upon forwarding. The key is the original
+        # name, value is the new one.        
+        self.forwarded_channels = {}
+        
+        if parent is not None:
+            self.SetParent(parent)
+
+    def __getattr__(self, name):
+        #print "ContextProxy(%i): can't find " % id(self), name
+        if self.parent is None:
+            raise AttributeError()
+        return getattr(self.parent, name)
+
+    def forward(self, *args):
+        args = (args[-1],) + args[:-1]
+        #print "forwarding ", args, self
+        apply(self.issue, args)
+    
+    def SetParent(self, parent):
+        if self.parent is not None:
+            self.UnsetParent()
+        self.parent = parent
+        for channel, alias in self.forwarded_channels.items():
+            parent.Subscribe(channel, self.forward, alias)
+
+    def UnsetParent(self):
+        parent = self.parent
+        for channel, alias in self.forwarded_channels.items():
+            parent.Unsubscribe(channel, self.forward, alias)
+        self.parent = None
+
+    def AddForward(self, channel, alias=None):
+        if alias is None:
+            alias = channel
+        if self.parent is not None:
+            self.parent.Subscribe(channel, self.forward, alias)
+        self.forwarded_channels[channel] = alias
+
+    def Subscribe(self, channel, func, *args):
+
+        # check whether the channel needs to be forwarded
+        if not (channel in self.channels or
+                channel in self.forwarded_channels):
+            self.AddForward(channel)
+        
+        Publisher.Subscribe(self, channel, func, *args)
+       
+    def Destroy(self):
+        #print "destroying context ", self
+        self.UnsetParent()
+        Publisher.Destroy(self)
+        
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/dnd.py sketch1/Sketch/UI/dnd.py
--- sketch.orig/Sketch/UI/dnd.py	1970-01-01 01:00:00.000000000 +0100
+++ sketch1/Sketch/UI/dnd.py	2005-03-19 20:27:08.000000000 +0100
@@ -0,0 +1,23 @@
+from Sketch.Graphics import CreateRGBColor
+
+color_target = [('application/x-color', 0, 0)]
+
+
+def encode_color(color, opacity=1.):
+    data = ""
+    for a in tuple(color)+(opacity,):
+        i = int(a*65535.+0.5)                
+        data += chr(i & 255) + chr(i >> 8)
+    return data
+            
+def decode_color(data):
+    def decode(s):
+        b, a = map(ord, s)                
+        return ((a << 8) + b)/ 65535.
+    bin = data.data
+    r = decode(bin[:2])
+    g = decode(bin[2:4])
+    b = decode(bin[4:6])
+    #opacity = decode(bin[6:8])            
+    color = CreateRGBColor(r, g, b)
+    return color
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/fillpanel.py sketch1/Sketch/UI/fillpanel.py
--- sketch.orig/Sketch/UI/fillpanel.py	2005-03-19 18:48:56.000000000 +0100
+++ sketch1/Sketch/UI/fillpanel.py	2005-03-19 21:36:01.000000000 +0100
@@ -1,3 +1,7 @@
+# TODO:
+#
+# - check whether the selection changed before removing the und info
+
 # Sketch - A Python-based interactive drawing program
 # Copyright (C) 1997, 1998, 1999, 2000, 2001, 2003 by Bernhard Herzog
 #
@@ -21,17 +25,21 @@
 
 import Sketch
 import Sketch.Graphics
-from Sketch import _, Rect, Trafo, Point, Polar, _skgtk
+from Sketch import _, const, Rect, Trafo, Point, Polar, _skgtk
 from Sketch.Base import Publisher
-from Sketch.Base.const import EDITED, CHANGED
+from Sketch.Base.const import EDITED, CHANGED, SELECTION, OBJECT_CHANGED
 from Sketch.Graphics import StandardColors, EmptyLineStyle, PropertyStack,\
      LinearGradient, RadialGradient, ConicalGradient, HatchingPattern, \
      EmptyPattern, SolidPattern, CreateRGBColor, CreateSimpleGradient, \
      MultiGradient
+from Sketch import Editor
 from Sketch.Editor.const import ConstraintMask
+from gtkmisc import SketchDrawingArea, get_color
+from skpanel import MultiPanel, PanelMenu, Card
+from command import make_command
+from request import ColorButton, ColorRequestButton 
 
-from gtkmisc import SketchDrawingArea, get_color, ColorButton
-from skpanel import SketchPanel
+import dnd
 import skpixmaps
 pixmaps = skpixmaps.pixmaps
 
@@ -87,7 +95,7 @@
         self.UpdateWhenIdle()
         self.compute_trafo()
 
-
+    
 class InteractiveSample(PatternSample, Publisher):
 
     allow_edit_center = 0
@@ -107,11 +115,13 @@
         self.drawn = 0
         self.dragging = 0
         self.drag_button = None
-
+        
+    
     def destroy(self):
         Publisher.Destroy(self)
         PatternSample.destroy(self)
 
+        
     def SetEditMode(self, center, direction):
         self.allow_edit_center = center
         self.allow_edit_dir = direction
@@ -121,7 +131,7 @@
         x, y = pos
         x =      x  * width
         y = (1 - y) * height
-        self.center = (x, y)
+        self.center = map(int, (x, y))
 
     def init_gcs(self):
         PatternSample.init_gcs(self)
@@ -184,9 +194,9 @@
         if self.current_mode == 0:
             if constraint:
                 x, y = self.constrain_center(x, y)
-                x = int(x)
-                y = int(y)
             width, height = self.window.get_size()
+            x = int(x)
+            y = int(y)
             self.window.draw_line(self.inv_gc, 0, y, width, y)
             self.window.draw_line(self.inv_gc, x, 0, x, height)
         else:
@@ -261,6 +271,9 @@
     def EditModes(self):
         return (0, 0)
 
+    def Passivate(self):
+        pass
+    
 
 class EmptyPatternFrame(gtk.VBox, PatternFrame):
 
@@ -274,35 +287,47 @@
 
 class SolidPatternFrame(gtk.VBox, PatternFrame):
 
-    def __init__(self):
+    def __init__(self, application):
         gtk.VBox.__init__(self)
-        PatternFrame.__init__(self, SolidPattern(StandardColors.black))
-        button = gtk.Button(_("Choose Color..."))
-        self.pack_start(button, expand = 0)
-        button.show()
-        button.connect("clicked", self.ChooseColor)
+        color = StandardColors.black
+        PatternFrame.__init__(self, SolidPattern(color))
+        self.button = ColorRequestButton(application,
+                                         _("Choose Color..."),
+                                         color)
+        self.pack_start(self.button, expand = 0)
+        self.button.show()
+        self.button.Subscribe(CHANGED, self.Set)
 
     def ChooseColor(self, *args):
         color = get_color(_("Select Fill Color"), self.pattern.Color())
         if color is not None:
-            self.SetPattern(SolidPattern(color))
-            self.changed()
+            self.Set(color)
+            
+    def Set(self, color):        
+        self.SetPattern(SolidPattern(color))
+        self.changed()
 
+    def Passivate(self):
+        self.button.Passivate()
+        
 
 class GradientPatternFrame(gtk.VBox, PatternFrame):
 
-    def __init__(self):
+    def __init__(self, application):
         gtk.VBox.__init__(self)
         start_color = StandardColors.black
         end_color = StandardColors.white
         gradient = CreateSimpleGradient(start_color, end_color)
         PatternFrame.__init__(self, LinearGradient(gradient))
         box = gtk.HBox()
-        self.start_button = ColorButton(_("Start"), _("Start Color"),
+        self.start_button = ColorButton(application,
+                                        _("Start"),
                                         start_color)
         self.start_button.Subscribe(CHANGED, self.set_color, 0)
         box.pack_start(self.start_button)
-        self.end_button = ColorButton(_("End"), _("End Color"), end_color)
+        self.end_button = ColorButton(application,
+                                      _("End"),
+                                      end_color)
         self.end_button.Subscribe(CHANGED, self.set_color, 1)
         box.pack_start(self.end_button)
         self.pack_start(box, expand = 0)
@@ -460,17 +485,22 @@
             self.SetPattern(self.pattern.WithDirection(dir))
             self.changed()
 
+    def Passivate(self):
+        self.start_button.Passivate()
+        self.end_button.Passivate()
+        
+
 class HatchingPatternFrame(gtk.VBox, PatternFrame):
 
-    def __init__(self):
+    def __init__(self, application):
         gtk.VBox.__init__(self)
         fg = StandardColors.red
         bg = StandardColors.white
         PatternFrame.__init__(self, HatchingPattern(fg, bg))
-        self.fg_button = ColorButton(_("Foreground"),_("Foreground Color"), fg)
+        self.fg_button = ColorButton(application, _("Foreground"), fg)
         self.fg_button.Subscribe(CHANGED, self.set_color, 0)
         self.pack_start(self.fg_button, expand = 0)
-        self.bg_button = ColorButton(_("Background"),_("Background Color"), bg)
+        self.bg_button = ColorButton(application, _("Background"), bg)
         self.bg_button.Subscribe(CHANGED, self.set_color, 1)
         self.pack_start(self.bg_button, expand = 0)
 
@@ -494,6 +524,10 @@
         self.SetPattern(pattern)
         self.changed()
 
+    def Passivate(self):
+        self.bg_button.Passivate()
+        self.fg_button.Passivate()
+
 
 class ImageTilePatternFrame(gtk.VBox, PatternFrame):
 
@@ -504,32 +538,40 @@
         self.pack_start(label)
         label.show()
 
-class FillPanel(SketchPanel):
-
-    def __init__(self, context):
-        SketchPanel.__init__(self, context, _("Fill"))
-        self.context.Subscribe(Sketch.Base.const.SELECTION,
-                               self.context_changed)
+class FillInspector(Card):
+    def __init__(self, application):
+        Card.__init__(self, application, _("Fill"))
         self.active_frame = None
-        self.set_name("fill_panel")
-
-    def destroyed(self, *args):
-        self.context.Unsubscribe(Sketch.Base.const.SELECTION,
-                                 self.context_changed)
-        apply(SketchPanel.destroyed, (self,) + args)
-
-    def build_window(self):
-        vbox = self.vbox
+        self.has_changes = 1
 
+        # locks fulfill two tasks here: blocking "apply" and blocking
+        # the unsetting of the "undo_last" flag.        
+        self.locked = 0
+
+        # a flag which signals whether the last transactions should be
+        # undone when the next one is applied. The flag is unset
+        # automatically when antoher transaction happend or the
+        # selection changes .
+        self.undo_last = 0
+
+        for signal in (CHANGED, SELECTION, EDITED, OBJECT_CHANGED):
+            self.context.Subscribe(signal, self.context_changed)
+
+    def build_card(self):
+        application = self.context.application
+        
         self.sample = InteractiveSample()
         self.sample.set_size_request(100, 100)
-        vbox.pack_start(self.sample)
+        self.pack_start(self.sample)
         self.sample.Subscribe(EDITED, self.sample_edited)
+        self.sample.drag_dest_set(gtk.DEST_DEFAULT_ALL, dnd.color_target,
+                                  gtk.gdk.ACTION_COPY | gtk.gdk.ACTION_MOVE)
+        self.sample.connect('drag_data_received', self.drag_data_received)
 
         self.empty_frame = EmptyPatternFrame()
-        self.solid_frame = SolidPatternFrame()
-        self.gradient_frame = GradientPatternFrame()
-        self.hatching_frame = HatchingPatternFrame()
+        self.solid_frame = SolidPatternFrame(application)
+        self.gradient_frame = GradientPatternFrame(application)
+        self.hatching_frame = HatchingPatternFrame(application)
         self.imagetile_frame = ImageTilePatternFrame()
 
         menu = gtk.Menu()
@@ -544,12 +586,12 @@
             item.show()
             item.connect("activate", self.set_pattern_type, frame)
             notebook.append_page(frame, gtk.Label(name))
-            frame.Subscribe(CHANGED, self.update_sample, frame)
+            frame.Subscribe(CHANGED, self.frame_changed, frame)
 
         self.optionmenu = gtk.OptionMenu()
         self.optionmenu.set_menu(menu)
-        vbox.pack_start(self.optionmenu, expand = 0)
-        vbox.pack_start(notebook)
+        self.pack_start(self.optionmenu, expand = 0)
+        self.pack_start(notebook)
         notebook.set_show_tabs(0)
 
         box = gtk.HBox()
@@ -562,57 +604,75 @@
                                              lower = 0, upper = 100,
                                              step_incr = 1)
         entry = gtk.SpinButton(adjustment = self.adjust_opacity, digits = 0)
+        entry.connect('changed', self.auto_apply)
         box.pack_start(entry)
-        vbox.pack_start(box, expand = 0)
+        self.pack_start(box, expand = 0)
+        self.show_all()
 
-        self.apply_button = button = gtk.Button(_("Apply"))
-        button.connect("clicked", self.do_apply)
-        self.action_area.pack_start(button, expand = 0, fill = 0)
-        button = gtk.Button(_("Close"))
-        button.connect("clicked", self.close)
-        self.action_area.pack_start(button, expand = 0, fill = 0)
+    def drag_data_received(self, w, context, x, y, data, info, time):
+        d = data.data
 
-        vbox.show_all()
+        if data and data.format == 16:
+            color = dnd.decode_color(data)
+            self.activate_frame(self.solid_frame)            
+            self.solid_frame.Set(color)            
+            self.auto_apply()
+            context.finish(gtk.TRUE, gtk.FALSE, time)
+        else:
+            context.finish(gtk.FALSE, gtk.FALSE, time)
 
     def set_pattern_type(self, widget, frame):
         self.activate_frame(frame)
 
-    def context_changed(self, *args):
-        if self.context.HasEditor():
-            self.apply_button.set_sensitive(1)
-            object = self.context.editor.CurrentObject()
-            if object is not None:
-                if object.has_fill:
-                    pattern = object.Properties().fill_pattern
-                    frame = self.empty_frame
-                    if pattern.is_Solid:
-                        frame = self.solid_frame
-                    elif pattern.is_Gradient:
-                        frame = self.gradient_frame
-                    elif pattern.is_Hatching:
-                        frame = self.hatching_frame
-                    elif pattern.is_Image:
-                        frame = self.imagetile_frame
-                    if frame is not None:
-                        frame.SetPattern(pattern)
-                        self.activate_frame(frame)
+    def lock(self):        
+        self.locked = 1
 
-                    opacity = object.Properties().fill_opacity
-                    self.adjust_opacity.set_value(round(opacity * 100))
-        else:
-            self.apply_button.set_sensitive(0)
+    def unlock(self):
+        self.locked = 0
+        
+    def update(self):
+        if not self.context.HasEditor() or self.window is None:
+            return
+        
+        self.lock()        
+        object = self.context.editor.CurrentObject()
+        if object is not None:
+            if object.has_fill:
+                pattern = object.Properties().fill_pattern
+                frame = self.empty_frame
+                if pattern.is_Solid:
+                    frame = self.solid_frame
+                elif pattern.is_Gradient:
+                    frame = self.gradient_frame
+                elif pattern.is_Hatching:
+                    frame = self.hatching_frame
+                elif pattern.is_Image:
+                    frame = self.imagetile_frame
+                if frame is not None:
+                    frame.SetPattern(pattern)
+                    self.activate_frame(frame)
+
+                opacity = object.Properties().fill_opacity
+                self.adjust_opacity.set_value(round(opacity * 100))
+                
+        self.has_changes = 0
+        self.changed()
+        self.unlock()
 
     def activate_frame(self, frame):
+        if self.active_frame is not None:
+            self.active_frame.Passivate()
         self.active_frame = frame
         index = self.notebook.page_num(frame)
         self.notebook.set_current_page(index)
         self.optionmenu.set_history(index)
-        self.update_sample(frame)
+        self.frame_changed(frame)
 
-    def update_sample(self, frame):
+    def frame_changed(self, frame):
         self.sample.SetPattern(frame.Pattern())
         self.sample.SetCenter(frame.Center())
         apply(self.sample.SetEditMode, frame.EditModes())
+        self.auto_apply()
 
     def sample_edited(self, what, detail):
         if self.active_frame is not None:
@@ -620,11 +680,77 @@
                 self.active_frame.SetCenter(detail)
             else:
                 self.active_frame.SetDirection(detail)
+            self.auto_apply()
+        
+    def CanApply(self):
+        return self.has_changes
+    
+    def context_changed(self, *args):        
+        if self.locked:
+            return
+        
+        self.has_changes = 1
+        self.undo_last = 0
+        if self.IsAutoUpdate():
+            self.update()
+        else:
+            self.changed()
 
-    def do_apply(self, *args):
-        if self.active_frame is not None and self.context.HasEditor():
-            pattern = self.active_frame.Pattern()
-            opacity = self.adjust_opacity.value / 100.0
-            self.context.editor.SetProperties(fill_pattern = pattern,
-                                              fill_opacity = opacity)
+    def auto_apply(self, *args):
+        if self.locked:
+            return
+        
+        self.has_changes = 1
+        if self.IsAutoApply():
+            self.do_apply()
+        else:
+            self.changed()
+            
+    def do_apply(self, all=0):
+        context = self.context
+        if not context.HasEditor():
+            return
 
+        self.has_changes = 0
+        
+        # prevent the undo_last flag from being unset  
+        self.lock()
+        pattern = self.active_frame.Pattern()
+        opacity = self.adjust_opacity.value / 100.0
+        undo = context.document.undo
+        
+        if self.undo_last:
+            undo.Undo() 
+            
+        context.editor.SetProperties(fill_pattern = pattern,
+                                     fill_opacity = opacity)
+        self.undo_last = 1
+        self.unlock()
+        self.changed()
+        
+
+class FillPanel(MultiPanel):
+    multiple = 1
+    def __init__(self, context):
+        MultiPanel.__init__(self, context, _("Fill"))
+
+    def build_window(self):
+        MultiPanel.build_window(self)
+        card = FillInspector(self.context.application)
+        self.build_menu()
+        self.attach_card(card)
+
+    def build_menu(self):        
+        items = (
+            #'issue_panel_changed',
+                 'close_panel',
+                 'duplicate_panel',
+                 None,
+                 'auto_update',
+                 'auto_apply',
+                 'update_panel',
+                 'do_apply')
+        #tree = Sketch.BuildMenuTree('<fillmenu>', '', items)
+        import tmpmenu
+        tree = tmpmenu.BuildMenuTree('<fillmenu>', '', items)
+        self.set_menudata(tree)
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/gtkmisc.py sketch1/Sketch/UI/gtkmisc.py
--- sketch.orig/Sketch/UI/gtkmisc.py	2005-03-19 18:48:56.000000000 +0100
+++ sketch1/Sketch/UI/gtkmisc.py	2005-03-19 20:27:08.000000000 +0100
@@ -17,10 +17,38 @@
 
 import os
 import gtk
+import Sketch
+import types
+import math
 from Sketch import _, _skgtk
 from Sketch.Base import Publisher, const
 from Sketch.Graphics import CreateRGBColor
 from gtkdevice import GraphicsDevice
+from Sketch.Lib.units import unit_dict
+import skpixmaps
+import gobject
+
+                  
+class CommandButton(gtk.Button):
+    def __init__(self, command, context):
+        gtk.Button.__init__(self)
+        self.command = command
+        self.context = context
+        self.subscribe()
+        self.clicked_id = self.connect('clicked', self.call_command)
+
+    def subscribe(self):
+        for channel in self.command.channels:
+            self.context.Subscribe(channel, self.changed)
+        self.context.Subscribe(Sketch.Base.const.CHANGED, self.changed)
+        self.changed()
+
+    def changed(self, *args):
+        self.set_sensitive(self.command.Sensitive(self.context))
+
+    def call_command(self, widget):
+        self.command.Execute(self.context)
+
 
 
 class MessageBox(gtk.Dialog):
@@ -184,59 +212,29 @@
         return CreateRGBColor(gdk_color.red / 65536.0,
                               gdk_color.green / 65536.0,
                               gdk_color.blue / 65536.0)
-
-
+    
 def get_color(title, color = None):
     win = ColorDialog(title, color)
     return win.run()
 
-def _gtk_colors(cmap, color):
-    r, g, b = color
-    bg = cmap.alloc_color(int(round(r * 0xFFFF)),
-                          int(round(g * 0xFFFF)),
-                          int(round(b * 0xFFFF)))
-    luminance = 0.30*r + 0.59*g + 0.11*b
-    #luminance = (min(color) + max(color)) / 2
-    if luminance > 0.5:
-        fg = cmap.alloc_color(0, 0, 0)
-    else:
-        fg = cmap.alloc_color(0xFFFF, 0xFFFF, 0xFFFF)
-    return fg, bg
-
-class ColorButton(gtk.Button, Publisher):
-
-    def __init__(self, title, dialog_title, color):
-        gtk.Button.__init__(self, title)
-        self.dialog_title = dialog_title
-        self.color = color
-        self.connect("clicked", self.choose_color)
-        self.connect("realize", self.realized)
-
-    def realized(self, *args):
-        self.SetColor(self.color)
-
-    def SetColor(self, color):
-        self.color = color
-        style = self.get_style().copy()
-        fg, bg = _gtk_colors(self.get_colormap(), color)
-        for i in range(5):
-            style.fg[i] = fg
-            style.bg[i] = bg
-        self.set_style(style)
-        self.get_children()[0].set_style(style)
-
-    def Color(self):
-        return self.color
+            
+class FontDialog(ModalDialogMixin, gtk.FontSelectionDialog):
 
-    def choose_color(self, *args):
-        color = get_color(self.dialog_title, self.color)
-        if color is not None:
-            self.SetColor(color)
-            self.issue(const.CHANGED, self.color)
+    def __init__(self, title, font = None):
+        gtk.FontSelectionDialog.__init__(self, title)
+        ModalDialogMixin.__init__(self)
+        if font is not None:
+            pass
 
+    def get_result(self):
+        return self.get_font_name()
+    
+def get_color(title, color = None):
+    win = ColorDialog(title, color)
+    return win.run()
 
 class SketchDrawingArea(gtk.DrawingArea):
-
+    _size = (0,0)
     def __init__(self, **kw):
         gtk.DrawingArea.__init__(self, **kw)
         self.region = _skgtk.create_region()
@@ -251,7 +249,17 @@
         self.connect("configure_event", self.configure_event)
         self.connect("focus_in_event", self.focus_in_event)
         self.connect("focus_out_event", self.focus_out_event)
+        self.connect("size-allocate", self.size_allocate)
 
+    def size_allocate(self, widget, rectangle): #XXX chris
+        """Widget is resized"""
+        x, y, w, h = rectangle
+        self._size = w, h
+        #print "allocated resize:", w, h
+        
+    def get_size(self):
+        return self._size
+    
     def focus_in_event(self, widget, event):
         """Handle focus_in_event
 
@@ -271,7 +279,9 @@
         return gtk.TRUE
 
     def configure_event(self, widget, event):
+        #print "configure event", widget, event.width, event.height
         self.ResizedMethod(event.width, event.height)
+        
 
     def ResizedMethod(self, width, height):
         # to be implemented by derived classes
@@ -299,3 +309,186 @@
     def init_gcs(self):
         self.gc.init_gc(self.window, graphics_exposures = 1)
         self.gcs_initialized = 1
+
+
+
+class CommonOptionMenu(gtk.OptionMenu, Publisher):
+
+    """Base class for specialized option menus
+
+    Base class for specialized option menus such as line widths and
+    arrows. The derived class should pass VALUES to the CommonOptionMenu
+    constructor. VALUES is a list of the actual option values (numbers,
+    class instances, tuples, etc.) that are returned by GetOption() and
+    passed with the CHANGED signal when an option is selected.
+
+    Override build_options() to return a two-tuple: a list of the items
+    to be placed in the menu and the default menu index. The menu items
+    can be strings (converted to vanilla GtkMenuItems) or gtk widgets
+    (such as GtkPixmaps) which are added to the GtkMenuItems.
+
+    The class variable MODE can be FIXED (SetOption ignores values not
+    already in the option menu), EXTEND (SetOption adds new values to
+    the menu), or TEMPORARY_EXTENSION (SetOption adds a new value by
+    combining it with the values set at construction). See the
+    specialized menu classes below for more details.
+    """
+
+    FIXED = 0
+    EXTEND = 1
+    TEMPORARY_EXTENSION = 2
+
+    mode = FIXED
+
+    def __init__(self, values = []):
+        gtk.OptionMenu.__init__(self)
+        self.base_values = values
+        self.values = values
+        self.idx = None
+        self.initialized = 0
+        self.connect('realize', self.on_realize)
+        self.connect('expose_event', self.on_expose_event)
+        self.connect('configure_event', self.on_configure_event)
+
+    def on_realize(self, *args):
+        if not self.initialized:
+            self.initialize()
+
+    def on_expose_event(self, *args):
+        if not self.initialized:
+            self.initialize()
+
+    def on_configure_event(self, widget, event):
+        self.resized(width, height)
+                
+    def initialize(self):
+        self.update()
+
+    def resized(self, width, height):
+        pass
+
+    def build_options(self):
+        return [], None
+
+    def update(self):
+        self.menu = menu = gtk.Menu()
+        options, default = self.build_options()
+        idx = 0
+        for option in options:
+            if isinstance(option, types.StringType):
+                item = gtk.MenuItem(option)
+            else:
+                item = gtk.MenuItem()
+                item.add(option)
+            item.connect('activate', self.on_select, idx)
+            menu.append(item)
+            idx = idx + 1
+        menu.show_all()
+        self.set_menu(menu)
+        self.idx = default
+        self.set_history(default)
+        self.initialized = 1
+
+    def on_select(self, widget, idx, *args):
+        self.issue(const.CHANGED, self.values[idx])
+        self.idx = idx
+
+    def GetOption(self):
+        return self.values[self.idx]
+
+    def HasOption(self, value):
+        return not not self.values.count(value)
+
+    def SetOption(self, value):
+        if not self.values.count(value):
+            if self.mode == CommonOptionMenu.FIXED:
+                return
+            if self.mode == CommonOptionMenu.TEMPORARY_EXTENSION:
+                self.values = self.base_values
+            self.values.append(value)
+            self.update()
+        idx = self.values.index(value)
+        self.idx = idx
+        self.set_history(self.idx)
+
+
+
+class PixmapMenu(CommonOptionMenu):
+
+    """OptionMenu displaying Pixmaps"""
+
+    def __init__(self, pixmaps, values):
+        """Initialize the PixmapMenu
+
+        Parameters:
+           pixmaps - list of GtkPixmaps or strings (from skpixmaps)
+           values  - list of associated values
+        """
+        CommonOptionMenu.__init__(self, values)
+        self.pixmaps = pixmaps
+
+    def build_options(self):
+        options = []
+        for pixmap in self.pixmaps:
+            if isinstance(pixmap, types.StringType):
+                pixmap = getattr(skpixmaps.pixmaps, pixmap)
+            image = gtk.Image()
+            image.set_from_pixmap(*pixmap)
+            options.append(image)
+        return options, 0
+
+class UnitMenu(CommonOptionMenu):
+
+    """Unit Menu
+
+    The values are (unit, scale) tuples, such as ("in", 72.0)
+    The default unit is taken from Sketch.UI.preferences
+    """
+    def __init__(self):
+        self.units = unit_dict.keys()
+        self.units.sort()
+        values = map(lambda x: (x, unit_dict[x]), self.units)
+        CommonOptionMenu.__init__(self, values)
+        default_unit = Sketch.UI.preferences.default_unit
+        if self.units.count(default_unit) > 0:
+            self.idx = self.units.index(default_unit)
+
+    def build_options(self):
+        return self.units, self.idx
+
+class AngleUnitMenu(CommonOptionMenu):
+    angular_units = {
+        'deg' : 1.,
+        'rad' : 180./math.pi
+        }
+    def __init__(self):
+        unit_dict = self.angular_units
+        self.units = unit_dict.keys()
+        self.units.sort()
+        values = map(lambda x: (x, unit_dict[x]), self.units)
+        CommonOptionMenu.__init__(self, values)
+        default_unit = 'deg'
+        if self.units.count(default_unit) > 0:
+            self.idx = self.units.index(default_unit)
+
+    def build_options(self):
+        return self.units, self.idx
+
+    
+def create_length_widgets(command):
+    label = gtk.Label(_("Width:"))
+    adjust = gtk.Adjustment(1, 0, 72, 1, 0, 0)
+    width = gtk.SpinButton(adjust)
+    width.connect('changed', command)
+    unit_menu = UnitMenu()
+    unit_menu.Subscribe(const.CHANGED, command)
+    return label, width, unit_menu
+
+def create_length_entry(master, command):
+    width_hbox = gtk.HBox(homogeneous = 0)
+    label, width, unit_menu = create_length_widgets(command)
+    width_hbox.pack_start(label, 0, 1)
+    width_hbox.pack_start(width, 1, 1)
+    width_hbox.pack_start(unit_menu, 0, 1) 
+    master.pack_start(width_hbox)
+    return width
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/inspectorpanel.py sketch1/Sketch/UI/inspectorpanel.py
--- sketch.orig/Sketch/UI/inspectorpanel.py	1970-01-01 01:00:00.000000000 +0100
+++ sketch1/Sketch/UI/inspectorpanel.py	2005-03-19 20:27:08.000000000 +0100
@@ -0,0 +1,39 @@
+from Sketch import _, Editor
+from skpanel import MultiPanel
+from linepanel import LineInspector
+from fillpanel import FillInspector
+from properties import PropertyInspector
+
+class InspectorPanel(MultiPanel):
+    multiple = 1
+    def __init__(self, context):
+        MultiPanel.__init__(self, context, _("Inspector"))
+        self.set_resizable(0)
+
+    def build_window(self):
+        MultiPanel.build_window(self)
+        self.build_menu()
+
+        application = self.context.application
+        card = LineInspector(application)
+        self.attach_card(card)        
+        card = FillInspector(application)
+        self.attach_card(card)
+        card = PropertyInspector(application)
+        self.attach_card(card)
+
+
+    def build_menu(self):        
+        items = (
+            #'issue_panel_changed',
+                 'close_panel',
+                 'duplicate_panel',
+                 None,
+                 'auto_update',
+                 'auto_apply',
+                 'update_panel',
+                 'do_apply')
+
+        import tmpmenu
+        menudata = tmpmenu.BuildMenuTree('<fillmenu>', '', items)
+        self.set_menudata(menudata)
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/linepanel.py sketch1/Sketch/UI/linepanel.py
--- sketch.orig/Sketch/UI/linepanel.py	1970-01-01 01:00:00.000000000 +0100
+++ sketch1/Sketch/UI/linepanel.py	2005-03-19 21:36:34.000000000 +0100
@@ -0,0 +1,562 @@
+
+import gtk
+import types
+import operator
+from math import log10
+from sets import Set
+
+import Sketch
+import Sketch.Graphics
+from Sketch import _, Point, CreatePath
+from Sketch._sketch import ContAngle
+from Sketch.Lib.units import unit_dict
+from Sketch.Base import Publisher
+from Sketch.Base.const import EDITED, CHANGED, SELECTION, OBJECT_CHANGED, \
+     STYLE, TRANSACTION_END, CapButt, CapRound, CapProjecting, \
+     JoinMiter, JoinRound, JoinBevel
+from Sketch.Graphics import StandardColors, PropertyStack,\
+     EmptyPattern, SolidPattern, CreateRGBColor, StandardDashes, \
+     LineStyle
+from Sketch.Graphics.arrow import StandardArrows
+from Sketch.Graphics.properties import set_graphics_defaults, \
+     DefaultGraphicsProperties, property_names
+
+from gtkmisc import CommonOptionMenu, PixmapMenu, UnitMenu
+from scratchpad import ScratchPad
+from request import ColorButton
+import skpixmaps
+
+pixmaps = skpixmaps.pixmaps
+black = StandardColors.black
+
+from Sketch import _, const
+from skpanel import MultiPanel, PanelMenu, Card
+
+# find properties common to several PropertyStacks
+def common_properties(plist):
+    """Return the properties all PropertyStacks in plist have in common.
+
+    The return value is a dict mapping property names to their values.
+    """
+    common = {}
+    conflicts = {}
+    for p in plist:
+        for property in property_names:
+            if hasattr(p, property):
+                value = getattr(p, property)
+                if common.has_key(property):
+                    if value != common[property]:
+                        conflicts[property] = 1
+                else:
+                    common[property] = value
+    for conflict in conflicts.keys():
+        del common[conflict]
+    return common
+
+# construct a PropertyStack with properties common to several objects
+def common_object_properties(objects):
+    plist = [obj.Properties() for obj in objects if obj.has_properties]
+    common = common_properties(plist)
+    pstack = DefaultGraphicsProperties() # XXX or text properties?
+    apply(pstack.SetProperty, (), common)
+    return pstack
+
+
+
+
+class ArrowMenu(CommonOptionMenu):
+
+    """Option Menu for Arrow Heads.
+
+    The arrows shown are taken from StandardArrows()
+    """
+
+    mode = CommonOptionMenu.TEMPORARY_EXTENSION
+
+    def __init__(self, direction):
+        """Initialize the arrow menu.
+
+        If direction is 1 the arrow points to the right, if it's 0 to
+        the left.
+        """
+        CommonOptionMenu.__init__(self, [None] + StandardArrows())
+        self.direction = direction
+
+    def GeneratePixmaps(self, window, width = 32, height = 16):
+        pad = ScratchPad(window, width, height, 1.0, libart = 1)
+        maps = []
+        scale = 1.0
+        arrow_props = PropertyStack()
+        arrow_props.AddStyle(LineStyle(black, 1))
+        origin_props = PropertyStack()
+        origin_props.AddStyle(LineStyle(CreateRGBColor(0.1, 0.1, 0.1), 1,
+                                        dashes = (2, 2)))
+        origin_path = CreatePath()
+        origin_path.AppendLine(Point(0, -height/2), ContAngle)
+        origin_path.AppendLine(Point(0, height/2), ContAngle)
+        for arrow in self.values:
+            pad.BeginDraw()
+            pad.DrawRectangle(-width/2, -height/2, width/2, height/2)
+            pad.SetProperties(origin_props)
+            pad.MultiBezier((origin_path,))
+            pad.SetProperties(arrow_props)
+            pad.PushTrafo()
+            pad.Scale(2)
+            if not self.direction:
+                pad.Scale(-1)
+            if arrow is not None:
+                retract = arrow.retract
+                arrow.Draw(pad)
+            else:
+                retract = 0
+            path = CreatePath()
+            path.AppendLine(Point(-width/2, 0), ContAngle)
+            path.AppendLine(Point(-retract, 0), ContAngle)
+            pad.MultiBezier((path,))
+            pad.PopTrafo()
+            pad.EndDraw()
+            maps.append(pad.GetPixmap())
+        return maps
+
+    def build_options(self):
+        window = self.window
+        images = []
+        for pixmap in self.GeneratePixmaps(window):
+            image = gtk.Image()
+            image.set_from_pixmap(pixmap, None)
+            images.append(image)
+        return images, 0
+
+
+from rendermisc import DashesSample, LineThicknessSample, WidgetRenderer
+
+class DashesWidget(DashesSample, WidgetRenderer):
+    default_size = (110, 16)
+
+class ThicknessWidget(LineThicknessSample, WidgetRenderer):
+    default_size = (90, 16)
+
+class LineDashesMenu(CommonOptionMenu):
+
+    """LineDashes Menu
+
+    The values are taken from StandardDashes()
+    """
+
+    mode = CommonOptionMenu.TEMPORARY_EXTENSION
+
+    def __init__(self):
+        CommonOptionMenu.__init__(self, StandardDashes())
+
+    def build_options(self):
+        window = self.window
+        options = []
+        for dash in self.values:
+            widget = DashesWidget(dash)
+            #widget = gtk.Label(str(dash))
+            options.append(widget)
+
+        return options, 0
+
+
+
+class LineWidthMenu(CommonOptionMenu):
+
+    """Line Width Menu
+
+    Values are taken from and saved in Sketch.UI.preferences.
+    Values are stored internally in points (pts), but the unit of display
+    can be set with SetUnits().
+    New widths can be added with AddWidth(), which will remove the oldest
+    width if the menu grows too big (size set in Sketch.UI.preferences).
+    """
+
+    def __init__(self):
+        values = Sketch.UI.preferences.user_line_widths
+        limit = Sketch.UI.preferences.user_line_widths_length
+        if len(values) > limit:
+            values = values[:limit]
+        self.order = values[:]
+        values.sort()
+        self.scale = 1.0
+        self.digits = 1
+        CommonOptionMenu.__init__(self, values)
+
+    def build_options(self):
+        options = []
+        for width in self.values:
+            widget = ThicknessWidget(width)
+            options.append(widget)
+        return options, 0
+
+    def SetUnits(self, scale, digits):
+        update = self.scale != scale or self.digits != digits
+        self.scale = scale
+        self.digits = digits
+        if update:
+            self.update()
+
+    def AddWidth(self, width):
+        if self.order.count(width):
+            self.order.remove(width)
+            self.order[:0] = [width]
+        else:
+            self.order[:0] = [width]
+            limit = Sketch.UI.preferences.user_line_widths_length
+            if len(self.order) > limit:
+                self.order = self.order[:limit]
+            self.values = self.order[:]
+            self.values.sort()
+            self.base_values = self.values
+            self.update()
+        self.SetOption(width)
+        Sketch.UI.preferences.user_line_widths = self.order
+
+class LineInspector(Card):
+    """Inspector for setting line styles."""
+    
+    def __init__(self, application):
+        self.active_frame = None
+
+        # changes are collected in a buffer before they are executed.
+        self.buffer = {}
+
+        # locks fulfill two tasks here: blocking "apply" and blocking
+        # the unsetting of the "undo_last" flag.        
+        self.locked = 0
+
+        # the changes that were done last time
+        self.last_changes = None
+
+        # whether the "apply" entry should be activeated
+        self.has_changes = 1
+        
+        self.no_line = 0
+        self.unit_scale = 1.0
+        self.current_width = 1.0
+        
+        Card.__init__(self, application, _("Line"))
+
+        for signal in (CHANGED, SELECTION, EDITED, OBJECT_CHANGED):            
+            self.context.Subscribe(signal, self.context_changed)
+
+    def build_card(self):
+        vbox = self
+        application = self.context.application
+        # no line, color
+        color_hbox = gtk.HBox(homogeneous = 1)
+        self.none = none = gtk.ToggleButton(_("No Line"))
+        none.connect('clicked', self.on_none)
+        
+        self.color = color = ColorButton(application, _("Color"),
+                                         StandardColors.black)
+        color.Subscribe(CHANGED, self.on_color)
+        color_hbox.pack_start(none, 0, 1)
+        color_hbox.pack_start(color, 0, 1)
+        
+        # width, units
+        label = gtk.Label(_("Width:"))
+        width_hbox = gtk.HBox(homogeneous = 0)
+        adjust = gtk.Adjustment(1, 0, 72, 1, 0, 0)
+        self.width = width = gtk.SpinButton(adjust)
+        width.set_width_chars(4)
+        width.connect('changed', self.on_width)
+        self.unit_menu = unit_menu = UnitMenu()
+        unit_menu.Subscribe(CHANGED, self.on_units)
+        width_hbox.pack_start(label, 0, 1)
+        width_hbox.pack_start(width, 1, 1)
+        width_hbox.pack_start(unit_menu, 0, 1)
+        
+        # width option menu
+        width_menu_hbox = gtk.HBox(homogeneous = 0)
+        self.width_menu = width_menu = LineWidthMenu()
+        width_menu.Subscribe(CHANGED, self.on_width_menu)
+        self.add_button = add_button = gtk.Button(_("Add"))
+        add_button.connect('clicked', self.on_add)
+        width_menu_hbox.pack_start(width_menu, 1, 1)
+        width_menu_hbox.pack_start(add_button, 0, 1)
+        
+        # reset width adjustment to default unit
+        self.on_units(unit_menu.GetOption())
+        
+        # dash option menu
+        self.dash_menu = dash_menu = LineDashesMenu()
+        dash_menu.Subscribe(CHANGED, self.on_dashes)
+        
+        # arrows
+        self.left_arrow = left_arrow = ArrowMenu(0)
+        left_arrow.Subscribe(CHANGED, self.on_line_arrow1)
+        self.right_arrow = right_arrow = ArrowMenu(1)
+        right_arrow.Subscribe(CHANGED, self.on_line_arrow2)
+
+        # cap
+        line_cap = PixmapMenu(['CapButt', 'CapRound', 'CapProjecting'],
+                               [CapButt, CapRound, CapProjecting])
+        line_cap.Subscribe(CHANGED, self.on_line_cap)
+        self.line_cap = line_cap
+
+        # join
+        line_join = PixmapMenu(['JoinMiter', 'JoinRound', 'JoinBevel'],
+                                 [JoinMiter, JoinRound, JoinBevel])
+        
+        line_join.Subscribe(CHANGED, self.on_line_join)
+        self.line_join = line_join
+        
+        table = gtk.Table(2, 2, homogeneous = 0)
+        table.attach(left_arrow, 0, 1, 0, 1)
+        table.attach(right_arrow, 1, 2, 0, 1)
+        table.attach(line_join, 0, 1, 1, 2)
+        table.attach(line_cap, 1, 2, 1, 2)
+
+        vbox.pack_start(color_hbox, 0, 1)
+        vbox.pack_start(width_hbox, 0, 1)
+        vbox.pack_start(width_menu_hbox, 0, 1)
+        vbox.pack_start(dash_menu, 0, 1)
+        vbox.pack_start(table, 0, 1)
+        vbox.show_all()
+
+    def CanApply(self):
+        return self.has_changes
+    
+    def context_changed(self, *args):
+        if self.locked: return        
+        self.last_changes = None
+        self.has_changes = 1
+        if self.IsAutoUpdate():
+            self.update()
+        else:
+            self.changed()
+
+    ### widget callbacks
+    def on_none(self, widget, *args):
+        # none is the "no line" button
+        if self.locked: return
+        self.no_line = widget.get_active()        
+        if self.no_line:
+            pattern = EmptyPattern
+        else:
+            pattern = SolidPattern(self.color.Color())
+        self.auto_apply(line_pattern = pattern)
+                
+    def on_color(self, color):
+        if self.locked: return
+        if color == EmptyPattern:
+            pattern = EmptyPattern
+        else:
+            pattern = SolidPattern(color)
+
+        self.lock()
+        self.set_pattern(pattern)
+        self.unlock()
+        self.auto_apply(line_pattern = pattern)
+        
+    def on_width(self, widget, *args):
+        if self.locked: return
+        width = widget.get_value()
+        width = width * self.unit_scale
+        self.lock()
+        self.set_width_menu(width)
+        self.unlock()
+        self.auto_apply(line_width = width)
+
+    def on_width_menu(self, width):
+        if self.locked: return
+        self.lock()
+        self.set_width(width)
+        self.unlock()
+        self.auto_apply(line_width = width)
+        
+    def on_add(self, widget, *args):
+        if self.locked: return
+        width = self.width.get_value()
+        width = width * self.unit_scale
+        self.lock()
+        self.width_menu.AddWidth(width)
+        self.unlock()
+        
+    def on_units(self, value):
+        if self.locked: return
+        unit, scale = value
+        adj = self.width.get_adjustment()
+        width = self.width.get_value()
+        width = self.unit_scale * width / scale
+        digits = max(0, int(1.5 + log10(scale)))
+        
+        # Black-magic (tm) step size calculation: Tries to get a nice
+        # "round" number close to 1 pt.  Yields 0.01 in, 1 pt, 1 mm,
+        # 0.1 cm and should give reasonable values for arbitrary
+        # units.
+        ten_scale = 10.0**(digits - 1)
+        step = max(1, int((1.0 / scale) * ten_scale + 0.5)) / ten_scale
+        self.lock()
+        # XXX Arbitary upper limit of 12 inches.  Is this reasonable
+        # or should it be larger or smaller?  Note that 0.6 had no
+        # upper limit.
+        adj.set_all(width, 0, 12.0 * 72.0 / scale, step, 0, 0)
+        self.unit_scale = scale
+
+        self.width.set_digits(digits)
+        self.width.set_adjustment(adj)
+        self.width_menu.SetUnits(scale, digits)
+        self.set_width(width)
+        self.unlock()
+        
+    def on_dashes(self, dashes):
+        if self.locked: return
+        self.auto_apply(line_dashes = dashes)
+
+    def on_line_arrow1(self, arrow):
+        if self.locked: return
+        self.auto_apply(line_arrow1 = arrow)
+
+    def on_line_arrow2(self, arrow):
+        if self.locked: return
+        self.auto_apply(line_arrow2 = arrow)
+
+    def on_line_cap(self, line_cap):
+        if self.locked: return
+        self.auto_apply(line_cap = line_cap)
+
+    def on_line_join(self, line_join):
+        if self.locked: return
+        self.auto_apply(line_join = line_join)
+                          
+    # helpers
+    def set_pattern(self, pattern):
+        #print "set pattern:locked= ", self.locked, pattern
+        if pattern is EmptyPattern:
+            self.no_line = 1
+            self.none.set_active(1)
+        else:
+            self.no_line = 0
+            self.none.set_active(0)
+            self.color.SetColor(pattern.color)
+        
+    def set_width(self, width):
+        self.width.set_value(width / self.unit_scale)
+        self.width_menu.SetOption(width)
+
+    def set_width_menu(self, width):
+        self.width_menu.SetOption(width)
+        
+    def extract_properties(self):
+        pattern = EmptyPattern
+        if not self.no_line:
+            pattern = SolidPattern(self.color.Color())
+        width = self.width.get_value()
+        width = width / self.unit_scale
+        properties = {}
+        properties['line_width'] = width
+        properties['line_dashes'] = self.dash_menu.GetOption()
+        properties['line_arrow1'] = self.left_arrow.GetOption()
+        properties['line_arrow2'] = self.right_arrow.GetOption()
+        properties['line_pattern'] = pattern
+        properties['line_join'] = self.line_join.GetOption()
+        properties['line_cap'] = self.line_cap.GetOption()
+        for property in ('line_width', 'line_dashes', 'line_pattern',
+                         'line_cap', 'line_join'):
+            if properties[property] is None:
+                del properties[property]
+        return properties
+    
+    def lock(self):
+        self.locked = 1
+
+    def unlock(self):
+        self.locked = 0
+
+    def update(self):
+        """Update the inspector from selection"""
+        if not self.context.HasEditor() or self.locked:
+            return
+        
+        # Updating the widges may trigger their callbacks which would
+        # lead to calls to apply. Set updating to true to tell apply
+        # to ignore this.
+        self.lock()
+
+        objects = self.context.editor.SelectedObjects()
+        properties = common_object_properties(objects)
+        
+        pattern = properties.line_pattern
+        self.set_pattern(pattern)
+        
+        width = properties.line_width        
+        if width is not None:
+            self.set_width(width)
+            
+        self.dash_menu.SetOption(properties.line_dashes)
+        self.line_cap.SetOption(properties.line_cap)
+        self.line_join.SetOption(properties.line_join)
+        self.left_arrow.SetOption(properties.line_arrow1)
+        self.right_arrow.SetOption(properties.line_arrow2)
+
+        self.has_changes = 0
+        self.buffer = {}
+        self.unlock()
+
+    def auto_apply(self, **kw):
+        if self.locked or self.context is None:
+            return
+        
+        self.buffer.update(kw)
+        self.has_changes = 1
+        if self.IsAutoApply():
+            self.do_apply()
+        else:
+            self.changed()
+            
+    def do_apply(self, all=0):
+        #print "do_apply: locked=", self.locked
+        context = self.context
+        if not context.HasEditor():
+            return
+
+        # prevent the undo_last flag from being unset  
+        self.lock()
+        undo = context.document.undo
+
+        # if "all" is true add properties from all widgets
+        if all:
+            self.buffer.update(self.extract_properties())
+            
+        changes = Set(self.buffer.keys())
+
+        # if this transaction will make the last one completely
+        # obsolete, we can save one undo.
+        last_changes = self.last_changes
+        if last_changes is not None and last_changes.issubset(changes):
+            undo.Undo() 
+            
+        context.editor.SetProperties(**self.buffer)
+        self.last_changes = changes
+        self.buffer = {}
+        self.has_changes = 0
+        self.unlock()
+        self.changed()
+        
+class LinePanel(MultiPanel):
+    multiple = 1
+    def __init__(self, context):
+        MultiPanel.__init__(self, context, _("Line"))
+
+    def build_window(self):
+        MultiPanel.build_window(self)
+        card = LineInspector(self.context.application)
+        self.build_menu()
+        self.attach_card(card)
+        
+    def build_menu(self):        
+        items = (
+            #'issue_panel_changed',
+                 'close_panel',
+                 'duplicate_panel',
+                 None,
+                 'auto_update',
+                 'auto_apply',
+                 'update_panel',
+                 'do_apply')
+        #menudata = Sketch.Editor.BuildMenuTree('<linemenu>', '', items)
+        import tmpmenu
+        menudata = tmpmenu.BuildMenuTree('<fillmenu>', '', items)
+        self.set_menudata(menudata)
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/properties.py sketch1/Sketch/UI/properties.py
--- sketch.orig/Sketch/UI/properties.py	2005-03-19 18:48:56.000000000 +0100
+++ sketch1/Sketch/UI/properties.py	2005-03-19 22:02:44.000000000 +0100
@@ -26,88 +26,15 @@
 import Sketch, Sketch.Graphics
 
 from gtkmisc import SketchDrawingArea
-from skpanel import SketchPanel
-from gtkdevice import PatternDevice
+from skpanel import MultiPanel, Card
+from rendermisc import PatternSample, ImageRenderer
+from command import make_command
+
 import skpixmaps
 pixmaps = skpixmaps.pixmaps
 
 
-class PatternSample(SketchDrawingArea):
 
-    def __init__(self,  **kw):
-        apply(SketchDrawingArea.__init__, (self,), kw)
-        self.pattern = EmptyPattern
-        self.properties = PropertyStack()
-        self.properties.AddStyle(EmptyLineStyle)
-        self.properties.SetProperty(fill_pattern = self.pattern)
-        self.gc.SetProperties(self.properties)
-        self.fill_rect = None
-
-    def realize_method(self, *args):
-        apply(SketchDrawingArea.realize_method, (self,) + args)
-        self.fill_rect = Rect(0, 0, self.window.width, self.window.height)
-        self.compute_trafo()
-
-    def compute_trafo(self):
-        if not hasattr(self, 'window'):
-            return
-        height = self.window.height
-        doc_to_win = Trafo(1, 0, 0, -1, 0, height)
-        win_to_doc = doc_to_win.inverse()
-        self.gc.SetViewportTransform(1.0, doc_to_win, win_to_doc)
-        self.fill_rect = Rect(0, 0, self.window.width, self.window.height)
-        self.gc.SetProperties(self.properties, self.fill_rect)
-
-    def SetPattern(self, pattern):
-        if pattern != self.pattern:
-            self.pattern = pattern
-            self.UpdateWhenIdle()
-            self.properties.SetProperty(fill_pattern = pattern)
-            self.gc.SetProperties(self.properties, self.fill_rect)
-
-    def RedrawMethod(self, region = None):
-        win = self.window
-        #self.gc.StartDblBuffer()
-        self.gc.SetFillColor(StandardColors.white)
-        self.gc.FillRectangle(0, 0, win.width, win.height)
-        if self.properties.HasFill():
-            self.gc.Rectangle(Trafo(win.width, 0, 0, win.height, 0, 0))
-        else:
-            self.gc.SetLineColor(StandardColors.black)
-            self.gc.DrawLineXY(0, win.height, win.width, 0)
-        #self.gc.EndDblBuffer()
-
-    def ResizedMethod(self, width, height):
-        self.gc.WindowResized(width, height)
-        self.UpdateWhenIdle()
-        self.compute_trafo()
-
-
-class PreviewFactory(PatternDevice):
-
-    def __init__(self):
-        PatternDevice.__init__(self, gradient_steps =
-                               Sketch.Base.preferences.gradient_steps_editor)
-
-    def CreatePreview(self, pattern, width = 16, height = 16):
-        if pattern.preview is not None:
-            return pattern.preview
-        pixmap = gtk.create_pixmap(self.window, width, height)
-        self.drawable = pixmap
-        doc_to_win = Trafo(1, 0, 0, -1, 0, height)
-        win_to_doc = doc_to_win.inverse()
-        self.SetViewportTransform(1.0, doc_to_win, win_to_doc)
-        self.ximage = None
-        if pattern.is_procedural:
-            self.fill_rect = Rect(0, 0, width, height)
-            pattern.Execute(self, self.fill_rect)
-        else:
-            pattern.Execute(self, None)
-            self.FillRectangle(0, 0, width, height)
-        self.drawable = self.window
-        pattern.preview = pixmap
-        return pixmap
-        
 
 
 property_defs = [
@@ -124,7 +51,10 @@
     ('font_size', 'MiniFontSize', "convert_length"),
 ]
 
-maskbitmap = None
+
+
+class PreviewFactory(PatternSample, ImageRenderer):
+    pass
 
 class StyleList(gtk.CList):
 
@@ -189,7 +119,10 @@
 
     def convert_pattern(self, value):
         if value is not EmptyPattern:
-            return self.preview_factory.CreatePreview(value), maskbitmap
+            factory = self.preview_factory
+            factory.SetData(value)
+            return factory.create_pixmap(), factory.create_bitmap()
+        
         else:
             return '-'
 
@@ -205,27 +138,16 @@
         return getattr(pixmaps,
                        ('', 'CapButt', 'CapRound', 'CapProjecting')[value])
 
-class PropertyPanel(SketchPanel):
 
+class Properties(Card):
+    was_realized = 0
     def __init__(self, context):
-        SketchPanel.__init__(self, context, _("Properties"))
-        self.context.Subscribe(Sketch.Base.const.SELECTION,
-                               self.context_changed)
-        self.context.Subscribe(Sketch.Base.const.TRANSACTION_END,
-                               self.context_changed)
-        self.context.Subscribe(Sketch.Base.const.STYLE, self.style_changed)
-        self.style_changed()
-        self.context_changed()
+        Card.__init__(self, context, _("Properties"))
 
-    def build_window(self):
-        vbox = self.vbox
+    def build_card(self):
+        vbox = self
         self.preview_factory = PreviewFactory()
 
-        #self.fillsample = PatternSample()
-        #self.fillsample.size(100, 100)
-        #self.fillsample.show()
-        #vbox.pack_start(self.fillsample)
-
         notebook = gtk.Notebook()
         vbox.pack_start(notebook)
 
@@ -239,23 +161,29 @@
         swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)        
         self.dynamic_styles = StyleList(self.preview_factory)
         swin.add(self.dynamic_styles)
-        notebook.append_page(swin, gtk.Label(_("Styles")))
-        
+        notebook.append_page(swin, gtk.Label(_("Styles")))        
         vbox.show_all()
 
-        vbox.realize()
-        self.preview_factory.init_gc(vbox.window)
-        global maskbitmap
-        maskbitmap = gtk.gdk.bitmap_create_from_data(vbox.window, '\377'*32,
-                                                     16, 16)
+
+    def realized(self, *rest):
+        self.was_realized = 1
+        self.preview_factory.init_gc(self.window)
+        self.context.Subscribe(Sketch.Base.const.SELECTION,
+                               self.context_changed)
+        self.context.Subscribe(Sketch.Base.const.TRANSACTION_END,
+                               self.context_changed)
+        self.context.Subscribe(Sketch.Base.const.STYLE, self.style_changed)
+        self.context_changed()
+
 
     def close(self, *args):
-        self.context.Unsubscribe(Sketch.Base.const.SELECTION,
-                                 self.context_changed)
-        self.context.Unsubscribe(Sketch.Base.const.STYLE, self.style_changed)
-        self.context.Unsubscribe(Sketch.const.TRANSACTION_END,
-                                 self.context_changed)
-        SketchPanel.close(self)
+        if self.was_realized:
+            self.context.Unsubscribe(Sketch.Base.const.SELECTION,
+                                     self.context_changed)
+            self.context.Unsubscribe(Sketch.Base.const.STYLE, self.style_changed)
+            self.context.Unsubscribe(Sketch.const.TRANSACTION_END,
+                                     self.context_changed)
+        Card.close(self)
 
     def context_changed(self, *args):
         #print 'context_changed'
@@ -280,3 +208,118 @@
             self.dynamic_styles.SetStyleList(self.context.document.Styles())
         else:
             self.dynamic_styles.SetStyleList([])
+
+        
+class PropertyInspector(Card):
+
+    was_realized = 0
+    def __init__(self, application):
+        Card.__init__(self, application, _("Properties"))    
+
+    def build_card(self):
+        swin = gtk.ScrolledWindow()
+        swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        self.add(swin)
+        self.preview_factory = PreviewFactory()
+        self.current_styles = StyleList(self.preview_factory)
+        swin.add(self.current_styles)
+     
+
+        def _stock_button(descr):            
+            button = gtk.Button()
+            image = gtk.Image()
+            image.set_from_stock(descr, gtk.ICON_SIZE_BUTTON)            
+            button.add(image)
+            return button
+
+        hbox = gtk.HBox()
+        
+        button = _stock_button(gtk.STOCK_NEW)
+        hbox.pack_start(button )
+
+        button = _stock_button(gtk.STOCK_GO_UP)
+        hbox.pack_start(button )
+        
+        button = _stock_button(gtk.STOCK_GO_DOWN)
+        hbox.pack_start(button )
+
+        button = _stock_button(gtk.STOCK_DELETE)
+        hbox.pack_start(button )
+        
+        self.pack_start(hbox, expand=0 )
+        self.show_all()
+
+    def realized(self, *args):
+        self.preview_factory.init_gc(self.window)
+        context = self.context
+        context.Subscribe(Sketch.Base.const.SELECTION,
+                          self.context_changed)
+        context.Subscribe(Sketch.Base.const.TRANSACTION_END,
+                          self.context_changed)
+        self.was_realized = 1
+
+    def destroyed(self, *args):
+        if self.was_realized:
+            self.context.Unsubscribe(Sketch.Base.const.SELECTION,
+                                     self.context_changed)
+            self.context.Unsubscribe(Sketch.const.TRANSACTION_END,
+                                     self.context_changed)
+        Card.destroyed(self)
+
+    def context_changed(self, *args):
+        pattern = EmptyPattern
+        properties = None
+        if self.context.HasEditor():
+            obj = self.context.editor.CurrentObject()
+            if obj is not None and obj.has_properties:
+                properties = obj.Properties()
+                if obj.has_fill:
+                    pattern = properties.fill_pattern.Copy()
+
+        self.properties = properties
+        if properties is not None:
+            self.current_styles.SetStyleList(properties.Styles())
+        else:
+            self.current_styles.SetStyleList([])
+
+    def create_memo(self, *args):
+        memo = PropertyStackMemo(self, self.context, self.properties)
+        memo.show()
+
+    def get(self):
+        return self.properties.Duplicate()
+
+    def set(self, data):
+        pass
+
+class PropertyPanel(MultiPanel):
+    multiple = 1
+    def __init__(self, context):
+        MultiPanel.__init__(self, context, _("Propety Inspector"))
+        self.set_resizable(1)
+
+    def build_window(self):
+        MultiPanel.build_window(self)
+        self.build_menu()
+
+        application = self.context.application
+        #card = PropertyInspector(application)
+        #self.attach_card(card)        
+        card = Properties(application)
+        self.attach_card(card)        
+
+    def build_menu(self):        
+        items = (
+            #'issue_panel_changed',
+                 'close_panel',
+                 'duplicate_panel',
+                 None,
+                 'auto_update',
+                 'auto_apply',
+                 'update_panel',
+                 'do_apply')
+            
+        #menudata = Sketch.Editor.BuildMenuTree('<stackinspectormenu>', '', items)
+        import tmpmenu
+        menudata = tmpmenu.BuildMenuTree('<stackinspectormenu>', '', items)
+        self.set_menudata(menudata)
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/rendermisc.py sketch1/Sketch/UI/rendermisc.py
--- sketch.orig/Sketch/UI/rendermisc.py	1970-01-01 01:00:00.000000000 +0100
+++ sketch1/Sketch/UI/rendermisc.py	2005-03-22 00:02:36.000000000 +0100
@@ -0,0 +1,329 @@
+# NOTE: mit widget.window.draw_image(...) lassen sich gtk-imaghes
+# zeichnen. Mit graphicsdevice geht das eher schlecht. -> Renderer
+# Backends fuer gtk ?
+
+
+# Sketch - A Python-based interactive drawing program
+# Copyright (C) 1997, 1998, 1999, 2000, 2001, 2003 by Bernhard Herzog
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Library General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Library General Public License for more details.
+#
+# You should have received a copy of the GNU Library General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+"""rendermisc
+
+Miscellaneous classes for rendering to pixmaps or to widgets,
+e.g. fill samples, lines, .... Renderer are derived from 3 general
+classes: PixmapRenderer, WidgetRenderer and CellRenderer. Specialised
+renderers are created by subclassing these baseclasses, implementing
+the methods 'default_size' and 'draw'.
+"""
+
+from types import StringType
+
+import gtk, gobject
+from gtk import gdk
+
+import Sketch
+from Sketch import _, Rect, Trafo, _skgtk
+from Sketch.Base import Publisher, preferences
+from Sketch.Graphics import StandardColors, LineStyle, EmptyLineStyle, \
+     PropertyStack,\
+     EmptyPattern, SolidPattern , CreateRGBColor
+from Sketch.const import CHANGED
+from gtkmisc import SketchDrawingArea
+from gtkdevice import PatternDevice, GraphicsDevice
+
+class Renderer:
+    default_size = 0,0
+    def __init__(self, data=None):
+        self.properties = PropertyStack()
+        self._data = data
+
+    def SetData(self, data):
+        self._data = data
+        
+    def draw(self, width, height):
+        """Do the drawing
+
+        This has to be implemented in the derived class"""
+        pass
+
+# This is the original implementation of widget renderer which also
+# works with pygtk < 2.4, however there are some rendering bugs which
+# I could not remove.
+
+class WidgetRenderer(SketchDrawingArea, Renderer):
+
+    def __init__(self,  data=None, **kw):
+        self.fill_rect = None
+        
+        Renderer.__init__(self, data)
+        apply(SketchDrawingArea.__init__, (self,), kw)
+
+        width, height = self.default_size
+        self.set_property("width-request", width)
+        self.set_property("height-request", height)
+
+    def compute_trafo(self):
+        width, height = self.get_size()
+        doc_to_win = Trafo(1, 0, 0, -1, 0, height)
+        win_to_doc = doc_to_win.inverse()
+        self.gc.SetViewportTransform(1.0, doc_to_win, win_to_doc)
+
+    def realize_method(self, *args):
+        apply(SketchDrawingArea.realize_method, (self,) + args)
+        self.gc.SetProperties(self.properties)
+
+    def RedrawMethod(self, region = None):
+        self.gc.gc.set_subwindow(0)
+        width, height = self.get_size()
+        #self.gc.StartDblBuffer()
+        self.gc.SetFillColor(StandardColors.white)
+        #self.gc.FillRectangle(0, 0, width, height)
+        self.gc.SetProperties(self.properties) # XXX needed ?
+        self.draw(width, height)
+        #self.gc.EndDblBuffer()
+
+    def ResizedMethod(self, width, height):        
+        width, height = self.get_size()
+        #self.fill_rect = Rect(0, 0, width, height)
+        self.gc.SetProperties(self.properties, self.fill_rect)
+        self.gc.WindowResized(width, height)
+        self.compute_trafo()
+        self.UpdateWhenIdle()
+
+# A widget renderer which works only from pygtk 2.4 on        
+class WidgetRenderer(Renderer, gtk.Widget):
+    __gsignals__ = { 'realize': 'override',
+                     'expose-event' : 'override',
+                     'size-allocate': 'override',
+                     'size-request': 'override',
+                     }
+    def __init__(self, data=None):
+        gtk.Widget.__init__(self)
+        Renderer.__init__(self, data)
+        self.gc = None
+        self.idle_redraw_tag = None
+        
+    def do_realize(self):
+        self.set_flags(self.flags() | gtk.REALIZED)
+
+        self.window = gdk.Window(self.get_parent_window(),
+                                 width=self.allocation.width,
+                                 height=self.allocation.height,
+                                 window_type=gdk.WINDOW_CHILD,
+                                 wclass=gdk.INPUT_OUTPUT,
+                                 event_mask= gdk.EXPOSURE_MASK)
+        self.gc = GraphicsDevice()
+        self.gc.init_gc(self.window, graphics_exposures = 1)
+        self.window.set_user_data(self)
+        self.style.attach(self.window)
+        self.style.set_background(self.window, gtk.STATE_NORMAL)
+        self.window.move_resize(*self.allocation)
+
+    def do_size_request(self, requisition):
+        width, height = self.default_size
+        requisition.width = width
+        requisition.height = height
+
+    def do_size_allocate(self, allocation):
+        self.allocation = allocation
+        if self.flags() & gtk.REALIZED:
+            self.window.move_resize(*allocation)
+
+    def redraw(self):
+        x0, y0, width, height = self.allocation
+        doc_to_win = Trafo(1, 0, 0, -1, 0, 0+height)
+        win_to_doc = doc_to_win.inverse()
+        self.gc.SetViewportTransform(1.0, doc_to_win, win_to_doc)        
+        self.draw(width, height)
+
+
+    def do_expose_event(self, event):
+        self.chain(event)
+        self.style.set_background(self.window, self.state)
+        self.window.clear()
+        self.redraw()
+        
+    def UpdateWhenIdle(self):
+        if self.idle_redraw_tag is None:
+            self.idle_redraw_tag = gtk.idle_add(self.idle_redraw)
+
+    def idle_redraw(self):
+        self.idle_redraw_tag = None
+        self.redraw()
+
+gobject.type_register(WidgetRenderer)
+
+class ImageRenderer(Renderer):
+    
+    def __init__(self, data=None):
+        Renderer.__init__(self, data)
+        self.gc = GraphicsDevice()        
+
+    def init_gc(self, window):
+        self.gc.init_gc(window)
+        
+    def CreateImage(self):
+        image = gtk.Image(self)
+        pixmap = self.create_pixmap()
+        bitmap = self.create_bitmap()
+        image.set_from_pixmap(pixmap, mask)        
+        return image
+
+    def create_pixmap(self):
+        width, height = self.default_size
+        gc = self.gc
+        pixmap = gtk.create_pixmap(gc.window, width, height)
+        gc.drawable = pixmap
+        doc_to_win = Trafo(1, 0, 0, -1, 0, height)
+        win_to_doc = doc_to_win.inverse()
+        gc.SetViewportTransform(1.0, doc_to_win, win_to_doc)
+        gc.ximage = None
+        self.draw(width, height)
+        gc.drawable = gc.window
+        return pixmap
+
+    def create_bitmap(self):
+        width, height = self.default_size        
+        create = gtk.gdk.bitmap_create_from_data
+        return create(self.gc.window, '\377'*(width*height/8), width, height)
+
+        
+                    
+
+class SketchCellRenderer(gtk.GenericCellRenderer, Renderer):
+
+    __gproperties__ = {
+        "data": (gobject.TYPE_PYOBJECT, "data",
+                 "cell data", 
+                 gobject.PARAM_READWRITE),
+    }
+
+    def __init__(self):
+        Renderer.__init__(self)
+        self.__gobject_init__()
+        self.gcs_initialized = 0
+        self.gc = GraphicsDevice()
+            
+    def init_gcs(self, window):
+        self.gc.init_gc(window, graphics_exposures = 1)
+        self.gcs_initialized = 1
+
+    def do_set_property(self, pspec, value):
+        setattr(self, pspec.name, value)
+ 
+    def do_get_property(self, pspec):
+        return getattr(self, pspec.name)
+                
+    def on_render(self, window, widget, background_area,
+                  cell_area, expose_area, flags):
+               
+        self.gc.drawable = window
+        if not self.gcs_initialized:
+            self.init_gcs(window)
+
+        data = self.get_property("data")
+        self.SetData(data)
+        
+        x0, y0, width, height = self.on_get_size(widget, cell_area)
+        x0 += cell_area.x
+        y0 += cell_area.y
+        doc_to_win = Trafo(1, 0, 0, -1, x0, y0+height)
+        win_to_doc = doc_to_win.inverse()
+        self.gc.SetViewportTransform(1.0, doc_to_win, win_to_doc)        
+        self.draw(width, height)
+        
+    def on_get_size(self, widget, cell_area):
+        xpad = self.get_property("xpad")
+        ypad = self.get_property("ypad")
+        if cell_area:
+            width = cell_area.width
+            height = cell_area.height
+            x_offset = xpad
+            y_offset = ypad
+        else:
+            default_w, default_h = self.default_size
+            width = self.get_property("width")
+            height = self.get_property("height")
+            if width == -1:
+                width = default_w
+            if height == -1:
+                height = default_h
+            width += xpad*2
+            height += ypad*2
+            x_offset = 0
+            y_offset = 0
+        return x_offset, y_offset, width, height
+
+gobject.type_register(SketchCellRenderer)
+
+
+class PatternSample:
+    default_size = 16, 16
+    def SetData(self, pattern):        
+        if pattern != self._data:
+            self._data = pattern
+            self.properties.SetProperty(fill_pattern = pattern)
+            self.gc.SetProperties(self.properties)
+
+    def draw(self, width, height):
+        w = width
+        h = height
+        if self.properties.HasFill():
+            self.gc.Rectangle(Trafo(w, 0, 0, h, 0, 0))
+        else:
+            self.gc.SetLineColor(StandardColors.black)
+            self.gc.DrawLineXY(0, h, w, 0)
+        
+        self.gc.DrawLineXY(0, 0, w-1, 0)
+        self.gc.DrawLineXY(0, h-1, w-1, h-1)
+        self.gc.DrawLineXY(0, 0, 0, h-1)
+        self.gc.DrawLineXY(w-1, 0, w-1, h-1)
+
+class ColorSample(PatternSample):
+    default_size = 16, 16
+    def SetData(self, color):        
+        if color is not None:
+            pattern = SolidPattern(color)            
+        else:
+            pattern = EmptyPattern
+        self.properties.SetProperty(fill_pattern = pattern)
+        self.gc.SetProperties(self.properties)
+
+class LineThicknessSample:
+    default_size = 16, 16
+    def draw(self, width, height):        
+        self.gc.SetProperties(self.properties)        
+        self.gc.SetLineAttributes(self._data)
+        self.gc.SetLineColor(StandardColors.black)
+        self.gc.DrawLineXY(0, height/2, width, height/2)
+    
+class _LinePropertySample:
+    """Common base class for several line sample classes."""
+    default_size = 16, 16
+    line_width = 2
+    attribute_name = ""
+    def draw(self, width, height):
+        self.gc.SetProperties(self.properties)
+        attributes = {}
+        attributes[self.attribute_name] = self._data
+        self.gc.SetLineAttributes(self.line_width, **attributes)
+        self.gc.SetFillColor(StandardColors.black)
+        self.gc.DrawLineXY(0, height/2, width, height/2)
+
+class DashesSample(_LinePropertySample):
+    default_size = 40, 10
+    attribute_name = 'dashes'
+
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/request.py sketch1/Sketch/UI/request.py
--- sketch.orig/Sketch/UI/request.py	1970-01-01 01:00:00.000000000 +0100
+++ sketch1/Sketch/UI/request.py	2005-03-19 20:27:08.000000000 +0100
@@ -0,0 +1,136 @@
+
+import gtk
+from Sketch import _, const
+from Sketch.Base import Publisher
+from Sketch.Graphics import CreateRGBColor
+
+
+class RequestHandler(Publisher):
+    def __init__(self, context):
+        self.colorpanel_count = 0
+        self.context = context
+        
+    def RequestColor(self, widget, color):
+        self.issue(const.CLEAR_REQUEST, widget)
+        self.issue(const.REQUEST_COLOR, color)
+        
+        # popup color panel        
+        if self.colorpanel_count == 0:
+            from colorpanel import ColorPanel
+            app = self.context.application
+            app.InstantiatePanel(ColorPanel)
+
+    def SetColor(self, color):
+        self.issue(const.SET_COLOR, color)
+        
+    def AddColorPanel(self):
+        self.colorpanel_count += 1
+
+    def RemoveColorPanel(self):
+        self.colorpanel_count -= 1
+        
+
+def _gtk_colors(cmap, color):
+    r, g, b = color
+    bg = cmap.alloc_color(int(round(r * 0xFFFF)),
+                          int(round(g * 0xFFFF)),
+                          int(round(b * 0xFFFF)))
+    luminance = 0.30*r + 0.59*g + 0.11*b
+    #luminance = (min(color) + max(color)) / 2
+    if luminance > 0.5:
+        fg = cmap.alloc_color(0, 0, 0)
+    else:
+        fg = cmap.alloc_color(0xFFFF, 0xFFFF, 0xFFFF)
+    return fg, bg
+
+
+#
+# This button triggers a color request
+#
+class ColorRequestButton(gtk.ToggleButton, Publisher):
+
+    def __init__(self, application, title, color):
+        gtk.ToggleButton.__init__(self, title)
+        self.connect("destroy", self.destroyed)
+        self.connect("toggled", self.toggled)
+        handler = application.RequestHandler()
+        handler.Subscribe(const.CLEAR_REQUEST, self.clear_request)
+        handler.Subscribe(const.SET_COLOR, self.set_color)
+        self.handler = handler
+        self.color = color
+
+    def destroyed(self, *args):
+        self.handler.Unsubscribe(const.CLEAR_REQUEST, self.clear_request)
+        self.handler.Unsubscribe(const.SET_COLOR, self.set_color)
+        self.handler = None
+        
+    def Color(self):
+        return self.color
+
+    def SetColor(self, color):
+        self.color = color
+
+    def toggled(self, *args):
+        if self.get_active():
+            self.handler.RequestColor(self, self.color)
+
+    def set_color(self, color):
+        if self.get_active():
+            self.SetColor(color)
+            self.issue(const.CHANGED, self.color)
+
+    def clear_request(self, widget):
+        if widget != self:
+            self.set_active(gtk.FALSE)
+
+    def Passivate(self):
+        self.set_active(gtk.FALSE)
+
+
+#
+# A color request button which displays the chosen color
+# and supports drag and drop
+#
+class ColorButton(ColorRequestButton):
+    dnd_target = [('application/x-color', 0, 0)]
+    def __init__(self, *args):
+        ColorRequestButton.__init__(self, *args)
+        self.connect("realize", self.realized)
+        self.connect('drag_data_received', self.drag_data_received)
+
+        self.drag_dest_set(gtk.DEST_DEFAULT_ALL, self.dnd_target,
+                                  gtk.gdk.ACTION_COPY | gtk.gdk.ACTION_MOVE)
+
+    def realized(self, *args):
+        self.SetColor(self.color)
+        
+    def SetColor(self, color):
+        ColorRequestButton.SetColor(self, color)
+        style = self.get_style().copy()
+        #print "color button: color=", color
+        fg, bg = _gtk_colors(self.get_colormap(), color)
+        for i in range(5):
+            style.fg[i] = fg
+            style.bg[i] = bg
+        self.set_style(style)
+        self.get_children()[0].set_style(style)
+
+    def drag_data_received(self, w, context, x, y, data, info, time):
+        d = data.data
+        if data and data.format == 16:
+            def decode(s):
+                b, a = map(ord, s)
+                return ((a << 8) + b)/ 65535.
+            bin = data.data
+            r = decode(bin[:2])
+            g = decode(bin[2:4])
+            b = decode(bin[4:6])
+
+            self.SetColor(CreateRGBColor(r, g, b))
+            context.finish(gtk.TRUE, gtk.FALSE, time)
+            self.issue(const.CHANGED, self.color)
+            
+        else:
+            context.finish(gtk.FALSE, gtk.FALSE, time)
+
+
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/skapp.py sketch1/Sketch/UI/skapp.py
--- sketch.orig/Sketch/UI/skapp.py	2005-03-19 18:48:56.000000000 +0100
+++ sketch1/Sketch/UI/skapp.py	2005-03-19 20:27:08.000000000 +0100
@@ -43,6 +43,7 @@
 from toolbox import SketchToolbox
 from menu import make_menu
 import gtkmisc
+import request
 
 #
 # meta info defaults
@@ -179,6 +180,7 @@
         self.file_dialog = None
         self.tooltips = gtk.Tooltips()
         self.context = AppContext(self)
+        self.requesthandler = request.RequestHandler(self.context)
         self.SetTool('SelectionTool')
 
         # Keep track of the open panels. To that end panels maps the
@@ -358,6 +360,9 @@
                                   accel = self.accelgroup)
         return self.menu
 
+    def RequestHandler(self):
+        return self.requesthandler
+
     def SetContext(self, window):
         self.context.SetWindow(window)
 
@@ -498,7 +503,22 @@
 Sketch.Editor.Add(Sketch.Editor.Command('object_panel',
                                         _("Object Panel"), object_panel))
 
+
+def layer_panel(context):
+    import layerpanel
+    context.application.InstantiatePanel(layerpanel.LayerPanel)
+
+Sketch.Editor.Add(Sketch.Editor.Command('layer_panel',
+                                        _("Layer Panel"), layer_panel))
+
+def inspector_panel(context):
+    import inspectorpanel
+    context.application.InstantiatePanel(inspectorpanel.InspectorPanel)
+
+Sketch.Editor.Add(Sketch.Editor.Command('inspector_panel',
+                                        _("Inspector Panel"), inspector_panel))
+
+                                                
 # import some development commands
 
 import devcmds
-
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/skpanel.py sketch1/Sketch/UI/skpanel.py
--- sketch.orig/Sketch/UI/skpanel.py	2005-03-19 18:48:56.000000000 +0100
+++ sketch1/Sketch/UI/skpanel.py	2005-03-19 22:12:46.000000000 +0100
@@ -1,3 +1,11 @@
+# TODO:
+# - rename this to "panel.py" ?
+# - with gtk2.4 it should be possible to change the behavior of
+#   ordinary windows so that they do stay above --> it is then possible
+#   to use gtk.Window instead of gtk.Dialog as base class for panel.
+
+
+
 # Sketch - A Python-based interactive drawing program
 # Copyright (C) 1999, 2000, 2001, 2003 by Bernhard Herzog
 #
@@ -16,12 +24,52 @@
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 
+
+# Panels are in general windows which can be permanently openend and are
+# offering a certain functionality for the user. Examples include the toolbox,
+# the color panel, the inspector panel, etc. .
+#
+# The class "Panel" offers the simplest form of a panel: a bare
+# window. More complex panels are realized via the class
+# "MultiPanel". Using MulitPanels ensures a uniform look and
+# simplifies future modifications of the gui design, e.g. a switch
+# from SDI to MDI.  There is only one kind of widget which can be
+# placed into a MultiPanel: a "Card". If the panel holds several cards,
+# they are organized into a notebook. Cards can be attached or removed
+# at runtime.
+
+
 import gtk
 
 import Sketch
+from Sketch import _, const
+
+from Sketch.const import CHANGED, OBJECT_CHANGED, SELECTION
+
+from Sketch.Base import Publisher
 
-class SketchPanel(gtk.Dialog):
+# XXX:
+# from menu import MenuMixin
+from tmpmenu import MenuMixin
 
+import command
+from contextproxy import ContextProxy
+
+        
+# signal used when the panel is changed
+PANEL = "PANEL"
+
+class PanelContext(ContextProxy):
+    channels = ContextProxy.channels[:] + (PANEL,)
+    def __init__(self, panel, app_context):
+        self.panel = panel
+        ContextProxy.__init__(self, app_context)
+        panel.Subscribe(CHANGED, self.forward, PANEL)
+
+
+class Panel(gtk.Dialog):
+    """The basic panel class. New code should use MultiPanel instead. """
+    
     # if true, there can be multiple panels of this class, otherwise
     # only one.
     multiple = 0
@@ -32,17 +80,328 @@
         self.set_title(title)
         self.build_window()
         self.connect("destroy", self.destroyed)
-        self.context.Subscribe(Sketch.Base.const.CHANGED, self.context_changed)
+        self.connect("realize", self.realized)
+        self.context.Subscribe(const.CHANGED, self.context_changed)
 
     def build_window(self):
         pass
 
     def close(self, *args):
+        # XXX Do we really need this ?
         self.destroy()
 
+    def Close(self):
+        self.destroy()
+        
+    def Raise(self):        
+        # XXX the original "raise_" of gtk does not work with some
+        # windowmanagers. Hiding and showing the window does the same job.
+                
+        pos = self.get_position()
+        self.hide()
+        self.show()
+        self.move(*pos)
+
+    ### Callbacks
     def destroyed(self, *args):
-        self.context.Unsubscribe(Sketch.Base.const.CHANGED,
+        self.context.Unsubscribe(const.CHANGED,
                                  self.context_changed)
-                
+    
+    def realized(self, *args):
+        """Called when the widgets a are realized"""
+        pass
+
     def context_changed(self, *args):
         pass
+    
+
+# XXX "SketchPanel" should be replaced by "Panel". For the moment:
+SketchPanel = Panel
+
+class MultiPanel(Panel, Publisher):
+    auto_apply = 1
+    auto_update = 1
+
+    def __init__(self, context=None, title=""):
+        panel_context = PanelContext(self, context)
+        Panel.__init__(self, panel_context, title)
+    
+    def Duplicate(self):
+        clone = self.context.application.InstantiatePanel(self.__class__)
+        clone.auto_apply = self.auto_apply
+        clone.auto_update = self.auto_update
+        i = self.notebook.get_current_page()
+        clone.notebook.set_current_page(i)        
+        clone.changed()
+        clone.DoUpdate()
+        
+    def build_window(self):
+        self.header = gtk.HBox()
+        self.vbox.pack_start(self.header, expand=0)
+        self.notebook = gtk.Notebook()
+        self.notebook.set_property("homogeneous", 1)
+        
+        self.vbox.add(self.notebook)
+
+        self.menu = PanelMenu(self.context, allow_tearoff=0)
+        self.menu.show()
+        self.header.add(self.menu)
+
+        self.header.show()
+        self.vbox.show()
+        self.vbox.show()
+        self.notebook.show()
+                        
+    def set_menudata(self, data):        
+        self.menu.set_menudata(data)
+
+    def check_show_tabs(self):
+        childs = self.notebook.get_children()
+        if len(childs) > 1:
+            self.notebook.set_show_tabs(1)
+        else:
+            self.notebook.set_show_tabs(0)
+            
+    def attach_card(self, card):
+        assert isinstance(card, Card)        
+
+        label = gtk.Label(card.GetTitle())
+        self.notebook.append_page(card, label)
+        self.check_show_tabs()
+        card.Subscribe(const.CHANGED, self.changed)
+        card.attached(self)        
+        self.changed()
+
+    def detach_card(self, card):
+        self.notebook.remove(card)
+        self.check_show_tabs()
+        card.detached()
+        self.changed()
+
+    def changed(self):
+        """Notify that the panel has changed. """
+        self.issue(const.CHANGED)
+
+    def current_card(self):
+        children = self.notebook.get_children()
+        i = self.notebook.get_current_page()
+        if i >= 0:
+            return children[i]
+
+    ### commands
+    def ToggleAutoUpdate(self):
+        self.auto_update = not self.auto_update
+        self.changed()
+
+    def IsAutoUpdate(self):
+        return self.auto_update
+
+    def ToggleAutoApply(self):
+        self.auto_apply = not self.auto_apply
+        self.changed()
+
+    def IsAutoApply(self):
+        return self.auto_apply
+
+    def CanApply(self):
+        card = self.current_card()
+        if card is not None:
+            return card.CanApply()
+        return 0
+    
+    def DoApply(self, *args):
+        self.current_card().do_apply(all=1)
+        # inform the context that the state might have changed
+        self.changed()        
+
+    def DoUpdate(self):
+        self.current_card().update()                
+        # inform the context that the state might have changed
+        self.changed()
+
+    ### callbacks
+    def destroyed(self, *args):        
+        self.context.Destroy()
+        self.context = None
+        for card in self.notebook.get_children():
+            self.notebook.remove(card)
+            card.destroy()
+        
+
+class CardContext(ContextProxy):
+    def __init__(self, application):
+        self.panel = None
+        self.application = application
+        ContextProxy.__init__(self, application.context)
+        
+    def SetPanel(self, panel):
+        self.panel = panel
+
+    def UnsetPanel(self):
+        self.panel = None
+
+    def HasPanel(self):
+        return not self.panel is None
+    
+class Card(gtk.VBox, Publisher):
+    """A Card is a box which can be placed in a multipanel"""
+    
+    def __init__(self, application=None, title=None, duplicate=None):
+        if duplicate is not None:
+            application = duplicate.application
+            title = duplicate.title
+
+        gtk.VBox.__init__(self)
+        self.context = CardContext(application)
+        self.SetTitle(title)
+        self.build_card()
+        self.connect("realize", self.realized)
+        self.connect("destroy", self.destroyed)        
+
+    def Duplicate(self):
+        return self.__class__(duplicate=self)
+        
+    def build_card(self):
+        pass
+
+    def GetTitle(self):
+        return self.title
+
+    def SetTitle(self, title):
+        self.title = title
+        self.changed()
+
+    def HasPanel(self):
+        return self.context.HasPanel()
+    
+    def GetPanel(self):
+        if not self.context.HasPanel(): return
+        return self.context.panel
+
+    def IsAutoUpdate(self):
+        if not self.context.HasPanel(): return
+        return self.context.panel.auto_update
+
+    def IsAutoApply(self):
+        if not self.context.HasPanel(): return
+        return self.context.panel.auto_apply
+
+    def CanApply(self):
+        """Does the card have changes which can be applied ?"""
+        return 0
+    
+    def changed(self):
+        self.issue(const.CHANGED)
+
+    def update(self, *args):
+        """Update from selection"""
+        pass
+
+    def do_apply(self, all = 0):
+        """Apply all the changes"""
+        pass
+
+    ### callbacks
+    def realized(self, *args):
+        """Called when the widgets a are realized"""
+        pass
+    
+    def destroyed(self, *args):        
+        self.detached()
+        self.context.Destroy()
+
+    def detached(self):
+        """Called when the card is removed from a multipanel"""
+        self.context.UnsetPanel()
+        
+    def attached(self, panel):
+        """Called when the card is attached to a multipanel"""
+        self.context.SetPanel(panel)
+
+        
+#
+# Class PanelMenu
+#
+# Each panel should have panel menu. The menu is hidden under a button showing
+# an arrow.
+
+class PanelMenu(gtk.Table, MenuMixin):
+    def __init__(self, context, data=None, **kw):
+        MenuMixin.__init__(self, context, data, **kw)
+        gtk.Table.__init__(self, 3, 1)
+        
+        button = gtk.Button()
+        arrow = gtk.Arrow(gtk.ARROW_RIGHT, gtk.SHADOW_OUT)
+        button.add(arrow)
+        button.connect('button_press_event', self.show_menu)        
+        self.attach(button, 0, 1, 0, 1, 0, 0)
+        button.show()
+        arrow.show()
+        
+        spacer = gtk.HBox()
+        self.attach(spacer, 1, 2, 0, 1, xoptions=gtk.EXPAND)
+        
+        self.show_all()
+        self.show()
+        self.menu = gtk.Menu()
+
+    def add(self, widget):
+        """Put a widget right of the panel button"""
+        self.attach(widget, 2, 3, 0, 1, xoptions=0)
+        
+    def show_menu(self, widget, event):
+        """Popup the menu"""
+        self.menu.popup(None, None, None, event.button, event.time)
+        widget.emit_stop_by_name('button_press_event')
+
+    ### Methods called from MenuMixin:
+    def clear(self):
+        """Remove all gtk menu entries. Used by MenuMixin"""
+        for widget in self.menu.get_children():
+            self.menu.remove(widget)
+     
+    def append(self, menuitem):
+        """Append a gtk menu entry. Used by MenuMixin"""
+        self.menu.append(menuitem)
+                                                                           
+
+from Sketch.Editor import Command
+class PanelCommand(Command):
+    def Sensitive(self, context):
+        return context.panel is not None and Command.Sensitive(self, context)
+
+
+add = Sketch.Editor.Add
+cmd = PanelCommand
+
+# XXX remove this
+add(cmd('issue_panel_changed', _("Issue Changed"),
+        lambda context: context.panel.changed()))
+
+
+add(cmd('close_panel', _("Close"),
+        lambda context: context.panel.Close()))
+add(cmd('detach_card', _("Detach Card"),
+        lambda context: context.panel.current_card()))
+add(cmd('duplicate_panel', _("Duplicate"),
+        lambda context: context.panel.Duplicate(),
+        sensitive = lambda context: context.panel.multiple))
+add(cmd('update_panel', _("Update"),
+        lambda context: context.panel.DoUpdate(),
+        sensitive = lambda context: context.panel.CanApply(),
+        channels = (PANEL, SELECTION, OBJECT_CHANGED)))
+
+add(cmd('auto_update', _("Auto Update"),
+        lambda context: context.panel.ToggleAutoUpdate(),
+        checked = lambda context: context.panel.IsAutoUpdate(),
+        channels = (PANEL,)))
+add(cmd('auto_apply', _("Immediate Apply"),
+        lambda context: context.panel.ToggleAutoApply(),
+        checked = lambda context: context.panel.IsAutoApply(),
+        channels = (PANEL,)))
+add(cmd('do_apply', _("Apply"),
+        lambda context: context.panel.DoApply(),
+        sensitive = lambda context: context.panel.CanApply(),
+        channels = (PANEL, SELECTION, OBJECT_CHANGED)))
+    
+    
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/tmpmenu.py sketch1/Sketch/UI/tmpmenu.py
--- sketch.orig/Sketch/UI/tmpmenu.py	1970-01-01 01:00:00.000000000 +0100
+++ sketch1/Sketch/UI/tmpmenu.py	2005-03-19 22:29:18.000000000 +0100
@@ -0,0 +1,351 @@
+# This is a temporary solution to provide the menu infrastructure
+# which is needed by some of the panels. It is quickly pasted together
+# from several files. 
+
+
+from types import StringType, TupleType
+
+import gtk
+
+import Sketch.Editor
+from Sketch.Base import Publisher, const
+
+from command import Command
+
+import skapp
+
+# Map sketch command names to gtk stock ids. The stock ids will be used
+# to create menu items with stock items.
+stock_item_map = {
+    "file_new": "gtk-new",
+    "file_open": "gtk-open",
+    "file_save": "gtk-save",
+    "file_save_as": "gtk-save-as",
+    "cut_selected": "gtk-cut",
+    "copy_selected": "gtk-copy",
+    "paste_clipboard": "gtk-paste",
+    "exit": "gtk-quit",
+    "undo": "gtk-undo",
+    "redo": "gtk-redo",
+    "delete": "gtk-delete",
+    "zoom_100": "gtk-zoom-100",
+    "zoom_in": "gtk-zoom-in",
+    "zoom_out": "gtk-zoom-out",
+}
+
+class SketchMenuItem:
+
+    def __init__(self, command, context, accel, stroke = ''):
+        self.command = command
+        self.context = context
+        self.activate_id = self.connect("activate", self.call_command)
+        self.subscribe()
+        if stroke:
+            key, mods = stroke
+            self.add_accelerator("activate", accel, key, mods, 'visible')
+        # FIXME: Handle user changes to the accelerator. In PyGTK
+        # 0.6/GTK1.2 this was done with these events:
+        # self.connect('add-accelerator', self.accel_added)
+        # self.connect('remove-accelerator', self.accel_removed)
+
+    def subscribe(self):
+        for channel in self.command.channels:
+            self.context.Subscribe(channel, self.changed)
+        self.context.Subscribe(Sketch.Base.const.CHANGED, self.changed)    
+        self.changed()
+
+    def changed(self, *args):
+        self.set_sensitive(self.command.Sensitive(self.context))
+        if self.command.HasDynText():
+            text = self.command.DynText(self.context)
+            if text:
+                self.get_child().set_text(text)
+                self.show()
+            else:
+                self.hide()
+
+    def call_command(self, widget):
+        self.command.Execute(self.context)
+
+    def accel_added(self, item, signalid, accel, key, mod, flags):
+        skapp.keymap.AddItem(self.command.Name(), (key, mod))
+
+    def accel_removed(self, item, accel, key, mod):
+        skapp.keymap.RemoveItem((key, mod))
+
+class SketchMenuCommand(SketchMenuItem, gtk.ImageMenuItem):
+
+    def __init__(self, command, context, accel, stroke = '',
+                 stock_id = ""):
+        """Initialize the SketchMenuCommand
+
+        If the stock_id parameter is given it should be the gtk stock id
+        to use when initializing the GtkImageMenuItem.
+        """
+        if stock_id:
+            gtk.ImageMenuItem.__init__(self, stock_id = stock_id)
+        else:
+            gtk.ImageMenuItem.__init__(self)
+            label = gtk.AccelLabel(command.Title())
+            label.set_alignment(0.0, 0.5)
+            label.show()
+            self.add(label)
+        SketchMenuItem.__init__(self, command, context, accel, stroke = stroke)
+
+class SketchMenuCheck(SketchMenuItem, gtk.CheckMenuItem):
+
+    def __init__(self, command, context, accel, stroke = ''):
+        gtk.CheckMenuItem.__init__(self, command.Title())
+        SketchMenuItem.__init__(self, command, context, accel, stroke)
+
+    def changed(self):
+        SketchMenuItem.changed(self)
+        checked = self.command.Checked(self.context)
+        active = self.active
+        if checked and not active or not checked and active:
+            self.handler_block(self.activate_id)
+            self.set_active(checked)
+            self.handler_unblock(self.activate_id)
+
+
+#
+# Class MenuMixin
+#
+#
+# We distinguish between menudata and the actual graphical
+# representation of a menu. Several menues can share the same menu
+# data tree. When the data changes they are automatically updated. The
+# MenuMixin connects to the data tree and provides the updating
+# mechanism. Derived Menues have to implement the methods "clear" and
+# "append". 
+
+
+class MenuMixin:
+    def __init__(self, context, data=None, keymap = None, accel = None,
+                 allow_tearoff = 1):
+        self.context = context
+        if keymap is None:
+            keymap = Sketch.UI.skapp.keymap
+        self.keymap = keymap
+        
+        self.allow_tearoff = allow_tearoff
+        if accel is None:
+            accel = gtk.AccelGroup()
+        self.accel = accel
+        if data is not None:
+            self.set_menudata(data)
+            
+    def set_menudata(self, data):
+        self.data = data
+        data.Subscribe(Sketch.Base.const.CHANGED, self.rebuild)
+        self.build_menu()
+        
+    def rebuild(self):
+        print "menu changed", self.data.name, id(self)
+        self.clear()
+        self.build_menu()
+        
+    def build_menu(self):
+        if self.allow_tearoff:
+            menuitem = gtk.TearoffMenuItem()
+            menuitem.show()
+            self.append(menuitem)
+
+        last = None
+        for item in self.data.Items():
+            if item is None and last is None:
+                continue
+            ok = self.append_menu_item(self, item)
+            if ok:
+                last = item
+
+    def append_menu_item(self, menu, item, show=1):        
+        # the return value indicates whether in item was appended
+        # (1 --> success) or not (0 --> failed)
+        accel = self.accel
+        keymap = self.keymap
+        context = self.context
+        
+        stroke = ()
+
+        if isinstance(item, StringType):
+            #item = Sketch.Editor.Registry.Menu(item) or \
+            #       Sketch.Editor.Registry.Command(item)
+            item = Sketch.Editor.Registry.Command(item)
+            if item is None:
+                #print "warning: command '%s' not found" % item
+                return 0
+                
+        if item is None:
+            # a separator            
+            menuitem = gtk.MenuItem()            
+        elif isinstance(item, Command):
+            # a command
+            if keymap is not None:
+                stroke = keymap.Keystroke(item)
+            if item.IsCheckCommand():
+                menuitem = SketchMenuCheck(item, context, accel, stroke)
+            else:
+                stock_id = stock_item_map.get(item.Name(), "")
+                menuitem = SketchMenuCommand(item, context, accel, stroke,
+                                             stock_id = stock_id)
+        else:
+            # must be a submenu
+            item.Subscribe(Sketch.Base.const.CHANGED, self.rebuild)
+            
+            submenu = gtk.Menu()
+            if self.allow_tearoff:
+                menuitem = gtk.TearoffMenuItem()
+                menuitem.show()
+                submenu.append(menuitem)
+
+            last = None
+            for subitem in item.Items():                
+                if subitem is None and last is None:
+                    continue
+
+                if self.append_menu_item(submenu, subitem, show):
+                    last = subitem
+                
+            menuitem = gtk.MenuItem(item.Title())
+            menuitem.set_submenu(submenu)
+        
+        if show:
+            menuitem.show()
+        menu.append(menuitem)
+
+        return 1
+
+    def clear(self):
+        """Remove all gtk menu entries. Must be provided by deried class."""
+        raise "not implemented"
+
+    def append(self, menuitem):
+        """Append an entry to a gtk menu. Must be provided by derived class."""
+        raise "not implemented"
+
+
+#
+# A drop-down menu
+#
+class SketchMenu(gtk.Menu, MenuMixin):
+    def __init__(self, *args, **kw):
+        gtk.Menu.__init__(self)
+        apply(MenuMixin.__init__, (self,)+args, kw)
+        
+    def clear(self):
+        for widget in self.get_children():
+            self.remove(widget)
+    
+#
+# A menu bar
+#
+class SketchMenuBar(gtk.MenuBar, MenuMixin):
+    def __init__(self, *args, **kw):
+        gtk.MenuBar.__init__(self)
+        kw['allow_tearoff'] = gtk.FALSE
+        apply(MenuMixin.__init__, (self,)+args, kw) 
+
+    def clear(self):
+        for widget in self.get_children():
+            self.remove(widget)
+
+class MenuNode(Publisher):
+    """Baseclass for menu nodes."""
+    
+    def __init__(self, name, title, translatable = 0):
+        self.name = name
+        self.title = title
+        self.translatable = translatable
+
+    def changed(self):
+        self.issue(const.CHANGED)
+
+    def Title(self):
+        if self.translatable:
+            return _(self.title)
+        else:
+            return self.title
+
+    def Items(self):
+        return ()
+
+
+class ListMenu(MenuNode):
+    
+    def __init__(self, *args, **kw):
+        apply(MenuNode.__init__, (self,)+args, kw)
+        self.items = []
+        
+    def Items(self):
+        return self.items
+
+    def AppendItem(self, item):        
+        self.items.append(item)
+        self.changed()
+
+    def RemoveItem(self, item):
+        self.items.remove(item)
+        self.changed()
+
+class Menu(ListMenu):
+
+    def Submenu(self, title):
+        for item in self.items:
+            # access title directly to avoid translation:
+            if isinstance(item, Menu) and item.title == title: 
+                return item
+        return None
+
+    def AddItem(self, path, item):
+        menu = self
+        if path:
+            if isinstance(path, StringType):
+                path = (path,)
+            for title in path:
+                submenu = menu.Submenu(title)
+                if submenu is None:
+                    submenu = Menu('', title)
+                    menu.AppendItem(submenu)
+                menu = submenu
+        menu.AppendItem(item)
+
+def build_menu_tree(name, title, description, registry, translatable = 1):
+    """Build a menu tree from a description table.
+
+    The syntax of the description in EBNF is:
+    
+      description = [descr] | menu_data
+      descr = None | command_name | command | menu_name | menu_tree |
+              (name,description)
+
+    None gives a separator, command_name or menu_name will be looked
+    up in the registry, command is directly a command.
+    """
+
+    if isinstance(description, MenuNode):
+        menu = MenuNode(name, title, translatable)
+        for item in description.Items():
+            menu.AppendItem(item)
+        return menu
+
+    menu = Menu(name, title, translatable)
+    
+    for descr in description:
+        if isinstance(descr, StringType):
+            item = descr
+        elif descr is None:
+            item = descr
+        elif isinstance(descr, MenuNode):
+            item = descr
+        elif isinstance(descr, Command):
+            item = descr
+        else:
+            _title, _descr = descr
+            item = build_menu_tree('', _title, _descr, registry, 1)
+        menu.AppendItem(item)
+    return menu
+
+
+def BuildMenuTree(name, title, description):
+    return build_menu_tree(name, title, description, Sketch.Editor.Registry)
diff -trNau -U 3 -x '*o' -x '*.pyc' -x '*~*' -x '*.Log' -x NEWS -x CVS -x build sketch.orig/Sketch/UI/toolbox.py sketch1/Sketch/UI/toolbox.py
--- sketch.orig/Sketch/UI/toolbox.py	2005-03-19 18:48:56.000000000 +0100
+++ sketch1/Sketch/UI/toolbox.py	2005-03-19 20:27:08.000000000 +0100
@@ -157,7 +157,7 @@
         # writers, so it should at least require a config-flag.
         descr = ['message_panel', 'reload_panel',
                  None, 'property_panel', 'fill_panel', 'meta_panel',
-                 'palette_panel', 'object_panel']
+                 'palette_panel', 'object_panel', 'inspector_panel']
         tree = Sketch.Editor.BuildMenuTree('<toolbox>', descr)
         return menu.make_menu(tree, None, self.context)
 
