root/trunk/chavier/gui.py

Revision 163, 19.1 kB (checked in by lgs, 3 years ago)

Add support for stacked bar charts to chavier. See #27

Line 
1# Copyright(c) 2007-2009 by Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
2#
3# This file is part of Chavier.
4#
5# Chavier is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Chavier 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 Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public License
16# along with Chavier.  If not, see <http://www.gnu.org/licenses/>.
17
18import pygtk
19pygtk.require('2.0')
20import gtk
21
22from chavier.dialogs import (
23    TextInputDialog, PointDialog, OptionDialog, RandomGeneratorDialog,
24    AboutDialog, warning,
25    )
26
27
28class GUI(object):
29
30    def __init__(self, app):
31        self.app = app
32
33        self.chart = None
34        self.surface = None
35
36        self.main_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
37        self.main_window.connect('delete_event', self.delete_event)
38        self.main_window.connect('destroy', self.destroy)
39        self.main_window.set_default_size(640, 480)
40        self.main_window.set_title(u'Chavier')
41
42        vbox = gtk.VBox()
43        self.main_window.add(vbox)
44        vbox.show()
45
46        menubar, toolbar = self._create_ui_manager()
47
48        vbox.pack_start(menubar, False, False)
49        menubar.show()
50
51        vbox.pack_start(toolbar, False, False)
52        toolbar.show()
53
54        hpaned = gtk.HPaned()
55        vbox.pack_start(hpaned, True, True)
56        hpaned.show()
57
58        vpaned = gtk.VPaned()
59        hpaned.add1(vpaned)
60        vpaned.show()
61
62        block1 = self._create_sidebar_block(u'Data sets',
63                                            self._datasets_notebook_creator)
64        self._create_dataset("Dataset 1")
65        block1.set_size_request(-1, 200)
66
67        vpaned.add1(block1)
68        block1.show()
69
70        block2 = self._create_sidebar_block(u'Options',
71                                            self._options_treeview_creator)
72        vpaned.add2(block2)
73        block2.show()
74
75        self.drawing_area = gtk.DrawingArea()
76        self.drawing_area.connect('expose_event',
77                                  self.drawing_area_expose_event)
78        self.drawing_area.connect('size_allocate',
79                                  self.drawing_area_size_allocate_event)
80        hpaned.add2(self.drawing_area)
81        self.drawing_area.show()
82
83        self.main_window.show()
84
85    def _create_ui_manager(self):
86        self.uimanager = gtk.UIManager()
87        accel_group = self.uimanager.get_accel_group()
88        self.main_window.add_accel_group(accel_group)
89
90        action_group = gtk.ActionGroup('default')
91        action_group.add_actions([
92                ('file', None, '_File', None, 'File', None),
93                ('quit', gtk.STOCK_QUIT, None, None, 'Quit the program',
94                 self.quit),
95
96                ('edit', None, '_Edit', None, 'Edit', None),
97                ('add_dataset', gtk.STOCK_ADD, '_Add dataset',
98                 '<ctrl><alt>plus', 'Add another dataset', self.add_dataset),
99                ('remove_dataset', gtk.STOCK_REMOVE, '_Remove dataset',
100                 '<ctrl><alt>minus', 'Remove the current dataset',
101                 self.remove_dataset),
102                ('edit_dataset', gtk.STOCK_EDIT, '_Edit dataset name',
103                 '<ctrl><alt>e', 'Edit the name of the current dataset',
104                 self.edit_dataset),
105                ('add_point', gtk.STOCK_ADD, 'Add _point', '<ctrl>plus',
106                 'Add another point to the current dataset', self.add_point),
107                ('remove_point', gtk.STOCK_REMOVE, 'Remove p_oint',
108                 '<ctrl>minus',
109                 'Remove the current point of the current dataset',
110                 self.remove_point),
111                ('edit_point', gtk.STOCK_EDIT, 'Edit po_int', '<ctrl>e',
112                 'Edit the current point of the current dataset',
113                 self.edit_point),
114                ('edit_option', gtk.STOCK_EDIT, 'Edit op_tion', None,
115                 'Edit the current option',
116                 self.edit_option),
117
118                ('view', None, '_View', None, 'View', None),
119                ('refresh', gtk.STOCK_REFRESH, None, '<ctrl>r',
120                 'Update the chart', self.refresh),
121
122                ('tools', None, '_Tools', None, 'Tools', None),
123                ('random-points', gtk.STOCK_EXECUTE, '_Generate random points',
124                 '<ctrl>g', 'Generate random points',
125                 self.generate_random_points),
126                ('dump-chart-state', gtk.STOCK_CONVERT, '_Dump chart state',
127                 '<ctrl>d', 'Dump internal chart variables',
128                 self.dump_chart_state),
129                ('help', None, '_Help', None, 'Help', None),
130                ('about', gtk.STOCK_ABOUT, None, None, 'About this program',
131                 self.about),
132                ])
133        action_group.add_radio_actions([
134                ('verticalbar', None, '_Vertical bars', None,
135                 'Use vertical bars chart', self.app.VERTICAL_BAR_TYPE),
136                ('horizontalbar', None, '_Horizontal bars', None,
137                 'Use horizontal bars chart', self.app.HORIZONTAL_BAR_TYPE),
138                ('line', None, '_Line', None,
139                 'Use lines chart', self.app.LINE_TYPE),
140                ('pie', None, '_Pie', None,
141                 'Use pie chart', self.app.PIE_TYPE),
142                ('scatter', None, '_Scatter', None,
143                 'Use scatter chart', self.app.SCATTER_TYPE),
144                ('stackedverticalbar', None, '_Stacked Vertical bars', None,
145                 'Use stacked vertical bars chart',
146                 self.app.STACKED_VERTICAL_BAR_TYPE),
147                ('stackedhorizontalbar', None, '_Stacked Horizontal bars', None,
148                 'Use stacked horizontal bars chart',
149                 self.app.STACKED_HORIZONTAL_BAR_TYPE),
150                ], self.app.VERTICAL_BAR_TYPE, self.on_chart_type_change)
151        self.uimanager.insert_action_group(action_group, -1)
152
153        ui = """<ui>
154  <menubar name="MenuBar">
155    <menu action="file">
156      <menuitem action="quit"/>
157    </menu>
158    <menu action="edit">
159      <menuitem action="add_dataset"/>
160      <menuitem action="remove_dataset"/>
161      <menuitem action="edit_dataset"/>
162      <separator />
163      <menuitem action="add_point"/>
164      <menuitem action="remove_point"/>
165      <menuitem action="edit_point"/>
166      <separator />
167      <menuitem action="edit_option"/>
168    </menu>
169    <menu action="view">
170      <menuitem action="refresh"/>
171      <separator />
172      <menuitem action="verticalbar"/>
173      <menuitem action="horizontalbar"/>
174      <menuitem action="stackedverticalbar"/>
175      <menuitem action="stackedhorizontalbar"/>
176      <menuitem action="line"/>
177      <menuitem action="pie"/>
178      <menuitem action="scatter"/>
179    </menu>
180    <menu action="tools">
181      <menuitem action="random-points"/>
182      <menuitem action="dump-chart-state"/>
183    </menu>
184    <menu action="help">
185      <menuitem action="about"/>
186    </menu>
187  </menubar>
188  <toolbar name="ToolBar">
189    <toolitem action="quit"/>
190    <separator />
191    <toolitem action="add_dataset"/>
192    <toolitem action="remove_dataset"/>
193    <separator />
194    <toolitem action="add_point"/>
195    <toolitem action="remove_point"/>
196    <separator />
197    <toolitem action="refresh"/>
198  </toolbar>
199</ui>
200"""
201        self.uimanager.add_ui_from_string(ui)
202        self.uimanager.ensure_update()
203        menubar = self.uimanager.get_widget('/MenuBar')
204        toolbar = self.uimanager.get_widget('/ToolBar')
205
206        return menubar, toolbar
207
208    def _create_sidebar_block(self, title, child_widget_creator):
209        box = gtk.VBox(spacing=6)
210        box.set_border_width(6)
211        label = gtk.Label()
212        label.set_markup(u'<span size="large" weight="bold">%s</span>' % title)
213        label.set_alignment(0.0, 0.5)
214        box.pack_start(label, False, False)
215        label.show()
216
217        child_widget = child_widget_creator()
218        box.pack_start(child_widget, True, True)
219        child_widget.show()
220
221        return box
222
223    def _datasets_notebook_creator(self):
224        self.datasets_notebook = gtk.Notebook()
225        self.datasets_notebook.set_scrollable(True)
226        return self.datasets_notebook
227
228    def _dataset_treeview_creator(self):
229        store = gtk.ListStore(float, float)
230        treeview = gtk.TreeView(store)
231
232        column1 = gtk.TreeViewColumn('x', gtk.CellRendererText(), text=0)
233        treeview.append_column(column1)
234
235        column2 = gtk.TreeViewColumn('y', gtk.CellRendererText(), text=1)
236        treeview.append_column(column2)
237
238        treeview.connect('row-activated', self.dataset_treeview_row_activated)
239
240        scrolled_window = gtk.ScrolledWindow()
241        scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
242        scrolled_window.add(treeview)
243        treeview.show()
244
245        return scrolled_window
246
247    def _options_treeview_creator(self):
248        self.options_store = gtk.TreeStore(str, str, object)
249        options = self.app.get_default_options()
250        self._fill_options_store(options, None, self.app.OPTIONS_TYPES)
251
252        self.options_treeview = gtk.TreeView(self.options_store)
253
254        column1 = gtk.TreeViewColumn('Name', gtk.CellRendererText(), text=0)
255        self.options_treeview.append_column(column1)
256
257        column2 = gtk.TreeViewColumn('Value', gtk.CellRendererText(), text=1)
258        self.options_treeview.append_column(column2)
259
260        self.options_treeview.expand_all()
261
262        self.options_treeview.connect('row-activated',
263                                      self.options_treeview_row_activated)
264
265        scrolled_window = gtk.ScrolledWindow()
266        scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
267        scrolled_window.add(self.options_treeview)
268        self.options_treeview.show()
269
270        return scrolled_window
271
272    def _fill_options_store(self, options, parent_node, types):
273        for name, value in options.items():
274            value_type = types[name]
275            if isinstance(value, dict):
276                current_parent = self.options_store.append(parent_node,
277                                                           (name, None, None))
278                self._fill_options_store(value, current_parent, value_type)
279
280            else:
281                if value is not None:
282                    value = str(value)
283                self.options_store.append(parent_node,
284                                          (name, value, value_type))
285
286    def _get_current_dataset_tab(self):
287        current_tab = self.datasets_notebook.get_current_page()
288        if current_tab != -1:
289            return self.datasets_notebook.get_nth_page(current_tab)
290
291    def _create_dataset(self, name):
292        scrolled_window = self._dataset_treeview_creator()
293        scrolled_window.show()
294        label = gtk.Label(name)
295        self.datasets_notebook.append_page(scrolled_window, label)
296
297    def _get_datasets(self):
298        datasets = []
299        n_pages = self.datasets_notebook.get_n_pages()
300        for i in range(n_pages):
301            tab = self.datasets_notebook.get_nth_page(i)
302            label = self.datasets_notebook.get_tab_label(tab)
303            name = label.get_label()
304            treeview = tab.get_children()[0]
305            model = treeview.get_model()
306            points = [(x, y) for x, y in model]
307            if len(points) > 0:
308                datasets.append((name, points))
309        return datasets
310
311    def _get_chart_type(self):
312        action_group = self.uimanager.get_action_groups()[0]
313        action = action_group.get_action('verticalbar')
314        return action.get_current_value()
315
316    def _get_options(self, iter):
317        options = {}
318        while iter is not None:
319            name, value, value_type = self.options_store.get(iter, 0, 1, 2)
320            if value_type is None:
321                child = self.options_store.iter_children(iter)
322                options[name] = self._get_options(child)
323            else:
324                if value is not None:
325                    converter = str_converters[value_type]
326                    value = converter(value)
327                options[name] = value
328
329            iter = self.options_store.iter_next(iter)
330
331        return options
332
333    def _edit_point_internal(self, model, iter):
334        x, y = model.get(iter, 0, 1)
335
336        dialog = PointDialog(self.main_window, x, y)
337        response = dialog.run()
338        if response == gtk.RESPONSE_ACCEPT:
339            x, y = dialog.get_point()
340            model.set(iter, 0, x, 1, y)
341            self.refresh()
342        dialog.destroy()
343
344    def _edit_option_internal(self, model, iter):
345        name, value, value_type = model.get(iter, 0, 1, 2)
346        parents = []
347        parent = model.iter_parent(iter)
348        while parent is not None:
349            parents.append(model.get_value(parent, 0))
350            parent = model.iter_parent(parent)
351        parents.reverse()
352        parents.append(name)
353        label = u'.'.join(parents)
354
355        dialog = OptionDialog(self.main_window, label, value, value_type)
356        response = dialog.run()
357        if response == gtk.RESPONSE_ACCEPT:
358            new_value = dialog.get_value()
359            if new_value == "":
360                new_value = None
361            model.set_value(iter, 1, new_value)
362            self.refresh()
363        dialog.destroy()
364
365    def delete_event(self, widget, event, data=None):
366        return False
367
368    def destroy(self, widget, data=None):
369        gtk.main_quit()
370
371    def drawing_area_expose_event(self, widget, event, data=None):
372        if self.chart is None:
373            return
374
375        cr = widget.window.cairo_create()
376        cr.rectangle(event.area.x, event.area.y,
377                     event.area.width, event.area.height)
378        cr.clip()
379        cr.set_source_surface(self.chart.surface, 0, 0)
380        cr.paint()
381
382    def drawing_area_size_allocate_event(self, widget, event, data=None):
383        if self.chart is not None:
384            self.refresh()
385
386    def on_chart_type_change(self, action, current, data=None):
387        if self.chart is not None:
388            self.refresh()
389
390    def dataset_treeview_row_activated(self, treeview, path, view_column):
391        model = treeview.get_model()
392        iter = model.get_iter(path)
393        self._edit_point_internal(model, iter)
394
395    def options_treeview_row_activated(self, treeview, path, view_column):
396        model = treeview.get_model()
397        iter = model.get_iter(path)
398        self._edit_option_internal(model, iter)
399
400    def quit(self, action):
401        self.main_window.destroy()
402
403    def add_dataset(self, action):
404        n_pages = self.datasets_notebook.get_n_pages()
405        suggested_name = u'Dataset %d' % (n_pages + 1)
406        dialog = TextInputDialog(self.main_window, suggested_name)
407        response = dialog.run()
408        if response == gtk.RESPONSE_ACCEPT:
409            name = dialog.get_name()
410            self._create_dataset(name)
411            self.datasets_notebook.set_current_page(n_pages)
412        dialog.destroy()
413
414    def remove_dataset(self, action):
415        current_tab = self.datasets_notebook.get_current_page()
416        assert current_tab != -1
417
418        self.datasets_notebook.remove_page(current_tab)
419
420    def edit_dataset(self, action):
421        tab = self._get_current_dataset_tab()
422        assert tab is not None
423
424        label = self.datasets_notebook.get_tab_label(tab)
425        name = label.get_label()
426        dialog = TextInputDialog(self.main_window, name)
427        response = dialog.run()
428        if response == gtk.RESPONSE_ACCEPT:
429            name = dialog.get_name()
430            label.set_label(name)
431        dialog.destroy()
432
433    def add_point(self, action):
434        tab = self._get_current_dataset_tab()
435        assert tab is not None
436
437        treeview = tab.get_children()[0]
438        model = treeview.get_model()
439
440        dialog = PointDialog(self.main_window, len(model) * 1.0, 0.0)
441        response = dialog.run()
442        if response == gtk.RESPONSE_ACCEPT:
443            x, y = dialog.get_point()
444            model.append((x, y))
445            self.refresh()
446        dialog.destroy()
447
448    def remove_point(self, action):
449        tab = self._get_current_dataset_tab()
450        assert tab is not None
451
452        treeview = tab.get_children()[0]
453        selection = treeview.get_selection()
454        model, selected = selection.get_selected()
455        if selected is None:
456            warning(self.main_window, "You must select the point to remove")
457            return
458
459        model.remove(selected)
460        self.refresh()
461
462    def edit_point(self, action):
463        tab = self._get_current_dataset_tab()
464        assert tab is not None
465
466        treeview = tab.get_children()[0]
467        selection = treeview.get_selection()
468        model, selected = selection.get_selected()
469        if selected is None:
470            warning(self.main_window, "You must select the point to edit")
471            return
472
473        self._edit_point_internal(model, selected)
474
475    def edit_option(self, action):
476        selection = self.options_treeview.get_selection()
477        model, selected = selection.get_selected()
478        if selected is None:
479            warning(self.main_window, "You must select the option to edit")
480            return
481
482        self._edit_option_internal(model, selected)
483
484    def refresh(self, action=None):
485        datasets = self._get_datasets()
486        if datasets:
487            root = self.options_store.get_iter_first()
488            options = self._get_options(root)
489
490            chart_type = self._get_chart_type()
491            alloc = self.drawing_area.get_allocation()
492            self.chart = self.app.get_chart(datasets, options, chart_type,
493                                            alloc.width, alloc.height)
494            self.drawing_area.queue_draw()
495        else:
496            self.chart = None
497
498    def generate_random_points(self, action=None):
499        tab = self._get_current_dataset_tab()
500        assert tab is not None
501
502        treeview = tab.get_children()[0]
503        model = treeview.get_model()
504
505        dialog = RandomGeneratorDialog(self.main_window)
506        response = dialog.run()
507        if response == gtk.RESPONSE_ACCEPT:
508            points = dialog.generate_points()
509            for point in points:
510                model.append(point)
511            self.refresh()
512        dialog.destroy()
513
514    def dump_chart_state(self, action=None):
515        if self.chart is None:
516            return
517
518        alloc = self.drawing_area.get_allocation()
519
520        print 'CHART STATE'
521        print '-' * 70
522        print 'surface: %d x %d' % (alloc.width, alloc.height)
523        print 'area   :', self.chart.area
524        print
525        print 'minxval:', self.chart.minxval
526        print 'maxxval:', self.chart.maxxval
527        print 'xrange :', self.chart.xrange
528        print
529        print 'minyval:', self.chart.minyval
530        print 'maxyval:', self.chart.maxyval
531        print 'yrange :', self.chart.yrange
532
533    def about(self, action=None):
534        dialog = AboutDialog(self.main_window)
535        dialog.run()
536        dialog.destroy()
537
538    def run(self):
539        gtk.main()
540
541
542def str2bool(str):
543    if str.lower() == "true":
544        return True
545    else:
546        return False
547
548
549str_converters = {
550    str: str,
551    int: int,
552    float: float,
553    unicode: unicode,
554    bool: str2bool,
555}
Note: See TracBrowser for help on using the browser.