[hamster-applet] moving widgets out to separate tools module as the code base has grown rather lengthy



commit b952ee5920bc8f8123b38adf4fdc61f7f23cba0f
Author: Toms Bauģis <toms baugis gmail com>
Date:   Thu Nov 19 12:33:07 2009 +0000

    moving widgets out to separate tools module as the code base has grown rather lengthy

 hamster/Makefile.am            |    2 +-
 hamster/edit_activity.py       |   12 +-
 hamster/preferences.py         |    4 +-
 hamster/standalone.py          |  272 +------------------------
 hamster/stats.py               |   14 +-
 hamster/stuff.py               |   36 ----
 hamster/tools/Makefile.am      |    9 +
 hamster/tools/__init__.py      |   92 +++++++++
 hamster/tools/activityentry.py |  291 ++++++++++++++++++++++++++
 hamster/tools/dateinput.py     |  176 ++++++++++++++++
 hamster/tools/timeinput.py     |  249 +++++++++++++++++++++++
 hamster/widgets.py             |  439 ----------------------------------------
 12 files changed, 844 insertions(+), 752 deletions(-)
---
diff --git a/hamster/Makefile.am b/hamster/Makefile.am
index 273752f..3adc9e2 100644
--- a/hamster/Makefile.am
+++ b/hamster/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS = keybinder
+SUBDIRS = keybinder tools
 ACLOCAL_AMFLAGS = -I m4
 
 CPPFLAGS = \
diff --git a/hamster/edit_activity.py b/hamster/edit_activity.py
index 14383b9..a1e8986 100644
--- a/hamster/edit_activity.py
+++ b/hamster/edit_activity.py
@@ -26,7 +26,11 @@ import gtk
 import gobject
 
 import stuff
-import graphics, widgets
+import graphics
+
+from tools.dateinput import DateInput
+from tools.timeinput import TimeInput
+
 import eds
 from configuration import runtime
 
@@ -414,15 +418,15 @@ class CustomFactController:
         end_date = end_date or start_date + dt.timedelta(minutes = 30)
 
 
-        self.start_date = widgets.DateInput(start_date)
+        self.start_date = DateInput(start_date)
         self.get_widget("start_date_placeholder").add(self.start_date)
         self.start_date.connect("date-entered", self.on_start_date_entered)
 
-        self.start_time = widgets.TimeInput(start_date)
+        self.start_time = TimeInput(start_date)
         self.get_widget("start_time_placeholder").add(self.start_time)
         self.start_time.connect("time-entered", self.on_start_time_entered)
         
-        self.end_time = widgets.TimeInput(end_date, start_date)
+        self.end_time = TimeInput(end_date, start_date)
         self.get_widget("end_time_placeholder").add(self.end_time)
         self.end_time.connect("time-entered", self.on_end_time_entered)
         self.set_end_date_label(end_date)
diff --git a/hamster/preferences.py b/hamster/preferences.py
index d773eb1..e852f38 100755
--- a/hamster/preferences.py
+++ b/hamster/preferences.py
@@ -27,7 +27,7 @@ import gtk
 import dispatcher, storage, stuff
 
 import datetime as dt
-import widgets
+from tools.timeinput import TimeInput
 
 
 from configuration import GconfStore, runtime
@@ -143,7 +143,7 @@ class PreferencesEditor:
         selection = self.category_tree.get_selection()
         selection.connect('changed', self.category_changed_cb, self.category_store)
 
-        self.day_start = widgets.TimeInput(dt.time(5,30))
+        self.day_start = TimeInput(dt.time(5,30))
         self.get_widget("day_start_placeholder").add(self.day_start)
         self.day_start.connect("time-entered", self.on_day_start_changed)
 
diff --git a/hamster/standalone.py b/hamster/standalone.py
index b99a838..7152a43 100755
--- a/hamster/standalone.py
+++ b/hamster/standalone.py
@@ -27,272 +27,14 @@ import gtk
 #gtk.gdk.threads_init()
 
 from configuration import GconfStore, runtime
-import stuff, widgets
+import tools
+from tools.activityentry import ActivityEntry
 
-import gobject
-
-class ActivityEntry(gtk.Entry):
-    __gsignals__ = {
-        'value-entered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
-    }
-
-
-    def __init__(self):
-        gtk.Entry.__init__(self)
-        self.news = False
-        self.activities = None
-        self.categories = None
-        self.filter = None
-        self.max_results = 10 # limit popup size to 10 results
-        
-        self.popup = gtk.Window(type = gtk.WINDOW_POPUP)
-        
-        box = gtk.ScrolledWindow()
-        box.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
-
-        self.tree = gtk.TreeView()
-        self.tree.set_headers_visible(False)
-        self.tree.set_hover_selection(True)
-
-        bgcolor = gtk.Style().bg[gtk.STATE_NORMAL].to_string()
-        time_cell = gtk.CellRendererPixbuf()
-        time_cell.set_property("icon-name", "appointment-new")
-        time_cell.set_property("cell-background", bgcolor)
-        
-        self.time_icon_column = gtk.TreeViewColumn("",
-                                              time_cell)
-        self.tree.append_column(self.time_icon_column)
-        
-        time_cell = gtk.CellRendererText()
-        time_cell.set_property("scale", 0.8)
-        time_cell.set_property("cell-background", bgcolor)
-
-        self.time_column = gtk.TreeViewColumn("Time",
-                                              time_cell,
-                                              text = 3)
-        self.tree.append_column(self.time_column)
-
-
-        self.activity_column = gtk.TreeViewColumn("Activity",
-                                                  gtk.CellRendererText(),
-                                                  text=1)
-        self.activity_column.set_expand(True)
-        self.tree.append_column(self.activity_column)
-        
-        self.category_column = gtk.TreeViewColumn("Category",
-                                                  stuff.CategoryCell(),
-                                                  text=2)
-        self.tree.append_column(self.category_column)
-
-
-
-        self.tree.connect("button-press-event", self._on_tree_button_press_event)
-
-        box.add(self.tree)
-        self.popup.add(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-in-event", self._on_focus_in_event)
-        self.connect("focus-out-event", self._on_focus_out_event)
-        self.connect("changed", self._on_text_changed)
-        self.show()
-        self.populate_suggestions()
-
-    def populate_suggestions(self):
-        self.activities = self.activities or runtime.storage.get_autocomplete_activities()
-        self.categories = self.categories or runtime.storage.get_category_list()
-
-        if self.get_selection_bounds():
-            cursor = self.get_selection_bounds()[0]
-        else:
-            cursor = self.get_position()
-            
-
-        if self.filter == self.get_text()[:cursor]:
-            return #same thing, no need to repopulate
-        
-        self.filter = self.get_text()[:cursor]
-        
-        input_activity = stuff.parse_activity_input(self.filter)
-        
-        time = ''
-        if input_activity.start_time:
-            time = input_activity.start_time.strftime("%H:%M")
-            if input_activity.end_time:
-                time += "-%s" % input_activity.end_time.strftime("%H:%M")
-        
-        
-        store = self.tree.get_model()
-        if not store:
-            store = gtk.ListStore(str, str, str, str)
-            self.tree.set_model(store)            
-        store.clear()
-
-        if self.filter.find("@") > 0:
-            key = self.filter[self.filter.find("@")+1:].lower()
-            for category in self.categories:
-                if key in category['name'].lower():
-                    fillable = (self.filter[:self.filter.find("@") + 1] + category['name'])
-                    store.append([fillable, category['name'], fillable, time])
-        else:
-            for activity in self.activities:
-                if input_activity.activity_name == "" or activity['name'].startswith(input_activity.activity_name): #self.filter in activity['name']:
-                    fillable = activity['name']
-                    if activity['category']:
-                        fillable += "@%s" % activity['category']
-
-                    if time:
-                        fillable = "%s %s" % (time, fillable)
-        
-                    store.append([fillable, activity['name'], activity['category'], time])
-
-
-    def show_popup(self):
-        result_count = self.tree.get_model().iter_n_children(None)
-        if result_count <= 1:
-            self.popup.hide()
-            return
-
-        activity = stuff.parse_activity_input(self.filter)        
-        time = ''
-        if activity.start_time:
-            time = activity.start_time.strftime("%H:%M")
-            if activity.end_time:
-                time += "-%s" % activity.end_time.strftime("%H:%M")
-
-        self.time_icon_column.set_visible(activity.start_time != None and self.filter.find("@") == -1)
-        self.time_column.set_visible(activity.start_time != None and self.filter.find("@") == -1)
-        
-
-        self.category_column.set_visible(self.filter.find("@") == -1)
-        
-        
-        #move popup under the widget
-        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
-        
-        #TODO - this is clearly unreliable as we calculate tree row size based on our gtk entry
-        self.tree.parent.set_size_request(w,(alloc.height-6) * min([result_count, self.max_results]))
-        self.popup.resize(w, (alloc.height-6) * min([result_count, self.max_results]))
-
-        
-        self.popup.show_all()
-        
-    def complete_inline(self):
-        model = self.tree.get_model()
-        activity = stuff.parse_activity_input(self.filter)
-        subject = self.get_text()
-        
-        if not subject or model.iter_n_children(None) == 0:
-            return
-        
-        prefix_length = 0
-        
-        labels = [row[0] for row in model]
-        shortest = min([len(label) for label in labels])
-        first = labels[0] #since we are looking for common prefix, we don't care which label we use for comparisons
-        
-        for i in range(len(subject), shortest):
-            letter_matching = all([label[i]==first[i] for label in labels])
-                
-            if not letter_matching:
-                break
-            
-            prefix_length +=1
-        
-        if prefix_length:
-            prefix = first[len(subject):len(subject)+prefix_length]
-            self.set_text("%s%s" % (self.filter, prefix))
-            self.select_region(len(self.filter), len(self.filter) + prefix_length)
+import stuff
 
 
+import gobject
 
-    def _on_text_changed(self, widget):
-        self.news = True
-        
-
-    def _on_button_press_event(self, button, event):
-        self.populate_suggestions()
-        self.show_popup()
-
-    def _on_focus_in_event(self, entry, event):
-        self.populate_suggestions()
-        self.show_popup()
-
-    def _on_focus_out_event(self, event, something):
-        self.popup.hide()
-        if self.news:
-            self.emit("value-entered")
-            self.news = False
-
-    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"):
-                if self.tree.get_cursor()[0]:
-                    self.set_text(self.tree.get_model()[self.tree.get_cursor()[0][0]][0])
-
-                self._on_selected()
-                
-                self.popup.hide()
-            else:
-                self._on_selected()
-        elif (event.keyval == gtk.keysyms.Escape):
-            self.popup.hide()
-        elif event.keyval in (gtk.keysyms.Up, gtk.keysyms.Down):
-            return False
-        else:
-            self.populate_suggestions()
-            self.show_popup()
-            
-            if event.keyval not in (gtk.keysyms.Delete, gtk.keysyms.BackSpace):
-                self.complete_inline()
-
-        
-
-
-    def _on_key_press_event(self, entry, event):
-
-        if event.keyval in (gtk.keysyms.Up, gtk.keysyms.Down):
-            cursor = self.tree.get_cursor()
-    
-            if not cursor or not cursor[0]:
-                self.tree.set_cursor(0)
-                return True
-            
-            i = cursor[0][0]
-
-            if event.keyval == gtk.keysyms.Up:
-                i-=1
-            elif event.keyval == gtk.keysyms.Down:
-                i+=1
-
-            # keep it in the sane borders
-            i = min(max(i, 0), len(self.tree.get_model()) - 1)
-            
-            self.tree.set_cursor(i)
-            self.tree.scroll_to_cell(i, use_align = True, row_align = 0.4)
-            return True
-        else:
-            return False
-        
-    def _on_tree_button_press_event(self, tree, event):
-        model, iter = tree.get_selection().get_selected()
-        value = model.get_value(iter, 0)
-        self.set_text(value)
-        self.popup.hide()
-        self._on_selected()
-
-    def _on_selected(self):
-        if self.news:
-            self.emit("value-entered")
-            self.news = False
-            self.set_position(len(self.get_text()))
         
 
 class MainWindow(object):
@@ -319,14 +61,14 @@ class MainWindow(object):
         self.get_widget("todays_activities_ebox").modify_bg(gtk.STATE_NORMAL,
                                                                  gtk.gdk.Color(65536.0,65536.0,65536.0))
         
-        self.new_name = ActivityEntry() #widgets.HintEntry(_("Time and Name"), self.get_widget("new_name"))
-        widgets.add_hint(self.new_name, _("Time and Name"))
+        self.new_name = ActivityEntry()
+        tools.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"))
+        tools.add_hint(self.new_description, _("Tags or Description"))
         
 
     def set_last_activity(self):
diff --git a/hamster/stats.py b/hamster/stats.py
index 675a677..9b9413d 100644
--- a/hamster/stats.py
+++ b/hamster/stats.py
@@ -29,7 +29,11 @@ import stuff
 import charting
 
 from edit_activity import CustomFactController
-import reports, widgets, graphics
+import reports, graphics
+
+import tools
+from tools.dateinput import DateInput
+
 from configuration import runtime, GconfStore
 import webbrowser
 
@@ -95,9 +99,9 @@ class ReportChooserDialog(gtk.Dialog):
         filter.add_pattern("*")
         self.dialog.add_filter(filter)
         
-        self.start_date = widgets.DateInput()
+        self.start_date = DateInput()
         ui.get_object('from_date_box').add(self.start_date)
-        self.end_date = widgets.DateInput()
+        self.end_date = DateInput()
         ui.get_object('to_date_box').add(self.end_date)
 
         self.category_box = ui.get_object('category_box')
@@ -852,7 +856,7 @@ than 15 minutes you seem to be a busy bee." % ("<b>%d</b>" % short_percent))
             day_row = self.fact_store.append(None,
                                              [-1,
                                               fact_date,
-                                              stuff.format_duration(day_total),
+                                              tools.format_duration(day_total),
                                               current_date.strftime('%Y-%m-%d'),
                                               "",
                                               "",
@@ -863,7 +867,7 @@ than 15 minutes you seem to be a busy bee." % ("<b>%d</b>" % short_percent))
                                        [fact["id"],
                                         fact["start_time"].strftime('%H:%M') + " " +
                                         fact["name"],
-                                        stuff.format_duration(fact["delta"]),
+                                        tools.format_duration(fact["delta"]),
                                         fact["start_time"].strftime('%Y-%m-%d'),
                                         fact["description"],
                                         fact["category"],
diff --git a/hamster/stuff.py b/hamster/stuff.py
index 428c1db..c52ae8e 100644
--- a/hamster/stuff.py
+++ b/hamster/stuff.py
@@ -124,42 +124,6 @@ class ActivityColumn(gtk.TreeViewColumn):
         cell.set_property("ellipsize", pango.ELLIPSIZE_END)
         self.set_cell_data_func(cell, self.activity_painter)
 
-def duration_minutes(duration):
-    """returns minutes from duration, otherwise we keep bashing in same math"""
-    return duration.seconds / 60 + duration.days * 24 * 60
-    
-def format_duration(minutes, human = True):
-    """formats duration in a human readable format.
-    accepts either minutes or timedelta"""
-    
-    if isinstance(minutes, dt.timedelta):
-        minutes = duration_minutes(minutes)
-        
-    if not minutes:
-        if human:
-            return ""
-        else:
-            return "00:00"
-    
-    hours = minutes / 60
-    minutes = minutes % 60
-    formatted_duration = ""
-    
-    if human:
-        if minutes % 60 == 0:
-            # duration in round hours
-            formatted_duration += _("%dh") % (hours)
-        elif hours == 0:
-            # duration less than hour
-            formatted_duration += _("%dmin") % (minutes % 60.0)
-        else:
-            # x hours, y minutes
-            formatted_duration += _("%dh %dmin") % (hours, minutes % 60)
-    else:
-        formatted_duration += "%02d:%02d" % (hours, minutes)
-    
-    
-    return formatted_duration
 
 def totals(iter, keyfunc, sumfunc):
     """groups items by field described in keyfunc and counts totals using value
diff --git a/hamster/tools/Makefile.am b/hamster/tools/Makefile.am
new file mode 100644
index 0000000..059791c
--- /dev/null
+++ b/hamster/tools/Makefile.am
@@ -0,0 +1,9 @@
+hamsterdir = $(pyexecdir)/hamster/tools
+hamster_PYTHON = \
+	__init__.py \
+	activityentry.py \
+	timeinput.py \
+	dateinput.py
+
+clean-local:
+	rm -rf *.pyc *.pyo
diff --git a/hamster/tools/__init__.py b/hamster/tools/__init__.py
new file mode 100644
index 0000000..dc5fbde
--- /dev/null
+++ b/hamster/tools/__init__.py
@@ -0,0 +1,92 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2007-2009 Toms Bauģis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster 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.
+
+# Project Hamster 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 Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
+
+import datetime as dt
+
+def add_hint(entry, hint):
+    entry.hint = hint        
+    
+    def _set_hint(self, widget, event):
+        if self.get_text(): # don't mess with user entered text
+            return 
+
+        self.modify_text(gtk.STATE_NORMAL, gtk.gdk.Color("gray"))
+        hint_font = pango.FontDescription(gtk.Style().font_desc.to_string())
+        hint_font.set_style(pango.STYLE_ITALIC)
+        self.modify_font(hint_font)
+        
+        self.set_text(self.hint)
+        
+    def _set_normal(self, widget, event):
+        self.modify_text(gtk.STATE_NORMAL, gtk.Style().fg[gtk.STATE_NORMAL])
+        hint_font = pango.FontDescription(gtk.Style().font_desc.to_string())
+        self.modify_font(hint_font)
+
+        if self.get_text() == self.hint:
+            self.set_text("")
+
+    import types
+    instancemethod = types.MethodType
+
+    entry._set_hint = instancemethod(_set_hint, entry, gtk.Entry)
+    entry._set_normal = instancemethod(_set_normal, entry, gtk.Entry)
+    
+    entry.connect('focus-in-event', entry._set_normal)
+    entry.connect('focus-out-event', entry._set_hint)
+
+    entry._set_hint(entry, None)
+
+
+
+def format_duration(minutes, human = True):
+    """formats duration in a human readable format.
+    accepts either minutes or timedelta"""
+    
+    if isinstance(minutes, dt.timedelta):
+        minutes = duration_minutes(minutes)
+        
+    if not minutes:
+        if human:
+            return ""
+        else:
+            return "00:00"
+    
+    hours = minutes / 60
+    minutes = minutes % 60
+    formatted_duration = ""
+    
+    if human:
+        if minutes % 60 == 0:
+            # duration in round hours
+            formatted_duration += _("%dh") % (hours)
+        elif hours == 0:
+            # duration less than hour
+            formatted_duration += _("%dmin") % (minutes % 60.0)
+        else:
+            # x hours, y minutes
+            formatted_duration += _("%dh %dmin") % (hours, minutes % 60)
+    else:
+        formatted_duration += "%02d:%02d" % (hours, minutes)
+    
+    
+    return formatted_duration
+
+def duration_minutes(duration):
+    """returns minutes from duration, otherwise we keep bashing in same math"""
+    return duration.seconds / 60 + duration.days * 24 * 60
diff --git a/hamster/tools/activityentry.py b/hamster/tools/activityentry.py
new file mode 100644
index 0000000..d9692ff
--- /dev/null
+++ b/hamster/tools/activityentry.py
@@ -0,0 +1,291 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2008-2009 Toms Bauģis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster 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.
+
+# Project Hamster 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 Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
+
+from hamster import tools
+from hamster.configuration import GconfStore, runtime
+
+from stuff import format_duration
+import gtk
+import datetime as dt
+import calendar
+import gobject
+import re
+
+class ActivityEntry(gtk.Entry):
+    __gsignals__ = {
+        'value-entered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+    }
+
+
+    def __init__(self):
+        gtk.Entry.__init__(self)
+        self.news = False
+        self.activities = None
+        self.categories = None
+        self.filter = None
+        self.max_results = 10 # limit popup size to 10 results
+        
+        self.popup = gtk.Window(type = gtk.WINDOW_POPUP)
+        
+        box = gtk.ScrolledWindow()
+        box.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+
+        self.tree = gtk.TreeView()
+        self.tree.set_headers_visible(False)
+        self.tree.set_hover_selection(True)
+
+        bgcolor = gtk.Style().bg[gtk.STATE_NORMAL].to_string()
+        time_cell = gtk.CellRendererPixbuf()
+        time_cell.set_property("icon-name", "appointment-new")
+        time_cell.set_property("cell-background", bgcolor)
+        
+        self.time_icon_column = gtk.TreeViewColumn("",
+                                              time_cell)
+        self.tree.append_column(self.time_icon_column)
+        
+        time_cell = gtk.CellRendererText()
+        time_cell.set_property("scale", 0.8)
+        time_cell.set_property("cell-background", bgcolor)
+
+        self.time_column = gtk.TreeViewColumn("Time",
+                                              time_cell,
+                                              text = 3)
+        self.tree.append_column(self.time_column)
+
+
+        self.activity_column = gtk.TreeViewColumn("Activity",
+                                                  gtk.CellRendererText(),
+                                                  text=1)
+        self.activity_column.set_expand(True)
+        self.tree.append_column(self.activity_column)
+        
+        self.category_column = gtk.TreeViewColumn("Category",
+                                                  stuff.CategoryCell(),
+                                                  text=2)
+        self.tree.append_column(self.category_column)
+
+
+
+        self.tree.connect("button-press-event", self._on_tree_button_press_event)
+
+        box.add(self.tree)
+        self.popup.add(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-in-event", self._on_focus_in_event)
+        self.connect("focus-out-event", self._on_focus_out_event)
+        self.connect("changed", self._on_text_changed)
+        self.show()
+        self.populate_suggestions()
+
+    def populate_suggestions(self):
+        self.activities = self.activities or runtime.storage.get_autocomplete_activities()
+        self.categories = self.categories or runtime.storage.get_category_list()
+
+        if self.get_selection_bounds():
+            cursor = self.get_selection_bounds()[0]
+        else:
+            cursor = self.get_position()
+            
+
+        if self.filter == self.get_text()[:cursor]:
+            return #same thing, no need to repopulate
+        
+        self.filter = self.get_text()[:cursor]
+        
+        input_activity = stuff.parse_activity_input(self.filter)
+        
+        time = ''
+        if input_activity.start_time:
+            time = input_activity.start_time.strftime("%H:%M")
+            if input_activity.end_time:
+                time += "-%s" % input_activity.end_time.strftime("%H:%M")
+        
+        
+        store = self.tree.get_model()
+        if not store:
+            store = gtk.ListStore(str, str, str, str)
+            self.tree.set_model(store)            
+        store.clear()
+
+        if self.filter.find("@") > 0:
+            key = self.filter[self.filter.find("@")+1:].lower()
+            for category in self.categories:
+                if key in category['name'].lower():
+                    fillable = (self.filter[:self.filter.find("@") + 1] + category['name'])
+                    store.append([fillable, category['name'], fillable, time])
+        else:
+            for activity in self.activities:
+                if input_activity.activity_name == "" or activity['name'].startswith(input_activity.activity_name): #self.filter in activity['name']:
+                    fillable = activity['name']
+                    if activity['category']:
+                        fillable += "@%s" % activity['category']
+
+                    if time:
+                        fillable = "%s %s" % (time, fillable)
+        
+                    store.append([fillable, activity['name'], activity['category'], time])
+
+
+    def show_popup(self):
+        result_count = self.tree.get_model().iter_n_children(None)
+        if result_count <= 1:
+            self.popup.hide()
+            return
+
+        activity = stuff.parse_activity_input(self.filter)        
+        time = ''
+        if activity.start_time:
+            time = activity.start_time.strftime("%H:%M")
+            if activity.end_time:
+                time += "-%s" % activity.end_time.strftime("%H:%M")
+
+        self.time_icon_column.set_visible(activity.start_time != None and self.filter.find("@") == -1)
+        self.time_column.set_visible(activity.start_time != None and self.filter.find("@") == -1)
+        
+
+        self.category_column.set_visible(self.filter.find("@") == -1)
+        
+        
+        #move popup under the widget
+        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
+        
+        #TODO - this is clearly unreliable as we calculate tree row size based on our gtk entry
+        self.tree.parent.set_size_request(w,(alloc.height-6) * min([result_count, self.max_results]))
+        self.popup.resize(w, (alloc.height-6) * min([result_count, self.max_results]))
+
+        
+        self.popup.show_all()
+        
+    def complete_inline(self):
+        model = self.tree.get_model()
+        activity = stuff.parse_activity_input(self.filter)
+        subject = self.get_text()
+        
+        if not subject or model.iter_n_children(None) == 0:
+            return
+        
+        prefix_length = 0
+        
+        labels = [row[0] for row in model]
+        shortest = min([len(label) for label in labels])
+        first = labels[0] #since we are looking for common prefix, we don't care which label we use for comparisons
+        
+        for i in range(len(subject), shortest):
+            letter_matching = all([label[i]==first[i] for label in labels])
+                
+            if not letter_matching:
+                break
+            
+            prefix_length +=1
+        
+        if prefix_length:
+            prefix = first[len(subject):len(subject)+prefix_length]
+            self.set_text("%s%s" % (self.filter, prefix))
+            self.select_region(len(self.filter), len(self.filter) + prefix_length)
+
+
+
+    def _on_text_changed(self, widget):
+        self.news = True
+        
+
+    def _on_button_press_event(self, button, event):
+        self.populate_suggestions()
+        self.show_popup()
+
+    def _on_focus_in_event(self, entry, event):
+        self.populate_suggestions()
+        self.show_popup()
+
+    def _on_focus_out_event(self, event, something):
+        self.popup.hide()
+        if self.news:
+            self.emit("value-entered")
+            self.news = False
+
+    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"):
+                if self.tree.get_cursor()[0]:
+                    self.set_text(self.tree.get_model()[self.tree.get_cursor()[0][0]][0])
+
+                self._on_selected()
+                
+                self.popup.hide()
+            else:
+                self._on_selected()
+        elif (event.keyval == gtk.keysyms.Escape):
+            self.popup.hide()
+        elif event.keyval in (gtk.keysyms.Up, gtk.keysyms.Down):
+            return False
+        else:
+            self.populate_suggestions()
+            self.show_popup()
+            
+            if event.keyval not in (gtk.keysyms.Delete, gtk.keysyms.BackSpace):
+                self.complete_inline()
+
+        
+
+
+    def _on_key_press_event(self, entry, event):
+
+        if event.keyval in (gtk.keysyms.Up, gtk.keysyms.Down):
+            cursor = self.tree.get_cursor()
+    
+            if not cursor or not cursor[0]:
+                self.tree.set_cursor(0)
+                return True
+            
+            i = cursor[0][0]
+
+            if event.keyval == gtk.keysyms.Up:
+                i-=1
+            elif event.keyval == gtk.keysyms.Down:
+                i+=1
+
+            # keep it in the sane borders
+            i = min(max(i, 0), len(self.tree.get_model()) - 1)
+            
+            self.tree.set_cursor(i)
+            self.tree.scroll_to_cell(i, use_align = True, row_align = 0.4)
+            return True
+        else:
+            return False
+        
+    def _on_tree_button_press_event(self, tree, event):
+        model, iter = tree.get_selection().get_selected()
+        value = model.get_value(iter, 0)
+        self.set_text(value)
+        self.popup.hide()
+        self._on_selected()
+
+    def _on_selected(self):
+        if self.news:
+            self.emit("value-entered")
+            self.news = False
+            self.set_position(len(self.get_text()))
diff --git a/hamster/tools/dateinput.py b/hamster/tools/dateinput.py
new file mode 100644
index 0000000..ef18365
--- /dev/null
+++ b/hamster/tools/dateinput.py
@@ -0,0 +1,176 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2008-2009 Toms Bauģis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster 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.
+
+# Project Hamster 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 Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
+
+from hamster.tools import format_duration
+import gtk
+import datetime as dt
+import calendar
+import gobject
+import re
+
+class DateInput(gtk.Entry):
+    """ a text entry widget with calendar popup"""
+    __gsignals__ = {
+        'date-entered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+    }
+
+
+    def __init__(self, date = None):
+        gtk.Entry.__init__(self)
+        
+        self.set_width_chars(12) #12 is enough for 12-oct-2009, which is verbose
+        self.date = date
+        if date:
+            self.set_date(date)
+
+        self.news = False
+        self.prev_cal_day = None #workaround
+        self.popup = gtk.Window(type = gtk.WINDOW_POPUP)
+        calendar_box = gtk.HBox()
+
+        self.date_calendar = gtk.Calendar()
+        self.date_calendar.connect("day-selected", self._on_day_selected)
+        self.date_calendar.connect("day-selected-double-click",
+                                   self.__on_day_selected_double_click)
+        self.date_calendar.connect("button-press-event",
+                                   self._on_cal_button_press_event)
+        calendar_box.add(self.date_calendar)
+        self.popup.add(calendar_box)
+
+        self.connect("button-press-event", self._on_button_press_event)
+        self.connect("key-press-event", self._on_key_press_event)
+        self.connect("focus-in-event", self._on_focus_in_event)
+        self.connect("focus-out-event", self._on_focus_out_event)
+        self.connect("changed", self._on_text_changed)
+        self.show()
+
+    def set_date(self, date):
+        """sets date to specified, using default format"""
+        self.date = date
+        self.set_text(self._format_date(self.date))
+
+    def get_date(self):
+        """sets date to specified, using default format"""
+        self.date = self._figure_date(self.get_text())
+        self.set_text(self._format_date(self.date))
+        return self.date
+
+    def _figure_date(self, date_str):
+        try:
+            return dt.datetime.strptime(date_str, "%x")
+        except:
+            return self.date
+
+    def _format_date(self, date):
+        if not date:
+            return ""
+        else:
+            return date.strftime("%x")
+
+    def _on_text_changed(self, widget):
+        self.news = True
+        
+    def __on_day_selected_double_click(self, calendar):
+        self.prev_cal_day = None
+        self._on_day_selected(calendar) #forward
+        
+    def _on_cal_button_press_event(self, calendar, event):
+        self.prev_cal_day = calendar.get_date()[2]
+
+    def _on_day_selected(self, calendar):
+        if self.popup.get_property("visible") == False:
+            return
+        
+        if self.prev_cal_day == calendar.get_date()[2]:
+            return
+        
+        cal_date = calendar.get_date()
+
+        self.date = dt.date(cal_date[0], cal_date[1] + 1, cal_date[2])
+        self.set_text(self._format_date(self.date))
+
+        self.popup.hide()
+        if self.news:
+            self.emit("date-entered")
+            self.news = False
+        
+    
+    def show_popup(self):
+        window = self.get_parent_window()
+        x, y= window.get_origin()
+
+        alloc = self.get_allocation()
+        
+        date = self._figure_date(self.get_text())
+        if date:
+            self.prev_cal_day = date.day #avoid 
+            self.date_calendar.select_month(date.month-1, date.year)
+            self.date_calendar.select_day(date.day)
+        
+        self.popup.move(x + alloc.x,y + alloc.y + alloc.height)
+        self.popup.show_all()
+    
+    def _on_focus_in_event(self, entry, event):
+        self.show_popup()
+
+    def _on_button_press_event(self, button, event):
+        self.show_popup()
+
+
+    def _on_focus_out_event(self, event, something):
+        self.popup.hide()
+        if self.news:
+            self.emit("date-entered")
+            self.news = False
+    
+    def _on_key_press_event(self, entry, event):
+        if self.popup.get_property("visible"):
+            cal_date = self.date_calendar.get_date()
+            date = dt.date(cal_date[0], cal_date[1], cal_date[2])
+        else:
+            date = self._figure_date(entry.get_text())
+            if not date:
+                return
+
+        enter_pressed = False
+
+        if event.keyval == gtk.keysyms.Up:
+            date = date - dt.timedelta(days=1)
+        elif event.keyval == gtk.keysyms.Down:
+            date = date + dt.timedelta(days=1)
+        elif (event.keyval == gtk.keysyms.Return or
+              event.keyval == gtk.keysyms.KP_Enter):
+            enter_pressed = True
+        elif (event.keyval == gtk.keysyms.Escape):
+            self.popup.hide()
+        elif event.keyval in (gtk.keysyms.Left, gtk.keysyms.Right):
+            return False #keep calendar open and allow user to walk in text
+        else:
+            self.popup.hide()
+            return False
+        
+        if enter_pressed:
+            self.prev_cal_day = "borken"
+        else:
+            #prev_cal_day is our only way of checking that date is right
+            self.prev_cal_day = date.day 
+        
+        self.date_calendar.select_month(date.month, date.year)
+        self.date_calendar.select_day(date.day)
+        return True
diff --git a/hamster/tools/timeinput.py b/hamster/tools/timeinput.py
new file mode 100644
index 0000000..a060813
--- /dev/null
+++ b/hamster/tools/timeinput.py
@@ -0,0 +1,249 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2008-2009 Toms Bauģis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster 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.
+
+# Project Hamster 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 Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
+
+from hamster.tools import format_duration
+import gtk
+import datetime as dt
+import calendar
+import gobject
+import re
+
+class TimeInput(gtk.Entry):
+    __gsignals__ = {
+        'time-entered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+    }
+
+
+    def __init__(self, time = None, start_time = None):
+        gtk.Entry.__init__(self)
+
+        self.start_time = start_time
+        self.news = False
+
+        self.set_width_chars(7) #7 is like 11:24pm
+        self.time = time
+        if time:
+            self.set_time(time)
+
+
+        self.popup = gtk.Window(type = gtk.WINDOW_POPUP)
+        time_box = gtk.ScrolledWindow()
+        time_box.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
+
+        self.time_tree = gtk.TreeView()
+        self.time_tree.set_headers_visible(False)
+        self.time_tree.set_hover_selection(True)
+
+        self.time_tree.append_column(gtk.TreeViewColumn("Time",
+                                                        gtk.CellRendererText(),
+                                                        text=0))
+        self.time_tree.connect("button-press-event",
+                               self._on_time_tree_button_press_event)
+
+        time_box.add(self.time_tree)
+        self.popup.add(time_box)
+
+        self.connect("button-press-event", self._on_button_press_event)
+        self.connect("key-press-event", self._on_key_press_event)
+        self.connect("focus-in-event", self._on_focus_in_event)
+        self.connect("focus-out-event", self._on_focus_out_event)
+        self.connect("changed", self._on_text_changed)
+        self.show()
+
+
+    def set_start_time(self, start_time):
+        """ set the start time. when start time is set, drop down list
+            will start from start time and duration will be displayed in
+            brackets
+        """
+        self.start_time = start_time
+
+    def set_time(self, time):
+        self.time = time
+        self.set_text(self._format_time(time))
+        
+    def _on_text_changed(self, widget):
+        self.news = True
+        
+    def figure_time(self, str_time):
+        if not str_time:
+            return self.time
+        
+        # strip everything non-numeric and consider hours to be first number
+        # and minutes - second number
+        numbers = re.split("\D", str_time)
+        numbers = filter(lambda x: x!="", numbers)
+        
+        hours, minutes = None, None
+        
+        if len(numbers) == 1 and len(numbers[0]) == 4:
+            hours, minutes = int(numbers[0][:2]), int(numbers[0][2:])
+        else:
+            if len(numbers) >= 1:
+                hours = int(numbers[0])
+            if len(numbers) >= 2:
+                minutes = int(numbers[1])
+            
+        if (hours is None or minutes is None) or hours > 24 or minutes > 60:
+            return self.time #no can do
+    
+        return dt.datetime.now().replace(hour = hours, minute = minutes,
+                                         second = 0, microsecond = 0)
+
+
+    def _select_time(self, time_text):
+        #convert forth and back so we have text formated as we want
+        time = self.figure_time(time_text)
+        time_text = self._format_time(time) 
+        
+        self.set_text(time_text)
+        self.set_position(len(time_text))
+        self.popup.hide()
+        if self.news:
+            self.emit("time-entered")
+            self.news = False
+    
+    def get_time(self):
+        self.time = self.figure_time(self.get_text())
+        self.set_text(self._format_time(self.time))
+        return self.time
+
+    def _format_time(self, time):
+        if time is None:
+            return None
+        
+        #return time.strftime("%I:%M%p").lstrip("0").lower()
+        return time.strftime("%H:%M").lower()
+    
+
+    def _on_focus_in_event(self, entry, event):
+        self.show_popup()
+
+    def _on_button_press_event(self, button, event):
+        self.show_popup()
+
+    def _on_focus_out_event(self, event, something):
+        self.popup.hide()
+        if self.news:
+            self.emit("time-entered")
+            self.news = False
+        
+
+    def show_popup(self):
+        focus_time = self.figure_time(self.get_text())
+        
+        hours = gtk.ListStore(gobject.TYPE_STRING)
+        
+        # populate times
+        i_time = self.start_time or dt.datetime(1900, 1, 1, 0, 0)
+        
+        if focus_time and focus_time < i_time:
+            focus_time += dt.timedelta(days = 1)
+        
+        if self.start_time:
+            end_time = i_time + dt.timedelta(hours = 12)
+            i_time += dt.timedelta(minutes = 15)
+        else:
+            end_time = i_time + dt.timedelta(hours = 24)
+        
+        i, focus_row = 0, None
+        
+        while i_time < end_time:
+            row_text = self._format_time(i_time)
+            if self.start_time:
+                delta = (i_time - self.start_time).seconds / 60
+                delta_text = format_duration(delta)
+                
+                row_text += " (%s)" % delta_text
+
+            hours.append([row_text])
+            
+            if focus_time and i_time <= focus_time <= i_time + \
+                                                     dt.timedelta(minutes = 30):
+                focus_row = i
+            
+            if self.start_time:
+                i_time += dt.timedelta(minutes = 15)
+            else:
+                i_time += dt.timedelta(minutes = 30)
+
+            i += 1
+
+        self.time_tree.set_model(hours)        
+
+        #focus on row
+        if focus_row != None:
+            self.time_tree.set_cursor(focus_row)
+            self.time_tree.scroll_to_cell(focus_row, use_align = True, row_align = 0.4)
+        
+        #move popup under the widget
+        alloc = self.get_allocation()
+        w = alloc.width
+        if self.start_time:
+            w = w * 2
+        self.time_tree.set_size_request(w, alloc.height * 5)
+
+        window = self.get_parent_window()
+        x, y= window.get_origin()
+
+        self.popup.move(x + alloc.x,y + alloc.y + alloc.height)
+        self.popup.show_all()
+
+    
+    def _on_time_tree_button_press_event(self, tree, event):
+        model, iter = tree.get_selection().get_selected()
+        time = model.get_value(iter, 0)
+        self._select_time(time)
+        
+        
+    def _on_key_press_event(self, entry, event):
+        cursor = self.time_tree.get_cursor()
+
+        if not cursor or not cursor[0]:
+            return
+        
+        i = cursor[0][0]
+
+        if event.keyval == gtk.keysyms.Up:
+            i-=1
+        elif event.keyval == gtk.keysyms.Down:
+            i+=1
+        elif (event.keyval == gtk.keysyms.Return or
+              event.keyval == gtk.keysyms.KP_Enter):
+            
+            if self.popup.get_property("visible"):
+                self._select_time(self.time_tree.get_model()[i][0])
+            else:
+                self._select_time(entry.get_text())
+        elif (event.keyval == gtk.keysyms.Escape):
+            self.popup.hide()
+        else:
+            #any kind of other input
+            self.popup.hide()
+            return False
+        
+        # keep it in the sane borders
+        i = min(max(i, 0), len(self.time_tree.get_model()) - 1)
+        
+        self.time_tree.set_cursor(i)
+        self.time_tree.scroll_to_cell(i, use_align = True, row_align = 0.4)
+        return True
+
+
+    



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