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
