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/>.
23
from .hamster import stuff
24
from .hamster import graphics
31
class DayLine(graphics.Area):
32
def get_value_at_pos(self, x):
33
"""returns mapped value at the coordinates x,y"""
34
return x / float(self.width / self.view_minutes)
39
graphics.Area.__init__(self)
41
self.set_events(gtk.gdk.EXPOSURE_MASK
42
| gtk.gdk.LEAVE_NOTIFY_MASK
43
| gtk.gdk.BUTTON_PRESS_MASK
44
| gtk.gdk.BUTTON_RELEASE_MASK
45
| gtk.gdk.POINTER_MOTION_MASK
46
| gtk.gdk.POINTER_MOTION_HINT_MASK)
47
self.connect("button_release_event", self.on_button_release)
48
self.connect("motion_notify_event", self.draw_cursor)
49
self.highlight_start, self.highlight_end = None, None
50
self.drag_start = None
52
self.on_time_changed = None #override this with your func to get notified when user changes date
53
self.on_more_data = None #supplement with more data func that accepts single date
54
self.in_progress = False
56
self.range_start = None
57
self.range_start_int = None #same date just expressed as integer so we can interpolate it (a temporar hack)
59
self.in_motion = False
62
self.view_minutes = float(12 * 60) #how many minutes are we going to show
64
# TODO - get rid of these
65
# use these to mark area where the "real" drawing is going on
66
self.graph_x, self.graph_y = 0, 0
68
def draw(self, day_facts, highlight = None):
69
"""Draw chart with given data"""
70
self.facts = day_facts
72
self.days.append(self.facts[0]["start_time"].date())
74
start_time = highlight[0] - dt.timedelta(minutes = highlight[0].minute) - dt.timedelta(hours = 10)
76
start_time_int = int(time.mktime(start_time.timetuple()))
79
self.range_start = start_time
80
self.scroll_to_range_start()
82
self.range_start = start_time
83
self.range_start_int = start_time_int
86
self.highlight = highlight
93
def on_button_release(self, area, event):
94
if not self.drag_start:
97
self.drag_start, self.move_type = None, None
99
if event.state & gtk.gdk.BUTTON1_MASK:
100
self.__call_parent_time_changed()
102
def set_in_progress(self, in_progress):
103
self.in_progress = in_progress
105
def __call_parent_time_changed(self):
106
#now calculate back from pixels into minutes
107
start_time = self.highlight[0]
108
end_time = self.highlight[1]
110
if self.on_time_changed:
111
self.on_time_changed(start_time, end_time)
113
def get_time(self, pixels):
114
minutes = self.get_value_at_pos(x = pixels)
115
return dt.datetime.fromtimestamp(self.range_start_int) + dt.timedelta(minutes = minutes)
119
def draw_cursor(self, area, event):
121
x, y, state = event.window.get_pointer()
123
x = event.x + self.graph_x
124
y = event.y + self.graph_y
127
mouse_down = state & gtk.gdk.BUTTON1_MASK
129
highlight_start, highlight_end = None, None
130
if self.highlight_start:
131
highlight_start = self.highlight_start + self.graph_x
132
highlight_end = self.highlight_end + self.graph_x
134
if highlight_start != None:
135
start_drag = 10 > (highlight_start - x) > -1
137
end_drag = 10 > (x - highlight_end) > -1
139
if start_drag and end_drag:
140
start_drag = abs(x - highlight_start) < abs(x - highlight_end)
142
in_between = highlight_start <= x <= highlight_end
149
if mouse_down and not self.drag_start:
152
self.move_type = "start"
154
self.move_type = "end"
156
self.move_type = "move"
157
self.drag_start = x - self.highlight_start + self.graph_x
159
self.move_type = "scale_drag"
160
self.drag_start_time = dt.datetime.fromtimestamp(self.range_start_int)
163
if mouse_down and self.drag_start:
165
if self.move_type and self.move_type != "scale_drag":
166
if self.move_type == "start":
167
if 0 <= x <= self.width:
168
start = x - self.graph_x
169
end = self.highlight_end
170
elif self.move_type == "end":
171
if 0 <= x <= self.width:
172
start = self.highlight_start
173
end = x - self.graph_x
174
elif self.move_type == "move":
175
width = self.highlight_end - self.highlight_start
176
start = x - self.drag_start + self.graph_x
181
self.highlight = (self.get_time(start), self.get_time(end))
184
self.__call_parent_time_changed()
186
self.range_start = self.drag_start_time + dt.timedelta(minutes = self.get_value_at_pos(self.drag_start) - self.get_value_at_pos(x))
187
self.scroll_to_range_start()
191
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_SIDE))
193
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.RIGHT_SIDE))
195
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
197
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
200
def _minutes_from_start(self, date):
201
delta = (date - dt.datetime.fromtimestamp(self.range_start_int))
202
return delta.days * 24 * 60 + delta.seconds / 60
204
def scroll_to_range_start(self):
205
self.tweener.killTweensOf(self)
206
self.animate(self, {"range_start_int": int(time.mktime(self.range_start.timetuple())),
207
"tweenType": graphics.Easing.Expo.easeOut,
211
# check if maybe we are approaching day boundaries and should ask for
213
now = dt.datetime.fromtimestamp(self.range_start_int)
214
if self.on_more_data:
215
date_plus = (now + dt.timedelta(hours = 12 + 2*4 + 1)).date()
216
date_minus = (now - dt.timedelta(hours=1)).date()
218
if date_minus != now.date() and date_minus not in self.days:
219
self.facts += self.on_more_data(date_minus)
220
self.days.append(date_minus)
221
elif date_plus != now.date() and date_plus not in self.days:
222
self.facts += self.on_more_data(date_plus)
223
self.days.append(date_plus)
226
context = self.context
227
#TODO - use system colors and fonts
229
context.set_line_width(1)
231
#we will buffer 4 hours to both sides so partial labels also appear
232
range_end = now + dt.timedelta(hours = 12 + 2 * 4)
233
self.graph_x = -self.width / 3 #so x moves one third out of screen
235
pixels_in_minute = self.width / self.view_minutes
237
minutes = self._minutes_from_start(range_end)
241
graph_height = self.height - 10
242
graph_y2 = graph_y + graph_height
246
self.fill_area(0, graph_y - 1, self.width, graph_height, (1,1,1))
249
context.translate(self.graph_x, self.graph_y)
252
for fact in self.facts:
253
start_minutes = self._minutes_from_start(fact["start_time"])
256
end_minutes = self._minutes_from_start(fact["end_time"])
258
if fact["start_time"].date() > dt.date.today() - dt.timedelta(days=1):
259
end_minutes = self._minutes_from_start(dt.datetime.now())
261
end_minutes = start_minutes
263
if end_minutes * pixels_in_minute > 0 and \
264
start_minutes * pixels_in_minute + self.graph_x < self.width:
265
context.set_source_rgba(0.86, 0.86, 0.86, 0.5)
267
context.rectangle(round(start_minutes * pixels_in_minute),
269
round(end_minutes * pixels_in_minute - start_minutes * pixels_in_minute),
274
context.set_source_rgba(0.86, 0.86, 0.86, 1)
275
self.context.move_to(round(start_minutes * pixels_in_minute) + 0.5, graph_y)
276
self.context.line_to(round(start_minutes * pixels_in_minute) + 0.5, graph_y2)
277
self.context.move_to(round(end_minutes * pixels_in_minute) + 0.5, graph_y)
278
self.context.line_to(round(end_minutes * pixels_in_minute) + 0.5, graph_y2)
284
context.set_source_rgb(0, 0, 0)
285
self.layout.set_width(-1)
286
for i in range(minutes):
287
label_time = (now + dt.timedelta(minutes=i))
289
if label_time.minute == 0:
290
context.set_source_rgb(0.8, 0.8, 0.8)
291
self.context.move_to(round(i * pixels_in_minute) + 0.5, graph_y2 - 15)
292
self.context.line_to(round(i * pixels_in_minute) + 0.5, graph_y2)
294
elif label_time.minute % 15 == 0:
295
context.set_source_rgb(0.8, 0.8, 0.8)
296
self.context.move_to(round(i * pixels_in_minute) + 0.5, graph_y2 - 5)
297
self.context.line_to(round(i * pixels_in_minute) + 0.5, graph_y2)
302
if label_time.minute == 0 and label_time.hour % 2 == 0:
303
if label_time.hour == 0:
304
context.set_source_rgb(0.8, 0.8, 0.8)
305
self.context.move_to(round(i * pixels_in_minute) + 0.5, graph_y)
306
self.context.line_to(round(i * pixels_in_minute) + 0.5, graph_y2)
307
label_minutes = label_time.strftime("%b %d")
309
label_minutes = label_time.strftime("%H<small><sup>%M</sup></small>")
311
context.set_source_rgb(0.4, 0.4, 0.4)
312
self.layout.set_markup(label_minutes)
313
label_w, label_h = self.layout.get_pixel_size()
315
context.move_to(round(i * pixels_in_minute) + 2, graph_y2 - label_h - 8)
317
context.show_layout(self.layout)
322
self.highlight_start = round(self._minutes_from_start(self.highlight[0]) * pixels_in_minute)
323
self.highlight_end = round(self._minutes_from_start(self.highlight[1]) * pixels_in_minute)
325
#TODO - make a proper range check here
326
if self.highlight_end + self.graph_x > 0 and self.highlight_start + self.graph_x < self.width:
327
rgb = colorsys.hls_to_rgb(.6, .7, .5)
329
self.fill_area(self.highlight_start,
331
self.highlight_end - self.highlight_start,
333
(rgb[0], rgb[1], rgb[2], 0.5))
336
context.set_source_rgb(*rgb)
337
self.context.move_to(self.highlight_start + 0.5, graph_y)
338
self.context.line_to(self.highlight_start + 0.5, graph_y + graph_height)
339
self.context.move_to(self.highlight_end + 0.5, graph_y)
340
self.context.line_to(self.highlight_end + 0.5, graph_y + graph_height)
345
#and now put a frame around the whole thing
346
context.set_source_rgb(0.7, 0.7, 0.7)
347
context.rectangle(0, graph_y-0.5, self.width - 0.5, graph_height)
350
if self.move_type == "move" and (self.highlight_start + self.graph_x <= 0 or self.highlight_end + self.graph_x >= self.width):
351
if self.highlight_start + self.graph_x <= 0:
352
self.range_start = self.range_start - dt.timedelta(minutes=30)
353
if self.highlight_end + self.graph_x >= self.width:
354
self.range_start = self.range_start + dt.timedelta(minutes=30)
356
self.scroll_to_range_start()