[hamster-applet] hooray for tags autocomplete



commit 11b85b293396fe0000d8d9bd2d6c0ddc90cd04c8
Author: Toms Bauģis <toms baugis gmail com>
Date:   Sun Nov 22 18:41:21 2009 +0000

    hooray for tags autocomplete

 data/hamster.ui       |   17 +---
 hamster/standalone.py |  251 +++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 226 insertions(+), 42 deletions(-)
---
diff --git a/data/hamster.ui b/data/hamster.ui
index bb341c1..abff98d 100644
--- a/data/hamster.ui
+++ b/data/hamster.ui
@@ -139,15 +139,11 @@
                     <property name="visible">True</property>
                     <property name="spacing">4</property>
                     <child>
-                      <object class="GtkAlignment" id="alignment1">
+                      <object class="GtkAlignment" id="new_name_box">
                         <property name="width_request">300</property>
                         <property name="visible">True</property>
                         <child>
-                          <object class="GtkEntry" id="new_name">
-                            <property name="visible">True</property>
-                            <property name="can_focus">True</property>
-                            <property name="invisible_char">&#x2022;</property>
-                          </object>
+                          <placeholder/>
                         </child>
                       </object>
                       <packing>
@@ -156,14 +152,11 @@
                       </packing>
                     </child>
                     <child>
-                      <object class="GtkAlignment" id="alignment4">
+                      <object class="GtkAlignment" id="new_tags_box">
+                        <property name="width_request">300</property>
                         <property name="visible">True</property>
                         <child>
-                          <object class="GtkEntry" id="new_description">
-                            <property name="visible">True</property>
-                            <property name="can_focus">True</property>
-                            <property name="invisible_char">&#x2022;</property>
-                          </object>
+                          <placeholder/>
                         </child>
                       </object>
                       <packing>
diff --git a/hamster/standalone.py b/hamster/standalone.py
index e699517..1156b98 100755
--- a/hamster/standalone.py
+++ b/hamster/standalone.py
@@ -36,13 +36,191 @@ import pango, cairo
 
 from math import pi
 
-class TagEntry(graphics.Area):
+class TagsEntry(gtk.Entry):
+    __gsignals__ = {
+        'tags-selected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+    }
+
+    def __init__(self):
+        gtk.Entry.__init__(self)
+        self.tags = None
+        self.filter = None # currently applied filter string
+        self.filter_tags = None #filtered tags
+        
+        self.popup = gtk.Window(type = gtk.WINDOW_POPUP)
+        self.scroll_box = gtk.ScrolledWindow()
+        self.scroll_box.set_shadow_type(gtk.SHADOW_IN)
+        self.scroll_box.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+        viewport = gtk.Viewport()
+        viewport.set_shadow_type(gtk.SHADOW_NONE)
+        
+        self.tag_box = TagBox()
+        self.tag_box.connect("tag-selected", self.on_tag_selected)
+        self.tag_box.connect("tag-unselected", self.on_tag_unselected)
+
+        
+        viewport.add(self.tag_box)
+        self.scroll_box.add(viewport)
+        self.popup.add(self.scroll_box)
+        
+        self.connect("button-press-event", self._on_button_press_event)
+        self.connect("key-press-event", self._on_key_press_event)
+        self.connect("key-release-event", self._on_key_release_event)
+        self.connect("focus-out-event", self._on_focus_out_event)
+        self.show()
+        self.populate_suggestions()
+
+    def get_tags(self):
+        # splits the string by comma and filters out blanks
+        return [tag.strip() for tag in self.get_text().split(",") if tag.strip()]
+
+    def on_tag_selected(self, tag_box, tag):
+        tags = self.get_tags()
+        tags.append(tag)
+
+        self.set_text(", ".join(tags))
+        self.set_position(len(self.get_text()))
+
+    def on_tag_unselected(self, tag_box, tag):
+        tags = self.get_tags()
+        print tags
+        while tag in tags: #it could be that dear user is mocking us and entering same tag over and over again
+            tags.remove(tag)
+            
+        print tags
+
+        self.set_text(", ".join(tags))
+        self.set_position(len(self.get_text()))
+        
+        
+    def hide_popup(self):
+        self.popup.hide()
+
+    def show_popup(self):
+        if not self.filter_tags:
+            self.popup.hide()
+            return
+        
+        alloc = self.get_allocation()
+        x, y = self.get_parent_window().get_origin()
+
+        self.popup.move(x + alloc.x,y + alloc.y + alloc.height)
+
+        w = alloc.width
+        
+
+        self.popup.show_all()
+        self.scroll_box.set_size_request(w, self.tag_box.count_height())
+        self.popup.resize(w, self.tag_box.count_height())
+
+        
+    def complete_inline(self):
+        return         
+
+    def refresh_activities(self):
+        # scratch activities and categories so that they get repopulated on demand
+        self.activities = None
+        self.categories = None
+
+    def populate_suggestions(self):
+        cursor_tag = self.get_cursor_tag()
+            
+        self.tags = ["peh", "poh", "and so on", "etc", "true magic",
+                     "and so we go", "on and on", "until you drop",  "somewhere",
+                     "and forget", "what we", "were", "actually doing"]
+
+        
+        self.filter = cursor_tag
+
+        entered_tags = self.get_tags()
+        self.tag_box.selected_tags = entered_tags
+        
+        self.filter_tags = [tag for tag in self.tags if tag.startswith(self.filter)]
+        
+        self.tag_box.draw(self.filter_tags)
+        
+        
+
+    def _on_focus_out_event(self, widget, event):
+        self.hide_popup()
+
+    def _on_button_press_event(self, button, event):
+        self.populate_suggestions()
+        self.show_popup()
+
+    def _on_key_release_event(self, entry, event):
+        if (event.keyval in (gtk.keysyms.Return, gtk.keysyms.KP_Enter)):
+            if self.popup.get_property("visible"):
+                self.hide_popup()
+                return True
+            else:
+                self.emit("tags-selected")
+                return False
+        elif (event.keyval == gtk.keysyms.Escape):
+            if self.popup.get_property("visible"):
+                self.hide_popup()
+                return True
+            else:
+                return False
+        else:
+            self.populate_suggestions()
+            self.show_popup()
+            
+            if event.keyval not in (gtk.keysyms.Delete, gtk.keysyms.BackSpace):
+                self.complete_inline()
+
+    
+    def get_cursor_tag(self):
+        #returns the tag on which the cursor is on right now
+        if self.get_selection_bounds():
+            cursor = self.get_selection_bounds()[0]
+        else:
+            cursor = self.get_position()
+
+        text = self.get_text()
+
+        return text[text.rfind(",", 0, cursor)+1:cursor].strip()
+
+
+    def replace_tag(self, old_tag, new_tag):
+        tags = self.get_tags()
+        if old_tag in tags:
+            tags[tags.index(old_tag)] = new_tag
+
+        if self.get_selection_bounds():
+            cursor = self.get_selection_bounds()[0]
+        else:
+            cursor = self.get_position()
+        
+        self.set_text(", ".join(tags))
+        self.set_position(cursor + len(new_tag)-len(old_tag)) # put the cursor back
+
+    def _on_key_press_event(self, entry, event):
+
+        if event.keyval == gtk.keysyms.Tab:
+            if self.popup.get_property("visible"):
+                #we have to replace
+                self.replace_tag(self.get_cursor_tag(), self.filter_tags[0])
+                return True
+            else:
+                return False
+
+        return False
+
+
+class TagBox(graphics.Area):
+    __gsignals__ = {
+        'tag-selected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (str,)),
+        'tag-unselected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (str,)),
+    }
+
     def __init__(self):
         graphics.Area.__init__(self)
         self.font_size = 10
         self.connect("mouse-over", self.on_tag_hover)
         self.connect("button-release", self.on_tag_click)
         self.hover_tag = None
+        self.tags = []
         self.selected_tags = []
 
     def on_tag_hover(self, widget, regions):
@@ -57,13 +235,16 @@ class TagEntry(graphics.Area):
         tag = regions[0]
         if tag in self.selected_tags:
             self.selected_tags.remove(tag)
+            self.emit("tag-unselected", tag)
         else:
             self.selected_tags.append(tag)
+            self.emit("tag-selected", tag)
 
         self.redraw_canvas()
         
-    def draw(self):
+    def draw(self, tags):
         """Draw chart with given data"""
+        self.tags = tags
         self.show()        
         self.redraw_canvas()
 
@@ -72,8 +253,7 @@ class TagEntry(graphics.Area):
         w = text_w + 18 # padding (we have some diagonals to draw)
         h = text_h + 4
         return w, h
-        
-        
+    
     def draw_tag(self, label, x, y, color):
         self.context.set_line_width(1)
 
@@ -109,16 +289,34 @@ class TagEntry(graphics.Area):
         
         self.context.show_layout(self.layout)
         
+
+    def count_height(self):
+        if not self.width:
+            return 120
+        
+        # tells you how much height in this width is needed to show all tags
+        if not self.tags:
+            return None
+        
+        cur_x, cur_y = 4, 4
+        for tag in self.tags:
+            w, h = self.tag_size(tag)
+            if cur_x + w >= self.width - 5:  #if we don't fit, we wrap
+                cur_x = 5
+                cur_y += h + 6
+            
+            cur_x += w + 8 #some padding too, please
+        
+        cur_y = cur_y + h + 6
+        
+        return cur_y
+
     
     def _render(self):
         self.fill_area(0, 0, self.width, self.height, (255,255,255))
-        
-        tags = ["peh", "poh", "and so on", "etc", "true magic",
-                "and so we go", "on and on", "until you drop",  "somewhere",
-                "and forget", "what we", "were", "actually doing"]
-        
-        cur_x, cur_y = 5, 5
-        for tag in tags:
+
+        cur_x, cur_y = 4, 4
+        for tag in self.tags:
             w, h = self.tag_size(tag)
             if cur_x + w >= self.width - 5:  #if we don't fit, we wrap
                 cur_x = 5
@@ -136,16 +334,23 @@ class TagEntry(graphics.Area):
 
             cur_x += w + 8 #some padding too, please
 
-        
-
-        
 
 class MainWindow(object):
     def __init__(self):
         self._gui = stuff.load_ui_file("hamster.ui")
         self.window = self._gui.get_object('main-window')
 
-        self.style_widgets()
+        #TODO - replace with the tree background color (can't get it atm!)
+        self.get_widget("todays_activities_ebox").modify_bg(gtk.STATE_NORMAL,
+                                                                 gtk.gdk.Color(65536.0,65536.0,65536.0))
+        
+        self.new_name = widgets.ActivityEntry()
+        widgets.add_hint(self.new_name, _("Time and Name"))
+        self.get_widget("new_name_box").add(self.new_name)
+
+        self.new_tags = TagsEntry()
+        widgets.add_hint(self.new_tags, _("Tags or Description"))
+        self.get_widget("new_tags_box").add(self.new_tags)
         
         self.get_widget("tabs").set_current_page(0)
 
@@ -154,7 +359,7 @@ class MainWindow(object):
         
         gtk.link_button_set_uri_hook(self.magic)
         
-        self.tag_box = TagEntry()
+        self.tag_box = TagBox()
         self.get_widget("tag_box").add(self.tag_box)
         
         self._gui.connect_signals(self)
@@ -163,20 +368,6 @@ class MainWindow(object):
     def magic(self, button, uri):
         print uri, button
         
-    def style_widgets(self):
-        #TODO - replace with the tree background color (can't get it atm!)
-        self.get_widget("todays_activities_ebox").modify_bg(gtk.STATE_NORMAL,
-                                                                 gtk.gdk.Color(65536.0,65536.0,65536.0))
-        
-        self.new_name = widgets.ActivityEntry()
-        widgets.add_hint(self.new_name, _("Time and Name"))
-        parent = self.get_widget("new_name").parent
-        parent.remove(self.get_widget("new_name"))
-        parent.add(self.new_name)
-        
-        self.new_description = self.get_widget("new_description")
-        widgets.add_hint(self.new_description, _("Tags or Description"))
-        
 
     def set_last_activity(self):
         activity = runtime.storage.get_last_activity()



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]