columns: cleanup default size calculation
[platform/upstream/gstreamer.git] / debug-viewer / GstDebugViewer / GUI / columns.py
1 # -*- coding: utf-8; mode: python; -*-
2 #
3 #  GStreamer Debug Viewer - View and analyze GStreamer debug log files
4 #
5 #  Copyright (C) 2007 RenĂ© Stadler <mail@renestadler.de>
6 #
7 #  This program is free software; you can redistribute it and/or modify it
8 #  under the terms of the GNU General Public License as published by the Free
9 #  Software Foundation; either version 3 of the License, or (at your option)
10 #  any later version.
11 #
12 #  This program is distributed in the hope that it will be useful, but WITHOUT
13 #  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 #  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
15 #  more details.
16 #
17 #  You should have received a copy of the GNU General Public License along with
18 #  this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 """GStreamer Debug Viewer GUI module."""
21
22 def _ (s):
23     return s
24
25 import logging
26
27 import gtk
28
29 from GstDebugViewer import Common, Data
30 from GstDebugViewer.GUI.colors import LevelColorThemeTango
31 from GstDebugViewer.GUI.models import LazyLogModel, LogModelBase
32
33 # Sync with gst-inspector!
34 class Column (object):
35
36     """A single list view column, managed by a ColumnManager instance."""
37
38     name = None
39     id = None
40     label_header = None
41     get_modify_func = None
42     get_data_func = None
43     get_row_data_func = None
44     get_sort_func = None
45
46     def __init__ (self):
47
48         view_column = gtk.TreeViewColumn (self.label_header)
49         view_column.props.reorderable = True
50
51         self.view_column = view_column
52
53 class SizedColumn (Column):
54
55     default_size = None
56
57     def compute_default_size (self):
58
59         return None
60
61 # Sync with gst-inspector?
62 class TextColumn (SizedColumn):
63
64     font_family = None
65
66     def __init__ (self):
67
68         Column.__init__ (self)
69
70         column = self.view_column
71         cell = gtk.CellRendererText ()
72         column.pack_start (cell)
73
74         cell.props.yalign = 0.
75         cell.props.ypad = 0
76
77         if self.font_family:
78             cell.props.family = self.font_family
79             cell.props.family_set = True
80
81         if self.get_data_func:
82             data_func = self.get_data_func ()
83             assert data_func
84             id_ = self.id
85             if id_ is not None:
86                 def cell_data_func (column, cell, model, tree_iter):
87                     data_func (cell.props, model.get_value (tree_iter, id_), model.get_path (tree_iter))
88             else:
89                 cell_data_func = data_func
90             column.set_cell_data_func (cell, cell_data_func)
91         elif self.get_row_data_func:
92             data_func = self.get_row_data_func ()
93             assert data_func
94             def cell_data_func (column, cell, model, tree_iter):
95                 data_func (cell.props, model[tree_iter])
96             column.set_cell_data_func (cell, cell_data_func)
97         elif not self.get_modify_func:
98             column.add_attribute (cell, "text", self.id)
99         else:
100             self.update_modify_func (column, cell)
101
102         column.props.resizable = True
103
104     def update_modify_func (self, column, cell):
105
106         modify_func = self.get_modify_func ()
107         id_ = self.id
108         def cell_data_func (column, cell, model, tree_iter):
109             cell.props.text = modify_func (model.get (tree_iter, id_)[0])
110         column.set_cell_data_func (cell, cell_data_func)
111
112     def compute_default_size (self):
113
114         values = self.get_values_for_size ()
115         if not values:
116             return SizedColumn.compute_default_size (self)
117
118         cell = self.view_column.get_cell_renderers ()[0]
119
120         if self.get_modify_func is not None:
121             format = self.get_modify_func ()
122         else:
123             def identity (x):
124                 return x
125             format = identity
126         max_width = 0
127         for value in values:
128             cell.props.text = format (value)
129             rect, x, y, w, h = self.view_column.cell_get_size ()
130             max_width = max (max_width, w)
131
132         return max_width
133
134     def get_values_for_size (self):
135
136         return ()
137
138 class TimeColumn (TextColumn):
139
140     name = "time"
141     label_header = _("Time")
142     id = LazyLogModel.COL_TIME
143     font_family = "monospace"
144
145     def __init__ (self, *a, **kw):
146
147         self.base_time = 0
148
149         TextColumn.__init__ (self, *a, **kw)
150
151     def get_modify_func (self):
152
153         if self.base_time:
154             time_diff_args = Data.time_diff_args
155             base_time = self.base_time
156             def format_time (value):
157                 # TODO: Hard coded to omit trailing zeroes, see below.
158                 return time_diff_args (value - base_time)[:-3]
159         else:
160             time_args = Data.time_args
161             def format_time (value):
162                 # TODO: This is hard coded to omit hours as well as the last 3
163                 # digits at the end, since current gst uses g_get_current_time,
164                 # which has microsecond precision only.
165                 return time_args (value)[2:-3]
166
167         return format_time
168
169     def get_values_for_size (self):
170
171         values = [0]
172
173         return values
174
175     def set_base_time (self, base_time):
176
177         self.base_time = base_time
178
179         column = self.view_column
180         cell = column.get_cell_renderers ()[0]
181         self.update_modify_func (column, cell)
182
183 class LevelColumn (TextColumn):
184
185     name = "level"
186     label_header = _("L")
187     id = LazyLogModel.COL_LEVEL
188
189     def __init__ (self):
190
191         TextColumn.__init__ (self)
192
193         cell = self.view_column.get_cell_renderers ()[0]
194         cell.props.xalign = .5
195
196     @staticmethod
197     def get_modify_func ():
198
199         def format_level (value):
200             return value.name[0]
201
202         return format_level
203
204     @staticmethod
205     def get_data_func ():
206
207         theme = LevelColorThemeTango ()
208         colors = dict ((level, tuple ((c.gdk_color ()
209                                        for c in theme.colors[level])),)
210                        for level in Data.debug_levels
211                        if level != Data.debug_level_none)
212         def level_data_func (cell_props, level, path):
213             cell_props.text = level.name[0]
214             if level in colors:
215                 cell_colors = colors[level]
216             else:
217                 cell_colors = (None, None, None,)
218             cell_props.foreground_gdk = cell_colors[0]
219             cell_props.background_gdk = cell_colors[1]
220
221         return level_data_func
222
223     def get_values_for_size (self):
224
225         values = [Data.debug_level_log, Data.debug_level_debug,
226                   Data.debug_level_info, Data.debug_level_warning,
227                   Data.debug_level_error]
228
229         return values
230
231 class PidColumn (TextColumn):
232
233     name = "pid"
234     label_header = _("PID")
235     id = LazyLogModel.COL_PID
236     font_family = "monospace"
237
238     @staticmethod
239     def get_modify_func ():
240
241         return str
242
243     def get_values_for_size (self):
244
245         return ["999999"]
246
247 class ThreadColumn (TextColumn):
248
249     name = "thread"
250     label_header = _("Thread")
251     id = LazyLogModel.COL_THREAD
252     font_family = "monospace"
253
254     @staticmethod
255     def get_modify_func ():
256
257         def format_thread (value):
258             return "0x%07x" % (value,)
259
260         return format_thread
261
262     def get_values_for_size (self):
263
264         return [int ("ffffff", 16)]
265
266 class CategoryColumn (TextColumn):
267
268     name = "category"
269     label_header = _("Category")
270     id = LazyLogModel.COL_CATEGORY
271
272     def get_values_for_size (self):
273
274         return ["GST_LONG_CATEGORY", "somelongelement"]
275
276 class CodeColumn (TextColumn):
277
278     name = "code"
279     label_header = _("Code")
280     id = None
281
282     @staticmethod
283     def get_data_func ():
284
285         filename_id = LogModelBase.COL_FILENAME
286         line_number_id = LogModelBase.COL_LINE_NUMBER
287         def filename_data_func (column, cell, model, tree_iter):
288             args = model.get (tree_iter, filename_id, line_number_id)
289             cell.props.text = "%s:%i" % args
290
291         return filename_data_func
292
293     def get_values_for_size (self):
294
295         return ["gstsomefilename.c:1234"]
296
297 class FunctionColumn (TextColumn):
298
299     name = "function"
300     label_header = _("Function")
301     id = LazyLogModel.COL_FUNCTION
302
303     def get_values_for_size (self):
304
305         return ["gst_this_should_be_enough"]
306
307 class ObjectColumn (TextColumn):
308
309     name = "object"
310     label_header = _("Object")
311     id = LazyLogModel.COL_OBJECT
312
313     def get_values_for_size (self):
314
315         return ["longobjectname00"]
316
317 class MessageColumn (TextColumn):
318
319     name = "message"
320     label_header = _("Message")
321     id = LazyLogModel.COL_MESSAGE
322
323     def __init__ (self, *a, **kw):
324
325         self.highlighters = {}
326
327         TextColumn.__init__ (self, *a, **kw)
328
329     def get_row_data_func (self):
330
331         from pango import AttrList, AttrBackground, AttrForeground
332         highlighters = self.highlighters
333         id_ = self.id
334
335         # FIXME: This should be none; need to investigate
336         # `cellrenderertext.props.attributes = None' failure (param conversion
337         # error like `treeview.props.model = None').
338         no_attrs = AttrList ()
339
340         def message_data_func (props, row):
341
342             props.text = row[id_]
343             if not highlighters:
344                 props.attributes = no_attrs
345             for highlighter in highlighters.values ():
346                 ranges = highlighter (row)
347                 if not ranges:
348                     props.attributes = no_attrs
349                 else:
350                     attrlist = AttrList ()
351                     for start, end in ranges:
352                         attrlist.insert (AttrBackground (0, 0, 65535, start, end))
353                         attrlist.insert (AttrForeground (65535, 65535, 65535, start, end))
354                     props.attributes = attrlist
355
356         return message_data_func
357
358     def get_values_for_size (self):
359
360         values = ["Just some good minimum size"]
361
362         return values
363
364 class ColumnManager (Common.GUI.Manager):
365
366     column_classes = ()
367
368     @classmethod
369     def iter_item_classes (cls):
370
371         return iter (cls.column_classes)
372
373     def __init__ (self):
374
375         self.view = None
376         self.actions = None
377         self.zoom = 1.0
378         self.__columns_changed_id = None
379         self.columns = []
380         self.column_order = list (self.column_classes)
381
382         self.action_group = gtk.ActionGroup ("ColumnActions")
383
384         def make_entry (col_class):
385             return ("show-%s-column" % (col_class.name,),
386                     None,
387                     col_class.label_header,
388                     None,
389                     None,
390                     None,
391                     True,)
392
393         entries = [make_entry (cls) for cls in self.column_classes]
394         self.action_group.add_toggle_actions (entries)
395
396     def iter_items (self):
397
398         return iter (self.columns)
399
400     def attach (self):
401
402         for col_class in self.column_classes:
403             action = self.get_toggle_action (col_class)
404             if action.props.active:
405                 self._add_column (col_class ())
406             action.connect ("toggled",
407                             self.__handle_show_column_action_toggled,
408                             col_class.name)
409
410         self.__columns_changed_id = self.view.connect ("columns-changed",
411                                                        self.__handle_view_columns_changed)
412
413     def detach (self):
414
415         if self.__columns_changed_id is not None:
416             self.view.disconnect (self.__columns_changed_id)
417             self.__columns_changed_id = None
418
419     def attach_sort (self):
420
421         sort_model = self.view.get_model ()
422
423         # Inform the sorted tree model of any custom sorting functions.
424         for col_class in self.column_classes:
425             if col_class.get_sort_func:
426                 sort_func = col_class.get_sort_func ()
427                 sort_model.set_sort_func (col_class.id, sort_func)
428
429     def enable_sort (self):
430
431         sort_model = self.view.get_model ()
432
433         if sort_model:
434             self.logger.debug ("activating sort")
435             sort_model.set_sort_column_id (*self.default_sort)
436             self.default_sort = None
437         else:
438             self.logger.debug ("not activating sort (no model set)")
439
440     def disable_sort (self):
441
442         self.logger.debug ("deactivating sort")
443
444         sort_model = self.view.get_model ()
445
446         self.default_sort = tree_sortable_get_sort_column_id (sort_model)
447
448         sort_model.set_sort_column_id (TREE_SORTABLE_UNSORTED_COLUMN_ID,
449                                        gtk.SORT_ASCENDING)
450
451     def set_zoom (self, scale):
452
453         for column in self.columns:
454             cell = column.view_column.get_cell_renderers ()[0]
455             cell.props.scale = scale
456             column.view_column.queue_resize ()
457
458         self.zoom = scale
459
460     def get_toggle_action (self, column_class):
461
462         action_name = "show-%s-column" % (column_class.name,)
463         return self.action_group.get_action (action_name)
464
465     def get_initial_column_order (self):
466
467         return tuple (self.column_classes)
468
469     def _add_column (self, column):
470
471         name = column.name
472         pos = self.__get_column_insert_position (column)
473
474         if self.view.props.fixed_height_mode:
475             column.view_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
476
477         cell = column.view_column.get_cell_renderers ()[0]
478         cell.props.scale = self.zoom
479
480         self.columns.insert (pos, column)
481         self.view.insert_column (column.view_column, pos)
482
483     def _remove_column (self, column):
484
485         self.columns.remove (column)
486         self.view.remove_column (column.view_column)
487
488     def __get_column_insert_position (self, column):
489
490         col_class = self.find_item_class (name = column.name)
491         pos = self.column_order.index (col_class)
492         before = self.column_order[:pos]
493         shown_names = [col.name for col in self.columns]
494         for col_class in before:
495             if not col_class.name in shown_names:
496                 pos -= 1
497         return pos
498
499     def __iter_next_hidden (self, column_class):
500
501         pos = self.column_order.index (column_class)
502         rest = self.column_order[pos + 1:]
503         for next_class in rest:
504             try:
505                 self.find_item (name = next_class.name)
506             except KeyError:
507                 # No instance -- the column is hidden.
508                 yield next_class
509             else:
510                 break
511
512     def __handle_show_column_action_toggled (self, toggle_action, name):
513
514         if toggle_action.props.active:
515             try:
516                 # This should fail.
517                 column = self.find_item (name = name)
518             except KeyError:
519                 col_class = self.find_item_class (name = name)
520                 self._add_column (col_class ())
521             else:
522                 # Out of sync for some reason.
523                 return
524         else:
525             try:
526                 column = self.find_item (name = name)
527             except KeyError:
528                 # Out of sync for some reason.
529                 return
530             else:
531                 self._remove_column (column)
532
533     def __handle_view_columns_changed (self, element_view):
534
535         view_columns = element_view.get_columns ()
536         new_visible = [self.find_item (view_column = column)
537                        for column in view_columns]
538
539         # We only care about reordering here.
540         if len (new_visible) != len (self.columns):
541             return
542
543         if new_visible != self.columns:
544
545             new_order = []
546             for column in new_visible:
547                 col_class = self.find_item_class (name = column.name)
548                 new_order.append (col_class)
549                 new_order.extend (self.__iter_next_hidden (col_class))
550
551             names = (column.name for column in new_visible)
552             self.logger.debug ("visible columns reordered: %s",
553                                ", ".join (names))
554
555             self.columns[:] = new_visible
556             self.column_order[:] = new_order
557
558 class ViewColumnManager (ColumnManager):
559
560     column_classes = (TimeColumn, LevelColumn, PidColumn, ThreadColumn, CategoryColumn,
561                       CodeColumn, FunctionColumn, ObjectColumn, MessageColumn,)
562
563     def __init__ (self, state):
564
565         ColumnManager.__init__ (self)
566
567         self.logger = logging.getLogger ("ui.columns")
568
569         self.state = state
570
571     def attach (self, view):
572
573         self.view = view
574         view.connect ("notify::model", self.__handle_notify_model)
575
576         order = self.state.column_order
577         if len (order) == len (self.column_classes):
578             self.column_order[:] = order
579
580         visible = self.state.columns_visible
581         if not visible:
582             visible = self.column_classes
583         for col_class in self.column_classes:
584             action = self.get_toggle_action (col_class)
585             action.props.active = (col_class in visible)
586
587         ColumnManager.attach (self)
588
589         self.columns_sized = False
590
591     def detach (self):
592
593         self.state.column_order = self.column_order
594         self.state.columns_visible = self.columns
595
596         return ColumnManager.detach (self)
597
598     def set_zoom (self, scale):
599
600         ColumnManager.set_zoom (self, scale)
601
602         if self.view is None:
603             return
604
605         # Timestamp and log level columns are pretty much fixed size, so resize
606         # them back to default on zoom change:
607         for column in self.columns:
608             if column.name in (TimeColumn.name,
609                                LevelColumn.name):
610                 self.size_column (column)
611
612     def size_column (self, column):
613
614         if column.default_size is None:
615             default_size = column.compute_default_size ()
616         else:
617             default_size = column.default_size
618         # FIXME: Abstract away fixed size setting in Column class!
619         if default_size is None:
620             # Dummy fallback:
621             column.view_column.props.fixed_width = 50
622             self.logger.warning ("%s column does not implement default size", column.name)
623         else:
624             column.view_column.props.fixed_width = default_size
625
626     def _add_column (self, column):
627
628         result = ColumnManager._add_column (self, column)
629         self.size_column (column)
630         return result
631
632     def _remove_column (self, column):
633
634         column.default_size = column.view_column.props.fixed_width
635         return ColumnManager._remove_column (self, column)
636
637     def __handle_notify_model (self, view, gparam):
638
639         if self.columns_sized:
640             # Already sized.
641             return
642         model = self.view.get_model ()
643         if model is None:
644             return
645         self.logger.debug ("model changed, sizing columns")
646         for column in self.iter_items ():
647             self.size_column (column)
648         self.columns_sized = True
649
650 class WrappingMessageColumn (MessageColumn):
651
652     def wrap_to_width (self, width):
653
654         col = self.view_column
655         col.props.max_width = width
656         col.get_cell_renderers ()[0].props.wrap_width = width
657         col.queue_resize ()
658
659 class LineViewColumnManager (ColumnManager):
660
661     column_classes = (TimeColumn, WrappingMessageColumn,)
662
663     def __init__ (self):
664
665         ColumnManager.__init__ (self)
666
667     def attach (self, window):
668
669         self.__size_update = None
670
671         self.view = window.widgets.line_view
672         self.view.set_size_request (0, 0)
673         self.view.connect_after ("size-allocate", self.__handle_size_allocate)
674         ColumnManager.attach (self)
675
676     def __update_sizes (self):
677
678         view_width = self.view.get_allocation ().width
679         if view_width == self.__size_update:
680             # Prevent endless recursion.
681             return
682
683         self.__size_update = view_width
684
685         col = self.find_item (name = "time")
686         other_width = col.view_column.props.width
687
688         try:
689             col = self.find_item (name = "message")
690         except KeyError:
691             return
692
693         width = view_width - other_width
694         col.wrap_to_width (width)
695
696     def __handle_size_allocate (self, self_, allocation):
697
698         self.__update_sizes ()