mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-09-20 02:50:16 +00:00
867a312f93
When the view was unfiltered, this crashed. When the view was range filtered, this copied the wrong line. Spotted by Stefan Kost.
2282 lines
68 KiB
Python
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 ()
|