gstreamer/debug-viewer/GstDebugViewer/GUI.py
René Stadler 867a312f93 GUI: Fix edit-copy-line action crashing/copying wrong line
When the view was unfiltered, this crashed.  When the view was range filtered,
this copied the wrong line.

Spotted by Stefan Kost.
2014-09-11 20:51:45 +02:00

2282 lines
68 KiB
Python

# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# 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 <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer GUI module."""
__author__ = u"René Stadler <mail@renestadler.de>"
__version__ = "0.1"
def _ (s):
return s
import sys
import os
import os.path
from operator import add
from sets import Set
from bisect import bisect_right, bisect_left
import logging
import pygtk
pygtk.require ("2.0")
import gobject
import gtk
import gtk.glade
from GstDebugViewer import Common, Data, Main
class Color (object):
def __init__ (self, hex_24):
if hex_24.startswith ("#"):
s = hex_24[1:]
else:
s = hex_24
self._fields = tuple ((int (hs, 16) for hs in (s[:2], s[2:4], s[4:],)))
def gdk_color (self):
return gtk.gdk.color_parse (self.hex_string ())
def hex_string (self):
return "#%02x%02x%02x" % self._fields
def float_tuple (self):
return tuple ((float (x) / 255 for x in self._fields))
def byte_tuple (self):
return self._fields
def short_tuple (self):
return tuple ((x << 8 for x in self._fields))
class ColorPalette (object):
@classmethod
def get (cls):
try:
return cls._instance
except AttributeError:
cls._instance = cls ()
return cls._instance
class TangoPalette (ColorPalette):
def __init__ (self):
for name, r, g, b in [("black", 0, 0, 0,),
("white", 255, 255, 255,),
("butter1", 252, 233, 79),
("butter2", 237, 212, 0),
("butter3", 196, 160, 0),
("chameleon1", 138, 226, 52),
("chameleon2", 115, 210, 22),
("chameleon3", 78, 154, 6),
("orange1", 252, 175, 62),
("orange2", 245, 121, 0),
("orange3", 206, 92, 0),
("skyblue1", 114, 159, 207),
("skyblue2", 52, 101, 164),
("skyblue3", 32, 74, 135),
("plum1", 173, 127, 168),
("plum2", 117, 80, 123),
("plum3", 92, 53, 102),
("chocolate1", 233, 185, 110),
("chocolate2", 193, 125, 17),
("chocolate3", 143, 89, 2),
("scarletred1", 239, 41, 41),
("scarletred2", 204, 0, 0),
("scarletred3", 164, 0, 0),
("aluminium1", 238, 238, 236),
("aluminium2", 211, 215, 207),
("aluminium3", 186, 189, 182),
("aluminium4", 136, 138, 133),
("aluminium5", 85, 87, 83),
("aluminium6", 46, 52, 54)]:
setattr (self, name, Color ("%02x%02x%02x" % (r, g, b,)))
class ColorTheme (object):
def __init__ (self):
self.colors = {}
def add_color (self, key, *colors):
self.colors[key] = colors
class LevelColorTheme (ColorTheme):
pass
class LevelColorThemeTango (LevelColorTheme):
def __init__ (self):
LevelColorTheme.__init__ (self)
p = TangoPalette.get ()
self.add_color (Data.debug_level_none,
None, None, None)
self.add_color (Data.debug_level_log,
p.black, p.plum1, Color ("#e0a4d9"))
self.add_color (Data.debug_level_debug,
p.black, p.skyblue1, Color ("#8cc4ff"))
self.add_color (Data.debug_level_info,
p.black, p.chameleon1, Color ("#9dff3b"))
self.add_color (Data.debug_level_warning,
p.black, p.orange1, Color ("#ffc266"))
self.add_color (Data.debug_level_error,
p.white, p.scarletred1, Color ("#ff4545"))
class ThreadColorTheme (ColorTheme):
pass
class ThreadColorThemeTango (ThreadColorTheme):
def __init__ (self):
ThreadColorTheme.__init__ (self)
t = TangoPalette.get ()
for i, color in enumerate ([t.butter2,
t.orange2,
t.chocolate3,
t.chameleon2,
t.skyblue1,
t.plum1,
t.scarletred1,
t.aluminium6]):
self.add_color (i, color)
class LogModelBase (gtk.GenericTreeModel):
__metaclass__ = Common.GUI.MetaModel
columns = ("COL_TIME", gobject.TYPE_UINT64,
"COL_PID", int,
"COL_THREAD", gobject.TYPE_UINT64,
"COL_LEVEL", object,
"COL_CATEGORY", str,
"COL_FILENAME", str,
"COL_LINE_NUMBER", int,
"COL_FUNCTION", str,
"COL_OBJECT", str,
"COL_MESSAGE", str,)
def __init__ (self):
gtk.GenericTreeModel.__init__ (self)
##self.props.leak_references = False
self.line_offsets = []
self.line_levels = [] # FIXME: Not so nice!
self.line_cache = {}
def ensure_cached (self, line_offset):
raise NotImplementedError ("derived classes must override this method")
def access_offset (self, offset):
raise NotImplementedError ("derived classes must override this method")
def iter_rows_offset (self):
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")
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"), "<Ctrl>N"),
("open-file", gtk.STOCK_OPEN, _("_Open File"), "<Ctrl>O"),
("reload-file", gtk.STOCK_REFRESH, _("_Reload File"), "<Ctrl>R"),
("close-window", gtk.STOCK_CLOSE, _("Close _Window"), "<Ctrl>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"), "<Ctrl>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 ())
glade_filename = os.path.join (Main.Paths.data_dir, "gst-debug-viewer.glade")
self.widget_factory = Common.GUI.WidgetFactory (glade_filename)
self.widgets = self.widget_factory.make ("main_window")
ui_filename = os.path.join (Main.Paths.data_dir,
"gst-debug-viewer.ui")
self.ui_factory = Common.GUI.UIFactory (ui_filename, self.actions)
self.ui_manager = ui = self.ui_factory.make ()
menubar = ui.get_widget ("/ui/menubar")
self.widgets.vbox_main.pack_start (menubar, False, False, 0)
self.gtk_window = self.widgets.main_window
self.gtk_window.add_accel_group (ui.get_accel_group ())
self.log_view = self.widgets.log_view
self.log_view.drag_dest_unset ()
self.log_view.set_search_column (-1)
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):
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")
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 ()