3
# Copyright (C) 2008-2009 Toms Bauģis <toms.baugis at gmail.com>
5
# This file is part of Project Hamster.
7
# Project Hamster is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
12
# Project Hamster is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
17
# You should have received a copy of the GNU General Public License
18
# along with Project Hamster. If not, see <http://www.gnu.org/licenses/>.
23
from .hamster.configuration import runtime
25
from .hamster import stuff
26
from .hamster.stuff import format_duration
28
class ActivityEntry(gtk.Entry):
30
'value-entered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
35
gtk.Entry.__init__(self)
37
self.activities = None
38
self.categories = None
40
self.max_results = 10 # limit popup size to 10 results
42
self.popup = gtk.Window(type = gtk.WINDOW_POPUP)
44
box = gtk.ScrolledWindow()
45
box.set_shadow_type(gtk.SHADOW_IN)
46
box.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
48
self.tree = gtk.TreeView()
49
self.tree.set_headers_visible(False)
50
self.tree.set_hover_selection(True)
52
bgcolor = gtk.Style().bg[gtk.STATE_NORMAL].to_string()
53
time_cell = gtk.CellRendererPixbuf()
54
time_cell.set_property("icon-name", "appointment-new")
55
time_cell.set_property("cell-background", bgcolor)
57
self.time_icon_column = gtk.TreeViewColumn("",
59
self.tree.append_column(self.time_icon_column)
61
time_cell = gtk.CellRendererText()
62
time_cell.set_property("scale", 0.8)
63
time_cell.set_property("cell-background", bgcolor)
65
self.time_column = gtk.TreeViewColumn("Time",
68
self.tree.append_column(self.time_column)
71
self.activity_column = gtk.TreeViewColumn("Activity",
72
gtk.CellRendererText(),
74
self.activity_column.set_expand(True)
75
self.tree.append_column(self.activity_column)
77
self.category_column = gtk.TreeViewColumn("Category",
80
self.tree.append_column(self.category_column)
84
self.tree.connect("button-press-event", self._on_tree_button_press_event)
89
self.connect("button-press-event", self._on_button_press_event)
90
self.connect("key-press-event", self._on_key_press_event)
91
self.connect("key-release-event", self._on_key_release_event)
92
self.connect("focus-out-event", self._on_focus_out_event)
93
self.connect("changed", self._on_text_changed)
94
self.connect("parent-set", self._on_parent_set)
96
runtime.dispatcher.add_handler('activity_updated', self.after_activity_update)
99
self.populate_suggestions()
101
def hide_popup(self):
104
def show_popup(self):
105
result_count = self.tree.get_model().iter_n_children(None)
106
if result_count <= 1:
110
activity = stuff.parse_activity_input(self.filter)
112
if activity.start_time:
113
time = activity.start_time.strftime("%H:%M")
114
if activity.end_time:
115
time += "-%s" % activity.end_time.strftime("%H:%M")
117
self.time_icon_column.set_visible(activity.start_time != None and self.filter.find("@") == -1)
118
self.time_column.set_visible(activity.start_time != None and self.filter.find("@") == -1)
121
self.category_column.set_visible(self.filter.find("@") == -1)
124
#move popup under the widget
125
alloc = self.get_allocation()
127
#TODO - this is clearly unreliable as we calculate tree row size based on our gtk entry
128
popup_height = (alloc.height-6) * min([result_count, self.max_results])
129
self.tree.parent.set_size_request(alloc.width, popup_height)
130
self.popup.resize(alloc.width, popup_height)
132
x, y = self.get_parent_window().get_origin()
135
if y + alloc.height + popup_height < self.get_screen().get_height():
140
self.popup.move(x + alloc.x, y)
142
self.popup.show_all()
144
def complete_inline(self):
145
model = self.tree.get_model()
146
activity = stuff.parse_activity_input(self.filter)
147
subject = self.get_text()
149
if not subject or model.iter_n_children(None) == 0:
154
labels = [row[0] for row in model]
155
shortest = min([len(label) for label in labels])
156
first = labels[0] #since we are looking for common prefix, we do not care which label we use for comparisons
158
for i in range(len(subject), shortest):
159
letter_matching = all([label[i]==first[i] for label in labels])
161
if not letter_matching:
167
prefix = first[len(subject):len(subject)+prefix_length]
168
self.set_text("%s%s" % (self.filter, prefix))
169
self.select_region(len(self.filter), len(self.filter) + prefix_length)
171
def refresh_activities(self):
172
# scratch category cache so it gets repopulated on demand
173
self.categories = None
175
def populate_suggestions(self):
176
if self.get_selection_bounds():
177
cursor = self.get_selection_bounds()[0]
179
cursor = self.get_position()
181
if self.activities and self.categories and self.filter == self.get_text().decode('utf8', 'replace')[:cursor]:
182
return #same thing, no need to repopulate
184
self.filter = self.get_text().decode('utf8', 'replace')[:cursor]
185
input_activity = stuff.parse_activity_input(self.filter)
187
# do not cache as ordering and available options change over time
188
self.activities = runtime.storage.get_autocomplete_activities(input_activity.activity_name)
189
self.categories = self.categories or runtime.storage.get_category_list()
193
if input_activity.start_time:
194
time = input_activity.start_time.strftime("%H:%M")
195
if input_activity.end_time:
196
time += "-%s" % input_activity.end_time.strftime("%H:%M")
199
store = self.tree.get_model()
201
store = gtk.ListStore(str, str, str, str)
202
self.tree.set_model(store)
205
if self.filter.find("@") > 0:
206
key = self.filter[self.filter.find("@")+1:].decode('utf8', 'replace').lower()
207
for category in self.categories:
208
if key in category['name'].decode('utf8', 'replace').lower():
209
fillable = (self.filter[:self.filter.find("@") + 1] + category['name'])
210
store.append([fillable, category['name'], fillable, time])
212
key = input_activity.activity_name.decode('utf8', 'replace').lower()
213
for activity in self.activities:
214
fillable = activity['name']
215
if activity['category']:
216
fillable += "@%s" % activity['category']
218
if time: #as we also support deltas, for the time we will grab anything up to first space
219
fillable = "%s %s" % (self.filter.split(" ", 1)[0], fillable)
221
store.append([fillable, activity['name'], activity['category'], time])
223
def after_activity_update(self, widget, event):
224
self.refresh_activities()
226
def _on_focus_out_event(self, widget, event):
229
def _on_text_changed(self, widget):
232
def _on_button_press_event(self, button, event):
233
self.populate_suggestions()
236
def _on_key_release_event(self, entry, event):
237
if (event.keyval in (gtk.keysyms.Return, gtk.keysyms.KP_Enter)):
238
if self.popup.get_property("visible"):
239
if self.tree.get_cursor()[0]:
240
self.set_text(self.tree.get_model()[self.tree.get_cursor()[0][0]][0])
242
self.set_position(len(self.get_text()))
246
elif (event.keyval == gtk.keysyms.Escape):
247
if self.popup.get_property("visible"):
252
elif event.keyval in (gtk.keysyms.Up, gtk.keysyms.Down):
255
self.populate_suggestions()
258
if event.keyval not in (gtk.keysyms.Delete, gtk.keysyms.BackSpace):
259
self.complete_inline()
264
def _on_key_press_event(self, entry, event):
266
if event.keyval in (gtk.keysyms.Up, gtk.keysyms.Down):
267
cursor = self.tree.get_cursor()
269
if not cursor or not cursor[0]:
270
self.tree.set_cursor(0)
275
if event.keyval == gtk.keysyms.Up:
277
elif event.keyval == gtk.keysyms.Down:
280
# keep it in the sane borders
281
i = min(max(i, 0), len(self.tree.get_model()) - 1)
283
self.tree.set_cursor(i)
284
self.tree.scroll_to_cell(i, use_align = True, row_align = 0.4)
289
def _on_tree_button_press_event(self, tree, event):
290
model, iter = tree.get_selection().get_selected()
291
value = model.get_value(iter, 0)
294
self.set_position(len(self.get_text()))
296
def _on_selected(self):
297
if self.news and self.get_text():
298
self.set_position(len(self.get_text()))
299
self.emit("value-entered")
304
def _on_parent_set(self, old_parent, user_data):
305
# when parent changes to itself, that means that it has been actually deleted
306
if old_parent and old_parent == self.get_toplevel():
307
runtime.dispatcher.del_handler('activity_updated', self.after_activity_update)