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 = self.highlight_start + self.graph_x
130
highlight_end = self.highlight_end + self.graph_x
132
if highlight_start != None:
133
start_drag = 10 > (highlight_start - x) > -1
135
end_drag = 10 > (x - highlight_end) > -1
137
if start_drag and end_drag:
138
start_drag = abs(x - highlight_start) < abs(x - highlight_end)
140
in_between = highlight_start <= x <= highlight_end
147
if mouse_down and not self.drag_start:
150
self.move_type = "start"
152
self.move_type = "end"
154
self.move_type = "move"
155
self.drag_start = x - self.highlight_start + self.graph_x
157
self.move_type = "scale_drag"
158
self.drag_start_time = dt.datetime.fromtimestamp(self.range_start_int)
161
if mouse_down and self.drag_start:
163
if self.move_type and self.move_type != "scale_drag":
164
if self.move_type == "start":
165
if 0 <= x <= self.width:
166
start = x - self.graph_x
167
end = self.highlight_end
168
elif self.move_type == "end":
169
if 0 <= x <= self.width:
170
start = self.highlight_start
171
end = x - self.graph_x
172
elif self.move_type == "move":
173
width = self.highlight_end - self.highlight_start
174
start = x - self.drag_start + self.graph_x
179
self.highlight = (self.get_time(start), self.get_time(end))
182
self.__call_parent_time_changed()
184
self.range_start = self.drag_start_time + dt.timedelta(minutes = self.get_value_at_pos(self.drag_start) - self.get_value_at_pos(x))
185
self.scroll_to_range_start()
189
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_SIDE))
191
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.RIGHT_SIDE))
193
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
195
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
198
def _minutes_from_start(self, date):
199
delta = (date - dt.datetime.fromtimestamp(self.range_start_int))
200
return delta.days * 24 * 60 + delta.seconds / 60
202
def scroll_to_range_start(self):
203
self.tweener.removeTweeningFrom(self)
204
self.animate(self, {"range_start_int": int(time.mktime(self.range_start.timetuple())),
205
"tweenType": graphics.Easing.Expo.easeOut,
209
# check if maybe we are approaching day boundaries and should ask for
211
now = dt.datetime.fromtimestamp(self.range_start_int)
212
if self.on_more_data:
213
date_plus = (now + dt.timedelta(hours = 12 + 2*4 + 1)).date()
214
date_minus = (now - dt.timedelta(hours=1)).date()
216
if date_minus != now.date() and date_minus not in self.days:
217
self.facts += self.on_more_data(date_minus)
218
self.days.append(date_minus)
219
elif date_plus != now.date() and date_plus not in self.days:
220
self.facts += self.on_more_data(date_plus)
221
self.days.append(date_plus)
224
context = self.context
225
#TODO - use system colors and fonts
227
context.set_line_width(1)
229
#we will buffer 4 hours to both sides so partial labels also appear
230
range_end = now + dt.timedelta(hours = 12 + 2 * 4)
231
self.graph_x = -self.width / 3 #so x moves one third out of screen
233
pixels_in_minute = self.width / self.view_minutes
235
minutes = self._minutes_from_start(range_end)
239
graph_height = self.height - 10
240
graph_y2 = graph_y + graph_height
244
self.fill_area(0, graph_y - 1, self.width, graph_height, (1,1,1))
247
context.translate(self.graph_x, self.graph_y)
250
for fact in self.facts:
251
start_minutes = self._minutes_from_start(fact["start_time"])
254
end_minutes = self._minutes_from_start(fact["end_time"])
256
if fact["start_time"].date() > dt.date.today() - dt.timedelta(days=1):
257
end_minutes = self._minutes_from_start(dt.datetime.now())
259
end_minutes = start_minutes
261
if end_minutes * pixels_in_minute > 0 and \
262
start_minutes * pixels_in_minute + self.graph_x < self.width:
263
context.set_source_rgba(0.86, 0.86, 0.86, 0.5)
265
context.rectangle(round(start_minutes * pixels_in_minute),
267
round(end_minutes * pixels_in_minute - start_minutes * pixels_in_minute),
272
context.set_source_rgba(0.86, 0.86, 0.86, 1)
273
self.context.move_to(start_minutes * pixels_in_minute, graph_y)
274
self.context.line_to(start_minutes * pixels_in_minute, graph_y2)
275
self.context.move_to(end_minutes * pixels_in_minute, graph_y)
276
self.context.line_to(end_minutes * pixels_in_minute, graph_y2)
282
context.set_source_rgb(0, 0, 0)
283
self.layout.set_width(-1)
284
for i in range(minutes):
285
label_time = (now + dt.timedelta(minutes=i))
287
if label_time.minute == 0:
288
context.set_source_rgb(0.8, 0.8, 0.8)
289
self.context.move_to(i * pixels_in_minute, graph_y2 - 15)
290
self.context.line_to(i * pixels_in_minute, graph_y2)
292
elif label_time.minute % 15 == 0:
293
context.set_source_rgb(0.8, 0.8, 0.8)
294
self.context.move_to(i * pixels_in_minute, graph_y2 - 5)
295
self.context.line_to(i * pixels_in_minute, graph_y2)
300
if label_time.minute == 0 and label_time.hour % 2 == 0:
301
if label_time.hour == 0:
302
context.set_source_rgb(0.8, 0.8, 0.8)
303
self.context.move_to(i * pixels_in_minute, graph_y)
304
self.context.line_to(i * pixels_in_minute, graph_y2)
305
label_minutes = label_time.strftime("%b %d")
307
label_minutes = label_time.strftime("%H<small><sup>%M</sup></small>")
309
context.set_source_rgb(0.4, 0.4, 0.4)
310
self.layout.set_markup(label_minutes)
311
label_w, label_h = self.layout.get_pixel_size()
313
context.move_to(i * pixels_in_minute + 2, graph_y2 - label_h - 8)
315
context.show_layout(self.layout)
320
self.highlight_start = round(self._minutes_from_start(self.highlight[0]) * pixels_in_minute)
321
self.highlight_end = round(self._minutes_from_start(self.highlight[1]) * pixels_in_minute)
323
#TODO - make a proper range check here
324
if self.highlight_end + self.graph_x > 0 and self.highlight_start + self.graph_x < self.width:
325
rgb = colorsys.hls_to_rgb(.6, .7, .5)
327
self.fill_area(self.highlight_start,
329
self.highlight_end - self.highlight_start,
331
(rgb[0], rgb[1], rgb[2], 0.5))
334
context.set_source_rgb(*rgb)
335
self.context.move_to(self.highlight_start, graph_y)
336
self.context.line_to(self.highlight_start, graph_y + graph_height)
337
self.context.move_to(self.highlight_end, graph_y)
338
self.context.line_to(self.highlight_end, graph_y + graph_height)
343
#and now put a frame around the whole thing
344
context.set_source_rgb(0.7, 0.7, 0.7)
345
context.rectangle(0, graph_y-1, self.width - 1, graph_height)
348
if self.move_type == "move" and (self.highlight_start + self.graph_x <= 0 or self.highlight_end + self.graph_x >= self.width):
349
if self.highlight_start + self.graph_x <= 0:
350
self.range_start = self.range_start - dt.timedelta(minutes=30)
351
if self.highlight_end + self.graph_x >= self.width:
352
self.range_start = self.range_start + dt.timedelta(minutes=30)
354
self.scroll_to_range_start()