~kklimonda/ubuntu/natty/hamster-applet/lp.697667

« back to all changes in this revision

Viewing changes to src/hamster/widgets/dayline.py

  • Committer: Bazaar Package Importer
  • Author(s): Chris Coulson
  • Date: 2010-02-10 02:52:31 UTC
  • mfrom: (1.1.14 upstream)
  • Revision ID: james.westby@ubuntu.com-20100210025231-x0q5h4q7nlvihl09
Tags: 2.29.90-0ubuntu1
* New upstream version
  - workspace tracking - switch activity, when switching desktops
  - chart improvements - theme friendly and less noisier
  - for those without GNOME panel there is now a standalone version, 
    accessible via Applications -> Accessories -> Time Tracker
  - overview window remembers position
  - maintaining cursor on the selected row after edits / refreshes
  - descriptions once again in the main input field, delimited by comma
  - activity suggestion box now sorts items by recency (Patryk Zawadzki)
  - searching
  - simplified save report dialog, thanks to the what you see is what you 
    report revamp
  - overview/stats replaced with activities / totals and stats accessible 
    from totals
  - interactive graphs to drill down in totals
  - miscellaneous performance improvements
  - pixel-perfect graphs
* Updated 01_startup-fix.patch to apply to new source layout
* debian/control:
  - Add build-depend on gnome-doc-utils

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
 
 
3
# Copyright (C) 2007-2009 Toms Bauģis <toms.baugis at gmail.com>
 
4
 
 
5
# This file is part of Project Hamster.
 
6
 
 
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.
 
11
 
 
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.
 
16
 
 
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/>.
 
19
 
 
20
import gtk
 
21
import gobject
 
22
 
 
23
from .hamster import stuff
 
24
from .hamster import graphics
 
25
 
 
26
import time
 
27
import datetime as dt
 
28
import colorsys
 
29
 
 
30
 
 
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)
 
35
 
 
36
 
 
37
    #normal stuff
 
38
    def __init__(self):
 
39
        graphics.Area.__init__(self)
 
40
 
 
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
 
51
        self.move_type = ""
 
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
 
55
 
 
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)
 
58
 
 
59
        self.in_motion = False
 
60
        self.days = []
 
61
 
 
62
        self.view_minutes = float(12 * 60) #how many minutes are we going to show
 
63
 
 
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
 
67
 
 
68
    def draw(self, day_facts, highlight = None):
 
69
        """Draw chart with given data"""
 
70
        self.facts = day_facts
 
71
        if self.facts:
 
72
            self.days.append(self.facts[0]["start_time"].date())
 
73
 
 
74
        start_time = highlight[0] - dt.timedelta(minutes = highlight[0].minute) - dt.timedelta(hours = 10)
 
75
 
 
76
        start_time_int = int(time.mktime(start_time.timetuple()))
 
77
 
 
78
        if self.range_start:
 
79
            self.range_start = start_time
 
80
            self.scroll_to_range_start()
 
81
        else:
 
82
            self.range_start = start_time
 
83
            self.range_start_int = start_time_int
 
84
 
 
85
 
 
86
        self.highlight = highlight
 
87
 
 
88
        self.show()
 
89
 
 
90
        self.redraw_canvas()
 
91
 
 
92
 
 
93
    def on_button_release(self, area, event):
 
94
        if not self.drag_start:
 
95
            return
 
96
 
 
97
        self.drag_start, self.move_type = None, None
 
98
 
 
99
        if event.state & gtk.gdk.BUTTON1_MASK:
 
100
            self.__call_parent_time_changed()
 
101
 
 
102
    def set_in_progress(self, in_progress):
 
103
        self.in_progress = in_progress
 
104
 
 
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]
 
109
 
 
110
        if self.on_time_changed:
 
111
            self.on_time_changed(start_time, end_time)
 
112
 
 
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)
 
116
 
 
117
 
 
118
 
 
119
    def draw_cursor(self, area, event):
 
120
        if event.is_hint:
 
121
            x, y, state = event.window.get_pointer()
 
122
        else:
 
123
            x = event.x + self.graph_x
 
124
            y = event.y + self.graph_y
 
125
            state = event.state
 
126
 
 
127
        mouse_down = state & gtk.gdk.BUTTON1_MASK
 
128
 
 
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
 
133
 
 
134
        if highlight_start != None:
 
135
            start_drag = 10 > (highlight_start - x) > -1
 
136
 
 
137
            end_drag = 10 > (x - highlight_end) > -1
 
138
 
 
139
            if start_drag and end_drag:
 
140
                start_drag = abs(x - highlight_start) < abs(x - highlight_end)
 
141
 
 
142
            in_between = highlight_start <= x <= highlight_end
 
143
            scale = True
 
144
 
 
145
            if self.in_progress:
 
146
                end_drag = False
 
147
                in_between = False
 
148
 
 
149
            if mouse_down and not self.drag_start:
 
150
                self.drag_start = x
 
151
                if start_drag:
 
152
                    self.move_type = "start"
 
153
                elif end_drag:
 
154
                    self.move_type = "end"
 
155
                elif in_between:
 
156
                    self.move_type = "move"
 
157
                    self.drag_start = x - self.highlight_start + self.graph_x
 
158
                elif scale:
 
159
                    self.move_type = "scale_drag"
 
160
                    self.drag_start_time = dt.datetime.fromtimestamp(self.range_start_int)
 
161
 
 
162
 
 
163
            if mouse_down and self.drag_start:
 
164
                start, end = 0, 0
 
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
 
177
 
 
178
                        end = start + width
 
179
 
 
180
                    if end - start > 1:
 
181
                        self.highlight = (self.get_time(start), self.get_time(end))
 
182
                        self.redraw_canvas()
 
183
 
 
184
                    self.__call_parent_time_changed()
 
185
                else:
 
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()
 
188
 
 
189
 
 
190
            if start_drag:
 
191
                area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_SIDE))
 
192
            elif end_drag:
 
193
                area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.RIGHT_SIDE))
 
194
            elif in_between:
 
195
                area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
 
196
            else:
 
197
                area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
 
198
 
 
199
 
 
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
 
203
 
 
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,
 
208
                            "tweenTime": 0.4})
 
209
 
 
210
    def on_expose(self):
 
211
        # check if maybe we are approaching day boundaries and should ask for
 
212
        # more data!
 
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()
 
217
 
 
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)
 
224
 
 
225
 
 
226
        context = self.context
 
227
        #TODO - use system colors and fonts
 
228
 
 
229
        context.set_line_width(1)
 
230
 
 
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
 
234
 
 
235
        pixels_in_minute = self.width / self.view_minutes
 
236
 
 
237
        minutes = self._minutes_from_start(range_end)
 
238
 
 
239
 
 
240
        graph_y = 4
 
241
        graph_height = self.height - 10
 
242
        graph_y2 = graph_y + graph_height
 
243
 
 
244
 
 
245
        # graph area
 
246
        self.fill_area(0, graph_y - 1, self.width, graph_height, (1,1,1))
 
247
 
 
248
        context.save()
 
249
        context.translate(self.graph_x, self.graph_y)
 
250
 
 
251
        #bars
 
252
        for fact in self.facts:
 
253
            start_minutes = self._minutes_from_start(fact["start_time"])
 
254
 
 
255
            if fact["end_time"]:
 
256
                end_minutes = self._minutes_from_start(fact["end_time"])
 
257
            else:
 
258
                if fact["start_time"].date() > dt.date.today() - dt.timedelta(days=1):
 
259
                    end_minutes = self._minutes_from_start(dt.datetime.now())
 
260
                else:
 
261
                    end_minutes = start_minutes
 
262
 
 
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)
 
266
 
 
267
                    context.rectangle(round(start_minutes * pixels_in_minute),
 
268
                                      graph_y,
 
269
                                      round(end_minutes * pixels_in_minute - start_minutes * pixels_in_minute),
 
270
                                      graph_height - 1)
 
271
                    context.fill()
 
272
                    context.stroke()
 
273
 
 
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)
 
279
                    context.stroke()
 
280
 
 
281
 
 
282
 
 
283
        #time scale
 
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))
 
288
 
 
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)
 
293
                context.stroke()
 
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)
 
298
                context.stroke()
 
299
 
 
300
 
 
301
 
 
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")
 
308
                else:
 
309
                    label_minutes = label_time.strftime("%H<small><sup>%M</sup></small>")
 
310
 
 
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()
 
314
 
 
315
                context.move_to(round(i * pixels_in_minute) + 2, graph_y2 - label_h - 8)
 
316
 
 
317
                context.show_layout(self.layout)
 
318
        context.stroke()
 
319
 
 
320
        #highlight rectangle
 
321
        if self.highlight:
 
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)
 
324
 
 
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)
 
328
 
 
329
            self.fill_area(self.highlight_start,
 
330
                           graph_y,
 
331
                           self.highlight_end - self.highlight_start,
 
332
                           graph_height,
 
333
                           (rgb[0], rgb[1], rgb[2], 0.5))
 
334
            context.stroke()
 
335
 
 
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)
 
341
            context.stroke()
 
342
 
 
343
        context.restore()
 
344
 
 
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)
 
348
        context.stroke()
 
349
 
 
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)
 
355
 
 
356
            self.scroll_to_range_start()