# -*- coding: utf-8; mode: python; -*- # # GStreamer Debug Viewer - View and analyze GStreamer debug log files # # Copyright (C) 2007 René Stadler # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the Free # Software Foundation; either version 3 of the License, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see . """GStreamer Debug Viewer GUI module.""" __author__ = u"René Stadler " __version__ = "0.1" def _ (s): return s import sys import os import os.path from operator import add from sets import Set from bisect import bisect_right, bisect_left import logging import pygtk pygtk.require ("2.0") import gobject import gtk import gtk.glade from GstDebugViewer import Common, Data, Main class Color (object): def __init__ (self, hex_24): if hex_24.startswith ("#"): s = hex_24[1:] else: s = hex_24 self._fields = tuple ((int (hs, 16) for hs in (s[:2], s[2:4], s[4:],))) def gdk_color (self): return gtk.gdk.color_parse (self.hex_string ()) def hex_string (self): return "#%02x%02x%02x" % self._fields def float_tuple (self): return tuple ((float (x) / 255 for x in self._fields)) def byte_tuple (self): return self._fields def short_tuple (self): return tuple ((x << 8 for x in self._fields)) class ColorPalette (object): @classmethod def get (cls): try: return cls._instance except AttributeError: cls._instance = cls () return cls._instance class TangoPalette (ColorPalette): def __init__ (self): for name, r, g, b in [("black", 0, 0, 0,), ("white", 255, 255, 255,), ("butter1", 252, 233, 79), ("butter2", 237, 212, 0), ("butter3", 196, 160, 0), ("chameleon1", 138, 226, 52), ("chameleon2", 115, 210, 22), ("chameleon3", 78, 154, 6), ("orange1", 252, 175, 62), ("orange2", 245, 121, 0), ("orange3", 206, 92, 0), ("skyblue1", 114, 159, 207), ("skyblue2", 52, 101, 164), ("skyblue3", 32, 74, 135), ("plum1", 173, 127, 168), ("plum2", 117, 80, 123), ("plum3", 92, 53, 102), ("chocolate1", 233, 185, 110), ("chocolate2", 193, 125, 17), ("chocolate3", 143, 89, 2), ("scarletred1", 239, 41, 41), ("scarletred2", 204, 0, 0), ("scarletred3", 164, 0, 0), ("aluminium1", 238, 238, 236), ("aluminium2", 211, 215, 207), ("aluminium3", 186, 189, 182), ("aluminium4", 136, 138, 133), ("aluminium5", 85, 87, 83), ("aluminium6", 46, 52, 54)]: setattr (self, name, Color ("%02x%02x%02x" % (r, g, b,))) class ColorTheme (object): def __init__ (self): self.colors = {} def add_color (self, key, *colors): self.colors[key] = colors class LevelColorTheme (ColorTheme): pass class LevelColorThemeTango (LevelColorTheme): def __init__ (self): LevelColorTheme.__init__ (self) p = TangoPalette.get () self.add_color (Data.debug_level_none, None, None, None) self.add_color (Data.debug_level_log, p.black, p.plum1, Color ("#e0a4d9")) self.add_color (Data.debug_level_debug, p.black, p.skyblue1, Color ("#8cc4ff")) self.add_color (Data.debug_level_info, p.black, p.chameleon1, Color ("#9dff3b")) self.add_color (Data.debug_level_warning, p.black, p.orange1, Color ("#ffc266")) self.add_color (Data.debug_level_error, p.white, p.scarletred1, Color ("#ff4545")) class ThreadColorTheme (ColorTheme): pass class ThreadColorThemeTango (ThreadColorTheme): def __init__ (self): ThreadColorTheme.__init__ (self) t = TangoPalette.get () for i, color in enumerate ([t.butter2, t.orange2, t.chocolate3, t.chameleon2, t.skyblue1, t.plum1, t.scarletred1, t.aluminium6]): self.add_color (i, color) class LogModelBase (gtk.GenericTreeModel): __metaclass__ = Common.GUI.MetaModel columns = ("COL_TIME", gobject.TYPE_UINT64, "COL_PID", int, "COL_THREAD", gobject.TYPE_UINT64, "COL_LEVEL", object, "COL_CATEGORY", str, "COL_FILENAME", str, "COL_LINE_NUMBER", int, "COL_FUNCTION", str, "COL_OBJECT", str, "COL_MESSAGE", str,) def __init__ (self): gtk.GenericTreeModel.__init__ (self) ##self.props.leak_references = False self.line_offsets = [] self.line_levels = [] # FIXME: Not so nice! self.line_cache = {} def ensure_cached (self, line_offset): raise NotImplementedError ("derived classes must override this method") def access_offset (self, offset): raise NotImplementedError ("derived classes must override this method") def iter_rows_offset (self): for i, offset in enumerate (self.line_offsets): self.ensure_cached (offset) row = self.line_cache[offset] row[self.COL_LEVEL] = self.line_levels[i] # FIXME yield (row, offset,) def on_get_flags (self): flags = gtk.TREE_MODEL_LIST_ONLY | gtk.TREE_MODEL_ITERS_PERSIST return flags def on_get_n_columns (self): return len (self.column_types) def on_get_column_type (self, col_id): return self.column_types[col_id] def on_get_iter (self, path): if not path: return if len (path) > 1: # Flat model. return None line_index = path[0] if line_index > len (self.line_offsets) - 1: return None return line_index def on_get_path (self, rowref): line_index = rowref return (line_index,) def on_get_value (self, line_index, col_id): last_index = len (self.line_offsets) - 1 if line_index > last_index: return None if col_id == self.COL_LEVEL: return self.line_levels[line_index] line_offset = self.line_offsets[line_index] self.ensure_cached (line_offset) value = self.line_cache[line_offset][col_id] if col_id == self.COL_MESSAGE: message_offset = value value = self.access_offset (line_offset + message_offset).strip () return value def on_iter_next (self, line_index): last_index = len (self.line_offsets) - 1 if line_index >= last_index: return None else: return line_index + 1 def on_iter_children (self, parent): return self.on_iter_nth_child (parent, 0) def on_iter_has_child (self, rowref): return False def on_iter_n_children (self, rowref): if rowref is not None: return 0 return len (self.line_offsets) def on_iter_nth_child (self, parent, n): last_index = len (self.line_offsets) - 1 if parent or n > last_index: return None return n def on_iter_parent (self, child): return None ## def on_ref_node (self, rowref): ## pass ## def on_unref_node (self, rowref): ## pass class LazyLogModel (LogModelBase): def __init__ (self, log_obj = None): LogModelBase.__init__ (self) self.__log_obj = log_obj if log_obj: self.set_log (log_obj) def set_log (self, log_obj): self.__fileobj = log_obj.fileobj self.line_cache.clear () self.line_offsets = log_obj.line_cache.offsets self.line_levels = log_obj.line_cache.levels def access_offset (self, offset): # TODO: Implement using one slice access instead of seek+readline. self.__fileobj.seek (offset) return self.__fileobj.readline () def ensure_cached (self, line_offset): if line_offset in self.line_cache: return if len (self.line_cache) > 10000: self.line_cache.clear () self.__fileobj.seek (line_offset) line = self.__fileobj.readline () self.line_cache[line_offset] = Data.LogLine.parse_full (line) class FilteredLogModelBase (LogModelBase): def __init__ (self, super_model): LogModelBase.__init__ (self) self.logger = logging.getLogger ("filter-model-base") self.super_model = super_model self.access_offset = super_model.access_offset self.ensure_cached = super_model.ensure_cached self.line_cache = super_model.line_cache def line_index_to_super (self, line_index): raise NotImplementedError ("index conversion not supported") def line_index_from_super (self, super_line_index): raise NotImplementedError ("index conversion not supported") def line_index_to_top (self, line_index): _log_indices = [line_index] super_index = line_index for model in self._iter_hierarchy (): super_index = model.line_index_to_super (super_index) _log_indices.append (super_index) _log_trans = " -> ".join ([str (x) for x in _log_indices]) self.logger.debug ("translated index to top: %s", _log_trans) return super_index def line_index_from_top (self, super_index): _log_indices = [super_index] line_index = super_index for model in reversed (list (self._iter_hierarchy ())): line_index = model.line_index_from_super (line_index) _log_indices.append (line_index) _log_trans = " -> ".join ([str (x) for x in _log_indices]) self.logger.debug ("translated index from top: %s", _log_trans) return line_index def super_model_changed (self): pass def _iter_hierarchy (self): model = self while hasattr (model, "super_model") and model.super_model: yield model model = model.super_model class FilteredLogModelIdentity (FilteredLogModelBase): def __init__ (self, super_model): FilteredLogModelBase.__init__ (self, super_model) self.line_offsets = self.super_model.line_offsets self.line_levels = self.super_model.line_levels def line_index_from_super (self, super_line_index): return super_line_index def line_index_to_super (self, line_index): return line_index class FilteredLogModel (FilteredLogModelBase): def __init__ (self, super_model): FilteredLogModelBase.__init__ (self, super_model) self.logger = logging.getLogger ("filtered-log-model") self.filters = [] self.super_index = [] self.from_super_index = {} self.reset () self.__active_process = None self.__filter_progress = 0. self.__old_super_model_range = super_model.line_index_range def reset (self): del self.line_offsets[:] self.line_offsets += self.super_model.line_offsets del self.line_levels[:] self.line_levels += self.super_model.line_levels del self.super_index[:] self.from_super_index.clear () del self.filters[:] def __filter_process (self, filter): YIELD_LIMIT = 10000 self.logger.debug ("preparing new filter") self.filters.append (filter) ## del self.line_offsets[:] ## del self.line_levels[:] new_line_offsets = [] new_line_levels = [] new_super_index = [] new_from_super_index = {} level_id = self.COL_LEVEL func = filter.filter_func if len (self.filters) == 1: # This is the first filter that gets applied. def enum (): i = 0 for row, offset in self.iter_rows_offset (): yield (i, row, offset,) i += 1 else: def enum (): i = 0 for row, offset in self.iter_rows_offset (): line_index = self.super_index[i] yield (line_index, row, offset,) i += 1 self.logger.debug ("running filter") progress = 0. progress_full = float (len (self)) y = YIELD_LIMIT for i, row, offset in enum (): if func (row): new_line_offsets.append (offset) new_line_levels.append (row[level_id]) new_super_index.append (i) new_from_super_index[i] = len (new_super_index) - 1 y -= 1 if y == 0: progress += float (YIELD_LIMIT) self.__filter_progress = progress / progress_full y = YIELD_LIMIT yield True self.line_offsets = new_line_offsets self.line_levels = new_line_levels self.super_index = new_super_index self.from_super_index = new_from_super_index self.logger.debug ("filtering finished") self.__filter_progress = 1. self.__handle_filter_process_finished () yield False def add_filter (self, filter, dispatcher): if self.__active_process is not None: raise ValueError ("dispatched a filter process already") self.__dispatcher = dispatcher self.__active_process = self.__filter_process (filter) dispatcher (self.__active_process) def abort_process (self): if self.__active_process is None: return self.__dispatcher.cancel () self.__active_process = None self.__dispatcher = None def get_filter_progress (self): if self.__active_process is None: raise ValueError ("no filter process running") return self.__filter_progress def __handle_filter_process_finished (self): self.__active_process = None self.handle_process_finished () def handle_process_finished (self): pass def line_index_from_super (self, super_line_index): if len (self.filters) == 0: # Identity. return super_line_index try: return self.from_super_index[super_line_index] except KeyError: raise IndexError ("super index %i not handled" % (super_line_index,)) def line_index_to_super (self, line_index): if len (self.filters) == 0: # Identity. return line_index return self.super_index[line_index] def __filtered_indices_in_range (self, start, stop): if start < 0: raise ValueError ("start cannot be negative (got %r)" % (start,)) super_start = bisect_left (self.super_index, start) super_stop = bisect_left (self.super_index, stop) return super_stop - super_start def super_model_changed_range (self): range_model = self.super_model old_start, old_stop = self.__old_super_model_range super_start, super_stop = range_model.line_index_range super_start_offset = super_start - old_start if super_start_offset < 0: # TODO: raise NotImplementedError ("Only handling further restriction of the range" " (start offset = %i)" % (super_start_offset,)) super_end_offset = super_stop - old_stop if super_end_offset > 0: # TODO: raise NotImplementedError ("Only handling further restriction of the range" " (end offset = %i)" % (super_end_offset,)) if super_end_offset < 0: if not self.super_index: # Identity; there are no filters. end_offset = len (self.line_offsets) + super_end_offset else: n_filtered = self.__filtered_indices_in_range (super_stop - super_start, old_stop - super_start) end_offset = len (self.line_offsets) - n_filtered stop = len (self.line_offsets) # FIXME? assert end_offset < stop self.__remove_range (end_offset, stop) if super_start_offset > 0: if not self.super_index: # Identity; there are no filters. n_filtered = super_start_offset start_offset = n_filtered else: n_filtered = self.__filtered_indices_in_range (0, super_start_offset) start_offset = n_filtered if n_filtered > 0: self.__remove_range (0, start_offset) from_super = self.from_super_index for i in self.super_index: old_index = from_super[i] del from_super[i] from_super[i - super_start_offset] = old_index - start_offset for i in range (len (self.super_index)): self.super_index[i] -= super_start_offset self.__old_super_model_range = (super_start, super_stop,) def __remove_range (self, start, stop): if start < 0: raise ValueError ("start cannot be negative (got %r)" % (start,)) if start == stop: return if stop > len (self.line_offsets): raise ValueError ("stop value out of range (got %r)" % (stop,)) if start > stop: raise ValueError ("start cannot be greater than stop (got %r, %r)" % (start, stop,)) self.logger.debug ("removing line range (%i, %i)", start, stop) del self.line_offsets[start:stop] del self.line_levels[start:stop] for super_index in self.super_index[start:stop]: del self.from_super_index[super_index] del self.super_index[start:stop] class Filter (object): pass class DebugLevelFilter (Filter): def __init__ (self, debug_level): col_id = LogModelBase.COL_LEVEL def filter_func (row): return row[col_id] != debug_level self.filter_func = filter_func class CategoryFilter (Filter): def __init__ (self, category): col_id = LogModelBase.COL_CATEGORY def category_filter_func (row): return row[col_id] != category self.filter_func = category_filter_func class SubRange (object): __slots__ = ("l", "start", "stop",) def __init__ (self, l, start, stop): if start > stop: raise ValueError ("need start <= stop (got %r, %r)" % (start, stop,)) self.l = l self.start = start self.stop = stop def __getitem__ (self, i): return self.l[i + self.start] def __len__ (self): return self.stop - self.start def __iter__ (self): l = self.l for i in xrange (self.start, self.stop): yield l[i] class RangeFilteredLogModel (FilteredLogModelBase): def __init__ (self, super_model): FilteredLogModelBase.__init__ (self, super_model) self.logger = logging.getLogger ("range-filtered-model") self.line_index_range = None def set_range (self, start_index, stop_index): self.logger.debug ("setting range to start = %i, stop = %i", start_index, stop_index) self.line_index_range = (start_index, stop_index,) self.line_offsets = SubRange (self.super_model.line_offsets, start_index, stop_index) self.line_levels = SubRange (self.super_model.line_levels, start_index, stop_index) def reset (self): self.logger.debug ("reset") start_index = 0 stop_index = len (self.super_model) self.set_range (start_index, stop_index,) def line_index_to_super (self, line_index): start_index = self.line_index_range[0] return line_index + start_index def line_index_from_super (self, li): start, stop = self.line_index_range if li < start or li >= stop: raise IndexError ("not in range") return li - start # Sync with gst-inspector! class Column (object): """A single list view column, managed by a ColumnManager instance.""" name = None id = None label_header = None get_modify_func = None get_data_func = None get_row_data_func = None get_sort_func = None def __init__ (self): view_column = gtk.TreeViewColumn (self.label_header) view_column.props.reorderable = True self.view_column = view_column class SizedColumn (Column): default_size = None def compute_default_size (self, view, model): return None # Sync with gst-inspector? class TextColumn (SizedColumn): font_family = None def __init__ (self): Column.__init__ (self) column = self.view_column cell = gtk.CellRendererText () column.pack_start (cell) cell.props.yalign = 0. if self.font_family: cell.props.family = self.font_family cell.props.family_set = True if self.get_data_func: data_func = self.get_data_func () assert data_func id_ = self.id if id_ is not None: def cell_data_func (column, cell, model, tree_iter): data_func (cell.props, model.get_value (tree_iter, id_), model.get_path (tree_iter)) else: cell_data_func = data_func column.set_cell_data_func (cell, cell_data_func) elif self.get_row_data_func: data_func = self.get_row_data_func () assert data_func def cell_data_func (column, cell, model, tree_iter): data_func (cell.props, model[tree_iter]) column.set_cell_data_func (cell, cell_data_func) elif not self.get_modify_func: column.add_attribute (cell, "text", self.id) else: modify_func = self.get_modify_func () id_ = self.id def cell_data_func (column, cell, model, tree_iter): cell.props.text = modify_func (model.get (tree_iter, id_)[0]) column.set_cell_data_func (cell, cell_data_func) column.props.resizable = True def compute_default_size (self, view, model): values = self.get_values_for_size () if not values: return SizedColumn.compute_default_size (self, view, model) cell = self.view_column.get_cell_renderers ()[0] if self.get_modify_func is not None: format = self.get_modify_func () else: def identity (x): return x format = identity max_width = 0 for value in values: cell.props.text = format (value) rect, x, y, w, h = self.view_column.cell_get_size () max_width = max (max_width, w) return max_width def get_values_for_size (self): return () class TimeColumn (TextColumn): name = "time" label_header = _("Time") id = LazyLogModel.COL_TIME font_family = "monospace" @staticmethod def get_modify_func (): time_args = Data.time_args def format_time (value): # TODO: This is hard coded to omit hours as well as the last 3 # digits at the end, since current gst uses g_get_current_time, # which has microsecond precision only. return time_args (value)[2:-3] return format_time def get_values_for_size (self): values = [0] return values class LevelColumn (TextColumn): name = "level" label_header = _("L") id = LazyLogModel.COL_LEVEL def __init__ (self): TextColumn.__init__ (self) cell = self.view_column.get_cell_renderers ()[0] cell.props.xalign = .5 @staticmethod def get_modify_func (): def format_level (value): return value.name[0] return format_level @staticmethod def get_data_func (): theme = LevelColorThemeTango () colors = dict ((level, tuple ((c.gdk_color () for c in theme.colors[level])),) for level in Data.debug_levels if level != Data.debug_level_none) def level_data_func (cell_props, level, path): cell_props.text = level.name[0] if level in colors: cell_colors = colors[level] else: cell_colors = (None, None, None,) cell_props.foreground_gdk = cell_colors[0] if path[0] % 2: cell_props.background_gdk = cell_colors[1] else: cell_props.background_gdk = cell_colors[2] return level_data_func def get_values_for_size (self): values = [Data.debug_level_log, Data.debug_level_debug, Data.debug_level_info, Data.debug_level_warning, Data.debug_level_error] return values class PidColumn (TextColumn): name = "pid" label_header = _("PID") id = LazyLogModel.COL_PID font_family = "monospace" @staticmethod def get_modify_func (): return str def get_values_for_size (self): return ["999999"] class ThreadColumn (TextColumn): name = "thread" label_header = _("Thread") id = LazyLogModel.COL_THREAD font_family = "monospace" @staticmethod def get_modify_func (): def format_thread (value): return "0x%07x" % (value,) return format_thread def get_values_for_size (self): return [int ("ffffff", 16)] class CategoryColumn (TextColumn): name = "category" label_header = _("Category") id = LazyLogModel.COL_CATEGORY def get_values_for_size (self): return ["GST_LONG_CATEGORY", "somelongelement"] class CodeColumn (TextColumn): name = "code" label_header = _("Code") id = None @staticmethod def get_data_func (): filename_id = LogModelBase.COL_FILENAME line_number_id = LogModelBase.COL_LINE_NUMBER def filename_data_func (column, cell, model, tree_iter): args = model.get (tree_iter, filename_id, line_number_id) cell.props.text = "%s:%i" % args return filename_data_func def get_values_for_size (self): return ["gstsomefilename.c:1234"] class FunctionColumn (TextColumn): name = "function" label_header = _("Function") id = LazyLogModel.COL_FUNCTION def get_values_for_size (self): return ["gst_this_should_be_enough"] class ObjectColumn (TextColumn): name = "object" label_header = _("Object") id = LazyLogModel.COL_OBJECT def get_values_for_size (self): return ["longobjectname00"] class MessageColumn (TextColumn): name = "message" label_header = _("Message") id = LazyLogModel.COL_MESSAGE def __init__ (self, *a, **kw): self.highlighters = {} TextColumn.__init__ (self, *a, **kw) def get_row_data_func (self): from pango import AttrList, AttrBackground, AttrForeground highlighters = self.highlighters id_ = self.id # FIXME: This should be none; need to investigate # `cellrenderertext.props.attributes = None' failure (param conversion # error like `treeview.props.model = None'). no_attrs = AttrList () def message_data_func (props, row): props.text = row[id_] if not highlighters: props.attributes = no_attrs for highlighter in highlighters.values (): ranges = highlighter (row) if not ranges: props.attributes = no_attrs else: attrlist = AttrList () for start, end in ranges: attrlist.insert (AttrBackground (0, 0, 65535, start, end)) attrlist.insert (AttrForeground (65535, 65535, 65535, start, end)) props.attributes = attrlist return message_data_func def get_values_for_size (self): values = ["Just some good minimum size"] return values class ColumnManager (Common.GUI.Manager): column_classes = () @classmethod def iter_item_classes (cls): return iter (cls.column_classes) def __init__ (self): self.view = None self.actions = None self.__columns_changed_id = None self.columns = [] self.column_order = list (self.column_classes) self.action_group = gtk.ActionGroup ("ColumnActions") def make_entry (col_class): return ("show-%s-column" % (col_class.name,), None, col_class.label_header, None, None, None, True,) entries = [make_entry (cls) for cls in self.column_classes] self.action_group.add_toggle_actions (entries) def iter_items (self): return iter (self.columns) def attach (self): for col_class in self.column_classes: action = self.get_toggle_action (col_class) if action.props.active: self._add_column (col_class ()) action.connect ("toggled", self.__handle_show_column_action_toggled, col_class.name) self.__columns_changed_id = self.view.connect ("columns-changed", self.__handle_view_columns_changed) def detach (self): if self.__columns_changed_id is not None: self.view.disconnect (self.__columns_changed_id) self.__columns_changed_id = None def attach_sort (self): sort_model = self.view.props.model # Inform the sorted tree model of any custom sorting functions. for col_class in self.column_classes: if col_class.get_sort_func: sort_func = col_class.get_sort_func () sort_model.set_sort_func (col_class.id, sort_func) def enable_sort (self): sort_model = self.view.props.model if sort_model: self.logger.debug ("activating sort") sort_model.set_sort_column_id (*self.default_sort) self.default_sort = None else: self.logger.debug ("not activating sort (no model set)") def disable_sort (self): self.logger.debug ("deactivating sort") sort_model = self.view.props.model self.default_sort = tree_sortable_get_sort_column_id (sort_model) sort_model.set_sort_column_id (TREE_SORTABLE_UNSORTED_COLUMN_ID, gtk.SORT_ASCENDING) def get_toggle_action (self, column_class): action_name = "show-%s-column" % (column_class.name,) return self.action_group.get_action (action_name) def get_initial_column_order (self): return tuple (self.column_classes) def _add_column (self, column): name = column.name pos = self.__get_column_insert_position (column) if self.view.props.fixed_height_mode: column.view_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED self.columns.insert (pos, column) self.view.insert_column (column.view_column, pos) def _remove_column (self, column): self.columns.remove (column) self.view.remove_column (column.view_column) def __get_column_insert_position (self, column): col_class = self.find_item_class (name = column.name) pos = self.column_order.index (col_class) before = self.column_order[:pos] shown_names = [col.name for col in self.columns] for col_class in before: if not col_class.name in shown_names: pos -= 1 return pos def __iter_next_hidden (self, column_class): pos = self.column_order.index (column_class) rest = self.column_order[pos + 1:] for next_class in rest: try: self.find_item (name = next_class.name) except KeyError: # No instance -- the column is hidden. yield next_class else: break def __handle_show_column_action_toggled (self, toggle_action, name): if toggle_action.props.active: try: # This should fail. column = self.find_item (name = name) except KeyError: col_class = self.find_item_class (name = name) self._add_column (col_class ()) else: # Out of sync for some reason. return else: try: column = self.find_item (name = name) except KeyError: # Out of sync for some reason. return else: self._remove_column (column) def __handle_view_columns_changed (self, element_view): view_columns = element_view.get_columns () new_visible = [self.find_item (view_column = column) for column in view_columns] # We only care about reordering here. if len (new_visible) != len (self.columns): return if new_visible != self.columns: new_order = [] for column in new_visible: col_class = self.find_item_class (name = column.name) new_order.append (col_class) new_order.extend (self.__iter_next_hidden (col_class)) names = (column.name for column in new_visible) self.logger.debug ("visible columns reordered: %s", ", ".join (names)) self.columns[:] = new_visible self.column_order[:] = new_order class ViewColumnManager (ColumnManager): column_classes = (TimeColumn, LevelColumn, PidColumn, ThreadColumn, CategoryColumn, CodeColumn, FunctionColumn, ObjectColumn, MessageColumn,) def __init__ (self, state): ColumnManager.__init__ (self) self.logger = logging.getLogger ("ui.columns") self.state = state def attach (self, view): self.view = view view.connect ("notify::model", self.__handle_notify_model) order = self.state.column_order if len (order) == len (self.column_classes): self.column_order[:] = order visible = self.state.columns_visible if not visible: visible = self.column_classes for col_class in self.column_classes: action = self.get_toggle_action (col_class) action.props.active = (col_class in visible) ColumnManager.attach (self) self.columns_sized = False def detach (self): self.state.column_order = self.column_order self.state.columns_visible = self.columns return ColumnManager.detach (self) def size_column (self, column, view, model): if column.default_size is None: default_size = column.compute_default_size (view, model) else: default_size = column.default_size # FIXME: Abstract away fixed size setting in Column class! if default_size is None: # Dummy fallback: column.view_column.props.fixed_width = 50 self.logger.warning ("%s column does not implement default size", column.name) else: column.view_column.props.fixed_width = default_size def _add_column (self, column): result = ColumnManager._add_column (self, column) model = self.view.props.model self.size_column (column, self.view, model) return result def _remove_column (self, column): column.default_size = column.view_column.props.fixed_width return ColumnManager._remove_column (self, column) def __handle_notify_model (self, view, gparam): if self.columns_sized: # Already sized. return model = self.view.props.model if model is None: return self.logger.debug ("model changed, sizing columns") for column in self.iter_items (): self.size_column (column, view, model) self.columns_sized = True class LineViewLogModel (FilteredLogModelBase): def __init__ (self, super_model): FilteredLogModelBase.__init__ (self, super_model) self.line_offsets = [] self.line_levels = [] self.parent_indices = [] def reset (self): del self.line_offsets[:] del self.line_levels[:] def line_index_to_super (self, line_index): return self.parent_indices[line_index] def insert_line (self, position, super_line_index): if position == -1: position = len (self.line_offsets) li = super_line_index self.line_offsets.insert (position, self.super_model.line_offsets[li]) self.line_levels.insert (position, self.super_model.line_levels[li]) self.parent_indices.insert (position, super_line_index) path = (position,) tree_iter = self.get_iter (path) self.row_inserted (path, tree_iter) def replace_line (self, line_index, super_line_index): li = line_index self.line_offsets[li] = self.super_model.line_offsets[super_line_index] self.line_levels[li] = self.super_model.line_levels[super_line_index] self.parent_indices[li] = super_line_index path = (line_index,) tree_iter = self.get_iter (path) self.row_changed (path, tree_iter) def remove_line (self, line_index): for l in (self.line_offsets, self.line_levels, self.parent_indices,): del l[line_index] path = (line_index,) self.row_deleted (path) class WrappingMessageColumn (MessageColumn): def wrap_to_width (self, width): col = self.view_column col.props.max_width = width col.get_cell_renderers ()[0].props.wrap_width = width col.queue_resize () class LineViewColumnManager (ColumnManager): column_classes = (TimeColumn, WrappingMessageColumn,) def __init__ (self): ColumnManager.__init__ (self) def attach (self, window): self.__size_update = None self.view = window.widgets.line_view self.view.set_size_request (0, 0) self.view.connect_after ("size-allocate", self.__handle_size_allocate) ColumnManager.attach (self) def __update_sizes (self): view_width = self.view.get_allocation ().width if view_width == self.__size_update: # Prevent endless recursion. return self.__size_update = view_width col = self.find_item (name = "time") other_width = col.view_column.props.width try: col = self.find_item (name = "message") except KeyError: return width = view_width - other_width col.wrap_to_width (width) def __handle_size_allocate (self, self_, allocation): self.__update_sizes () class LineView (object): def __init__ (self): self.column_manager = LineViewColumnManager () def attach (self, window): self.clear_action = window.actions.clear_line_view handler = self.handle_clear_line_view_action_activate self.clear_action.connect ("activate", handler) self.line_view = window.widgets.line_view self.line_view.connect ("row-activated", self.handle_line_view_row_activated) ui = window.ui_manager self.popup = ui.get_widget ("/ui/context/LineViewContextMenu").get_submenu () Common.GUI.widget_add_popup_menu (self.line_view, self.popup) self.log_view = log_view = window.log_view log_view.connect ("row-activated", self.handle_log_view_row_activated) sel = log_view.get_selection () sel.connect ("changed", self.handle_log_view_selection_changed) self.clear_action.props.sensitive = False self.column_manager.attach (window) def clear (self): model = self.line_view.props.model if len (model) == 0: return for i in range (1, len (model)): model.remove_line (1) self.clear_action.props.sensitive = False def handle_attach_log_file (self, window): self.line_view.props.model = LineViewLogModel (window.log_model) def handle_line_view_row_activated (self, view, path, column): line_index = path[0] line_model = view.props.model parent_index = line_model.line_index_to_super (line_index) path = (parent_index,) self.log_view.scroll_to_cell (path, use_align = True, row_align = .5) sel = self.log_view.get_selection () sel.select_path (path) def handle_log_view_row_activated (self, view, path, column): log_model = view.props.model line_index = path[0] super_line_index = log_model.line_index_to_super (line_index) line_model = self.line_view.props.model if line_model is None: return if len (line_model): timestamps = [row[line_model.COL_TIME] for row in line_model] row = log_model[(line_index,)] position = bisect_right (timestamps, row[line_model.COL_TIME]) else: position = 0 if len (line_model) > 1: other_index = line_model.line_index_to_super (position - 1) else: other_index = -1 if other_index == super_line_index and position != 1: # Already have the line. pass else: line_model.insert_line (position, super_line_index) self.clear_action.props.sensitive = True def handle_log_view_selection_changed (self, selection): line_model = self.line_view.props.model if line_model is None or len (line_model) == 0: return model, tree_iter = selection.get_selected () if tree_iter is None: return path = model.get_path (tree_iter) line_index = model.line_index_to_super (path[0]) if len (line_model) == 0: line_model.insert_line (0, line_index) else: line_model.replace_line (0, line_index) def handle_clear_line_view_action_activate (self, action): self.clear () class ProgressDialog (object): def __init__ (self, window, title = ""): widgets = window.widget_factory.make ("progress_dialog") dialog = widgets.progress_dialog dialog.connect ("response", self.__handle_dialog_response) self.__dialog = dialog self.__progress_bar = widgets.progress_bar self.__progress_bar.props.text = title dialog.set_transient_for (window.gtk_window) dialog.show () def __handle_dialog_response (self, dialog, resp): self.handle_cancel () def handle_cancel (self): pass def update (self, progress): if self.__progress_bar is None: return self.__progress_bar.props.fraction = progress def destroy (self): if self.__dialog is None: return self.__dialog.destroy () self.__dialog = None self.__progress_bar = None class Window (object): def __init__ (self, app): self.logger = logging.getLogger ("ui.window") self.app = app self.dispatcher = None self.progress_dialog = None self.update_progress_id = None self.window_state = Common.GUI.WindowState () self.column_manager = ViewColumnManager (app.state_section) self.actions = Common.GUI.Actions () group = gtk.ActionGroup ("MenuActions") group.add_actions ([("FileMenuAction", None, _("_File")), ("ViewMenuAction", None, _("_View")), ("ViewColumnsMenuAction", None, _("_Columns")), ("HelpMenuAction", None, _("_Help")), ("LineViewContextMenuAction", None, "")]) self.actions.add_group (group) group = gtk.ActionGroup ("WindowActions") group.add_actions ([("new-window", gtk.STOCK_NEW, _("_New Window"), "N"), ("open-file", gtk.STOCK_OPEN, _("_Open File"), "O"), ("reload-file", gtk.STOCK_REFRESH, _("_Reload File"), "R"), ("close-window", gtk.STOCK_CLOSE, _("Close _Window"), "W"), ("cancel-load", gtk.STOCK_CANCEL, None,), ("clear-line-view", gtk.STOCK_CLEAR, None), ("show-about", gtk.STOCK_ABOUT, None)]) self.actions.add_group (group) self.actions.reload_file.props.sensitive = False group = gtk.ActionGroup ("RowActions") group.add_actions ([("hide-before-line", None, _("Hide lines before this one")), ("hide-after-line", None, _("Hide lines after this one")), ("show-hidden-lines", None, _("Show hidden lines")), ("edit-copy-line", gtk.STOCK_COPY, _("Copy line"), "C"), ("edit-copy-message", gtk.STOCK_COPY, _("Copy message"), ""), ("hide-log-level", None, _("Hide log level")), ("hide-log-category", None, _("Hide log category")), ("hide-log-object", None, _("Hide object"))]) group.props.sensitive = False self.actions.add_group (group) self.actions.hide_log_object.props.visible = False self.actions.add_group (self.column_manager.action_group) self.log_file = None self.setup_model (LazyLogModel ()) glade_filename = os.path.join (Main.Paths.data_dir, "gst-debug-viewer.glade") self.widget_factory = Common.GUI.WidgetFactory (glade_filename) self.widgets = self.widget_factory.make ("main_window") ui_filename = os.path.join (Main.Paths.data_dir, "gst-debug-viewer.ui") self.ui_factory = Common.GUI.UIFactory (ui_filename, self.actions) self.ui_manager = ui = self.ui_factory.make () menubar = ui.get_widget ("/ui/menubar") self.widgets.vbox_main.pack_start (menubar, False, False, 0) self.gtk_window = self.widgets.main_window self.gtk_window.add_accel_group (ui.get_accel_group ()) self.log_view = self.widgets.log_view self.log_view.drag_dest_unset () self.log_view.set_search_column (-1) self.view_popup = ui.get_widget ("/ui/menubar/ViewMenu").get_submenu () Common.GUI.widget_add_popup_menu (self.log_view, self.view_popup) self.line_view = LineView () self.attach () self.column_manager.attach (self.log_view) def setup_model (self, model, filter = False): self.log_model = model self.log_range = RangeFilteredLogModel (self.log_model) if filter: self.log_filter = FilteredLogModel (self.log_range) self.log_filter.handle_process_finished = self.handle_log_filter_process_finished else: self.log_filter = None def get_top_attach_point (self): return self.widgets.vbox_main def get_side_attach_point (self): return self.widgets.hbox_view def attach (self): self.window_state.attach (window = self.gtk_window, state = self.app.state_section) self.clipboard = gtk.Clipboard (self.gtk_window.get_display (), gtk.gdk.SELECTION_CLIPBOARD) for action_name in ("new-window", "open-file", "reload-file", "close-window", "cancel-load", "hide-before-line", "hide-after-line", "show-hidden-lines", "edit-copy-line", "edit-copy-message", "hide-log-level", "hide-log-category", "hide-log-object", "show-about",): name = action_name.replace ("-", "_") action = getattr (self.actions, name) handler = getattr (self, "handle_%s_action_activate" % (name,)) action.connect ("activate", handler) self.gtk_window.connect ("delete-event", self.handle_window_delete_event) self.features = [] for plugin_feature in self.app.iter_plugin_features (): feature = plugin_feature (self.app) self.features.append (feature) for feature in self.features: feature.handle_attach_window (self) # FIXME: With multiple selection mode, browsing the list with key # up/down slows to a crawl! WTF is wrong with this stupid widget??? sel = self.log_view.get_selection () sel.set_mode (gtk.SELECTION_BROWSE) self.line_view.attach (self) self.gtk_window.show () def detach (self): self.set_log_file (None) for feature in self.features: feature.handle_detach_window (self) self.window_state.detach () self.column_manager.detach () def get_active_line_index (self): selection = self.log_view.get_selection () model, tree_iter = selection.get_selected () if tree_iter is None: raise ValueError ("no line selected") path = model.get_path (tree_iter) return path[0] def get_active_line (self): selection = self.log_view.get_selection () model, tree_iter = selection.get_selected () if tree_iter is None: raise ValueError ("no line selected") model = self.log_view.props.model return model.get (tree_iter, *LogModelBase.column_ids) def close (self, *a, **kw): self.logger.debug ("closing window, detaching") self.detach () self.gtk_window.hide () self.logger.debug ("requesting close from app") self.app.close_window (self) def push_view_state (self): self.default_index = None self.default_start_index = None model = self.log_view.props.model if model is None: return try: line_index = self.get_active_line_index () except ValueError: super_index = None self.logger.debug ("no line selected") else: super_index = model.line_index_to_top (line_index) self.logger.debug ("pushing selected line %i (abs %i)", line_index, super_index) self.default_index = super_index vis_range = self.log_view.get_visible_range () if vis_range is not None: start_path, end_path = vis_range start_index = start_path[0] self.default_start_index = model.line_index_to_top (start_index) def update_model (self, model = None): if model is None: model = self.log_view.props.model previous_model = self.log_view.props.model if previous_model == model: # Force update. self.log_view.set_model (None) self.log_view.props.model = model def pop_view_state (self, scroll_to_selection = False): model = self.log_view.props.model if model is None: return selected_index = self.default_index start_index = self.default_start_index if selected_index is not None: try: select_index = model.line_index_from_top (selected_index) except IndexError, exc: self.logger.debug ("abs line index %i filtered out, not reselecting", selected_index) else: assert select_index >= 0 sel = self.log_view.get_selection () path = (select_index,) sel.select_path (path) if start_index is None or scroll_to_selection: self.log_view.scroll_to_cell (path, use_align = True, row_align = .5) if start_index is not None and not scroll_to_selection: def traverse (): for i in xrange (start_index, len (model)): yield i for i in xrange (start_index - 1, 0, -1): yield i for current_index in traverse (): try: target_index = model.line_index_from_top (current_index) except IndexError: continue else: path = (target_index,) self.log_view.scroll_to_cell (path, use_align = True, row_align = 0.) break def update_view (self): view = self.log_view model = view.props.model start_path, end_path = view.get_visible_range () start_index, end_index = start_path[0], end_path[0] for line_index in range (start_index, end_index + 1): path = (line_index,) tree_iter = model.get_iter (path) model.row_changed (path, tree_iter) def handle_window_delete_event (self, window, event): self.actions.close_window.activate () def handle_new_window_action_activate (self, action): self.app.open_window () def handle_open_file_action_activate (self, action): dialog = gtk.FileChooserDialog (None, self.gtk_window, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT,)) response = dialog.run () dialog.hide () if response == gtk.RESPONSE_ACCEPT: self.set_log_file (dialog.get_filename ()) dialog.destroy () def handle_reload_file_action_activate (self, action): if self.log_file is None: return self.set_log_file (self.log_file.path) def handle_cancel_load_action_activate (self, action): self.logger.debug ("cancelling data load") self.set_log_file (None) if self.progress_dialog: self.progress_dialog.destroy () self.progress_dialog = None if self.update_progress_id is not None: gobject.source_remove (self.update_progress_id) self.update_progress_id = None def handle_close_window_action_activate (self, action): self.close () def handle_hide_after_line_action_activate (self, action): self.hide_range (after = True) def handle_hide_before_line_action_activate (self, action): self.hide_range (after = False) def hide_range (self, after): model = self.log_view.props.model try: filtered_line_index = self.get_active_line_index () except ValueError: return if after: first_index = model.line_index_to_top (0) last_index = model.line_index_to_top (filtered_line_index) self.logger.info ("hiding lines after %i (abs %i), first line is abs %i", filtered_line_index, last_index, first_index) else: first_index = model.line_index_to_top (filtered_line_index) last_index = model.line_index_to_top (len (model) - 1) self.logger.info ("hiding lines before %i (abs %i), last line is abs %i", filtered_line_index, first_index, last_index) self.push_view_state () start_index = first_index stop_index = last_index + 1 self.log_range.set_range (start_index, stop_index) if self.log_filter: self.log_filter.super_model_changed_range () self.update_model () self.pop_view_state () self.actions.show_hidden_lines.props.sensitive = True def handle_show_hidden_lines_action_activate (self, action): self.logger.info ("restoring model filter to show all lines") self.push_view_state () self.log_range.reset () self.log_filter = None self.update_model (self.log_range) self.pop_view_state (scroll_to_selection = True) self.actions.show_hidden_lines.props.sensitive = False def handle_edit_copy_line_action_activate (self, action): line_index = self.get_active_line_index () line = self.log_file.get_full_line (line_index) # FIXME: level = self.log_filter[(line_index,)][LogModelBase.COL_LEVEL] line[LogModelBase.COL_LEVEL] = level self.clipboard.set_text (line.line_string ()) def handle_edit_copy_message_action_activate (self, action): col_id = LogModelBase.COL_MESSAGE self.clipboard.set_text (self.get_active_line ()[col_id]) def add_model_filter (self, filter): self.progress_dialog = ProgressDialog (self, _("Filtering")) self.progress_dialog.handle_cancel = self.handle_filter_progress_dialog_cancel dispatcher = Common.Data.GSourceDispatcher () self.filter_dispatcher = dispatcher # FIXME: Unsetting the model to keep e.g. the dispatched timeline # sentinel from collecting data while we filter idly, which slows # things down for nothing. self.push_view_state () self.log_view.set_model (None) if self.log_filter is None: self.log_filter = FilteredLogModel (self.log_range) self.log_filter.handle_process_finished = self.handle_log_filter_process_finished self.log_filter.add_filter (filter, dispatcher = dispatcher) gobject.timeout_add (250, self.update_filter_progress) def update_filter_progress (self): if self.progress_dialog is None: return False try: progress = self.log_filter.get_filter_progress () except ValueError: self.logger.warning ("no filter process running") return False self.progress_dialog.update (progress) return True def handle_filter_progress_dialog_cancel (self): return self.progress_dialog.destroy () self.progress_dialog = None # FIXME: Implement filter cancelling correctly; the stuff below does # not work. self.log_filter.abort_process () self.log_filter.reset () # FIXME: self.actions.show_hidden_lines.activate () def handle_log_filter_process_finished (self): self.progress_dialog.destroy () self.progress_dialog = None # No push_view_state here, did this in add_model_filter. self.update_model (self.log_filter) self.pop_view_state () self.actions.show_hidden_lines.props.sensitive = True def handle_hide_log_level_action_activate (self, action): row = self.get_active_line () debug_level = row[LogModelBase.COL_LEVEL] self.add_model_filter (DebugLevelFilter (debug_level)) def handle_hide_log_category_action_activate (self, action): row = self.get_active_line () category = row[LogModelBase.COL_CATEGORY] self.add_model_filter (CategoryFilter (category)) def handle_hide_log_object_action_activate (self, action): pass def handle_show_about_action_activate (self, action): from GstDebugViewer import version dialog = self.widget_factory.make_one ("about_dialog") dialog.props.version = version dialog.run () dialog.destroy () @staticmethod def _timestamp_cell_data_func (column, renderer, model, tree_iter): ts = model.get_value (tree_iter, LogModel.COL_TIME) renderer.props.text = Data.time_args (ts) def _message_cell_data_func (self, column, renderer, model, tree_iter): offset = model.get_value (tree_iter, LogModel.COL_MESSAGE_OFFSET) self.log_file.seek (offset) renderer.props.text = strip_escape (self.log_file.readline ().strip ()) def set_log_file (self, filename): if self.log_file is not None: for feature in self.features: feature.handle_detach_log_file (self, self.log_file) if filename is None: if self.dispatcher is not None: self.dispatcher.cancel () self.dispatcher = None self.log_file = None self.actions.groups["RowActions"].props.sensitive = False else: self.logger.debug ("setting log file %r", filename) try: self.setup_model (LazyLogModel ()) self.dispatcher = Common.Data.GSourceDispatcher () self.log_file = Data.LogFile (filename, self.dispatcher) except EnvironmentError, exc: try: file_size = os.path.getsize (filename) except EnvironmentError: pass else: if file_size == 0: # Trying to mmap an empty file results in an invalid # argument error. self.show_error (_("Could not open file"), _("The selected file is empty")) return self.handle_environment_error (exc, filename) return basename = os.path.basename (filename) self.gtk_window.props.title = _("%s - GStreamer Debug Viewer") % (basename,) self.log_file.consumers.append (self) self.log_file.start_loading () def handle_environment_error (self, exc, filename): self.show_error (_("Could not open file"), str (exc)) def show_error (self, message1, message2): dialog = gtk.MessageDialog (self.gtk_window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, message1) # The property for secondary text is new in 2.10, so we use this clunky # method instead. dialog.format_secondary_text (message2) dialog.set_default_response (0) dialog.run () dialog.destroy () def handle_load_started (self): self.logger.debug ("load has started") self.progress_dialog = ProgressDialog (self, _("Loading log file")) self.progress_dialog.handle_cancel = self.handle_load_progress_dialog_cancel self.update_progress_id = gobject.timeout_add (250, self.update_load_progress) def handle_load_progress_dialog_cancel (self): self.actions.cancel_load.activate () def update_load_progress (self): if self.progress_dialog is None: self.logger.debug ("progress dialog is gone, removing progress update timeout") self.update_progress_id = None return False progress = self.log_file.get_load_progress () self.progress_dialog.update (progress) return True def handle_load_finished (self): self.logger.debug ("load has finshed") self.progress_dialog.destroy () self.progress_dialog = None self.log_model.set_log (self.log_file) self.log_range.reset () self.log_filter = None self.actions.reload_file.props.sensitive = True self.actions.groups["RowActions"].props.sensitive = True self.actions.show_hidden_lines.props.sensitive = False def idle_set (): self.log_view.props.model = self.log_range self.line_view.handle_attach_log_file (self) for feature in self.features: feature.handle_attach_log_file (self, self.log_file) if len (self.log_range): sel = self.log_view.get_selection () sel.select_path ((0,)) return False gobject.idle_add (idle_set) class AppStateSection (Common.GUI.StateSection): _name = "state" geometry = Common.GUI.StateInt4 ("window-geometry") maximized = Common.GUI.StateBool ("window-maximized") column_order = Common.GUI.StateItemList ("column-order", ViewColumnManager) columns_visible = Common.GUI.StateItemList ("columns-visible", ViewColumnManager) class AppState (Common.GUI.State): def __init__ (self, *a, **kw): Common.GUI.State.__init__ (self, *a, **kw) self.add_section_class (AppStateSection) class App (object): def __init__ (self): self.attach () def load_plugins (self): from GstDebugViewer import Plugins plugin_classes = list (Plugins.load ([os.path.dirname (Plugins.__file__)])) self.plugins = [] for plugin_class in plugin_classes: plugin = plugin_class (self) self.plugins.append (plugin) def iter_plugin_features (self): for plugin in self.plugins: for feature in plugin.features: yield feature def attach (self): config_home = Common.utils.XDG.CONFIG_HOME state_filename = os.path.join (config_home, "gst-debug-viewer", "state") self.state = AppState (state_filename) self.state_section = self.state.sections["state"] self.load_plugins () self.windows = [] self.open_window () def detach (self): # TODO: If we take over deferred saving from the inspector, specify now # = True here! self.state.save () def run (self): try: Common.Main.MainLoopWrapper (gtk.main, gtk.main_quit).run () except: raise else: self.detach () def open_window (self): self.windows.append (Window (self)) def close_window (self, window): self.windows.remove (window) if not self.windows: # GtkTreeView takes some time to go down for large files. Let's block # until the window is hidden: gobject.idle_add (gtk.main_quit) gtk.main () gtk.main_quit () def main (options): args = options["args"] app = App () # TODO: Once we support more than one window, open one window for each # supplied filename. window = app.windows[0] if len (args) > 0: window.set_log_file (args[0]) app.run () if __name__ == "__main__": main ()