On 03/01/13 12:50, Thomas wrote:
>     I'd like to see the return of the 'Advanced Color Sampler' from
> v.1.0 with the option to display/hide 'Color History', 'Details' and
> 'Harmonies'.
> 
> The new Color layout forces you to use either one method or the other,
> but you cannot use, say, 'Component Sliders' and 'HSV Cube' to
> complement one another.

I think we're well rid of the old fixed-angle Harmonies dial. Similar
but more flexible effects can be achieved with gamut masks, at least
that's the idea.

Others have mentioned being able to see more than one colour selector at
once. But we're going to need more sidebar space for that... :D
I've been pondering using tabbed sidebars for a while, and breaking out
the individual colour selectors, whatever they are, into individually
moveable tabs at the same time. Take a look at the attached code sketch
or at https://gist.github.com/4443517 in case the attachment doesn't
make it through the mailing list, and let me know what you think. It's
not a running program, but it's one possible way of organising the space
in a sensible fashion (people often ask for two sidebars too).

This is likely to work quite nicely in GTK3 as well, since it uses only
standard widgets :)

> and you have, as far as I can tell,
> no numerical RGB readout whatsoever.

Try double-clicking the preview widgets at the bottom if you need to
enter digits or hex values.

> People's workflow naturally vary, but overall I believe the color
> samplers in v.1.10 are generally inferior to those of 1.0, which makes a
> case for sticking with v.1.0.  Generally I thought the color
> samplers/pickers in v.1.0 worked well, and followed a similar logic as
> that of Photoshop or Painter, which made it easy to switch between those
> applications and MyPaint.

Specifics, please. We seek to improve :)

Bear in mind that I don't use those applications. There are also good
reasons for not slavishly following another app's interface.

> The only grievance I had with 1.0's color
> tools was that the 'Color Triangle' would rotate when changing hue.  I'd
> like to see an option to lock it in place, as is the case in Painter
> and, I believe, Photoshop.

We use the standard GtkHSV widget for the colour triangle, so there's
not much we can do about that.

-- 
Andrew Chadwick
#!/usr/bin/python
# Interface idea: dragging tabs and suchlike.
# Released as Creative Commons Zero: CC0 v1.0 <[email protected]>

import gobject
import gtk
from gtk import gdk
import cairo
from gettext import gettext as _

from warnings import warn


def is_class(obj):
    """True if its argument is a class object.

      >>> import xml.dom
      >>> is_class(xml.dom.Node)
      True
      >>> is_class("a string")
      False
    """

    if type(obj).__name__ == 'classobj':  # Old-style class
        return True
    # Potential new-style class
    try:
        return issubclass(obj, object)
    except TypeError:
        pass
    return False


def get_qualified_class_name(obj):
    """Returns the qualified name for a class or an instance.

      >>> import xml.dom as d
      >>> get_qualified_class_name(d.Node)
      'xml.dom.Node'
      >>> get_qualified_class_name(d.Node())
      'xml.dom.Node'

    The returned qualified names are strings that can be used by
    `load_class()` for importing the relevant class.
    """
    obj_class = obj
    if not is_class(obj_class):
        obj_class = getattr(obj, "__class__", None)
        if not is_class(obj_class):
            raise TypeError, "obj must be either a class or an instance"
    module = obj_class.__module__
    name = obj_class.__name__
    assert module != "__main__"
    sep = "."
    return sep.join((module, name))


def load_class(name):
    """Load a class object by qualified name.

    Returns either a class object, or `None` in the case of any error. The
    `name` parameter is a fully qualified name. The name is split, and used
    in an `__import__()` invocation which does the equivalent of "from
    namespace import classname".

      >>> Node = load_class("xml.dom.Node")  # "from xml.dom import Node"
      >>> import xml.dom
      >>> Node is xml.dom.Node
      True

    """
    sep = "."
    name_parts = name.split(sep)
    assert len(name_parts) > 1
    class_name = name_parts.pop(-1)
    module_name = sep.join(name_parts)
    try:
        module_obj = __import__(module_name, fromlist=[class_name], level=0)
    except ImportError, err:
        warn(err.message, category=ImportWarning)
    class_obj = getattr(module_obj, class_name, None)
    if is_class(class_obj):
        return class_obj
    else:
        warn('Imported "%s" from "%s", but it is not a class object'
             % (class_name, module_name),
             category=ImportWarning)
    return None



NOTEBOOK_DRAG_ID = 4242
TAB_ICON_SIZE = gtk.ICON_SIZE_SMALL_TOOLBAR
TAB_TOOLTIP_ICON_SIZE = gtk.ICON_SIZE_DIALOG



class ToolTab:
    """Interface for widgets which appear in ToolStacks.
    """

    title = "Untitled"
    description = "No Description"
    icon_name = "gtk-missing-image"


    def make_tab_label(self):
        """Creates and returns a new tab label widget for the page
        """
        img = gtk.Image()
        img.set_from_icon_name(self.icon_name, TAB_ICON_SIZE)
        img.connect("query-tooltip", self.__tab_label_tooltip_query_cb,
                    self.title, self.description, self.icon_name)
        img.set_property("has-tooltip", True)
        return img


    def __tab_label_tooltip_query_cb(self, widget, x, y, kbd, tooltip,
                                     title, desc, icon_name):
        tooltip.set_icon_from_icon_name(icon_name, TAB_TOOLTIP_ICON_SIZE)
        markup = "<b>%s</b>\n%s" % (title, desc)
        tooltip.set_markup(markup)
        return True


class _PlaceholderCanvas (gtk.DrawingArea):

    def __init__(self):
        gtk.DrawingArea.__init__(self)
        self.connect("expose-event", self.__expose_cb)
        self.set_size_request(64, 64)

    def __expose_cb(self, widget, event):
        import math
        cr = widget.get_window().cairo_create()
        cr.set_source_rgb(0.2, 0.3, 0.7)
        cr.paint()
        x, y, w, h = widget.get_allocation()
        r = min(w, h) * 0.4
        cr.arc(w/2, h/2, r, 0, 2*math.pi)
        cr.set_source_rgb(0.80, 0.85, 0.30)
        cr.set_line_width(5)
        cr.stroke()



class Workspace (gtk.EventBox):
    """A central canvas widget and two sidebar ToolStacks.
    """

    __lpaned = None  #: HPaned holding the left stack and `__rpaned`
    __rpaned = None  #: HPaned holding the canvas, and the right stack
    __lstack = None
    __rstack = None
    __floating = None

    def __init__(self):
        gtk.EventBox.__init__(self)
        self.__lpaned = lpaned = gtk.HPaned()
        self.__rpaned = rpaned = gtk.HPaned()
        self.__lstack = lstack = ToolStack()
        self.__rstack = rstack = ToolStack()
        lstack.set_workspace(self)
        rstack.set_workspace(self)
        lstack.connect("hide", self.__stack_hide_cb, lpaned)
        rstack.connect("hide", self.__stack_hide_cb, rpaned)
        #lstack.set_tab_pos(gtk.POS_RIGHT) # perhaps only if the screen is wide?
        #rstack.set_tab_pos(gtk.POS_LEFT)  # it does save vertical space...
        lpaned.pack1(lstack, resize=False, shrink=False)
        lpaned.pack2(rpaned, resize=True, shrink=False)
        rpaned.pack2(rstack, resize=False, shrink=False)
        self.set_canvas(_PlaceholderCanvas())
        self.add(lpaned)
        self.__floating = set()


    def set_canvas(self, widget):
        current = self.__rpaned.get_child1()
        if current is not None:
            self.__rpaned.remove(current)
        self.__rpaned.pack1(widget, resize=True, shrink=False)


    def build_from_layout(self, layout):
        llayout = layout.get("left_sidebar", [])
        rlayout = layout.get("right_sidebar", [])
        self.__lstack.build_from_layout(llayout)
        self.__rstack.build_from_layout(rlayout)


    def get_layout(self):
        llayout = self.__lstack.get_layout()
        rlayout = self.__rstack.get_layout()
        float_layouts = [w.get_layout() for w in self.__floating]
        return {
          "left_sidebar": llayout,
          "right_sidebar": rlayout,
          "floating": float_layouts,
          }


    def _tool_tab_drag_begin_cb(self):
        for stack in (self.__lstack, self.__rstack):
            if stack.is_empty():
                stack.show_all()


    def _tool_tab_drag_end_cb(self):
        for stack in (self.__lstack, self.__rstack):
            if stack.is_empty():
                stack.hide()


    def __stack_hide_cb(self, stack, paned):
        # Reset any user-modified paned position if the hide is due to the
        # sidebar stack having been emptied out. On the next show, the stack
        # wil use the size-request of its children, which should be a single
        # placeholder notebook, 16x8.
        if stack.is_empty():
            paned.set_position(-1)


    def register_floating_window(self, win):
        self.__floating.add(win)


    def unregister_floating_window(self, win):
        self.__floating.remove(win)



class ToolStack (gtk.EventBox):
    """Vertical stack of ToolTab groups.
    
    The layout has movable dividers between the groups of ToolTabs, and an
    empty group on the end which accepts tabs dragged to it. The groups are
    implmented as `gtk.Notebook`s, but that interface is not exposed; instead,
    ToolStacks are constructed from layout defnitions built from simple types.
    """

    # Class constants
    PLACEHOLDER_HEIGHT = 8
    PLACEHOLDER_WIDTH = 16
    PLACEHOLDER_PACKING_RESIZE = True
    PLACEHOLDER_PACKING_SHRINK = False
    NORMAL_PACKING_RESIZE = False
    NORMAL_PACKING_SHRINK = False
    SUBPANED_PACKING_RESIZE = True
    SUBPANED_PACKING_SHRINK = False

    # Instance var defaults
    __tab_pos = None  #: Tab position for new notebooks; `None` means default.
    __workspace = None  #: A central workspace to notify about dragging

    def __init__(self):
        """Constructs a new stack with a single placeholder group.
        """
        gtk.EventBox.__init__(self)
        self.add(self.__make_notebook())
        self.set_size_request(-1, -1)


    def set_workspace(self, workspace):
        self.__workspace = workspace


    def get_workspace(self):
        return self.__workspace


    def add_page(self, page):
        """Adds a page to the first group in the stack.
        """
        notebook = self.__get_first_notebook()
        notebook.append_page(page, page.make_tab_label())
        notebook.set_tab_reorderable(page, True)
        notebook.set_tab_detachable(page, True)


    def build_from_layout(self, desc):
        """Loads groups and pages from a layout description.

        Desc is a list of group defintions; each group definition is a list of
        page class names as used by `load_class()`.
        """
        next_nb = self.__get_first_notebook()
        assert next_nb.get_n_pages() == 0
        for nb_desc in desc:
            nb = next_nb
            for page_class_name in nb_desc:
                page_class = load_class(page_class_name)
                if page_class is None:
                    continue
                page = page_class()
                page_label = page.make_tab_label()
                page.__prev_size = (-1, -1)
                if nb.get_n_pages() == 0:
                    next_nb = self.__append_new_placeholder(nb)
                nb.append_page(page, page_label)
                nb.set_tab_reorderable(page, True)
                nb.set_tab_detachable(page, True)


    def get_layout(self):
        """Returns a description of the current layout using simple types.
        """
        layout_desc = []
        for nb in self.__get_notebooks():
            nb_desc = []
            for page in nb:
                page_qname = get_qualified_class_name(page)
                nb_desc.append(page_qname)
            if len(nb_desc) > 0:
                layout_desc.append(nb_desc)
        return layout_desc


    def set_tab_pos(self, tab_pos):
        """Sets the tab position for all groups (see `gtk.Notebook`).
        """
        for nb in self.__get_notebooks():
            nb.set_tab_pos(tab_pos)
        self.__tab_pos = tab_pos


    def is_empty(self):
        """Returns true if this stack contains only a tab drop placeholder.
        """
        widget = self.get_child()
        if isinstance(widget, gtk.Paned):
            return False
        assert isinstance(widget, gtk.Notebook)
        return widget.get_n_pages() == 0


    def __get_first_notebook(self):
        widget = self.get_child()
        if isinstance(widget, gtk.Paned):
            widget = widget.get_child1()
        assert isinstance(widget, gtk.Notebook)
        return widget


    def __get_notebooks(self):
        child = self.get_child()
        if child is None:
            return []
        queue = [child]
        notebooks = []
        while len(queue) > 0:
            widget = queue.pop(0)
            if isinstance(widget, gtk.Paned):
                queue.append(widget.get_child1())
                queue.append(widget.get_child2())
            elif isinstance(widget, gtk.Notebook):
                notebooks.append(widget)
            else:
                warn("Unknown member type: %s" % str(widget), RuntimeWarning)
        assert len(notebooks) > 0
        return notebooks


    def __make_notebook(self):
        nb = gtk.Notebook()
        nb.set_group_id(NOTEBOOK_DRAG_ID)
        nb.connect("create-window", self.__nb_create_window_cb)
        nb.connect("page-added", self.__nb_page_added_cb)
        nb.connect("page-removed", self.__nb_page_removed_cb)
        nb.connect("expose-event", self.__nb_expose_cb)
        nb.connect("size-request", self.__nb_size_request_cb)
        nb.connect_after("drag-begin", self.__nb_drag_begin_cb)
        nb.connect_after("drag-end", self.__nb_drag_end_cb)
        nb.set_scrollable(True)
        if self.__tab_pos is not None:
            nb.set_tab_pos(self.__tab_pos)
        return nb


    def __nb_drag_begin_cb(self, nb, *a):
        # Record the notebook's size in the page; this will be recreated
        # if a valid drop happens into a fresh ToolWindow or into a
        # placeholder notebook.
        alloc = nb.get_allocation()
        page_num = nb.get_current_page()
        page = nb.get_nth_page(page_num)
        page.__prev_size = (alloc.width, alloc.height)
        # Notify any workspace: causes empty sidebars to show.
        if self.__workspace is not None:
            self.__workspace._tool_tab_drag_begin_cb()


    def __nb_drag_end_cb(self, nb, *a):
        # Notify any workspace: causes empty sidebars to hide again.
        if self.__workspace is not None:
            self.__workspace._tool_tab_drag_end_cb()


    def __nb_size_request_cb(self, notebook, req):
        # Placeholder notebooks negotiate small sizes
        if notebook.get_n_pages() == 0:
            req.height = self.PLACEHOLDER_HEIGHT
            req.width = self.PLACEHOLDER_WIDTH


    def __nb_create_window_cb(self, notebook, page, x, y):
        # Dragging into empty space creates a new stack in a new window,
        # and stashes the page there.
        win = ToolStackWindow()
        if self.__workspace is not None:
            win.stack.set_workspace(self.__workspace)
            win.set_transient_for(self.__workspace.get_toplevel())
        notebook.remove(page)
        w, h = page.__prev_size
        new_nb = win.stack.__get_first_notebook()
        new_nb.append_page(page, page.make_tab_label())
        new_nb.set_tab_reorderable(page, True)
        new_nb.set_tab_detachable(page, True)
        new_placeholder = win.stack.__append_new_placeholder(new_nb)
        new_paned = new_placeholder.get_parent()
        new_paned.set_position(h)
        # Initial position. Hopefully this will work.
        win.move(x, y)
        win.set_default_size(w, h)
        win.show_all()


    def __nb_expose_cb(self, notebook, event):
        if notebook.get_n_pages() > 0:
            return False

        # Override placeholder drawing
        cr = notebook.get_window().cairo_create()
        cr.rectangle(event.area)
        cr.clip()

        # Pattern
        style = self.get_style()
        state = self.get_state()
        bg = style.bg[state]
        dark = style.dark[state]
        bg_rgb = [(bg.red_float + dark.red_float)/2.0,
                  (bg.green_float + dark.green_float)/2.0,
                  (bg.blue_float + dark.blue_float)/2.0]
        dark_rgb = [max(0, c-0.01) for c in bg_rgb]
        light_rgb = [min(1, c+0.01) for c in bg_rgb]
        cr.set_source_rgb(*dark_rgb)
        cr.paint()

        cr.set_source_rgb(*light_rgb)
        x, y, w, h = tuple(event.area)
        sw = 6
        y = event.area.y
        for x in range(event.area.x, event.area.x+w+h, sw*2):
            cr.move_to(x, y)
            cr.line_to(x-h, y+h)
            cr.line_to(x-h+sw, y+h)
            cr.line_to(x+sw, y)
            cr.close_path()
            cr.fill()

        # Slight shadow gradient under the final divider
        #dark_rgb = [dark.red_float, dark.green_float, dark.blue_float]
        shadow0 = [0, dark_rgb[0], dark_rgb[1], dark_rgb[2], 1]
        shadow1 = [1, dark_rgb[0], dark_rgb[1], dark_rgb[2], 0]
        x, y, w, h = tuple(event.area)
        h = self.PLACEHOLDER_HEIGHT / 2.0
        lg = cairo.LinearGradient(x, y, x, y+h)
        lg.add_color_stop_rgba(*shadow0)
        lg.add_color_stop_rgba(*shadow1)
        cr.set_source(lg)
        cr.rectangle(x, y, w, h)
        cr.fill()

        return True    # All placeholder drawing was handled here



    def __nb_page_added_cb(self, notebook, child, page_num):
        gobject.idle_add(self.__update_structure_cb)


    def __nb_page_removed_cb(self, notebook, child, page_num):
        gobject.idle_add(self.__update_structure_cb)


    def __append_new_placeholder(self, old_placeholder):
        """Appends a new placeholder after a current or former placeholder.
        """
        old_placeholder_parent = old_placeholder.get_parent()
        assert old_placeholder_parent is not None
        new_paned = gtk.VPaned()
        if isinstance(old_placeholder_parent, gtk.Paned):
            assert old_placeholder is not old_placeholder_parent.get_child1()
            assert old_placeholder     is old_placeholder_parent.get_child2()
            old_placeholder_parent.remove(old_placeholder)
            old_placeholder_parent.pack2(new_paned,
                                         self.SUBPANED_PACKING_RESIZE,
                                         self.SUBPANED_PACKING_SHRINK)
        else:
            assert old_placeholder_parent is self
            old_placeholder_parent.remove(old_placeholder)
            old_placeholder_parent.add(new_paned)
        new_placeholder = self.__make_notebook()
        new_paned.pack1(old_placeholder,
                        self.NORMAL_PACKING_RESIZE,
                        self.NORMAL_PACKING_SHRINK)
        new_paned.pack2(new_placeholder,
                        self.PLACEHOLDER_PACKING_RESIZE,
                        self.PLACEHOLDER_PACKING_SHRINK)
        new_paned.show_all()
        new_paned.queue_resize()
        return new_placeholder


    def __update_structure_cb(self):
        """Maintains structure after "page-added" & "page-deleted" events.

        If a page is added to the placeholder notebook on the end by the user
        dragging a tab there, a new placeholder must be created and the tree
        structure repacked. Similarly emptying out a notebook by dragging tabs
        around must result in the empty notebook being removed.

        This callback is queued as an idle function in response to the above
        events because moving from one paned to another invokes both remove and
        add. If the structure doesn't need changing, calling it multiple times
        is harmless.
        """

        # The final notebook should always be an empty placeholder. If
        # it isn't, then create a new paned with a placeholder in the
        # second slot.
        notebooks = self.__get_notebooks()
        if len(notebooks) == 0:
            return
        placeholder_nb = notebooks.pop(-1)
        nb_parent = placeholder_nb.get_parent()
        if placeholder_nb.get_n_pages() > 0:
            # Something was dropped into a former placeholder, populating it
            # Create a new placeholder, and set the bar position for the newly
            # populated notebook, which will be in child1 of the parent paned.
            newpop_nb = placeholder_nb
            assert newpop_nb.get_n_pages() == 1
            placeholder_nb = self.__append_new_placeholder(newpop_nb)
            paned = newpop_nb.get_parent()
            newpop_page = newpop_nb.get_nth_page(0)
            newpop_w, newpop_h = newpop_page.__prev_size
            paned.set_position(newpop_h)


        # Detect emptied middle notebooks and remove them. There should be no
        # notebooks in the stack whose parent is not a Paned at this point.
        while len(notebooks) > 0:
            nb = notebooks.pop(0)
            nb_parent = nb.get_parent()
            assert isinstance(nb_parent, gtk.Paned)
            if nb.get_n_pages() > 0:
                continue
            nb_grandparent = nb_parent.get_parent()
            assert nb     is nb_parent.get_child1()
            assert nb is not nb_parent.get_child2()
            sib = nb_parent.get_child2()
            nb_parent.remove(nb)
            nb_parent.remove(sib)
            if isinstance(nb_grandparent, gtk.Paned):
                assert nb_parent is not nb_grandparent.get_child1()
                assert nb_parent     is nb_grandparent.get_child2()
                nb_grandparent.remove(nb_parent)
                if sib is placeholder_nb:
                    nb_grandparent.pack2(sib,
                                         self.PLACEHOLDER_PACKING_RESIZE,
                                         self.PLACEHOLDER_PACKING_SHRINK)
                else:
                    nb_grandparent.pack2(sib,
                                         self.NORMAL_PACKING_RESIZE,
                                         self.NORMAL_PACKING_SHRINK)
            else:
                assert nb_grandparent is self
                nb_grandparent.remove(nb_parent)
                nb_grandparent.add(sib)

        # Detect empty stacks
        n_tabs_total = 0
        for nb in self.__get_notebooks():
            n_tabs_total += nb.get_n_pages()
        parent = self.get_parent()
        if n_tabs_total == 0:
            if isinstance(parent, ToolStackWindow):
                parent.destroy()
            else:
                self.hide()
            return

        # Update title of parent ToolStackWindows
        if isinstance(parent, ToolStackWindow):
            page_titles = []
            for nb in self.__get_notebooks():
                for p in nb:
                    page_titles.append(p.title)
            parent._update_title(page_titles)




class ToolStackWindow (gtk.Window):
    """A floating window containing a single `ToolStack`.
    """

    # Instance variable defaults and docs
    stack = None  #: The ToolStack child of the window
    __pos = None


    def __init__(self):
        gtk.Window.__init__(self)
        self.set_type_hint(gdk.WINDOW_TYPE_HINT_UTILITY)
        self.set_accept_focus(False)
        self.connect("destroy", self.__destroy_cb)
        self.stack = ToolStack()
        self.add(self.stack)
        self.connect("map-event", self.__map_cb)
        self.connect("configure-event", self.__configure_cb)
        self._update_title([])
        self.__pos = {}

    def __configure_cb(self, widget, event):
        f_ex = self.window.get_frame_extents()
        x = max(0, f_ex.x)
        y = max(0, f_ex.y)
        self.__pos = dict(x=x, y=y, w=event.width, h=event.height)

    def get_layout(self):
        return {
          "position": self.__pos,
          "contents": self.stack.get_layout(),
          }

    def __map_cb(self, widget, event):
        win = widget.get_window()
        win.set_decorations(gdk.DECOR_BORDER|gdk.DECOR_RESIZEH)
        win.set_functions(gdk.FUNC_RESIZE|gdk.FUNC_MOVE)
        workspace = self.stack.get_workspace()
        if workspace is not None:
            workspace.register_floating_window(self)

    def __destroy_cb(self, widget):
        workspace = self.stack.get_workspace()
        if workspace is not None:
            workspace.unregister_floating_window(self)

    def _update_title(self, tool_tab_titles):
        window_title_tmpl = _("%s - MyPaint")
        window_title_sep = _(", ")
        title = window_title_tmpl % (window_title_sep.join(tool_tab_titles))
        self.set_title(title)


class _TestToolTab (gtk.Label, ToolTab):
    body_label = "Test Page"
    icon_name = 'gtk-missing-image'
    def __init__(self):
        gtk.Label.__init__(self, self.body_label)
        self.set_size_request(150, 100)
    @property
    def title(self):
        return self.body_label

class _TestToolTab1 (_TestToolTab):
    body_label = "Apples"
    icon_name = 'gtk-dialog-error'

class _TestToolTab2 (_TestToolTab):
    body_label = "Oranges"
    icon_name = 'gtk-dialog-warning'

class _TestToolTab3 (_TestToolTab):
    body_label = "Limes"
    icon_name = 'gtk-dialog-info'

class _TestToolTab4 (_TestToolTab):
    body_label = "Grapes"
    icon_name = 'gtk-dialog-question'




if __name__ == '__main__':
    layout_def = {
        "left_sidebar": [['taboret_standalone._TestToolTab1',
                         'taboret_standalone._TestToolTab2',
                         'taboret_standalone._TestToolTab3'] ],
        "right_sidebar": [['taboret_standalone._TestToolTab4'],
                          ['taboret_standalone._TestToolTab1',
                           'taboret_standalone._TestToolTab2'] ],
      }
    win2 = gtk.Window()
    win2.set_title("Workspace Layout Test")
    workspace = Workspace()
    workspace.set_size_request(640, 480)
    workspace.build_from_layout(layout_def)
    win2.add(workspace)
    win2.show_all()
    def __test_quit_cb(*a):
        print "*** exiting, workspace layout dump follows"
        print workspace.get_layout()
        gtk.main_quit()
    win2.connect("destroy", __test_quit_cb)
    gtk.main()
_______________________________________________
Mypaint-discuss mailing list
[email protected]
https://mail.gna.org/listinfo/mypaint-discuss

Reply via email to