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

« back to all changes in this revision

Viewing changes to 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 = self.highlight_start + self.graph_x
130
 
        highlight_end = self.highlight_end + self.graph_x
131
 
            
132
 
        if highlight_start != None:
133
 
            start_drag = 10 > (highlight_start - x) > -1
134
 
 
135
 
            end_drag = 10 > (x - highlight_end) > -1
136
 
 
137
 
            if start_drag and end_drag:
138
 
                start_drag = abs(x - highlight_start) < abs(x - highlight_end)
139
 
 
140
 
            in_between = highlight_start <= x <= highlight_end
141
 
            scale = True
142
 
 
143
 
            if self.in_progress:
144
 
                end_drag = False
145
 
                in_between = False
146
 
 
147
 
            if mouse_down and not self.drag_start:
148
 
                self.drag_start = x
149
 
                if start_drag:
150
 
                    self.move_type = "start"
151
 
                elif end_drag:
152
 
                    self.move_type = "end"
153
 
                elif in_between:
154
 
                    self.move_type = "move"
155
 
                    self.drag_start = x - self.highlight_start + self.graph_x
156
 
                elif scale:
157
 
                    self.move_type = "scale_drag"
158
 
                    self.drag_start_time = dt.datetime.fromtimestamp(self.range_start_int)
159
 
 
160
 
            
161
 
            if mouse_down and self.drag_start:
162
 
                start, end = 0, 0
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
175
 
                        
176
 
                        end = start + width
177
 
    
178
 
                    if end - start > 1:
179
 
                        self.highlight = (self.get_time(start), self.get_time(end))
180
 
                        self.redraw_canvas()
181
 
 
182
 
                    self.__call_parent_time_changed()
183
 
                else:
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()
186
 
 
187
 
 
188
 
            if start_drag:
189
 
                area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_SIDE))
190
 
            elif end_drag:
191
 
                area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.RIGHT_SIDE))
192
 
            elif in_between:
193
 
                area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
194
 
            else:
195
 
                area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
196
 
                
197
 
        
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
201
 
 
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,
206
 
                            "tweenTime": 0.4})
207
 
        
208
 
    def on_expose(self):
209
 
        # check if maybe we are approaching day boundaries and should ask for
210
 
        # more data!
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()
215
 
 
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)
222
 
 
223
 
 
224
 
        context = self.context
225
 
        #TODO - use system colors and fonts
226
 
 
227
 
        context.set_line_width(1)
228
 
 
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
232
 
        
233
 
        pixels_in_minute = self.width / self.view_minutes
234
 
 
235
 
        minutes = self._minutes_from_start(range_end)
236
 
 
237
 
 
238
 
        graph_y = 4
239
 
        graph_height = self.height - 10
240
 
        graph_y2 = graph_y + graph_height
241
 
 
242
 
        
243
 
        # graph area
244
 
        self.fill_area(0, graph_y - 1, self.width, graph_height, (1,1,1))
245
 
 
246
 
        context.save()
247
 
        context.translate(self.graph_x, self.graph_y)
248
 
    
249
 
        #bars
250
 
        for fact in self.facts:
251
 
            start_minutes = self._minutes_from_start(fact["start_time"])
252
 
            
253
 
            if fact["end_time"]:
254
 
                end_minutes = self._minutes_from_start(fact["end_time"])
255
 
            else:
256
 
                if fact["start_time"].date() > dt.date.today() - dt.timedelta(days=1):
257
 
                    end_minutes = self._minutes_from_start(dt.datetime.now())
258
 
                else:
259
 
                    end_minutes = start_minutes
260
 
            
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)
264
 
 
265
 
                    context.rectangle(round(start_minutes * pixels_in_minute),
266
 
                                      graph_y,
267
 
                                      round(end_minutes * pixels_in_minute - start_minutes * pixels_in_minute),
268
 
                                      graph_height - 1)
269
 
                    context.fill()
270
 
                    context.stroke()
271
 
 
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)
277
 
                    context.stroke()
278
 
 
279
 
        
280
 
        
281
 
        #time scale
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))
286
 
            
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)
291
 
                context.stroke()
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)
296
 
                context.stroke()
297
 
                
298
 
                
299
 
                
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")
306
 
                else:
307
 
                    label_minutes = label_time.strftime("%H<small><sup>%M</sup></small>")
308
 
 
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()
312
 
                
313
 
                context.move_to(i * pixels_in_minute + 2, graph_y2 - label_h - 8)                
314
 
 
315
 
                context.show_layout(self.layout)
316
 
        context.stroke()
317
 
        
318
 
        #highlight rectangle
319
 
        if self.highlight:
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)
322
 
 
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)
326
 
 
327
 
            self.fill_area(self.highlight_start,
328
 
                           graph_y,
329
 
                           self.highlight_end - self.highlight_start,
330
 
                           graph_height,
331
 
                           (rgb[0], rgb[1], rgb[2], 0.5))
332
 
            context.stroke()
333
 
 
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)
339
 
            context.stroke()
340
 
 
341
 
        context.restore()            
342
 
 
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)
346
 
        context.stroke()
347
 
        
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)
353
 
            
354
 
            self.scroll_to_range_start()