root/trunk/umit/inventory/TLBarDisplay.py @ 4479

Revision 4479, 17.1 kB (checked in by getxsick, 4 years ago)

dict.keys() is no more needed for iterate. fix for #260

Line 
1# Copyright (C) 2007 Adriano Monteiro Marques
2#
3# Authors: Guilherme Polo <ggpolo@gmail.com>
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18# USA
19
20import gtk
21import cairo
22import pango
23import gobject
24
25from umit.core.I18N import _
26
27from umit.inventory.TLBase import colors_from_file, gradient_colors_from_file
28from umit.inventory.TLBase import changes_list
29
30PI = 3.1415926535897931
31GOLDENRATIO = 1.618033989
32GOLDENRADIUS = GOLDENRATIO ** 4 # my magical number
33
34EMPTY_COLOR =  ((0.3, 0.3, 0.3), (0, 0, 0))
35NOSELECTION = _("No Selection")
36NOCHANGES = _("No Changes")
37STATISTICS = _("Changes Statistics")
38
39class TLSelected(gtk.Widget):
40    """
41    A widget that shows statistics based on TLGraph selection.
42    """
43
44    def __init__(self, connector, datagrabber):
45        gtk.Widget.__init__(self)
46
47        self.connector = connector
48        self.datagrabber = datagrabber
49
50        self.title = self.create_pango_layout('')
51        self.title.set_alignment(pango.ALIGN_CENTER)
52        self.second_title = self.create_pango_layout('')
53        self.second_title.set_alignment(pango.ALIGN_CENTER)
54
55        # start with empty selection/colors
56        self.percent, self.currselection = self.emptygraph()
57        not_used, self.newselection = self.emptygraph()
58        self.currcolor = (list(EMPTY_COLOR[0]), list(EMPTY_COLOR[1]))
59        self.newcolor = EMPTY_COLOR
60        self.noselection = True
61        self.total = 0 # number of changes in selection
62        self.multiplier = { }
63
64        # animation effect control
65        self.in_anim = False # bar drawing effect
66        self.in_transition = False # color transition effect
67        self.piece_cut = 12.0 # how many divisions will be done in each bar
68                              # graph to do animation effect.
69        self.trans_progress = 0.04 # color transition increments/decrements
70
71        # bar drawing constants
72        self.bar_draw = { }
73
74        self.connector.connect('selection-update', self._update_graph)
75
76
77    def get_selection_data(self):
78        """
79        Get current selection data.
80        """
81        return self.__seldata
82
83
84    def set_selection_data(self, data):
85        """
86        Sets a dict containing changes for each category for current selection.
87        """
88        self.__seldata = data
89
90        if self.flags() & gtk.REALIZED:
91            self.queue_draw()
92
93
94    def emptygraph(self):
95        """
96        Return a dict where each category has 0 changes.
97        """
98        empty = { }
99        percent = { }
100
101        for item in changes_list:
102            empty[item] = 0
103            percent[item] = 0.0
104
105        return empty, percent
106
107
108    def colors_diff(self):
109        """
110        Check if colors are different.
111        """
112        for indx, color in enumerate(self.currcolor):
113            for c_indx, cvalue in enumerate(color):
114                nvalue = self.newcolor[indx][c_indx]
115
116                if nvalue != cvalue:
117                    return True
118
119        return False
120
121
122    def _update_graph(self, obj, range_start, range_end):
123        """
124        New selection, grab changes by category for selection timerange and
125        do everything needed to draw new selection.
126        """
127        if range_start is None and range_end is None: # no selection
128            self.noselection = True
129
130            self.total = self._changes_sum_in_selection(self.currselection)
131            self.multiplier = { }
132            for key, value in self.emptygraph()[0].items():
133                self.multiplier[key] = abs(value - self.currselection[key]) / \
134                                       self.piece_cut
135
136            self.newcolor = EMPTY_COLOR
137            self.percent, self.newselection = self.emptygraph()
138            return
139
140        self.noselection = False
141
142        categories = self.datagrabber.get_categories()
143        data = { }
144
145        # grab changes by category in current selection
146        for key in categories:
147            c = self.datagrabber.timerange_changes_count_generic(range_start,
148                range_end, key, self.datagrabber.inventory,
149                self.datagrabber.hostaddr)
150            data[categories[key][1]] = c
151
152        data_keys = data.keys()
153        for item in changes_list:
154            if item not in data_keys:
155                data[item] = 0
156
157        # changes sum
158        self.total = self._changes_sum_in_selection(data)
159
160        # calculate bars height based on their amount of changes
161        bars_height = { }
162
163        for key, value in data.items():
164            if self.total == 0:
165                bars_height[key] = 0
166            else:
167                bars_height[key] = ((value * self.bar_draw["bars_y_area"]) / \
168                                    float(self.total))
169
170        if not self.total: # range without changes was selected
171            self.total = self._changes_sum_in_selection(self.currselection)
172
173        self.percent = { }
174        self.multiplier = { }
175
176        for key, value in data.items():
177            if not self.total: # present and previous selection have no changes
178                self.percent[key] = 0.0
179            else:
180                self.percent[key] = (float(data[key])/self.total) * 100
181
182            # calculate increment needed to complete this bar animation
183            self.multiplier[key] = abs(bars_height[key] - \
184                                      self.currselection[key]) / self.piece_cut
185
186        # get new color
187        more_changes = self._category_with_more_changes(bars_height)
188        if more_changes.values()[0] == 0: # no changes in current selection
189            newcolor = EMPTY_COLOR
190            color_from = newcolor[0]
191            color_to = newcolor[1]
192        else:
193            color_name = more_changes.keys()[0]
194            color_from = colors_from_file()[color_name]
195            color_to = gradient_colors_from_file()[color_name]
196
197        self.newcolor = (color_from, color_to)
198        self.newselection = bars_height # set new selection, this will start
199                                        # animation effect.
200
201
202    def _changes_sum_in_selection(self, datad):
203        """
204        Returns changes sum in datad.
205        """
206        return sum(v for v in datad.values())
207
208
209    def _category_with_more_changes(self, datad):
210        """
211        Return {category: value} with more changes.
212        """
213        max_v = -1
214        max_d = {"invalid": max_v}
215
216        for key, value in datad.items():
217            if key == "nothing": # discarding this category for now
218                continue
219
220            if value > max_v:
221                max_d = {key: value}
222                max_v = value
223
224        if max_v == 0:
225            return {"nothing": datad["nothing"]}
226
227        return max_d
228
229
230    def _write_text(self, cr, text, bold):
231        """
232        Write centered text on widget.
233        """
234        cr.save()
235
236        if bold:
237            height = 1
238            self.title.set_markup("<b>%s</b>" % text)
239            cr.update_layout(self.title)
240            pango_layout = self.title
241
242        else:
243            width, height = self.title.get_size()
244            height = (height / pango.SCALE) + 2
245            self.second_title.set_text(text)
246            cr.update_layout(self.second_title)
247            pango_layout = self.second_title
248
249        cr.move_to(1, height + 1)
250        cr.set_source_rgb(0, 0, 0)
251        cr.show_layout(pango_layout)
252
253        cr.move_to(0, height)
254        cr.set_source_rgb(1, 1, 1)
255        cr.show_layout(pango_layout)
256
257        cr.restore()
258
259
260    def _write_title(self, cr):
261        """
262        Tells _write_text to write widget title.
263        """
264        if self.noselection:
265            self._write_text(cr, NOSELECTION, True)
266            self._write_text(cr, NOCHANGES, False)
267        else:
268            self._write_text(cr, self.datagrabber.title_by_graphmode(True),
269                True)
270            self._write_text(cr, STATISTICS, False)
271
272
273    def _draw_base(self, cr):
274        """
275        Draws __________
276        """
277        cr.save()
278
279        cr.move_to(self.bar_draw["start_x"], self.bar_draw["start_y"])
280        cr.line_to(self.bar_draw["end_x"], self.bar_draw["start_y"])
281
282        cr.set_source_rgba(1, 1, 1, 0.3)
283        cr.stroke()
284
285        cr.restore()
286
287
288    def _paint_background(self, cr):
289        """
290        Draw and paint widget background with a vertical gradient.
291        """
292        cr.save()
293
294        cr.new_path()
295
296        # draws a round rectangle, pure Art ;)
297        cr.arc(GOLDENRADIUS, self.allocation[3] - GOLDENRADIUS,
298               GOLDENRADIUS, PI / 2.0, PI)
299        cr.arc(GOLDENRADIUS, GOLDENRADIUS,
300               GOLDENRADIUS, PI, - PI / 2.0)
301        cr.arc(self.allocation[2] - GOLDENRADIUS, GOLDENRADIUS,
302               GOLDENRADIUS, PI + (PI / 2.0), 0)
303        cr.arc(self.allocation[2] - GOLDENRADIUS,
304               self.allocation[3] - GOLDENRADIUS, GOLDENRADIUS, 0, PI/2.0)
305
306        cr.close_path()
307
308        pat = cairo.LinearGradient(0, 0, 0, self.allocation[3])
309
310        color_from = self.currcolor[0]
311        color_to = self.currcolor[1]
312
313        pat.add_color_stop_rgb(0, *color_from)
314        pat.add_color_stop_rgb(1, *color_to)
315
316        cr.set_source(pat)
317        cr.fill()
318
319        cr.restore()
320
321
322    def _draw_bar(self, cr, value, color, x_pos):
323        """
324        Draws a vertical graph bar.
325        """
326        cr.save()
327
328        cr.rectangle(self.bar_draw["start_x"] + x_pos,
329                     self.bar_draw["start_y"], self.bar_draw["bar_width"],
330                     - value - 1)
331
332        cr.set_source_rgb(*color)
333        cr.fill()
334
335        cr.restore()
336
337        return self.bar_draw["start_y"] - value
338
339
340    def _write_bar_text(self, cr, text, bar_height, x_pos):
341        """
342        Write text above or inside bar.
343        """
344        if self.noselection:
345            # nothing to write
346            return
347
348        cr.save()
349        cr.select_font_face("Sans", cairo.FONT_SLANT_NORMAL,
350            cairo.FONT_WEIGHT_BOLD)
351
352        _, _, width, height, _, _ = cr.text_extents(text)
353        center = (self.bar_draw["bar_width"] / 2.0) + (height / 2.0)
354
355        if self.bar_draw["start_y"] - bar_height != 0:
356            if self.bar_draw["start_y"] - bar_height - width - 3 == 0:
357                h_pos = bar_height + width
358            elif self.bar_draw["start_y"] - bar_height - width - 3 >= 3:
359                h_pos = bar_height + width + 3
360            else:
361                h_pos = self.bar_draw["start_y"] - 3
362        else:
363            h_pos = bar_height - 3
364
365        cr.move_to(self.bar_draw["start_x"] + x_pos + center + 1, h_pos)
366
367        cr.rotate(- PI/2)
368        cr.set_source_rgb(0, 0, 0)
369        cr.show_text(text)
370        cr.rotate(PI/2)
371
372        cr.move_to(self.bar_draw["start_x"] + x_pos + center, h_pos - 1)
373
374        cr.rotate(- PI/2)
375        cr.set_source_rgb(1, 1, 1)
376        cr.show_text(text)
377        cr.rotate(PI/2)
378
379        cr.restore()
380
381
382    def _color_transition(self):
383        """
384        Color transition.
385        """
386        continue_trans = False # if this remains False, transition is complete
387
388        # check values to perform color transition
389        for indx, color in enumerate(self.currcolor):
390            for c_indx, cvalue in enumerate(color):
391                nvalue = self.newcolor[indx][c_indx]
392
393                # doing this so in some cases (float point with many digits)
394                # we don't stay here forever.
395                if abs(nvalue - cvalue) <= self.trans_progress and \
396                    nvalue != cvalue:
397
398                    self.currcolor[indx][c_indx] = nvalue
399                    continue_trans = True
400                    continue
401
402                if cvalue > nvalue:
403                    self.currcolor[indx][c_indx] = cvalue - self.trans_progress
404
405                    continue_trans = True
406
407                elif cvalue < nvalue:
408                    self.currcolor[indx][c_indx] = cvalue + self.trans_progress
409
410                    continue_trans = True
411
412        if not self.in_anim:
413            # This timer starts right after bar drawing starts, so
414            # queue_draw is already being called by _update_bars().
415            # But, this may end after _update_bars finishes, so it
416            # is needed to call queue_draw now.
417            self.queue_draw()
418
419        if not continue_trans:
420            self.in_transition = False
421            return False
422
423        return True
424
425
426    def _update_bars(self):
427        """
428        Increment/decrement bar sizes.
429        """
430        continue_anim = False
431
432        # increment/decrement bars height
433        for key, value in self.newselection.items():
434            if value > self.currselection[key]:
435                self.currselection[key] += self.multiplier[key]
436                continue_anim = True
437
438            elif value < self.currselection[key]:
439                self.currselection[key] -= self.multiplier[key]
440                continue_anim = True
441
442            # doing this so in some cases (float point with many digits)
443            # we don't stay here forever.
444            if abs(self.currselection[key] - self.newselection[key]) <= 0.5 \
445                and self.currselection[key] != self.newselection[key]:
446
447                self.currselection[key] = self.newselection[key]
448                continue_anim = True
449
450        if not continue_anim:
451            self.in_anim = False
452            return False
453
454        self.queue_draw()
455        return True
456
457
458    def _pre_bar_draw(self, cr):
459        """
460        Calculate where each bar will be draw, and then call _draw_bar to
461        draw it.
462        """
463        for multiplier, item in enumerate(changes_list):
464            curr_x_pos = self.bar_draw["bars_distance"] + \
465                         (self.bar_draw["bars_distance"] * multiplier) + \
466                         (self.bar_draw["bar_width"] * multiplier)
467
468            bar_height = self._draw_bar(cr, self.currselection[item],
469                colors_from_file()[item], curr_x_pos)
470
471            self._write_bar_text(cr, "%.1f%%" % self.percent[item], bar_height,
472                curr_x_pos)
473
474
475    def _calculate_bar_sizes(self):
476        """
477        Calculates everything needed during bars drawing.
478        """
479        self.bar_draw["start_x"] = 12
480        self.bar_draw["end_x"] = self.allocation[2] - 12
481        self.bar_draw["start_y"] = self.allocation[3] - 6
482        self.bar_draw["bars_distance"] = 5
483        self.bar_draw["bars_y_area"] = self.bar_draw["start_y"] - 60
484
485        bars_space = self.allocation[2] - (2 * self.bar_draw["start_x"]) - \
486                     self.bar_draw["bars_distance"]
487        bar_width = (bars_space / float(len(changes_list))) - \
488                    self.bar_draw["bars_distance"]
489
490        self.bar_draw["bar_width"] = bar_width
491
492
493    def do_realize(self):
494        """
495        Realizes widget with necessary event_mask and calculate initial
496        bar drawing constants.
497        """
498        self.set_flags(self.flags() | gtk.REALIZED | gtk.CAN_FOCUS)
499
500        self.window = gtk.gdk.Window(self.get_parent_window(),
501            width=self.allocation.width, height=self.allocation.height,
502            window_type=gtk.gdk.WINDOW_CHILD, wclass=gtk.gdk.INPUT_OUTPUT,
503            event_mask=self.get_events() | gtk.gdk.EXPOSURE_MASK)
504
505        self.window.set_user_data(self)
506        self.style.attach(self.window)
507        self.style.set_background(self.window, gtk.STATE_NORMAL)
508        self.window.move_resize(*self.allocation)
509
510        self._calculate_bar_sizes()
511
512
513    def do_unrealize(self):
514        """
515        Widget cleanup.
516        """
517        self.window.set_user_data(None)
518
519
520    def do_size_request(self, requisition):
521        """
522        Sets "optimal" minimal size for widget.
523        """
524        requisition.width = 190
525        requisition.height = 130
526
527
528    def do_size_allocate(self, allocation):
529        """
530        Handles size allocation and recalculate bar drawing constants.
531        """
532        self.allocation = allocation
533
534        if self.flags() & gtk.REALIZED:
535            self.window.move_resize(*self.allocation)
536
537        self._calculate_bar_sizes()
538
539        self.title.set_width(allocation[2] * pango.SCALE)
540        self.second_title.set_width(allocation[2] * pango.SCALE)
541
542
543    def do_expose_event(self, event):
544        """
545        Controls widget drawing.
546        """
547        cr = self.window.cairo_create()
548        cr.rectangle(*event.area)
549        cr.clip()
550
551        if self.currselection != self.newselection and not self.in_anim:
552            self.in_anim = True
553            gobject.timeout_add(20, self._update_bars)
554
555        if self.colors_diff() and not self.in_transition:
556            self.in_transition = True
557            gobject.timeout_add(20, self._color_transition)
558
559        self._paint_background(cr)
560        self._write_title(cr)
561        self._draw_base(cr)
562        self._pre_bar_draw(cr)
563
564
565    # Properties
566    newselection = property(get_selection_data, set_selection_data)
567
568
569gobject.type_register(TLSelected)
Note: See TracBrowser for help on using the browser.