From d71d09759bc0d097de3510616b9ffe0232a83d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Stadler?= Date: Fri, 7 Aug 2009 02:54:10 +0300 Subject: [PATCH] Split giant GUI module into submodules --- debug-viewer/GstDebugViewer/GUI.py | 2259 ----------------- debug-viewer/GstDebugViewer/GUI/__init__.py | 46 + debug-viewer/GstDebugViewer/GUI/app.py | 114 + debug-viewer/GstDebugViewer/GUI/colors.py | 156 ++ debug-viewer/GstDebugViewer/GUI/columns.py | 648 +++++ debug-viewer/GstDebugViewer/GUI/filters.py | 63 + debug-viewer/GstDebugViewer/GUI/models.py | 639 +++++ debug-viewer/GstDebugViewer/GUI/window.py | 780 ++++++ .../GstDebugViewer/Plugins/FindBar.py | 2 +- .../GstDebugViewer/Plugins/Timeline.py | 7 +- 10 files changed, 2451 insertions(+), 2263 deletions(-) create mode 100644 debug-viewer/GstDebugViewer/GUI/__init__.py create mode 100644 debug-viewer/GstDebugViewer/GUI/app.py create mode 100644 debug-viewer/GstDebugViewer/GUI/colors.py create mode 100644 debug-viewer/GstDebugViewer/GUI/columns.py create mode 100644 debug-viewer/GstDebugViewer/GUI/filters.py create mode 100644 debug-viewer/GstDebugViewer/GUI/models.py create mode 100644 debug-viewer/GstDebugViewer/GUI/window.py diff --git a/debug-viewer/GstDebugViewer/GUI.py b/debug-viewer/GstDebugViewer/GUI.py index 35ea0234e9..e0f0d37023 100644 --- a/debug-viewer/GstDebugViewer/GUI.py +++ b/debug-viewer/GstDebugViewer/GUI.py @@ -17,2262 +17,3 @@ # 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.path -from bisect import bisect_right, bisect_left -import logging - -import pygtk -pygtk.require ("2.0") - -import gobject -import gtk - -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): - - ensure_cached = self.ensure_cached - line_cache = self.line_cache - line_levels = self.line_levels - COL_LEVEL = self.COL_LEVEL - - for i, offset in enumerate (self.line_offsets): - ensure_cached (offset) - row = line_cache[offset] - row[COL_LEVEL] = 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") - ## 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.filters.append (filter) - - self.__dispatcher = dispatcher - self.__active_process = self.__filter_process (filter) - dispatcher (self.__active_process) - - def abort_process (self): - - if self.__active_process is None: - raise ValueError ("no filter process running") - - self.__dispatcher.cancel () - self.__active_process = None - self.__dispatcher = None - - del self.filters[-1] - - 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 ObjectFilter (Filter): - - def __init__ (self, object_): - - col_id = LogModelBase.COL_OBJECT - def object_filter_func (row): - return row[col_id] != object_ - self.filter_func = object_filter_func - -class FilenameFilter (Filter): - - def __init__ (self, filename): - - col_id = LogModelBase.COL_FILENAME - def filename_filter_func (row): - return row[col_id] != filename - self.filter_func = filename_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 - log_model = self.log_view.props.model - top_index = line_model.line_index_to_top (line_index) - log_index = log_model.line_index_from_top (top_index) - path = (log_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] - - top_line_index = log_model.line_index_to_top (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_top (position - 1) - else: - other_index = -1 - if other_index == top_line_index and position != 1: - # Already have the line. - pass - else: - line_model.insert_line (position, top_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: - 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_top (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.ui", "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")), - ("hide-filename", None, _("Hide filename"))]) - group.props.sensitive = False - self.actions.add_group (group) - - self.actions.add_group (self.column_manager.action_group) - - self.log_file = None - self.setup_model (LazyLogModel ()) - - self.widget_factory = Common.GUI.WidgetFactory (Main.Paths.data_dir) - self.widgets = self.widget_factory.make ("main-window.ui", "main_window") - - ui_filename = os.path.join (Main.Paths.data_dir, "menus.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) - sel = self.log_view.get_selection () - sel.connect ("changed", self.handle_log_view_selection_changed) - - self.view_popup = ui.get_widget ("/ui/context/LogViewContextMenu").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", - "hide-filename", "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_log_view_selection_changed (self, selection): - - try: - line_index = self.get_active_line_index () - except ValueError: - first_selected = True - last_selected = True - else: - first_selected = (line_index == 0) - last_selected = (line_index == len (self.log_view.props.model) - 1) - - self.actions.hide_before_line.props.sensitive = not first_selected - self.actions.hide_after_line.props.sensitive = not last_selected - - 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): - - # TODO: Should probably copy the _exact_ line as taken from the file. - - line = self.get_active_line () - log_line = Data.LogLine (line) - self.clipboard.set_text (log_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): - - self.progress_dialog.destroy () - self.progress_dialog = None - - self.log_filter.abort_process () - self.log_view.props.model = self.log_filter - self.pop_view_state () - - 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): - - row = self.get_active_line () - object_ = row[LogModelBase.COL_OBJECT] - self.add_model_filter (ObjectFilter (object_)) - - def handle_hide_filename_action_activate (self, action): - - row = self.get_active_line () - filename = row[LogModelBase.COL_FILENAME] - self.add_model_filter (FilenameFilter (filename)) - - def handle_show_about_action_activate (self, action): - - from GstDebugViewer import version - - dialog = self.widget_factory.make_one ("about-dialog.ui", "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 () diff --git a/debug-viewer/GstDebugViewer/GUI/__init__.py b/debug-viewer/GstDebugViewer/GUI/__init__.py new file mode 100644 index 0000000000..a3e3dd3e39 --- /dev/null +++ b/debug-viewer/GstDebugViewer/GUI/__init__.py @@ -0,0 +1,46 @@ +# -*- 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" + +import pygtk +pygtk.require ("2.0") +del pygtk + +from GstDebugViewer.GUI.app import App + +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 () diff --git a/debug-viewer/GstDebugViewer/GUI/app.py b/debug-viewer/GstDebugViewer/GUI/app.py new file mode 100644 index 0000000000..bd53a359a5 --- /dev/null +++ b/debug-viewer/GstDebugViewer/GUI/app.py @@ -0,0 +1,114 @@ +# -*- 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.""" + +import os.path + +import gobject +import gtk + +from GstDebugViewer import Common +from GstDebugViewer.GUI.columns import ViewColumnManager +from GstDebugViewer.GUI.window import Window + +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 () diff --git a/debug-viewer/GstDebugViewer/GUI/colors.py b/debug-viewer/GstDebugViewer/GUI/colors.py new file mode 100644 index 0000000000..6e84093677 --- /dev/null +++ b/debug-viewer/GstDebugViewer/GUI/colors.py @@ -0,0 +1,156 @@ +# -*- 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.""" + +import gtk + +from GstDebugViewer import Data + +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) diff --git a/debug-viewer/GstDebugViewer/GUI/columns.py b/debug-viewer/GstDebugViewer/GUI/columns.py new file mode 100644 index 0000000000..522285d160 --- /dev/null +++ b/debug-viewer/GstDebugViewer/GUI/columns.py @@ -0,0 +1,648 @@ +# -*- 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.""" + +def _ (s): + return s + +import logging + +import gtk + +from GstDebugViewer import Common, Data +from GstDebugViewer.GUI.colors import LevelColorThemeTango +from GstDebugViewer.GUI.models import LazyLogModel, LogModelBase + +# 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 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 () diff --git a/debug-viewer/GstDebugViewer/GUI/filters.py b/debug-viewer/GstDebugViewer/GUI/filters.py new file mode 100644 index 0000000000..8367e0e3fe --- /dev/null +++ b/debug-viewer/GstDebugViewer/GUI/filters.py @@ -0,0 +1,63 @@ +# -*- 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.""" + +from GstDebugViewer.GUI.models import LogModelBase + +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 ObjectFilter (Filter): + + def __init__ (self, object_): + + col_id = LogModelBase.COL_OBJECT + def object_filter_func (row): + return row[col_id] != object_ + self.filter_func = object_filter_func + +class FilenameFilter (Filter): + + def __init__ (self, filename): + + col_id = LogModelBase.COL_FILENAME + def filename_filter_func (row): + return row[col_id] != filename + self.filter_func = filename_filter_func + diff --git a/debug-viewer/GstDebugViewer/GUI/models.py b/debug-viewer/GstDebugViewer/GUI/models.py new file mode 100644 index 0000000000..f3139de2d5 --- /dev/null +++ b/debug-viewer/GstDebugViewer/GUI/models.py @@ -0,0 +1,639 @@ +# -*- 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.""" + +from bisect import bisect_left +import logging + +import gobject +import gtk + +from GstDebugViewer import Common, Data + +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): + + ensure_cached = self.ensure_cached + line_cache = self.line_cache + line_levels = self.line_levels + COL_LEVEL = self.COL_LEVEL + + for i, offset in enumerate (self.line_offsets): + ensure_cached (offset) + row = line_cache[offset] + row[COL_LEVEL] = 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") + ## 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.filters.append (filter) + + self.__dispatcher = dispatcher + self.__active_process = self.__filter_process (filter) + dispatcher (self.__active_process) + + def abort_process (self): + + if self.__active_process is None: + raise ValueError ("no filter process running") + + self.__dispatcher.cancel () + self.__active_process = None + self.__dispatcher = None + + del self.filters[-1] + + 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 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 + +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) + diff --git a/debug-viewer/GstDebugViewer/GUI/window.py b/debug-viewer/GstDebugViewer/GUI/window.py new file mode 100644 index 0000000000..b8320d935a --- /dev/null +++ b/debug-viewer/GstDebugViewer/GUI/window.py @@ -0,0 +1,780 @@ +# -*- 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.""" + +def _ (s): + return s + +import os.path +from bisect import bisect_right, bisect_left +import logging + +import gobject +import gtk + +from GstDebugViewer import Common, Data, Main +from GstDebugViewer.GUI.columns import LineViewColumnManager, ViewColumnManager +from GstDebugViewer.GUI.filters import (CategoryFilter, + DebugLevelFilter, + FilenameFilter, + ObjectFilter) +from GstDebugViewer.GUI.models import (FilteredLogModel, + LazyLogModel, + LineViewLogModel, + LogModelBase, + RangeFilteredLogModel) + +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 + log_model = self.log_view.props.model + top_index = line_model.line_index_to_top (line_index) + log_index = log_model.line_index_from_top (top_index) + path = (log_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] + + top_line_index = log_model.line_index_to_top (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_top (position - 1) + else: + other_index = -1 + if other_index == top_line_index and position != 1: + # Already have the line. + pass + else: + line_model.insert_line (position, top_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: + 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_top (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.ui", "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")), + ("hide-filename", None, _("Hide filename"))]) + group.props.sensitive = False + self.actions.add_group (group) + + self.actions.add_group (self.column_manager.action_group) + + self.log_file = None + self.setup_model (LazyLogModel ()) + + self.widget_factory = Common.GUI.WidgetFactory (Main.Paths.data_dir) + self.widgets = self.widget_factory.make ("main-window.ui", "main_window") + + ui_filename = os.path.join (Main.Paths.data_dir, "menus.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) + sel = self.log_view.get_selection () + sel.connect ("changed", self.handle_log_view_selection_changed) + + self.view_popup = ui.get_widget ("/ui/context/LogViewContextMenu").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", + "hide-filename", "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_log_view_selection_changed (self, selection): + + try: + line_index = self.get_active_line_index () + except ValueError: + first_selected = True + last_selected = True + else: + first_selected = (line_index == 0) + last_selected = (line_index == len (self.log_view.props.model) - 1) + + self.actions.hide_before_line.props.sensitive = not first_selected + self.actions.hide_after_line.props.sensitive = not last_selected + + 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): + + # TODO: Should probably copy the _exact_ line as taken from the file. + + line = self.get_active_line () + log_line = Data.LogLine (line) + self.clipboard.set_text (log_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): + + self.progress_dialog.destroy () + self.progress_dialog = None + + self.log_filter.abort_process () + self.log_view.props.model = self.log_filter + self.pop_view_state () + + 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): + + row = self.get_active_line () + object_ = row[LogModelBase.COL_OBJECT] + self.add_model_filter (ObjectFilter (object_)) + + def handle_hide_filename_action_activate (self, action): + + row = self.get_active_line () + filename = row[LogModelBase.COL_FILENAME] + self.add_model_filter (FilenameFilter (filename)) + + def handle_show_about_action_activate (self, action): + + from GstDebugViewer import version + + dialog = self.widget_factory.make_one ("about-dialog.ui", "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) diff --git a/debug-viewer/GstDebugViewer/Plugins/FindBar.py b/debug-viewer/GstDebugViewer/Plugins/FindBar.py index 69ec9455db..fe913c4acd 100644 --- a/debug-viewer/GstDebugViewer/Plugins/FindBar.py +++ b/debug-viewer/GstDebugViewer/Plugins/FindBar.py @@ -36,7 +36,7 @@ class SearchOperation (object): self.search_forward = search_forward self.start_position = start_position - col_id = GUI.LogModelBase.COL_MESSAGE + col_id = GUI.models.LogModelBase.COL_MESSAGE len_search_text = len (search_text) def match_func (model_row): diff --git a/debug-viewer/GstDebugViewer/Plugins/Timeline.py b/debug-viewer/GstDebugViewer/Plugins/Timeline.py index c4e5c377b4..f95a8d9f81 100644 --- a/debug-viewer/GstDebugViewer/Plugins/Timeline.py +++ b/debug-viewer/GstDebugViewer/Plugins/Timeline.py @@ -21,7 +21,8 @@ import logging -from GstDebugViewer import Common, Data, GUI +from GstDebugViewer import Common, Data +from GstDebugViewer.GUI.colors import LevelColorThemeTango, ThreadColorThemeTango from GstDebugViewer.Plugins import * import gobject @@ -264,7 +265,7 @@ class VerticalTimelineWidget (gtk.DrawingArea): self.logger = logging.getLogger ("ui.vtimeline") self.log_view = log_view - self.theme = GUI.ThreadColorThemeTango () + self.theme = ThreadColorThemeTango () self.params = None self.thread_colors = {} self.next_thread_color = 0 @@ -558,7 +559,7 @@ class TimelineWidget (gtk.DrawingArea): self.logger.debug ("level distribution sentinel has no data yet") return - colors = GUI.LevelColorThemeTango ().colors + colors = LevelColorThemeTango ().colors dist_data = self.process.dist_sentinel.data def cumulative_level_counts (*levels):