1
# -*- coding: utf-8 -*-
3
# Copyright (C) 2007-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/>.
29
import graphics, widgets
31
from configuration import runtime
40
* hook into notifications and refresh our days if some evil neighbour edit
41
fact window has dared to edit facts
43
class Dayline(graphics.Area):
45
graphics.Area.__init__(self)
47
self.set_events(gtk.gdk.EXPOSURE_MASK
48
| gtk.gdk.LEAVE_NOTIFY_MASK
49
| gtk.gdk.BUTTON_PRESS_MASK
50
| gtk.gdk.BUTTON_RELEASE_MASK
51
| gtk.gdk.POINTER_MOTION_MASK
52
| gtk.gdk.POINTER_MOTION_HINT_MASK)
53
self.connect("button_release_event", self.on_button_release)
54
self.connect("motion_notify_event", self.draw_cursor)
55
self.highlight_start, self.highlight_end = None, None
56
self.drag_start = None
58
self.on_time_changed = None #override this with your func to get notified when user changes date
59
self.on_more_data = None #supplement with more data func that accepts single date
60
self.in_progress = False
62
self.range_start = None
63
self.in_motion = False
67
def draw(self, day_facts, highlight = None):
68
"""Draw chart with given data"""
69
self.facts = day_facts
71
self.days.append(self.facts[0]["start_time"].date())
73
start_time = highlight[0] - dt.timedelta(minutes = highlight[0].minute) - dt.timedelta(hours = 10)
76
self.range_start.target(start_time)
77
self.scroll_to_range_start()
79
self.range_start = graphics.Integrator(start_time, damping = 0.35, attraction = 0.5)
81
self.highlight = highlight
88
def on_button_release(self, area, event):
89
if not self.drag_start:
92
self.drag_start, self.move_type = None, None
94
if event.state & gtk.gdk.BUTTON1_MASK:
95
self.__call_parent_time_changed()
97
def set_in_progress(self, in_progress):
98
self.in_progress = in_progress
100
def __call_parent_time_changed(self):
101
#now calculate back from pixels into minutes
102
start_time = self.highlight[0]
103
end_time = self.highlight[1]
105
if self.on_time_changed:
106
self.on_time_changed(start_time, end_time)
108
def get_time(self, pixels):
109
minutes = self.get_value_at_pos(x = pixels)
110
return self.range_start.value + dt.timedelta(minutes = minutes)
112
def scroll_to_range_start(self):
113
if not self.in_motion:
114
self.in_motion = True
115
gobject.timeout_add(1000 / 30, self.animate_scale)
118
def animate_scale(self):
119
moving = self.range_start.update() > 5
122
# check if maybe we are approaching day boundaries and should ask for
124
if self.on_more_data:
125
now = self.range_start.value
126
date_plus = (now + dt.timedelta(hours = 12 + 2*4 + 1)).date()
127
date_minus = (now - dt.timedelta(hours=1)).date()
129
if date_minus != now.date() and date_minus not in self.days:
130
self.facts += self.on_more_data(date_minus)
131
self.days.append(date_minus)
132
elif date_plus != now.date() and date_plus not in self.days:
133
self.facts += self.on_more_data(date_plus)
134
self.days.append(date_plus)
141
self.in_motion = False
146
def draw_cursor(self, area, event):
148
x, y, state = event.window.get_pointer()
154
mouse_down = state & gtk.gdk.BUTTON1_MASK
156
#print x, self.highlight_start, self.highlight_end
157
if self.highlight_start != None:
158
start_drag = 10 > (self.highlight_start - x) > -1
160
end_drag = 10 > (x - self.highlight_end) > -1
162
if start_drag and end_drag:
163
start_drag = abs(x - self.highlight_start) < abs(x - self.highlight_end)
165
in_between = self.highlight_start <= x <= self.highlight_end
172
if mouse_down and not self.drag_start:
175
self.move_type = "start"
177
self.move_type = "end"
179
self.move_type = "move"
180
self.drag_start = x - self.highlight_start
182
self.move_type = "scale_drag"
183
self.drag_start_time = self.range_start.value
186
if mouse_down and self.drag_start:
188
if self.move_type and self.move_type != "scale_drag":
189
if self.move_type == "start":
190
if 0 <= x <= self.width:
192
end = self.highlight_end
193
elif self.move_type == "end":
194
if 0 <= x <= self.width:
195
start = self.highlight_start
197
elif self.move_type == "move":
198
width = self.highlight_end - self.highlight_start
199
start = x - self.drag_start
200
start = max(0, min(start, self.width))
208
self.highlight = (self.get_time(start), self.get_time(end))
211
self.__call_parent_time_changed()
213
self.range_start.target(self.drag_start_time +
214
dt.timedelta(minutes = self.get_value_at_pos(x = self.drag_start) - self.get_value_at_pos(x = x)))
215
self.scroll_to_range_start()
220
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_SIDE))
222
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.RIGHT_SIDE))
224
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
226
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
229
def _minutes_from_start(self, date):
230
delta = (date - self.range_start.value)
231
return delta.days * 24 * 60 + delta.seconds / 60
234
context = self.context
235
#TODO - use system colors and fonts
237
context.set_line_width(1)
239
#we will buffer 4 hours to both sides so partial labels also appear
240
range_end = self.range_start.value + dt.timedelta(hours = 12 + 2 * 4)
241
self.graph_x = -self.width / 3 #so x moves one third out of screen
242
self.set_value_range(x_min = 0, x_max = 12 * 60)
244
minutes = self._minutes_from_start(range_end)
249
graph_height = self.height - 10
250
graph_y2 = graph_y + graph_height
254
self.fill_area(0, graph_y - 1, self.width, graph_height, (1,1,1))
257
for fact in self.facts:
258
start_minutes = self._minutes_from_start(fact["start_time"])
261
end_minutes = self._minutes_from_start(fact["end_time"])
263
if fact["start_time"].date() > dt.date.today() - dt.timedelta(days=1):
264
end_minutes = self._minutes_from_start(dt.datetime.now())
266
end_minutes = start_minutes
268
if self.get_pixel(end_minutes) > 0 and \
269
self.get_pixel(start_minutes) < self.width:
270
context.set_source_rgba(0.86, 0.86, 0.86, 0.5)
271
context.rectangle(self.get_pixel(start_minutes), graph_y,
272
self.get_pixel(end_minutes) - self.get_pixel(start_minutes), graph_height - 1)
276
context.set_source_rgba(0.86, 0.86, 0.86, 1)
277
self.move_to(start_minutes, graph_y)
278
self.line_to(start_minutes, graph_y2)
279
self.move_to(end_minutes, graph_y)
280
self.line_to(end_minutes, graph_y2)
286
context.set_source_rgb(0, 0, 0)
287
self.layout.set_width(-1)
288
for i in range(minutes):
289
label_time = (self.range_start.value + dt.timedelta(minutes=i))
291
if label_time.minute == 0:
292
context.set_source_rgb(0.8, 0.8, 0.8)
293
self.move_to(i, graph_y2 - 15)
294
self.line_to(i, graph_y2)
296
elif label_time.minute % 15 == 0:
297
context.set_source_rgb(0.8, 0.8, 0.8)
298
self.move_to(i, graph_y2 - 5)
299
self.line_to(i, graph_y2)
304
if label_time.minute == 0 and label_time.hour % 2 == 0:
305
if label_time.hour == 0:
306
context.set_source_rgb(0.8, 0.8, 0.8)
307
self.move_to(i, graph_y)
308
self.line_to(i, graph_y2)
309
label_minutes = label_time.strftime("%b %d")
311
label_minutes = label_time.strftime("%H<small><sup>%M</sup></small>")
313
context.set_source_rgb(0.4, 0.4, 0.4)
314
self.layout.set_markup(label_minutes)
315
label_w, label_h = self.layout.get_pixel_size()
317
context.move_to(self.get_pixel(i) + 2, graph_y2 - label_h - 8)
319
context.show_layout(self.layout)
324
self.highlight_start = self.get_pixel(self._minutes_from_start(self.highlight[0]))
325
self.highlight_end = self.get_pixel(self._minutes_from_start(self.highlight[1]))
327
#TODO - make a proper range check here
328
if self.highlight_end > 0 and self.highlight_start < self.width:
329
rgb = colorsys.hls_to_rgb(.6, .7, .5)
332
self.fill_area(self.highlight_start, graph_y,
333
self.highlight_end - self.highlight_start, graph_height,
334
(rgb[0], rgb[1], rgb[2], 0.5))
337
context.set_source_rgb(*rgb)
338
self.context.move_to(self.highlight_start, graph_y)
339
self.context.line_to(self.highlight_start, graph_y + graph_height)
340
self.context.move_to(self.highlight_end, graph_y)
341
self.context.line_to(self.highlight_end, graph_y + graph_height)
344
#and now put a frame around the whole thing
345
context.set_source_rgb(0.7, 0.7, 0.7)
346
context.rectangle(0, graph_y-1, self.width - 1, graph_height)
349
if self.move_type == "move" and (self.highlight_start == 0 or self.highlight_end == self.width):
350
if self.highlight_start == 0:
351
self.range_start.target(self.range_start.value - dt.timedelta(minutes=30))
352
if self.highlight_end == self.width:
353
self.range_start.target(self.range_start.value + dt.timedelta(minutes=30))
354
self.scroll_to_range_start()
358
class CustomFactController:
359
def __init__(self, parent = None, fact_date = None, fact_id = None):
360
self._gui = stuff.load_ui_file("edit_activity.ui")
361
self.window = self.get_widget('custom_fact_window')
363
self.parent, self.fact_id = parent, fact_id
365
start_date, end_date = None, None
367
fact = runtime.storage.get_fact(fact_id)
370
if fact['category'] != _("Unsorted"):
371
label += "@%s" % fact['category']
372
self.get_widget('activity_combo').child.set_text(label)
374
start_date = fact["start_time"]
375
end_date = fact["end_time"]
377
buf = gtk.TextBuffer()
378
buf.set_text(fact["description"] or "")
379
self.get_widget('description').set_buffer(buf)
382
self.get_widget("in_progress").set_active(True)
383
if (dt.datetime.now() - start_date).days == 0:
384
end_date = dt.datetime.now()
386
self.get_widget("save_button").set_label("gtk-save")
387
self.window.set_title(_("Update activity"))
389
elif fact_date and fact_date != dt.date.today():
390
# if there is previous activity with end time - attach to it
391
# otherwise let's start at 8am
392
last_activity = runtime.storage.get_facts(fact_date)
393
if last_activity and last_activity[len(last_activity)-1]["end_time"]:
394
start_date = last_activity[len(last_activity)-1]["end_time"]
396
start_date = dt.datetime(fact_date.year, fact_date.month,
399
start_date = start_date or dt.datetime.now()
400
end_date = end_date or start_date + dt.timedelta(minutes = 30)
403
self.start_date = widgets.DateInput(start_date)
404
self.get_widget("start_date_placeholder").add(self.start_date)
405
self.start_date.connect("date-entered", self.on_start_date_entered)
407
self.start_time = widgets.TimeInput(start_date)
408
self.get_widget("start_time_placeholder").add(self.start_time)
409
self.start_time.connect("time-entered", self.on_start_time_entered)
411
self.end_time = widgets.TimeInput(end_date, start_date)
412
self.get_widget("end_time_placeholder").add(self.end_time)
413
self.end_time.connect("time-entered", self.on_end_time_entered)
414
self.set_end_date_label(end_date)
420
self.dayline = Dayline()
421
self.dayline.on_time_changed = self.update_time
422
self.dayline.on_more_data = runtime.storage.get_facts
423
self._gui.get_object("day_preview").add(self.dayline)
425
self.on_in_progress_toggled(self.get_widget("in_progress"))
426
self._gui.connect_signals(self)
428
def update_time(self, start_time, end_time):
429
self.start_time.set_time(start_time)
430
self.start_date.set_date(start_time)
431
self.end_time.set_time(end_time)
432
self.set_end_date_label(end_time)
435
def draw_preview(self, date, highlight = None):
436
day_facts = runtime.storage.get_facts(date)
437
self.dayline.draw(day_facts, highlight)
441
def set_dropdown(self):
442
# set up drop down menu
443
self.activity_list = self._gui.get_object('activity_combo')
444
self.activity_list.set_model(gtk.ListStore(gobject.TYPE_STRING,
446
gobject.TYPE_STRING))
449
self.activity_list.set_property("text-column", 2)
450
self.activity_list.clear()
451
activity_cell = gtk.CellRendererText()
452
self.activity_list.pack_start(activity_cell, True)
453
self.activity_list.add_attribute(activity_cell, 'text', 0)
454
category_cell = stuff.CategoryCell()
455
self.activity_list.pack_start(category_cell, False)
456
self.activity_list.add_attribute(category_cell, 'text', 1)
458
self.activity_list.child.connect('key-press-event', self.on_activity_list_key_pressed)
461
# set up autocompletition
462
self.activities = gtk.ListStore(gobject.TYPE_STRING,
465
completion = gtk.EntryCompletion()
466
completion.set_model(self.activities)
468
activity_cell = gtk.CellRendererText()
469
completion.pack_start(activity_cell, True)
470
completion.add_attribute(activity_cell, 'text', 0)
471
completion.set_property("text-column", 2)
473
category_cell = stuff.CategoryCell()
474
completion.pack_start(category_cell, False)
475
completion.add_attribute(category_cell, 'text', 1)
477
completion.set_minimum_key_length(1)
478
completion.set_inline_completion(True)
480
self.activity_list.child.set_completion(completion)
483
def refresh_menu(self):
484
#first populate the autocomplete - contains all entries in lowercase
485
self.activities.clear()
486
all_activities = runtime.storage.get_autocomplete_activities()
487
for activity in all_activities:
488
activity_category = activity['name']
489
if activity['category']:
490
activity_category += "@%s" % activity['category']
491
self.activities.append([activity['name'],
492
activity['category'],
496
#now populate the menu - contains only categorized entries
497
store = self.activity_list.get_model()
500
#populate fresh list from DB
501
categorized_activities = runtime.storage.get_sorted_activities()
503
for activity in categorized_activities:
504
activity_category = activity['name']
505
if activity['category']:
506
activity_category += "@%s" % activity['category']
507
item = store.append([activity['name'],
508
activity['category'],
511
# finally add TODO tasks from evolution to both lists
512
tasks = eds.get_eds_tasks()
513
for activity in tasks:
514
activity_category = "%s@%s" % (activity['name'], activity['category'])
515
self.activities.append([activity['name'],activity['category'],activity_category])
516
store.append([activity['name'], activity['category'], activity_category])
520
def get_widget(self, name):
521
""" skip one variable (huh) """
522
return self._gui.get_object(name)
527
def _get_datetime(self, prefix):
528
start_time = self.start_time.get_time()
529
start_date = self.start_date.get_date()
532
end_time = self.end_time.get_time()
533
end_date = start_date
534
if end_time < start_time:
535
end_date = start_date + dt.timedelta(days=1)
538
self.set_end_date_label(end_date)
539
time, date = end_time, end_date
541
time, date = start_time, start_date
544
return dt.datetime.combine(date, time.time())
548
def figure_description(self):
549
activity = self.get_widget("activity_combo").child.get_text().decode("utf-8")
551
# juggle with description - break into parts and then put together
552
buf = self.get_widget('description').get_buffer()
553
description = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), 0)\
555
description = description.strip()
557
# user might also type description in the activity name - strip it here
559
inline_description = None
560
if activity.find(",") != -1:
561
activity, inline_description = activity.split(",", 1)
562
inline_description = inline_description.strip()
564
# description field is prior to inline description
565
return description or inline_description
567
def on_save_button_clicked(self, button):
568
activity = self.get_widget("activity_combo").child.get_text().decode("utf-8")
573
description = self.figure_description()
576
activity = "%s, %s" % (activity, description)
579
start_time = self._get_datetime("start")
581
if self.get_widget("in_progress").get_active():
584
end_time = self._get_datetime("end")
586
# we don't do updates, we do insert/delete. So now it is time to delete
588
runtime.storage.remove_fact(self.fact_id)
590
runtime.storage.add_fact(activity, start_time, end_time)
593
# hide panel only on add - on update user will want to see changes
595
runtime.dispatcher.dispatch('panel_visible', False)
599
def on_activity_list_key_pressed(self, entry, event):
600
#treating tab as keydown to be able to cycle through available values
601
if event.keyval == gtk.keysyms.Tab:
602
event.keyval = gtk.keysyms.Down
605
def on_in_progress_toggled(self, check):
606
sensitive = not check.get_active()
607
self.end_time.set_sensitive(sensitive)
608
self.get_widget("end_label").set_sensitive(sensitive)
609
self.get_widget("end_date_label").set_sensitive(sensitive)
610
self.validate_fields()
611
self.dayline.set_in_progress(not sensitive)
613
def on_cancel_clicked(self, button):
616
def on_activity_combo_changed(self, combo):
617
self.validate_fields()
619
def on_start_date_entered(self, widget):
620
self.validate_fields()
621
self.start_time.grab_focus()
623
def on_start_time_entered(self, widget):
624
start_time = self.start_time.get_time()
628
self.end_time.set_time(start_time + dt.timedelta(minutes = 30))
629
self.end_time.set_start_time(start_time)
630
self.validate_fields()
631
self.end_time.grab_focus()
633
def on_end_time_entered(self, widget):
634
self.validate_fields()
636
def set_end_date_label(self, some_date):
637
self.get_widget("end_date_label").set_text(some_date.strftime("%x"))
639
def validate_fields(self, widget = None):
640
activity_text = self.get_widget("activity_combo").child.get_text()
641
start_time = self._get_datetime("start")
643
end_time = self._get_datetime("end")
644
if self.get_widget("in_progress").get_active():
645
end_time = dt.datetime.now()
647
if start_time and end_time:
648
# if we are too far, just roll back for one day
649
if ((end_time - start_time).days > 0):
650
end_time -= dt.timedelta(days=1)
651
self.update_time(start_time, end_time)
653
# if end time is not in proper distance, do the brutal +30 minutes reset
654
if (end_time < start_time or (end_time - start_time).days > 0):
655
end_time = start_time + dt.timedelta(minutes = 30)
656
self.update_time(start_time, end_time)
658
self.draw_preview(start_time.date(), [start_time, end_time])
660
self.draw_preview(dt.datetime.today().date(), [dt.datetime.now(),
664
if activity_text != "" and start_time and end_time and \
665
(end_time - start_time).days == 0:
668
self.get_widget("save_button").set_sensitive(looks_good)
671
def on_window_key_pressed(self, tree, event_key):
672
if (event_key.keyval == gtk.keysyms.Escape
673
or (event_key.keyval == gtk.keysyms.w
674
and event_key.state & gtk.gdk.CONTROL_MASK)):
676
if self.start_date.popup.get_property("visible") or \
677
self.start_time.popup.get_property("visible") or \
678
self.end_time.popup.get_property("visible"):
683
def on_close(self, widget, event):
686
def close_window(self):
690
self.window.destroy()