← Back to team overview

gtg team mailing list archive

[Merge] lp:~bertrand-rousseau/gtg/new-tag-editor into lp:gtg

 

Bertrand Rousseau has proposed merging lp:~bertrand-rousseau/gtg/new-tag-editor into lp:gtg.

Requested reviews:
  Gtg developers (gtg)

For more details, see:
https://code.launchpad.net/~bertrand-rousseau/gtg/new-tag-editor/+merge/104271

This branch implements a new tag editor. It is based on alba. mockups : http://i.imgur.com/U6LFg.png

Unfortunately, in alba's design, the color picker is the stock color picker from GNOEM 3.4, which is not available in GTG since we're not coded in GTK3, and we don't have a dependancy against GNOME 3.4 neither. So a custom one has had ot be coded. It's been made to look like the GNOME one.

The tag editor allows to:

 - pick a icon for a tag from the current "emblems" available in the user system.
 - change the tag name (though the tag renaming feature is currently dsibaled in the requester)
 - toggle the "show/hide in work view" state of a tag
 - pick a color from a default palette (= the Tango palette, which is the same as in the GNOE 3.4 colro picker), or define and uses a custom color.

Notes:

 - The custom colors are stored in the GTG configuration.
 - The available icons depends on the user system. We should probably consider shipping a bunch of emblems with GTG to insure a minimal and useful set of icons.
 - Currently the tag context menu is just a shell. In the future, this widget will become the generic sidebar context menu. It will maybe also be used to create custom context menus with non-std widgets (like a color picker). This would reduce the amount of required user intereaction before getting to the desired effect (e.g.: pick a color for a tag). However, this is non-trivial, and therefore right now a more conventional implement with a std context menu and an editor window is preferable.
  - When changing the tag name, a tag rename is *not* sent after each character change to reduce the overhead. Right now, the UI waits for 1 seconds and checks if the user has further updated the tag name. If yes, it waits again, if no, it request a tag rename. Note that tag rename actually doesn't work right now since it is disabled in the requester.
  - tag_context_menu, simple_color_Selector and tag_Editor have been runned againt pyflakes, pep8 and pylint. Most remarks are corrected or disabled when irrelevant.
-- 
https://code.launchpad.net/~bertrand-rousseau/gtg/new-tag-editor/+merge/104271
Your team Gtg developers is requested to review the proposed merge of lp:~bertrand-rousseau/gtg/new-tag-editor into lp:gtg.
=== modified file 'GTG/core/__init__.py'
--- GTG/core/__init__.py	2012-04-23 22:03:22 +0000
+++ GTG/core/__init__.py	2012-05-01 16:52:19 +0000
@@ -66,6 +66,9 @@
             'y_pos': 10,
             'tasklist_sort_column': 5,
             'tasklist_sort_order': 1,
+            },
+'tag_editor': {
+            "custom_colors" : []
             }
 }
 
@@ -117,6 +120,10 @@
         # Save immediately
         self.__conf.parent.write()
 
+    def set_lst(self, name, value_lst):
+        self.__conf[name] = [ str(s) for s in value_lst]
+        # Save immediately
+        self.__conf.parent.write()
 
 class CoreConfig(Borg):
     #The projects and tasks are of course DATA !

=== modified file 'GTG/gtk/browser/browser.py'
--- GTG/gtk/browser/browser.py	2012-04-23 21:50:54 +0000
+++ GTG/gtk/browser/browser.py	2012-05-01 16:52:19 +0000
@@ -179,7 +179,7 @@
         self.vbox_toolbars      = self.builder.get_object("vbox_toolbars")
         
         self.closed_pane        = None
-        self.tagpopup           = TagContextMenu(self.req)
+        self.tagpopup           = TagContextMenu(self.req, self.vmanager)
 
     def _init_ui_widget(self):
         """

=== added file 'GTG/gtk/browser/simple_color_selector.py'
--- GTG/gtk/browser/simple_color_selector.py	1970-01-01 00:00:00 +0000
+++ GTG/gtk/browser/simple_color_selector.py	2012-05-01 16:52:19 +0000
@@ -0,0 +1,366 @@
+# -*- coding: utf-8 -*-
+# pylint: disable-msg=W0201
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program 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 General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+"""
+simple_color_selector: a module defining a widget allowing to pick a color
+from a palette. The widget also allows to define and add new colors.
+"""
+
+import pygtk
+pygtk.require('2.0')
+import gobject
+import gtk
+import math
+
+from GTG import _
+
+DEFAULT_PALETTE = [
+  "#EF2929", "#AD7FA8", "#729FCF", "#8AE234", "#E9B96E",
+  "#FCAF3E", "#FCE94F", "#EEEEEC", "#888A85",
+  "#CC0000", "#75507B", "#3465A4", "#73D216", "#C17D11",
+  "#F57900", "#EDD400", "#D3D7CF", "#555753",
+  "#A40000", "#5C3566", "#204A87", "#4E9A06", "#8F5902",
+  "#CE5C00", "#C4A000", "#BABDB6", "#2E3436",
+]
+
+BUTTON_WIDTH  = 36
+BUTTON_HEIGHT = 24
+
+class SimpleColorSelectorPaletteItem(gtk.DrawingArea): # pylint: disable-msg=R0904,C0301
+    """An item of the color selecctor palette"""
+
+    def __init__(self, color="#FFFFFF"):
+        gtk.DrawingArea.__init__(self)
+        self.__gobject_init__()
+        self.color = color
+        self.selected = False
+        self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
+        # Connect callbacks
+        self.connect("expose_event", self.on_expose)
+        self.connect("configure_event", self.on_configure)
+
+    def __draw(self):
+        """Draws the widget"""
+        alloc = self.get_allocation()
+        alloc_w, alloc_h = alloc[2], alloc[3]
+        # Drawing context
+        cr_ctxt    = self.window.cairo_create() # pylint: disable-msg=E1101
+        gdkcontext = gtk.gdk.CairoContext(cr_ctxt)
+
+        # Draw rounded rectangle
+        my_color = gtk.gdk.color_parse(self.color)
+        gdkcontext.set_source_color(my_color)
+        gdkcontext.rectangle(0, 0, alloc_w, alloc_h)
+        gdkcontext.fill()
+
+        # Outer line
+        gdkcontext.set_source_rgba(0, 0, 0, 0.30)
+        gdkcontext.set_line_width(2.0)
+        gdkcontext.rectangle(0, 0, alloc_w, alloc_h)
+        gdkcontext.stroke()
+
+          # If selected draw a symbol
+        if(self.selected):
+            size = alloc_h * 0.50 - 3
+            pos_x = math.floor((alloc_w-size)/2)
+            pos_y = math.floor((alloc_h-size)/2)
+            gdkcontext.set_source_rgba(255, 255, 255, 0.80)
+            gdkcontext.arc(alloc_w/2, alloc_h/2, size/2 + 3, 0, 2*math.pi)
+            gdkcontext.fill()
+            gdkcontext.set_line_width(1.0)
+            gdkcontext.set_source_rgba(0, 0, 0, 0.20)
+            gdkcontext.arc(alloc_w/2, alloc_h/2, size/2 + 3, 0, 2*math.pi)
+            gdkcontext.stroke()
+            gdkcontext.set_source_rgba(0, 0, 0, 0.50)
+            gdkcontext.set_line_width(3.0)
+            gdkcontext.move_to(pos_x       , pos_y+size/2)
+            gdkcontext.line_to(pos_x+size/2, pos_y+size)
+            gdkcontext.line_to(pos_x+size  , pos_y)
+            gdkcontext.stroke()
+
+    ### callbacks ###
+
+    def on_expose(self, widget, params): # pylint: disable-msg=W0613
+        """Callback: redraws the widget when it is exposed"""
+        self.__draw()
+
+    def on_configure(self, widget, params): # pylint: disable-msg=W0613
+        """Callback: redraws the widget when it is exposed"""
+        self.__draw()
+
+    ### PUBLIC IF ###
+
+    def set_color(self, color):
+        """Defines the widget color"""
+        self.color = color
+
+    def set_selected(self, sel):
+        """Toggle the selected state of the widget"""
+        self.selected = sel
+        self.queue_draw()
+
+    def get_selected(self):
+        """Returns the selected state of the widget"""
+        return self.selected
+
+
+class SimpleColorSelectorPalette(gtk.VBox): # pylint: disable-msg=R0904,C0301
+    """Widget displaying a palette of colors, possibly with a button allowing to
+    define new colors."""
+
+    def __init__(self, width, height, colors, show_add=False):
+        gtk.VBox.__init__(self)
+        self.__gobject_init__()
+        self.width = width
+        self.height = height
+        self.colors = colors
+        self.buttons = {}
+        self.selected_col = None
+        self.show_add = show_add
+        # Build up the widget
+        self.__draw()
+        # Make it visible
+        self.show_all()
+
+    def __draw(self):
+        """Draws the widget"""
+        max_n_col = self.width*self.height
+        # Empty the palette
+        for i in self:
+            for j in i:
+                i.remove(j)
+                del j
+            self.remove(i)
+            del i
+        # Draw the palette container
+        self.set_spacing(4)
+        for i in xrange(len(self.colors)):
+            if i > max_n_col-1:
+                break
+            if i % self.width == 0:
+                cur_hbox = gtk.HBox()
+                self.pack_start(cur_hbox)
+            # add the color box
+            img = SimpleColorSelectorPaletteItem()
+            img.set_size_request( \
+                BUTTON_WIDTH, BUTTON_HEIGHT)
+            img.set_color(self.colors[i])
+            img.connect("button-press-event", self.on_color_clicked)
+            self.buttons[self.colors[i]] = img
+            cur_hbox.pack_start(img, expand=False, fill=False)
+            cur_hbox.set_spacing(4)
+        # Draw the add button if required
+        if self.show_add:
+            cur_hbox = gtk.HBox()
+            self.pack_start(cur_hbox)
+            self.add_button = gtk.Button(stock=gtk.STOCK_ADD)
+            cur_hbox.pack_end(self.add_button)
+            self.add_button.connect("clicked", self.on_color_add)
+        # set as visible
+        self.show_all()
+
+    def __prepend_color(self, col):
+        """Add a color to the palette, at the first position"""
+        self.colors.insert(0, col)
+        if len(self.colors) > self.width:
+            self.colors.pop()
+
+    # Handlers
+    def on_color_clicked(self, widget, event): # pylint: disable-msg=W0613
+        """Callback: when a color is clicked, update the model and
+        notify the parent"""
+        # if re-click: unselect
+        if self.selected_col == widget:
+            self.selected_col.set_selected(False)
+            self.selected_col = None
+        else:
+            # if previous selection: unselect
+            if self.selected_col is not None:
+                self.selected_col.set_selected(False)
+            self.selected_col = widget
+            self.selected_col.set_selected(True)
+        self.emit("color-clicked", widget.color)
+
+    def on_color_add(self, widget): # pylint: disable-msg=W0613
+        """Callback: when adding a new color, show the color definition
+        window, update the model, notifies the parent."""
+        color_dialog = gtk.ColorSelectionDialog(_('Choose a color'))
+        colorsel = color_dialog.colorsel
+        response = color_dialog.run()
+        new_color = colorsel.get_current_color() # pylint: disable-msg=E1101
+        # Check response_id and set color if required
+        if response == gtk.RESPONSE_OK and new_color:
+            strcolor = gtk.color_selection_palette_to_string([new_color])
+            # Add the color to the palette and notify
+            self.add_color(strcolor)
+            # Select the new colro and notify
+            self.selected_col = self.buttons[strcolor]
+            self.selected_col.set_selected(True)
+            self.emit("color-clicked", strcolor)
+        # Clean up
+        color_dialog.destroy()
+
+    # public IF
+
+    def get_colors(self):
+        """Return the list of displayed colors"""
+        return self.colors
+
+    def add_color(self, col):
+        """Add a color to the palette"""
+        self.__prepend_color(col)
+        self.__draw()
+        self.emit("color-added")
+
+    def set_colors(self, col_lst):
+        """Defines the list of displayed colors"""
+        self.colors = col_lst
+        self.__draw()
+
+    def has_color(self, col):
+        """Returns true if the color is displayed in the palette"""
+        return col in self.buttons.keys()
+
+    def get_color_selected(self, col):
+        """Return the selected state of a particular color"""
+        if self.has_color(col):
+            return self.buttons[col].get_selected()
+        else:
+            return None
+
+    def set_color_selected(self, col):
+        """Defines the selected state of a displayed color"""
+        if self.has_color(col):
+            self.buttons[col].set_selected(True)
+            self.selected_col = self.buttons[col]
+
+    def unselect_color(self):
+        """Deselect all colors"""
+        if self.selected_col is not None:
+            self.selected_col.set_selected(False)
+            self.selected_col = None
+
+
+gobject.type_register(SimpleColorSelectorPalette)
+gobject.signal_new("color-clicked", SimpleColorSelectorPalette,
+                   gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, \
+                  (gobject.TYPE_STRING,))
+gobject.signal_new("color-added", SimpleColorSelectorPalette,
+                   gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
+
+class SimpleColorSelector(gtk.VBox): # pylint: disable-msg=R0904,C0301
+    """A widget allowing to picka color from a displayed palette. The user can
+    either pick a color from a default palette, or define and use user-defined
+    colors."""
+
+    def __init__(self):
+        gtk.VBox.__init__(self)
+        self.__gobject_init__()
+        self.sel_color = None
+        # Build up the menu
+        self.__build_widget()
+        # Make it visible
+        self.show_all()
+
+    def __build_widget(self):
+        """Build up the widget"""
+        self.set_spacing(10)
+        self.colsel_pal = SimpleColorSelectorPalette(9, 3, DEFAULT_PALETTE)
+        self.colsel_pal.connect("color-clicked", \
+            self.on_colsel_pal_color_clicked)
+        self.pack_start(self.colsel_pal)
+        self.colsel_custcol = SimpleColorSelectorPalette(9, 1, [], True)
+        self.colsel_custcol.connect("color-clicked", \
+            self.on_colsel_custcol_color_clicked)
+        self.colsel_custcol.connect("color-added", \
+            self.on_colsel_custcol_color_added)
+        self.pack_start(self.colsel_custcol)
+
+    ### handlers ###
+    def on_colsel_pal_color_clicked(self, widget, color): # pylint: disable-msg=W0613,C0301
+        """Callback: identifies the selected color and notifies the parent for 
+        colro from the default palette"""
+        # if we click on the palette, we can unselect a potentially slected
+        # custom color
+        self.colsel_custcol.unselect_color()
+        # Determine if it's a selection or an de-selection
+        if self.colsel_pal.get_color_selected(color) == True:
+            self.sel_color = color
+        else:
+            self.sel_color = None
+        self.emit("color-defined")
+
+    def on_colsel_custcol_color_clicked(self, widget, color): # pylint: disable-msg=W0613,C0301
+        """Callback: identifies the selected color and notifies the parent for 
+        colro from the user-defined palette"""
+        # if we click on the custom colors, we can unselect a potentially slctd
+        # palette color
+        self.colsel_pal.unselect_color()
+        # Determine if it's a selection or an de-selection
+        if self.colsel_custcol.get_color_selected(color) == True:
+            self.sel_color = color
+        else:
+            self.sel_color = None
+        self.emit("color-defined")
+
+    def on_colsel_custcol_color_added(self, widget): # pylint: disable-msg=W0613,C0301
+        """Callback: notifies the parent when a new color is defined"""
+        self.colsel_custcol.get_colors()
+        self.emit("color-added")
+
+    ### public API ###
+
+    def unselected_color(self):
+        """Unselects all colors in the palettes"""
+        self.colsel_pal.unselect_color()
+        self.colsel_custcol.unselect_color()
+
+    def set_selected_color(self, col):
+        """Defines the selected color"""
+        self.sel_color = col
+        if self.colsel_pal.has_color(col):
+            self.colsel_pal.set_color_selected(col)
+        else:
+            # it's not in the std palette, maybe it's in the custom palette?
+            if self.colsel_custcol.has_color(col):
+                self.colsel_custcol.set_color_selected(col)
+           # it's not in the cust. palette: insert as a new custom color
+            else:
+                self.colsel_custcol.add_color(col)
+                self.colsel_custcol.set_color_selected(col)
+
+    def get_selected_color(self):
+        """Returns the currently selected color"""
+        return self.sel_color
+
+    def set_custom_colors(self, col_lst):
+        """Defines the color for the used-defined palette"""
+        self.colsel_custcol.set_colors(col_lst)
+
+    def get_custom_colors(self):
+        """Return the list of user-defined palette"""
+        return self.colsel_custcol.get_colors()
+
+
+gobject.type_register(SimpleColorSelector)
+gobject.signal_new("color-defined", SimpleColorSelector,
+                   gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
+gobject.signal_new("color-added", SimpleColorSelector,
+                   gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())

=== modified file 'GTG/gtk/browser/tag_context_menu.py'
--- GTG/gtk/browser/tag_context_menu.py	2012-04-21 17:10:01 +0000
+++ GTG/gtk/browser/tag_context_menu.py	2012-05-01 16:52:19 +0000
@@ -18,19 +18,29 @@
 # this program.  If not, see <http://www.gnu.org/licenses/>.
 # -----------------------------------------------------------------------------
 
+"""
+tag_context_menu:
+Implements a context (pop-up) menu for the tag item in the sidebar.
+Right now it is just a void shell It is supposed to become a more generic 
+sidebar context for all kind of item displayed there.
+Also, it is supposed to handle more complex menus (with non-std widgets,
+like a color picker)
+"""
+
 import pygtk
 pygtk.require('2.0')
-import gobject
 import gtk
 
 from GTG import _
-from GTG.gtk.browser import GnomeConfig
-
-class TagContextMenu(gtk.Menu):
-
-    def __init__(self, req, tag=None):
+
+class TagContextMenu(gtk.Menu): # pylint: disable-msg=R0904
+    """Context menu fo the tag i the sidebar"""
+
+    def __init__(self, req, vmanager, tag=None):
         self.__gobject_init__()
+        gtk.Menu.__init__(self)
         self.req = req
+        self.vmanager = vmanager
         self.tag = tag
         # Build up the menu
         self.__build_menu()
@@ -39,80 +49,22 @@
         self.show_all()
 
     def __build_menu(self):
+        """Build up the widget"""
         # Color chooser FIXME: SHOULD BECOME A COLOR PICKER
         self.mi_cc = gtk.MenuItem()
-        self.mi_cc.set_label(_("Set color..."))
+        self.mi_cc.set_label(_("Edit Tag..."))
         self.append(self.mi_cc)
-        # Reset color
-        self.mi_rc = gtk.MenuItem()
-        self.mi_rc.set_label(_("Reset color"))
-        self.append(self.mi_rc)
-        # Don't display in work view mode
-        self.mi_wv = gtk.CheckMenuItem()
-        self.mi_wv.set_label(GnomeConfig.TAG_IN_WORKVIEW_TOGG)
-        self.append(self.mi_wv)
         # Set the callbacks
         self.mi_cc.connect('activate', self.on_mi_cc_activate)
-        self.mi_rc.connect('activate', self.on_mi_rc_activate)
-        self.mi_wv_toggle_hid = self.mi_wv.connect('activate', self.on_mi_wv_activate)
-
-    def __set_default_values(self):
-        # Don't set "Hide in workview" as active
-        self.mi_wv.set_active(False)
-
-    def __disable_all(self):
-        pass
-
-    def __enable_all(self):
-        pass
 
     ### PUBLIC API ###
 
     def set_tag(self, tag):
         """Update the context menu items using the tag attributes."""
-        # set_active emit the 'toggle' signal, so we have to disable the handler
-        # when we update programmatically
-        self.mi_wv.handler_block(self.mi_wv_toggle_hid)
-        if tag is None:
-            self.tag = None
-            self.__set_default_values()
-            self.__disable_all()
-        else:
-            self.tag = tag
-            self.__enable_all()
-            is_hidden_in_wv = (self.tag.get_attribute("nonworkview") == "True")
-            self.mi_wv.set_active(is_hidden_in_wv)
-        self.mi_wv.handler_unblock(self.mi_wv_toggle_hid)
+        self.tag = tag
 
     ### CALLBACKS ###
 
-    def on_mi_wv_activate(self, widget):
-        """Toggle the nonworkview attribute of the tag, update the view"""
-        is_hidden_in_wv = not (self.tag.get_attribute("nonworkview") == "True")
-        self.tag.set_attribute("nonworkview", str(is_hidden_in_wv))
-
-    def on_mi_cc_activate(self, widget):
-        color_dialog = gtk.ColorSelectionDialog('Choose color')
-        colorsel = color_dialog.colorsel
-
-        # Get previous color
-        color = self.tag.get_attribute("color")
-        if color is not None:
-            colorspec = gtk.gdk.color_parse(color)
-            colorsel.set_previous_color(colorspec)
-            colorsel.set_current_color(colorspec)
-        response = color_dialog.run()
-        new_color = colorsel.get_current_color()
-        
-        # Check response_id and set color if required
-        if response == gtk.RESPONSE_OK and new_color:
-            strcolor = gtk.color_selection_palette_to_string([new_color])
-            self.tag.set_attribute("color", strcolor)
-        color_dialog.destroy()
-
-    def on_mi_rc_activate(self, widget):
-        """
-        handler for the right click popup menu item from tag tree, when its a @tag
-        """
-        self.tag.del_attribute("color")
-
+    def on_mi_cc_activate(self, widget): # pylint: disable-msg=W0613
+        """Callback: show the tag editor upon request"""
+        self.vmanager.open_tag_editor(self.tag)

=== added file 'GTG/gtk/browser/tag_editor.py'
--- GTG/gtk/browser/tag_editor.py	1970-01-01 00:00:00 +0000
+++ GTG/gtk/browser/tag_editor.py	2012-05-01 16:52:19 +0000
@@ -0,0 +1,406 @@
+# -*- coding: utf-8 -*-
+# pylint: disable-msg=W0201
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program 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 General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+"""
+tag_editor: this module contains two classes: TagIconSelector and TagEditor.
+
+TagEditor implement a dialog window that can be used to edit the properties
+of a tag.
+
+TagIconSelector is intended as a floating window that allows to select an icon
+for a tag.
+"""
+
+import pygtk
+pygtk.require('2.0')
+import gobject
+import gtk
+import gtk.gdk as gdk # pylint: disable-msg=F0401
+
+from GTG import _
+from GTG.gtk.browser.simple_color_selector import SimpleColorSelector
+
+class TagIconSelector(gtk.Window): # pylint: disable-msg=R0904
+    """
+    TagIconSelector is intended as a floating window that allows to select
+    an icon for a tag. It display a list of icon in a popup window.
+    """
+
+    WIDTH  = 310
+    HEIGHT = 200
+
+    def __init__(self):
+        self.__gobject_init__(type=gtk.WINDOW_POPUP)
+        gtk.Window.__init__(self)
+        self.loaded = False
+        self.selected_icon = None
+        self.symbol_model = None
+        # Build up the window
+        self.__build_window()
+        # Make it visible
+        self.hide_all()
+
+    def __build_window(self):
+        """Build up the widget"""
+        self.set_size_request(TagIconSelector.WIDTH, TagIconSelector.HEIGHT)
+        self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_POPUP_MENU)
+        vbox = gtk.VBox()
+        self.add(vbox)
+        scld_win = gtk.ScrolledWindow()
+        vbox.pack_start(scld_win)
+        self.symbol_iv = gtk.IconView()
+        self.symbol_iv.set_pixbuf_column(0)
+        self.symbol_iv.set_property("item-padding", 2)
+        self.symbol_iv.set_property("column-spacing", 0)
+        self.symbol_iv.set_property("row-spacing", 0)
+        scld_win.add(self.symbol_iv)
+        self.remove_bt = gtk.Button(stock=gtk.STOCK_REMOVE)
+        vbox.pack_start(self.remove_bt, fill=False, expand=False)
+        # set the callbacks
+        self.symbol_iv.connect("selection-changed", self.on_selection_changed)
+        self.remove_bt.connect("clicked", self.on_remove_bt_clicked)
+
+    def __focus_out(self):
+        """Hides the window if the user clicks out of it"""
+        win_ptr = self.window.get_pointer() # pylint: disable-msg=E1101
+        win_size = self.get_size()
+        if not(0 <= win_ptr[0] <= win_size[0] and \
+               0 <= win_ptr[1] <= win_size[1]):
+            self.close_selector()
+
+    def __load_icon(self):
+        """Loads emblem icons from the current icon theme"""
+        self.symbol_model = gtk.ListStore(gtk.gdk.Pixbuf, str)
+        for icon in gtk.icon_theme_get_default().list_icons(context="Emblems"):
+            img = gtk.icon_theme_get_default().load_icon(icon, 16, 0)
+            self.symbol_model.append([img, icon])
+        self.symbol_iv.set_model(self.symbol_model)
+        self.loaded = True
+
+    ### callbacks ###
+
+    def on_selection_changed(self, widget): # pylint: disable-msg=W0613
+        """Callback: update the model according to the selected icon. Also
+        notifies the parent widget."""
+        my_path = self.symbol_iv.get_selected_items()
+        if len(my_path)>0:
+            my_iter  = self.symbol_model.get_iter(my_path[0])
+            self.selected_icon = self.symbol_model.get_value(my_iter, 1)
+        else:
+            self.selected_icon = None
+        self.emit('selection-changed')
+        self.close_selector()
+
+    def on_remove_bt_clicked(self, widget): # pylint: disable-msg=W0613
+        """Callback: unselect the current icon"""
+        self.selected_icon = None
+        self.emit('selection-changed')
+        self.close_selector()
+
+    ### PUBLIC IF ###
+
+    def show_at_position(self, pos_x, pos_y):
+        """Displays the window at a specific point on the screen"""
+        if not self.loaded:
+            self.__load_icon()
+        self.move(pos_x, pos_y)
+        self.show_all()
+        ##some window managers ignore move before you show a window. (which
+        # ones? question by invernizzi)
+        self.move(pos_x, pos_y)
+        self.grab_add()
+        #We grab the pointer in the calendar
+        gdk.pointer_grab(self.window, True,
+                         gdk.BUTTON1_MASK | gdk.MOD2_MASK)
+        self.connect('button-press-event', self.__focus_out)
+
+    def close_selector(self):
+        """Hides the window"""
+        self.hide()
+        gtk.gdk.pointer_ungrab()
+        self.grab_remove()
+
+    def get_selected_icon(self):
+        """Return the selected icon. None if no icon is selected."""
+        return self.selected_icon
+
+
+gobject.type_register(TagIconSelector)
+gobject.signal_new("selection-changed", TagIconSelector,
+                   gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
+
+
+class TagEditor(gtk.Window): # pylint: disable-msg=R0904
+    """Window allowing to edit a tag's properties."""
+
+    def __init__(self, req, vmanager, tag=None):
+        gtk.Window.__init__(self)
+        self.__gobject_init__()
+        self.req = req
+        self.vmanager = vmanager
+        self.tag = tag
+        self.config = self.req.get_config('tag_editor')
+        self.custom_colors = None
+        self.tn_entry_watch_id = None
+        self.tn_cb_clicked_hid = None
+        self.tn_entry_clicked_hid = None
+        self.tag_icon_selector = None
+        # Build up the window
+        self.set_position(gtk.WIN_POS_CENTER)
+        self.set_title('Edit tag')
+        self.set_border_width(10)
+        self.set_resizable(False)
+        self.__build_window()
+        self.__set_callbacks()
+        self.set_tag(tag)
+        # Make it visible
+        self.show_all()
+
+    def __build_window(self):
+        """Build up the widget"""
+        # toplevel widget
+        self.top_vbox = gtk.VBox()
+        self.add(self.top_vbox)
+        # header line: icon, table with name and "hide in wv"
+        self.hdr_align = gtk.Alignment()
+        self.top_vbox.pack_start(self.hdr_align)
+        self.hdr_align.set_padding(0, 25, 0, 0)
+        self.hdr_hbox = gtk.HBox()
+        self.hdr_align.add(self.hdr_hbox)
+        self.hdr_hbox.set_spacing(10)
+        # Button to tag icon selector
+        self.ti_bt = gtk.Button()
+        self.ti_bt_label = gtk.Label()
+        self.ti_bt.add(self.ti_bt_label)
+        self.hdr_hbox.pack_start(self.ti_bt)
+        self.ti_bt.set_size_request(64, 64)
+        self.ti_bt.set_relief(gtk.RELIEF_HALF)
+        # vbox for tag name and hid in WV
+        self.tp_table = gtk.Table(2, 2)
+        self.hdr_hbox.pack_start(self.tp_table)
+        self.tp_table.set_col_spacing(0, 5)
+        self.tn_entry_lbl_align = gtk.Alignment(0, 0.5)
+        self.tp_table.attach(self.tn_entry_lbl_align, 0, 1, 0, 1)
+        self.tn_entry_lbl = gtk.Label()
+        self.tn_entry_lbl.set_markup("<span weight='bold'>%s</span>" \
+            % _("Name : "))
+        self.tn_entry_lbl_align.add(self.tn_entry_lbl)
+        self.tn_entry = gtk.Entry()
+        self.tp_table.attach(self.tn_entry, 1, 2, 0, 1)
+        self.tn_entry.set_width_chars(20)
+        self.tn_cb_lbl_align = gtk.Alignment(0, 0.5)
+        self.tp_table.attach(self.tn_cb_lbl_align, 0, 1, 1, 2)
+        self.tn_cb_lbl = gtk.Label(_("Show Tag in Work View :"))
+        self.tn_cb_lbl_align.add(self.tn_cb_lbl)
+        self.tn_cb = gtk.CheckButton()
+        self.tp_table.attach(self.tn_cb, 1, 2, 1, 2)
+        # Tag color
+        self.tc_vbox = gtk.VBox()
+        self.top_vbox.pack_start(self.tc_vbox)
+        self.tc_label_align = gtk.Alignment()
+        self.tc_vbox.pack_start(self.tc_label_align)
+        self.tc_label_align.set_padding(0, 0, 0, 0)
+        self.tc_label = gtk.Label()
+        self.tc_label_align.add(self.tc_label)
+        self.tc_label.set_markup( \
+            "<span weight='bold'>%s</span>" % _("Select Tag Color:"))
+        self.tc_label.set_alignment(0, 0.5)
+        # Tag color chooser
+        self.tc_cc_align = gtk.Alignment(0.5, 0.5, 0, 0)
+        self.tc_vbox.pack_start(self.tc_cc_align)
+        self.tc_cc_align.set_padding(15, 15, 10, 10)
+        self.tc_cc_colsel = SimpleColorSelector()
+        self.tc_cc_align.add(self.tc_cc_colsel)
+        # Icon selector
+        self.tag_icon_selector = TagIconSelector()
+
+    def __set_callbacks(self):
+        """Define the widget callbacks"""
+        # Set the callbacks
+        self.ti_bt.connect('clicked', self.on_ti_bt_clicked)
+        self.tag_icon_selector.connect('selection-changed', \
+            self.on_tis_selection_changed)
+        self.tn_entry_clicked_hid = \
+            self.tn_entry.connect('changed', self.on_tn_entry_changed)
+        self.tn_cb_clicked_hid = self.tn_cb.connect('clicked', \
+            self.on_tn_cb_clicked)
+        self.tc_cc_colsel.connect('color-defined', self.on_tc_colsel_defined)
+        self.tc_cc_colsel.connect('color-added', self.on_tc_colsel_added)
+        self.connect('delete-event', self.on_close)
+        
+    def __set_default_values(self):
+        """Configure the widget components with their initial default values"""
+        # Disable some handlers while setting up the widget to avoid
+        # interferences
+        self.tn_cb.handler_block(self.tn_cb_clicked_hid)
+        self.tn_entry.handler_block(self.tn_entry_clicked_hid)
+        # Default icon
+        markup = "<span size='small'>%s</span>" % _("Click To\nSet Icon")
+        self.ti_bt_label.set_justify(gtk.JUSTIFY_CENTER)
+        self.ti_bt_label.set_markup(markup)
+        self.ti_bt_label.show()
+        # Show in WV
+        self.tn_cb.set_active(True)
+        # Name entry
+        self.tn_entry.set_text(_("Enter tag name here"))
+        self.tn_entry.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, None)
+        # Color selection
+        self.tc_cc_colsel.unselected_color()
+        # Custom colors
+        self.custom_colors = list(self.config.get('custom_colors'))
+        if len(self.custom_colors) > 0:
+            self.tc_cc_colsel.set_custom_colors(self.custom_colors)
+        # Focus
+        self.tn_entry.grab_focus()
+        # Re-enable checkbutton handler_block
+        self.tn_cb.handler_unblock(self.tn_cb_clicked_hid)
+        self.tn_entry.handler_unblock(self.tn_entry_clicked_hid)
+
+    def __set_icon(self, icon):
+        """Set the icon in the related button widget. Restore the label when
+        when no icon is selected."""
+        if icon is not None:
+            for i in self.ti_bt:
+                self.ti_bt.remove(i)
+            ti_bt_img = gtk.image_new_from_icon_name(icon, gtk.ICON_SIZE_BUTTON)
+            ti_bt_img.show()
+            self.ti_bt.add(ti_bt_img)
+        else:
+            for i in self.ti_bt:
+                self.ti_bt.remove(i)
+            self.ti_bt.add(self.ti_bt_label)
+
+    ### PUBLIC API ###
+
+    def set_tag(self, tag):
+        """Update the context menu items using the tag attributes."""
+        # set_active emit the 'toggle' signal, so we have to disable the handler
+        # when we update programmatically
+        self.__set_default_values()
+        if tag is None:
+            self.tag = None
+        else:
+            # Disable some handlers while setting up the widget to avoid
+            # interferences
+            self.tn_cb.handler_block(self.tn_cb_clicked_hid)
+            self.tn_entry.handler_block(self.tn_entry_clicked_hid)
+            self.tag = tag
+            # Update entry
+            name = tag.get_name()[1:]
+            self.tn_entry.set_text(name)
+            # Update visibility in Work View
+            s_hidden_in_wv = (self.tag.get_attribute("nonworkview") == "True")
+            self.tn_cb.set_active(not s_hidden_in_wv)
+            # If available, update icon
+            if (tag.get_attribute('icon') is not None):
+                icon = tag.get_attribute('icon')
+                self.__set_icon(icon)
+            # If available, update color selection
+            if (tag.get_attribute('color') is not None):
+                col = tag.get_attribute('color')
+                self.tc_cc_colsel.set_selected_color(col)
+            # Re-enable checkbutton handler_block
+            self.tn_cb.handler_unblock(self.tn_cb_clicked_hid)
+            self.tn_entry.handler_unblock(self.tn_entry_clicked_hid)
+
+    ### CALLBACKS ###
+
+    def watch_tn_entry_changes(self):
+        """Monitors the value changes in the tag name entry. If no updates have
+        been noticed after 1 second, request an update."""
+        cur_value = self.tn_entry.get_text()
+        if self.tn_entry_last_recorded_value != cur_value:
+            # they're different: there's been some updates, wait further
+            return True
+        else:
+            # they're the same. We can unregister the watcher and
+            # update the tag name
+            self.tn_entry_watch_id = None
+            if cur_value.strip() != '':
+                self.req.rename_tag(self.tag.get_name(), "@"+cur_value)
+            return False
+
+    def on_tis_selection_changed(self, widget): # pylint: disable-msg=W0613
+        """Callback: update tag attributes whenever an icon is (un)selected."""
+        icon = self.tag_icon_selector.get_selected_icon()
+        if icon is not None:
+            self.tag.set_attribute("icon", icon)
+            self.__set_icon(icon)
+        else:
+            self.tag.del_attribute("icon")
+            self.__set_icon(None)
+
+
+    def on_ti_bt_clicked(self, widget): # pylint: disable-msg=W0613
+        """Callback: displays the tag icon selector widget next
+        to the button."""
+        rect = self.ti_bt.get_allocation()
+        pos_x, pos_y = \
+            self.ti_bt.window.get_origin() # pylint: disable-msg=E1101
+        self.tag_icon_selector.show_at_position(pos_x+rect.x+rect.width, \
+            pos_y+rect.y)
+
+    def on_tn_entry_changed(self, widget): # pylint: disable-msg=W0613
+        """Callback: checks tag name validity and start value changes monitoring
+        to decide when to update a tag's name."""
+        self.tn_entry_last_recorded_value = self.tn_entry.get_text()
+        # check validity
+        if self.tn_entry_last_recorded_value.strip() == "":
+            self.tn_entry.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, \
+                gtk.STOCK_DIALOG_ERROR)
+        else:
+            self.tn_entry.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, None)
+        # filter out change requests to reduce commit overhead
+        if self.tn_entry_watch_id is None:
+            # There is no watchers for the text entry. Register one.
+            # Also, wait 1 second before commiting the change in order to
+            # reduce rename requests
+            self.tn_entry_watch_id = gobject.timeout_add(1000, \
+                self.watch_tn_entry_changes)
+
+    def on_tn_cb_clicked(self, widget): # pylint: disable-msg=W0613
+        """Callback: toggle the nonworkview property according to the related
+        widget's state."""
+        if self.tag is not None:
+            show_in_wv = self.tn_cb.get_active()
+            hide_in_wv = not show_in_wv
+            self.tag.set_attribute('nonworkview', str(hide_in_wv))
+
+    def on_tc_colsel_defined(self, widget): # pylint: disable-msg=W0613
+        """Callback: update the tag color depending on the current color
+        selection"""
+        color = self.tc_cc_colsel.get_selected_color()
+        if self.tag is not None:
+            if color is not None:
+                self.tag.set_attribute('color', color)
+            else:
+                self.tag.del_attribute('color')
+
+    def on_tc_colsel_added(self, widget): # pylint: disable-msg=W0613
+        """Callback: if a new color is added, we register it in the
+        configuration"""
+        self.custom_colors = self.tc_cc_colsel.get_custom_colors()
+        self.config.set_lst("custom_colors", [s for s in self.custom_colors])
+        self.req.save_config()
+
+    def on_close(self, widget, event): # pylint: disable-msg=W0613
+        """Callback: hide the tag editor when the close the window."""
+        self.vmanager.close_tag_editor()
+        return True

=== modified file 'GTG/gtk/manager.py'
--- GTG/gtk/manager.py	2012-03-15 13:48:05 +0000
+++ GTG/gtk/manager.py	2012-05-01 16:52:19 +0000
@@ -41,7 +41,7 @@
 from GTG.tools.logger        import Log
 from GTG.gtk.backends_dialog import BackendsDialog
 from GTG.backends.backendsignals import BackendSignals
-
+from GTG.gtk.browser.tag_editor import TagEditor
 
 
 class Manager(object):
@@ -85,6 +85,9 @@
         # Initialize  dialogs
         self.preferences_dialog = None
         self.edit_backends_dialog = None
+
+        # Tag Editor
+        self.tag_editor_dialog = None
         
         #DBus
         DBusTaskWrapper(self.req, self)
@@ -234,6 +237,16 @@
             if t.get_id() in self.opened_task:
                 self.close_task(t.get_id())
 
+    def open_tag_editor(self, tag):
+        if not self.tag_editor_dialog:
+            self.tag_editor_dialog = TagEditor(self.req, self, tag)
+        else:
+            self.tag_editor_dialog.set_tag(tag)
+        self.tag_editor_dialog.show()
+
+    def close_tag_editor(self):
+        self.tag_editor_dialog.hide()
+
 ### URIS ###################################################################
 
     def open_uri_list(self, unused, uri_list):