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 import stuff
24
from .hamster.stuff import format_duration, format_activity
29
def parent_painter(column, cell, model, iter):
30
fact = model.get_value(iter, 0)
31
parent_info = model.get_value(iter, 1)
34
parent_info["first"] = model.get_path(iter) == (0,) # first row
35
cell.set_property('data', parent_info)
37
cell.set_property('data', fact)
39
def action_painter(column, cell, model, iter):
40
cell.set_property('xalign', 1)
41
cell.set_property('yalign', 0)
43
if model.get_value(iter, 0) is None:
44
cell.set_property("stock_id", "")
46
cell.set_property("stock_id", "gtk-edit")
49
class FactTree(gtk.TreeView):
51
"edit-clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
52
"double-click": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, ))
56
gtk.TreeView.__init__(self)
58
self.set_headers_visible(False)
59
self.set_show_expanders(False)
61
# fact (None for parent), duration, parent data (if any)
62
self.store_model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)
63
self.set_model(self.store_model)
66
fact_cell = FactCellRenderer()
67
fact_column = gtk.TreeViewColumn("", fact_cell, data=0)
68
fact_column.set_cell_data_func(fact_cell, parent_painter)
69
fact_column.set_expand(True)
70
self.append_column(fact_column)
72
edit_cell = gtk.CellRendererPixbuf()
73
edit_cell.set_property("ypad", 2)
74
edit_cell.set_property("mode", gtk.CELL_RENDERER_MODE_ACTIVATABLE)
75
self.edit_column = gtk.TreeViewColumn("", edit_cell)
76
self.edit_column.set_cell_data_func(edit_cell, action_painter)
77
self.append_column(self.edit_column)
79
self.connect("row-activated", self._on_row_activated)
80
self.connect("button-release-event", self._on_button_release_event)
81
self.connect("key-release-event", self._on_key_released)
82
self.connect("configure-event", lambda *args: self.columns_autosize())
86
self.longest_activity_category = 0 # we will need this for the cell renderer
87
self.longest_interval = 0 # we will need this for the cell renderer
88
self.longest_duration = 0 # we will need this for the cell renderer
89
self.stored_selection = []
94
pixmap = gtk.gdk.Pixmap(None, 10, 10, 24)
95
_test_context = pixmap.cairo_create()
96
self._test_layout = _test_context.create_layout()
97
font = pango.FontDescription(gtk.Style().font_desc.to_string())
98
self._test_layout.set_font_description(font)
101
def fix_row_heights(self):
102
alloc = self.get_allocation()
103
if alloc != self.box:
105
self.columns_autosize()
108
self.store_model.clear()
109
self.longest_activity_category = 0
110
self.longest_interval = 0
111
self.longest_duration = 0
113
def update_longest_dimensions(self, fact):
114
interval = "%s -" % fact["start_time"].strftime("%H:%M")
116
interval = "%s %s" % (interval, fact["end_time"].strftime("%H:%M"))
117
self._test_layout.set_markup(interval)
118
w, h = self._test_layout.get_pixel_size()
119
self.longest_interval = max(self.longest_interval, w + 20)
122
self._test_layout.set_markup("%s - <small>%s</small> " % (fact["name"], fact["category"]))
123
w, h = self._test_layout.get_pixel_size()
124
self.longest_activity_category = max(self.longest_activity_category, w + 10)
126
self._test_layout.set_markup("%s" % stuff.format_duration(fact["delta"]))
127
w, h = self._test_layout.get_pixel_size()
128
self.longest_duration = max(self.longest_duration, w)
131
def add_fact(self, fact):
132
self.update_longest_dimensions(fact)
133
self.store_model.append([fact, None])
136
def add_group(self, group_label, group_date, facts):
137
total = sum([stuff.duration_minutes(fact["delta"]) for fact in facts])
139
# adds group of facts with the given label
140
self.store_model.append([None, dict(date = group_date,
148
def get_row(self, path):
149
"""checks if the path is valid and if so, returns the model row"""
150
if path is None or path < 0: return None
152
try: # see if path is still valid
153
iter = self.store_model.get_iter(path)
154
return self.store_model[path]
158
def id_or_label(self, path):
159
"""returns id or date, id if it is a fact row or date if it is a group row"""
160
row = self.get_row(path)
161
if not row: return None
166
return row[1]['label']
168
def detach_model(self):
169
# ooh, somebody is going for refresh!
170
# let's save selection too - maybe it will come handy
171
self.store_selection()
173
# and now do what we were asked to
177
def attach_model(self):
178
# attach model is also where we calculate the bounding box widths
179
self.set_model(self.store_model)
181
if self.stored_selection:
182
self.restore_selection()
185
def store_selection(self):
186
selection = self.get_selection()
187
model, iter = selection.get_selected()
188
self.stored_selection = None
190
path = model.get_path(iter)[0]
191
prev, cur, next = path - 1, path, path + 1
192
self.stored_selection = ((prev, self.id_or_label(prev)),
193
(cur, self.id_or_label(cur)),
194
(next, self.id_or_label(next)))
197
def restore_selection(self):
198
"""the code is quite hairy, but works with all kinds of deletes
199
and does not select row when it should not.
200
TODO - it might be worth replacing this with something much simpler"""
201
model = self.store_model
203
new_prev_val, new_cur_val, new_next_val = None, None, None
204
prev, cur, next = self.stored_selection
206
if cur: new_cur_val = self.id_or_label(cur[0])
207
if prev: new_prev_val = self.id_or_label(prev[0])
208
if next: new_next_val = self.id_or_label(next[0])
211
values = (new_prev_val, new_cur_val, new_next_val)
212
paths = (prev, cur, next)
214
if cur[1] and cur[1] in values: # simple case
215
# look if we can find previous current in the new threesome
216
path = paths[values.index(cur[1])][0]
217
elif prev[1] and prev[1] == new_prev_val and next[1] and next[1] == new_next_val:
218
# on update the ID changes so we find it by matching in between
220
elif prev[1] and prev[1] == new_prev_val: # all that's left is delete.
225
elif not new_prev_val and not new_next_val and new_cur_val:
226
# the only record in the tree (no next no previous, but there is current)
231
selection = self.get_selection()
232
selection.select_path(path)
234
self.set_cursor_on_cell(path)
236
def select_fact(self, fact_id):
238
while self.id_or_label(i) and self.id_or_label(i) != fact_id:
241
if self.id_or_label(i) == fact_id:
242
selection = self.get_selection()
243
selection.select_path(i)
245
def get_selected_fact(self):
246
selection = self.get_selection()
247
(model, iter) = selection.get_selected()
249
return model[iter][0] or model[iter][1]["date"]
254
def _on_button_release_event(self, tree, event):
255
# a hackish solution to make edit icon keyboard accessible
256
pointer = event.window.get_pointer() # x, y, flags
257
path = self.get_path_at_pos(pointer[0], pointer[1]) #column, innerx, innery
259
if path and path[1] == self.edit_column:
260
self.emit("edit-clicked", self.get_selected_fact())
265
def _on_row_activated(self, tree, path, column):
266
if column == self.edit_column:
267
self.emit_stop_by_name ('row-activated')
268
self.emit("edit-clicked", self.get_selected_fact())
272
def _on_key_released(self, tree, event):
273
# capture e keypress and pretend that user click on edit
274
if (event.keyval == gtk.keysyms.e):
275
self.emit("edit-clicked", self.get_selected_fact())
282
class FactCellRenderer(gtk.GenericCellRenderer):
283
""" We need all kinds of wrapping and spanning and the treeview just does
287
"data": (gobject.TYPE_PYOBJECT, "Data", "Data", gobject.PARAM_READWRITE),
291
gtk.GenericCellRenderer.__init__(self)
295
default_font = gtk.Style().font_desc.to_string()
296
self.label_font = pango.FontDescription(default_font)
297
self.label_font_size = 10
299
self.selected_color = gtk.Style().text[gtk.STATE_SELECTED]
300
self.normal_color = gtk.Style().text[gtk.STATE_NORMAL]
302
self.tag_font = pango.FontDescription(default_font)
303
self.tag_font.set_size(pango.SCALE * 8)
305
self.layout, self.tag_layout = None, None
307
self.col_padding = 10
313
def do_set_property (self, pspec, value):
314
setattr(self, pspec.name, value)
316
def do_get_property(self, pspec):
317
return getattr (self, pspec.name)
320
def set_text(self, text):
321
# sets text and returns width and height of the layout
322
self.layout.set_text(text)
323
w, h = self.layout.get_pixel_size()
326
def on_render (self, window, widget, background_area, cell_area, expose_area, flags):
332
--------------+--------------------------------------------+-------+---+
333
13:12 - 17:18 | Some activity - category, tag, tag, tag, | 14:44 | E |
334
| tag, tag, some description in grey italics | | |
335
--------------+--------------------------------------------+-------+---+
339
context = window.cairo_create()
341
self.layout = context.create_layout()
342
self.layout.set_font_description(self.label_font)
344
self.tag_layout = context.create_layout()
345
self.tag_layout.set_font_description(self.tag_font)
348
if "id" in self.data:
349
fact, parent = self.data, None
351
parent, fact = self.data, None
356
x, y, width, height = cell_area
357
context.translate(x, y)
359
current_fact = widget.get_selected_fact()
362
text_color = self.normal_color
363
# if we are selected, change font color appropriately
364
if current_fact and isinstance(current_fact, dt.date) \
365
and current_fact == parent["date"]:
366
text_color = self.selected_color
368
self.set_color(context, text_color)
370
self.layout.set_markup("<b>%s</b>" % stuff.escape_pango(parent["label"]))
371
if self.data["first"]:
376
context.move_to(5, y)
377
context.show_layout(self.layout)
379
self.layout.set_markup("<b>%s</b>" % stuff.format_duration(parent["duration"]))
380
label_w, label_h = self.layout.get_pixel_size()
382
context.move_to(width - label_w, y)
383
context.show_layout(self.layout)
386
text_color = self.normal_color
388
# if we are selected, change font color appropriately
389
if current_fact and isinstance(current_fact, dt.date) == False \
390
and current_fact["id"] == fact["id"]:
391
text_color = self.selected_color
394
def show_label(label, x, y, w):
395
self.layout.set_markup(label)
396
context.move_to(x, y)
398
self.layout.set_width(w)
399
context.show_layout(self.layout)
401
self.set_color(context, text_color)
403
labels = self.labels[fact["id"]]
404
show_label(*labels["interval"])
406
# for the right-aligned delta with have reserved space for scrollbar
407
# but about it's existance we find only on expose, so we realign
408
self.layout.set_markup(labels["delta"][0])
409
w, h = self.layout.get_pixel_size()
410
context.move_to(width - w, labels["delta"][2])
411
context.show_layout(self.layout)
413
show_label(*labels["activity"])
417
self.set_color(context, widget.get_style().text[gtk.STATE_INSENSITIVE])
419
show_label(*labels["category"])
423
start_x, start_y, cell_end = labels["tags"][1:]
424
cur_x, cur_y = start_x, start_y
426
for i, tag in enumerate(fact["tags"]):
427
tag_w, tag_h = Tag.tag_size(tag, self.tag_layout)
429
if i > 0 and cur_x + tag_w >= cell_end:
433
Tag(context, self.tag_layout, True, tag, None,
434
gtk.gdk.Rectangle(cur_x, cur_y, cell_end - cur_x, height - cur_y))
439
if fact["description"]:
440
self.set_color(context, text_color)
441
show_label(*labels["description"])
445
def set_color(self, context, color):
446
context.set_source_rgba(*self.color_to_cairo_rgba(color))
448
def color_to_cairo_rgba(self, c, a=1):
449
return c.red/65535.0, c.green/65535.0, c.blue/65535.0, a
452
def get_fact_size(self, widget):
453
"""determine size and save calculated coordinates"""
455
if not self.data or "id" not in self.data:
458
pixmap = gtk.gdk.Pixmap(None, 10, 10, 24)
459
context = pixmap.cairo_create()
461
layout = context.create_layout()
462
layout.set_font_description(self.label_font)
463
x, y, width, height = widget.get_allocation()
467
cell_width = width - 45
469
""" start time and end time at beginning of column """
470
interval = fact["start_time"].strftime("%H:%M -")
472
interval = "%s %s" % (interval, fact["end_time"].strftime("%H:%M"))
473
labels["interval"] = (interval, self.col_padding, 2, -1)
475
""" duration at the end """
476
delta = stuff.format_duration(fact["delta"])
477
layout.set_markup(delta)
478
duration_w, duration_h = layout.get_pixel_size()
479
labels["delta"] = (delta, cell_width - duration_w, 2, -1)
482
""" activity, category, tags, description in middle """
483
# we want our columns look aligned, so we will do fixed offset from
484
# both sides, in letter length
486
cell_start = widget.longest_interval
487
cell_width = cell_width - widget.longest_interval - widget.longest_duration
490
layout.set_markup(stuff.escape_pango(fact["name"]))
491
label_w, label_h = layout.get_pixel_size()
493
labels["activity"] = (stuff.escape_pango(fact["name"]), cell_start, 2, -1)
494
labels["category"] = (" - <small>%s</small>" % stuff.escape_pango(fact["category"]),
495
cell_start + label_w, 2, -1)
498
tag_cell_start = cell_start + widget.longest_activity_category
499
tag_cell_end = cell_start + cell_width
501
cell_height = label_h + 4
503
cur_x, cur_y = tag_cell_start, 2
505
layout.set_font_description(self.tag_font)
507
for i, tag in enumerate(fact["tags"]):
508
tag_w, tag_h = Tag.tag_size(tag, layout)
510
if i > 0 and cur_x + tag_w >= tag_cell_end:
511
cur_x = tag_cell_start
515
cell_height = max(cell_height, cur_y + tag_h + 4)
517
labels["tags"] = (None, tag_cell_start, 2, tag_cell_end)
519
layout.set_font_description(self.label_font)
522
# see if we can fit in single line
523
# if not, put description under activity
524
if fact["description"]:
525
description = "<small>%s</small>" % stuff.escape_pango(fact["description"])
526
layout.set_markup(description)
527
label_w, label_h = layout.get_pixel_size()
530
width = cell_start + cell_width - x
532
if x + label_w > width:
537
layout.set_width(width * pango.SCALE)
538
label_w, label_h = layout.get_pixel_size()
540
labels["description"] = (description, x, y, width * pango.SCALE)
542
cell_height = y + label_h + 4
544
self.labels[fact["id"]] = labels
545
return (0, 0, 0, cell_height)
548
def on_get_size (self, widget, cell_area):
549
if "id" in self.data: # fact
550
return self.get_fact_size(widget)
552
if self.data["first"]: