Split giant GUI module into submodules

This commit is contained in:
René Stadler 2009-08-07 02:54:10 +03:00 committed by Stefan Sauer
parent 7b57fe2423
commit d71d09759b
10 changed files with 2451 additions and 2263 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,46 @@
# -*- 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"
import pygtk
pygtk.require ("2.0")
del pygtk
from GstDebugViewer.GUI.app import App
def main (options):
args = options["args"]
app = App ()
# TODO: Once we support more than one window, open one window for each
# supplied filename.
window = app.windows[0]
if len (args) > 0:
window.set_log_file (args[0])
app.run ()
if __name__ == "__main__":
main ()

View file

@ -0,0 +1,114 @@
# -*- 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."""
import os.path
import gobject
import gtk
from GstDebugViewer import Common
from GstDebugViewer.GUI.columns import ViewColumnManager
from GstDebugViewer.GUI.window import Window
class AppStateSection (Common.GUI.StateSection):
_name = "state"
geometry = Common.GUI.StateInt4 ("window-geometry")
maximized = Common.GUI.StateBool ("window-maximized")
column_order = Common.GUI.StateItemList ("column-order", ViewColumnManager)
columns_visible = Common.GUI.StateItemList ("columns-visible", ViewColumnManager)
class AppState (Common.GUI.State):
def __init__ (self, *a, **kw):
Common.GUI.State.__init__ (self, *a, **kw)
self.add_section_class (AppStateSection)
class App (object):
def __init__ (self):
self.attach ()
def load_plugins (self):
from GstDebugViewer import Plugins
plugin_classes = list (Plugins.load ([os.path.dirname (Plugins.__file__)]))
self.plugins = []
for plugin_class in plugin_classes:
plugin = plugin_class (self)
self.plugins.append (plugin)
def iter_plugin_features (self):
for plugin in self.plugins:
for feature in plugin.features:
yield feature
def attach (self):
config_home = Common.utils.XDG.CONFIG_HOME
state_filename = os.path.join (config_home, "gst-debug-viewer", "state")
self.state = AppState (state_filename)
self.state_section = self.state.sections["state"]
self.load_plugins ()
self.windows = []
self.open_window ()
def detach (self):
# TODO: If we take over deferred saving from the inspector, specify now
# = True here!
self.state.save ()
def run (self):
try:
Common.Main.MainLoopWrapper (gtk.main, gtk.main_quit).run ()
except:
raise
else:
self.detach ()
def open_window (self):
self.windows.append (Window (self))
def close_window (self, window):
self.windows.remove (window)
if not self.windows:
# GtkTreeView takes some time to go down for large files. Let's block
# until the window is hidden:
gobject.idle_add (gtk.main_quit)
gtk.main ()
gtk.main_quit ()

View file

@ -0,0 +1,156 @@
# -*- 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."""
import gtk
from GstDebugViewer import Data
class Color (object):
def __init__ (self, hex_24):
if hex_24.startswith ("#"):
s = hex_24[1:]
else:
s = hex_24
self._fields = tuple ((int (hs, 16) for hs in (s[:2], s[2:4], s[4:],)))
def gdk_color (self):
return gtk.gdk.color_parse (self.hex_string ())
def hex_string (self):
return "#%02x%02x%02x" % self._fields
def float_tuple (self):
return tuple ((float (x) / 255 for x in self._fields))
def byte_tuple (self):
return self._fields
def short_tuple (self):
return tuple ((x << 8 for x in self._fields))
class ColorPalette (object):
@classmethod
def get (cls):
try:
return cls._instance
except AttributeError:
cls._instance = cls ()
return cls._instance
class TangoPalette (ColorPalette):
def __init__ (self):
for name, r, g, b in [("black", 0, 0, 0,),
("white", 255, 255, 255,),
("butter1", 252, 233, 79),
("butter2", 237, 212, 0),
("butter3", 196, 160, 0),
("chameleon1", 138, 226, 52),
("chameleon2", 115, 210, 22),
("chameleon3", 78, 154, 6),
("orange1", 252, 175, 62),
("orange2", 245, 121, 0),
("orange3", 206, 92, 0),
("skyblue1", 114, 159, 207),
("skyblue2", 52, 101, 164),
("skyblue3", 32, 74, 135),
("plum1", 173, 127, 168),
("plum2", 117, 80, 123),
("plum3", 92, 53, 102),
("chocolate1", 233, 185, 110),
("chocolate2", 193, 125, 17),
("chocolate3", 143, 89, 2),
("scarletred1", 239, 41, 41),
("scarletred2", 204, 0, 0),
("scarletred3", 164, 0, 0),
("aluminium1", 238, 238, 236),
("aluminium2", 211, 215, 207),
("aluminium3", 186, 189, 182),
("aluminium4", 136, 138, 133),
("aluminium5", 85, 87, 83),
("aluminium6", 46, 52, 54)]:
setattr (self, name, Color ("%02x%02x%02x" % (r, g, b,)))
class ColorTheme (object):
def __init__ (self):
self.colors = {}
def add_color (self, key, *colors):
self.colors[key] = colors
class LevelColorTheme (ColorTheme):
pass
class LevelColorThemeTango (LevelColorTheme):
def __init__ (self):
LevelColorTheme.__init__ (self)
p = TangoPalette.get ()
self.add_color (Data.debug_level_none,
None, None, None)
self.add_color (Data.debug_level_log,
p.black, p.plum1, Color ("#e0a4d9"))
self.add_color (Data.debug_level_debug,
p.black, p.skyblue1, Color ("#8cc4ff"))
self.add_color (Data.debug_level_info,
p.black, p.chameleon1, Color ("#9dff3b"))
self.add_color (Data.debug_level_warning,
p.black, p.orange1, Color ("#ffc266"))
self.add_color (Data.debug_level_error,
p.white, p.scarletred1, Color ("#ff4545"))
class ThreadColorTheme (ColorTheme):
pass
class ThreadColorThemeTango (ThreadColorTheme):
def __init__ (self):
ThreadColorTheme.__init__ (self)
t = TangoPalette.get ()
for i, color in enumerate ([t.butter2,
t.orange2,
t.chocolate3,
t.chameleon2,
t.skyblue1,
t.plum1,
t.scarletred1,
t.aluminium6]):
self.add_color (i, color)

View file

@ -0,0 +1,648 @@
# -*- 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."""
def _ (s):
return s
import logging
import gtk
from GstDebugViewer import Common, Data
from GstDebugViewer.GUI.colors import LevelColorThemeTango
from GstDebugViewer.GUI.models import LazyLogModel, LogModelBase
# Sync with gst-inspector!
class Column (object):
"""A single list view column, managed by a ColumnManager instance."""
name = None
id = None
label_header = None
get_modify_func = None
get_data_func = None
get_row_data_func = None
get_sort_func = None
def __init__ (self):
view_column = gtk.TreeViewColumn (self.label_header)
view_column.props.reorderable = True
self.view_column = view_column
class SizedColumn (Column):
default_size = None
def compute_default_size (self, view, model):
return None
# Sync with gst-inspector?
class TextColumn (SizedColumn):
font_family = None
def __init__ (self):
Column.__init__ (self)
column = self.view_column
cell = gtk.CellRendererText ()
column.pack_start (cell)
cell.props.yalign = 0.
if self.font_family:
cell.props.family = self.font_family
cell.props.family_set = True
if self.get_data_func:
data_func = self.get_data_func ()
assert data_func
id_ = self.id
if id_ is not None:
def cell_data_func (column, cell, model, tree_iter):
data_func (cell.props, model.get_value (tree_iter, id_), model.get_path (tree_iter))
else:
cell_data_func = data_func
column.set_cell_data_func (cell, cell_data_func)
elif self.get_row_data_func:
data_func = self.get_row_data_func ()
assert data_func
def cell_data_func (column, cell, model, tree_iter):
data_func (cell.props, model[tree_iter])
column.set_cell_data_func (cell, cell_data_func)
elif not self.get_modify_func:
column.add_attribute (cell, "text", self.id)
else:
modify_func = self.get_modify_func ()
id_ = self.id
def cell_data_func (column, cell, model, tree_iter):
cell.props.text = modify_func (model.get (tree_iter, id_)[0])
column.set_cell_data_func (cell, cell_data_func)
column.props.resizable = True
def compute_default_size (self, view, model):
values = self.get_values_for_size ()
if not values:
return SizedColumn.compute_default_size (self, view, model)
cell = self.view_column.get_cell_renderers ()[0]
if self.get_modify_func is not None:
format = self.get_modify_func ()
else:
def identity (x):
return x
format = identity
max_width = 0
for value in values:
cell.props.text = format (value)
rect, x, y, w, h = self.view_column.cell_get_size ()
max_width = max (max_width, w)
return max_width
def get_values_for_size (self):
return ()
class TimeColumn (TextColumn):
name = "time"
label_header = _("Time")
id = LazyLogModel.COL_TIME
font_family = "monospace"
@staticmethod
def get_modify_func ():
time_args = Data.time_args
def format_time (value):
# TODO: This is hard coded to omit hours as well as the last 3
# digits at the end, since current gst uses g_get_current_time,
# which has microsecond precision only.
return time_args (value)[2:-3]
return format_time
def get_values_for_size (self):
values = [0]
return values
class LevelColumn (TextColumn):
name = "level"
label_header = _("L")
id = LazyLogModel.COL_LEVEL
def __init__ (self):
TextColumn.__init__ (self)
cell = self.view_column.get_cell_renderers ()[0]
cell.props.xalign = .5
@staticmethod
def get_modify_func ():
def format_level (value):
return value.name[0]
return format_level
@staticmethod
def get_data_func ():
theme = LevelColorThemeTango ()
colors = dict ((level, tuple ((c.gdk_color ()
for c in theme.colors[level])),)
for level in Data.debug_levels
if level != Data.debug_level_none)
def level_data_func (cell_props, level, path):
cell_props.text = level.name[0]
if level in colors:
cell_colors = colors[level]
else:
cell_colors = (None, None, None,)
cell_props.foreground_gdk = cell_colors[0]
if path[0] % 2:
cell_props.background_gdk = cell_colors[1]
else:
cell_props.background_gdk = cell_colors[2]
return level_data_func
def get_values_for_size (self):
values = [Data.debug_level_log, Data.debug_level_debug,
Data.debug_level_info, Data.debug_level_warning,
Data.debug_level_error]
return values
class PidColumn (TextColumn):
name = "pid"
label_header = _("PID")
id = LazyLogModel.COL_PID
font_family = "monospace"
@staticmethod
def get_modify_func ():
return str
def get_values_for_size (self):
return ["999999"]
class ThreadColumn (TextColumn):
name = "thread"
label_header = _("Thread")
id = LazyLogModel.COL_THREAD
font_family = "monospace"
@staticmethod
def get_modify_func ():
def format_thread (value):
return "0x%07x" % (value,)
return format_thread
def get_values_for_size (self):
return [int ("ffffff", 16)]
class CategoryColumn (TextColumn):
name = "category"
label_header = _("Category")
id = LazyLogModel.COL_CATEGORY
def get_values_for_size (self):
return ["GST_LONG_CATEGORY", "somelongelement"]
class CodeColumn (TextColumn):
name = "code"
label_header = _("Code")
id = None
@staticmethod
def get_data_func ():
filename_id = LogModelBase.COL_FILENAME
line_number_id = LogModelBase.COL_LINE_NUMBER
def filename_data_func (column, cell, model, tree_iter):
args = model.get (tree_iter, filename_id, line_number_id)
cell.props.text = "%s:%i" % args
return filename_data_func
def get_values_for_size (self):
return ["gstsomefilename.c:1234"]
class FunctionColumn (TextColumn):
name = "function"
label_header = _("Function")
id = LazyLogModel.COL_FUNCTION
def get_values_for_size (self):
return ["gst_this_should_be_enough"]
class ObjectColumn (TextColumn):
name = "object"
label_header = _("Object")
id = LazyLogModel.COL_OBJECT
def get_values_for_size (self):
return ["longobjectname00"]
class MessageColumn (TextColumn):
name = "message"
label_header = _("Message")
id = LazyLogModel.COL_MESSAGE
def __init__ (self, *a, **kw):
self.highlighters = {}
TextColumn.__init__ (self, *a, **kw)
def get_row_data_func (self):
from pango import AttrList, AttrBackground, AttrForeground
highlighters = self.highlighters
id_ = self.id
# FIXME: This should be none; need to investigate
# `cellrenderertext.props.attributes = None' failure (param conversion
# error like `treeview.props.model = None').
no_attrs = AttrList ()
def message_data_func (props, row):
props.text = row[id_]
if not highlighters:
props.attributes = no_attrs
for highlighter in highlighters.values ():
ranges = highlighter (row)
if not ranges:
props.attributes = no_attrs
else:
attrlist = AttrList ()
for start, end in ranges:
attrlist.insert (AttrBackground (0, 0, 65535, start, end))
attrlist.insert (AttrForeground (65535, 65535, 65535, start, end))
props.attributes = attrlist
return message_data_func
def get_values_for_size (self):
values = ["Just some good minimum size"]
return values
class ColumnManager (Common.GUI.Manager):
column_classes = ()
@classmethod
def iter_item_classes (cls):
return iter (cls.column_classes)
def __init__ (self):
self.view = None
self.actions = None
self.__columns_changed_id = None
self.columns = []
self.column_order = list (self.column_classes)
self.action_group = gtk.ActionGroup ("ColumnActions")
def make_entry (col_class):
return ("show-%s-column" % (col_class.name,),
None,
col_class.label_header,
None,
None,
None,
True,)
entries = [make_entry (cls) for cls in self.column_classes]
self.action_group.add_toggle_actions (entries)
def iter_items (self):
return iter (self.columns)
def attach (self):
for col_class in self.column_classes:
action = self.get_toggle_action (col_class)
if action.props.active:
self._add_column (col_class ())
action.connect ("toggled",
self.__handle_show_column_action_toggled,
col_class.name)
self.__columns_changed_id = self.view.connect ("columns-changed",
self.__handle_view_columns_changed)
def detach (self):
if self.__columns_changed_id is not None:
self.view.disconnect (self.__columns_changed_id)
self.__columns_changed_id = None
def attach_sort (self):
sort_model = self.view.props.model
# Inform the sorted tree model of any custom sorting functions.
for col_class in self.column_classes:
if col_class.get_sort_func:
sort_func = col_class.get_sort_func ()
sort_model.set_sort_func (col_class.id, sort_func)
def enable_sort (self):
sort_model = self.view.props.model
if sort_model:
self.logger.debug ("activating sort")
sort_model.set_sort_column_id (*self.default_sort)
self.default_sort = None
else:
self.logger.debug ("not activating sort (no model set)")
def disable_sort (self):
self.logger.debug ("deactivating sort")
sort_model = self.view.props.model
self.default_sort = tree_sortable_get_sort_column_id (sort_model)
sort_model.set_sort_column_id (TREE_SORTABLE_UNSORTED_COLUMN_ID,
gtk.SORT_ASCENDING)
def get_toggle_action (self, column_class):
action_name = "show-%s-column" % (column_class.name,)
return self.action_group.get_action (action_name)
def get_initial_column_order (self):
return tuple (self.column_classes)
def _add_column (self, column):
name = column.name
pos = self.__get_column_insert_position (column)
if self.view.props.fixed_height_mode:
column.view_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
self.columns.insert (pos, column)
self.view.insert_column (column.view_column, pos)
def _remove_column (self, column):
self.columns.remove (column)
self.view.remove_column (column.view_column)
def __get_column_insert_position (self, column):
col_class = self.find_item_class (name = column.name)
pos = self.column_order.index (col_class)
before = self.column_order[:pos]
shown_names = [col.name for col in self.columns]
for col_class in before:
if not col_class.name in shown_names:
pos -= 1
return pos
def __iter_next_hidden (self, column_class):
pos = self.column_order.index (column_class)
rest = self.column_order[pos + 1:]
for next_class in rest:
try:
self.find_item (name = next_class.name)
except KeyError:
# No instance -- the column is hidden.
yield next_class
else:
break
def __handle_show_column_action_toggled (self, toggle_action, name):
if toggle_action.props.active:
try:
# This should fail.
column = self.find_item (name = name)
except KeyError:
col_class = self.find_item_class (name = name)
self._add_column (col_class ())
else:
# Out of sync for some reason.
return
else:
try:
column = self.find_item (name = name)
except KeyError:
# Out of sync for some reason.
return
else:
self._remove_column (column)
def __handle_view_columns_changed (self, element_view):
view_columns = element_view.get_columns ()
new_visible = [self.find_item (view_column = column)
for column in view_columns]
# We only care about reordering here.
if len (new_visible) != len (self.columns):
return
if new_visible != self.columns:
new_order = []
for column in new_visible:
col_class = self.find_item_class (name = column.name)
new_order.append (col_class)
new_order.extend (self.__iter_next_hidden (col_class))
names = (column.name for column in new_visible)
self.logger.debug ("visible columns reordered: %s",
", ".join (names))
self.columns[:] = new_visible
self.column_order[:] = new_order
class ViewColumnManager (ColumnManager):
column_classes = (TimeColumn, LevelColumn, PidColumn, ThreadColumn, CategoryColumn,
CodeColumn, FunctionColumn, ObjectColumn, MessageColumn,)
def __init__ (self, state):
ColumnManager.__init__ (self)
self.logger = logging.getLogger ("ui.columns")
self.state = state
def attach (self, view):
self.view = view
view.connect ("notify::model", self.__handle_notify_model)
order = self.state.column_order
if len (order) == len (self.column_classes):
self.column_order[:] = order
visible = self.state.columns_visible
if not visible:
visible = self.column_classes
for col_class in self.column_classes:
action = self.get_toggle_action (col_class)
action.props.active = (col_class in visible)
ColumnManager.attach (self)
self.columns_sized = False
def detach (self):
self.state.column_order = self.column_order
self.state.columns_visible = self.columns
return ColumnManager.detach (self)
def size_column (self, column, view, model):
if column.default_size is None:
default_size = column.compute_default_size (view, model)
else:
default_size = column.default_size
# FIXME: Abstract away fixed size setting in Column class!
if default_size is None:
# Dummy fallback:
column.view_column.props.fixed_width = 50
self.logger.warning ("%s column does not implement default size", column.name)
else:
column.view_column.props.fixed_width = default_size
def _add_column (self, column):
result = ColumnManager._add_column (self, column)
model = self.view.props.model
self.size_column (column, self.view, model)
return result
def _remove_column (self, column):
column.default_size = column.view_column.props.fixed_width
return ColumnManager._remove_column (self, column)
def __handle_notify_model (self, view, gparam):
if self.columns_sized:
# Already sized.
return
model = self.view.props.model
if model is None:
return
self.logger.debug ("model changed, sizing columns")
for column in self.iter_items ():
self.size_column (column, view, model)
self.columns_sized = True
class WrappingMessageColumn (MessageColumn):
def wrap_to_width (self, width):
col = self.view_column
col.props.max_width = width
col.get_cell_renderers ()[0].props.wrap_width = width
col.queue_resize ()
class LineViewColumnManager (ColumnManager):
column_classes = (TimeColumn, WrappingMessageColumn,)
def __init__ (self):
ColumnManager.__init__ (self)
def attach (self, window):
self.__size_update = None
self.view = window.widgets.line_view
self.view.set_size_request (0, 0)
self.view.connect_after ("size-allocate", self.__handle_size_allocate)
ColumnManager.attach (self)
def __update_sizes (self):
view_width = self.view.get_allocation ().width
if view_width == self.__size_update:
# Prevent endless recursion.
return
self.__size_update = view_width
col = self.find_item (name = "time")
other_width = col.view_column.props.width
try:
col = self.find_item (name = "message")
except KeyError:
return
width = view_width - other_width
col.wrap_to_width (width)
def __handle_size_allocate (self, self_, allocation):
self.__update_sizes ()

View file

@ -0,0 +1,63 @@
# -*- 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."""
from GstDebugViewer.GUI.models import LogModelBase
class Filter (object):
pass
class DebugLevelFilter (Filter):
def __init__ (self, debug_level):
col_id = LogModelBase.COL_LEVEL
def filter_func (row):
return row[col_id] != debug_level
self.filter_func = filter_func
class CategoryFilter (Filter):
def __init__ (self, category):
col_id = LogModelBase.COL_CATEGORY
def category_filter_func (row):
return row[col_id] != category
self.filter_func = category_filter_func
class ObjectFilter (Filter):
def __init__ (self, object_):
col_id = LogModelBase.COL_OBJECT
def object_filter_func (row):
return row[col_id] != object_
self.filter_func = object_filter_func
class FilenameFilter (Filter):
def __init__ (self, filename):
col_id = LogModelBase.COL_FILENAME
def filename_filter_func (row):
return row[col_id] != filename
self.filter_func = filename_filter_func

View file

@ -0,0 +1,639 @@
# -*- 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."""
from bisect import bisect_left
import logging
import gobject
import gtk
from GstDebugViewer import Common, Data
class LogModelBase (gtk.GenericTreeModel):
__metaclass__ = Common.GUI.MetaModel
columns = ("COL_TIME", gobject.TYPE_UINT64,
"COL_PID", int,
"COL_THREAD", gobject.TYPE_UINT64,
"COL_LEVEL", object,
"COL_CATEGORY", str,
"COL_FILENAME", str,
"COL_LINE_NUMBER", int,
"COL_FUNCTION", str,
"COL_OBJECT", str,
"COL_MESSAGE", str,)
def __init__ (self):
gtk.GenericTreeModel.__init__ (self)
##self.props.leak_references = False
self.line_offsets = []
self.line_levels = [] # FIXME: Not so nice!
self.line_cache = {}
def ensure_cached (self, line_offset):
raise NotImplementedError ("derived classes must override this method")
def access_offset (self, offset):
raise NotImplementedError ("derived classes must override this method")
def iter_rows_offset (self):
ensure_cached = self.ensure_cached
line_cache = self.line_cache
line_levels = self.line_levels
COL_LEVEL = self.COL_LEVEL
for i, offset in enumerate (self.line_offsets):
ensure_cached (offset)
row = line_cache[offset]
row[COL_LEVEL] = line_levels[i] # FIXME
yield (row, offset,)
def on_get_flags (self):
flags = gtk.TREE_MODEL_LIST_ONLY | gtk.TREE_MODEL_ITERS_PERSIST
return flags
def on_get_n_columns (self):
return len (self.column_types)
def on_get_column_type (self, col_id):
return self.column_types[col_id]
def on_get_iter (self, path):
if not path:
return
if len (path) > 1:
# Flat model.
return None
line_index = path[0]
if line_index > len (self.line_offsets) - 1:
return None
return line_index
def on_get_path (self, rowref):
line_index = rowref
return (line_index,)
def on_get_value (self, line_index, col_id):
last_index = len (self.line_offsets) - 1
if line_index > last_index:
return None
if col_id == self.COL_LEVEL:
return self.line_levels[line_index]
line_offset = self.line_offsets[line_index]
self.ensure_cached (line_offset)
value = self.line_cache[line_offset][col_id]
if col_id == self.COL_MESSAGE:
message_offset = value
value = self.access_offset (line_offset + message_offset).strip ()
return value
def on_iter_next (self, line_index):
last_index = len (self.line_offsets) - 1
if line_index >= last_index:
return None
else:
return line_index + 1
def on_iter_children (self, parent):
return self.on_iter_nth_child (parent, 0)
def on_iter_has_child (self, rowref):
return False
def on_iter_n_children (self, rowref):
if rowref is not None:
return 0
return len (self.line_offsets)
def on_iter_nth_child (self, parent, n):
last_index = len (self.line_offsets) - 1
if parent or n > last_index:
return None
return n
def on_iter_parent (self, child):
return None
## def on_ref_node (self, rowref):
## pass
## def on_unref_node (self, rowref):
## pass
class LazyLogModel (LogModelBase):
def __init__ (self, log_obj = None):
LogModelBase.__init__ (self)
self.__log_obj = log_obj
if log_obj:
self.set_log (log_obj)
def set_log (self, log_obj):
self.__fileobj = log_obj.fileobj
self.line_cache.clear ()
self.line_offsets = log_obj.line_cache.offsets
self.line_levels = log_obj.line_cache.levels
def access_offset (self, offset):
# TODO: Implement using one slice access instead of seek+readline.
self.__fileobj.seek (offset)
return self.__fileobj.readline ()
def ensure_cached (self, line_offset):
if line_offset in self.line_cache:
return
if len (self.line_cache) > 10000:
self.line_cache.clear ()
self.__fileobj.seek (line_offset)
line = self.__fileobj.readline ()
self.line_cache[line_offset] = Data.LogLine.parse_full (line)
class FilteredLogModelBase (LogModelBase):
def __init__ (self, super_model):
LogModelBase.__init__ (self)
self.logger = logging.getLogger ("filter-model-base")
self.super_model = super_model
self.access_offset = super_model.access_offset
self.ensure_cached = super_model.ensure_cached
self.line_cache = super_model.line_cache
def line_index_to_super (self, line_index):
raise NotImplementedError ("index conversion not supported")
def line_index_from_super (self, super_line_index):
raise NotImplementedError ("index conversion not supported")
def line_index_to_top (self, line_index):
_log_indices = [line_index]
super_index = line_index
for model in self._iter_hierarchy ():
super_index = model.line_index_to_super (super_index)
_log_indices.append (super_index)
_log_trans = " -> ".join ([str (x) for x in _log_indices])
self.logger.debug ("translated index to top: %s", _log_trans)
return super_index
def line_index_from_top (self, super_index):
_log_indices = [super_index]
line_index = super_index
for model in reversed (list (self._iter_hierarchy ())):
line_index = model.line_index_from_super (line_index)
_log_indices.append (line_index)
_log_trans = " -> ".join ([str (x) for x in _log_indices])
self.logger.debug ("translated index from top: %s", _log_trans)
return line_index
def super_model_changed (self):
pass
def _iter_hierarchy (self):
model = self
while hasattr (model, "super_model") and model.super_model:
yield model
model = model.super_model
class FilteredLogModelIdentity (FilteredLogModelBase):
def __init__ (self, super_model):
FilteredLogModelBase.__init__ (self, super_model)
self.line_offsets = self.super_model.line_offsets
self.line_levels = self.super_model.line_levels
def line_index_from_super (self, super_line_index):
return super_line_index
def line_index_to_super (self, line_index):
return line_index
class FilteredLogModel (FilteredLogModelBase):
def __init__ (self, super_model):
FilteredLogModelBase.__init__ (self, super_model)
self.logger = logging.getLogger ("filtered-log-model")
self.filters = []
self.super_index = []
self.from_super_index = {}
self.reset ()
self.__active_process = None
self.__filter_progress = 0.
self.__old_super_model_range = super_model.line_index_range
def reset (self):
del self.line_offsets[:]
self.line_offsets += self.super_model.line_offsets
del self.line_levels[:]
self.line_levels += self.super_model.line_levels
del self.super_index[:]
self.from_super_index.clear ()
del self.filters[:]
def __filter_process (self, filter):
YIELD_LIMIT = 10000
self.logger.debug ("preparing new filter")
## del self.line_offsets[:]
## del self.line_levels[:]
new_line_offsets = []
new_line_levels = []
new_super_index = []
new_from_super_index = {}
level_id = self.COL_LEVEL
func = filter.filter_func
if len (self.filters) == 1:
# This is the first filter that gets applied.
def enum ():
i = 0
for row, offset in self.iter_rows_offset ():
yield (i, row, offset,)
i += 1
else:
def enum ():
i = 0
for row, offset in self.iter_rows_offset ():
line_index = self.super_index[i]
yield (line_index, row, offset,)
i += 1
self.logger.debug ("running filter")
progress = 0.
progress_full = float (len (self))
y = YIELD_LIMIT
for i, row, offset in enum ():
if func (row):
new_line_offsets.append (offset)
new_line_levels.append (row[level_id])
new_super_index.append (i)
new_from_super_index[i] = len (new_super_index) - 1
y -= 1
if y == 0:
progress += float (YIELD_LIMIT)
self.__filter_progress = progress / progress_full
y = YIELD_LIMIT
yield True
self.line_offsets = new_line_offsets
self.line_levels = new_line_levels
self.super_index = new_super_index
self.from_super_index = new_from_super_index
self.logger.debug ("filtering finished")
self.__filter_progress = 1.
self.__handle_filter_process_finished ()
yield False
def add_filter (self, filter, dispatcher):
if self.__active_process is not None:
raise ValueError ("dispatched a filter process already")
self.filters.append (filter)
self.__dispatcher = dispatcher
self.__active_process = self.__filter_process (filter)
dispatcher (self.__active_process)
def abort_process (self):
if self.__active_process is None:
raise ValueError ("no filter process running")
self.__dispatcher.cancel ()
self.__active_process = None
self.__dispatcher = None
del self.filters[-1]
def get_filter_progress (self):
if self.__active_process is None:
raise ValueError ("no filter process running")
return self.__filter_progress
def __handle_filter_process_finished (self):
self.__active_process = None
self.handle_process_finished ()
def handle_process_finished (self):
pass
def line_index_from_super (self, super_line_index):
if len (self.filters) == 0:
# Identity.
return super_line_index
try:
return self.from_super_index[super_line_index]
except KeyError:
raise IndexError ("super index %i not handled" % (super_line_index,))
def line_index_to_super (self, line_index):
if len (self.filters) == 0:
# Identity.
return line_index
return self.super_index[line_index]
def __filtered_indices_in_range (self, start, stop):
if start < 0:
raise ValueError ("start cannot be negative (got %r)" % (start,))
super_start = bisect_left (self.super_index, start)
super_stop = bisect_left (self.super_index, stop)
return super_stop - super_start
def super_model_changed_range (self):
range_model = self.super_model
old_start, old_stop = self.__old_super_model_range
super_start, super_stop = range_model.line_index_range
super_start_offset = super_start - old_start
if super_start_offset < 0:
# TODO:
raise NotImplementedError ("Only handling further restriction of the range"
" (start offset = %i)" % (super_start_offset,))
super_end_offset = super_stop - old_stop
if super_end_offset > 0:
# TODO:
raise NotImplementedError ("Only handling further restriction of the range"
" (end offset = %i)" % (super_end_offset,))
if super_end_offset < 0:
if not self.super_index:
# Identity; there are no filters.
end_offset = len (self.line_offsets) + super_end_offset
else:
n_filtered = self.__filtered_indices_in_range (super_stop - super_start,
old_stop - super_start)
end_offset = len (self.line_offsets) - n_filtered
stop = len (self.line_offsets) # FIXME?
assert end_offset < stop
self.__remove_range (end_offset, stop)
if super_start_offset > 0:
if not self.super_index:
# Identity; there are no filters.
n_filtered = super_start_offset
start_offset = n_filtered
else:
n_filtered = self.__filtered_indices_in_range (0, super_start_offset)
start_offset = n_filtered
if n_filtered > 0:
self.__remove_range (0, start_offset)
from_super = self.from_super_index
for i in self.super_index:
old_index = from_super[i]
del from_super[i]
from_super[i - super_start_offset] = old_index - start_offset
for i in range (len (self.super_index)):
self.super_index[i] -= super_start_offset
self.__old_super_model_range = (super_start, super_stop,)
def __remove_range (self, start, stop):
if start < 0:
raise ValueError ("start cannot be negative (got %r)" % (start,))
if start == stop:
return
if stop > len (self.line_offsets):
raise ValueError ("stop value out of range (got %r)" % (stop,))
if start > stop:
raise ValueError ("start cannot be greater than stop (got %r, %r)" % (start, stop,))
self.logger.debug ("removing line range (%i, %i)",
start, stop)
del self.line_offsets[start:stop]
del self.line_levels[start:stop]
for super_index in self.super_index[start:stop]:
del self.from_super_index[super_index]
del self.super_index[start:stop]
class SubRange (object):
__slots__ = ("l", "start", "stop",)
def __init__ (self, l, start, stop):
if start > stop:
raise ValueError ("need start <= stop (got %r, %r)" % (start, stop,))
self.l = l
self.start = start
self.stop = stop
def __getitem__ (self, i):
return self.l[i + self.start]
def __len__ (self):
return self.stop - self.start
def __iter__ (self):
l = self.l
for i in xrange (self.start, self.stop):
yield l[i]
class RangeFilteredLogModel (FilteredLogModelBase):
def __init__ (self, super_model):
FilteredLogModelBase.__init__ (self, super_model)
self.logger = logging.getLogger ("range-filtered-model")
self.line_index_range = None
def set_range (self, start_index, stop_index):
self.logger.debug ("setting range to start = %i, stop = %i",
start_index, stop_index)
self.line_index_range = (start_index, stop_index,)
self.line_offsets = SubRange (self.super_model.line_offsets,
start_index, stop_index)
self.line_levels = SubRange (self.super_model.line_levels,
start_index, stop_index)
def reset (self):
self.logger.debug ("reset")
start_index = 0
stop_index = len (self.super_model)
self.set_range (start_index, stop_index,)
def line_index_to_super (self, line_index):
start_index = self.line_index_range[0]
return line_index + start_index
def line_index_from_super (self, li):
start, stop = self.line_index_range
if li < start or li >= stop:
raise IndexError ("not in range")
return li - start
class LineViewLogModel (FilteredLogModelBase):
def __init__ (self, super_model):
FilteredLogModelBase.__init__ (self, super_model)
self.line_offsets = []
self.line_levels = []
self.parent_indices = []
def reset (self):
del self.line_offsets[:]
del self.line_levels[:]
def line_index_to_super (self, line_index):
return self.parent_indices[line_index]
def insert_line (self, position, super_line_index):
if position == -1:
position = len (self.line_offsets)
li = super_line_index
self.line_offsets.insert (position, self.super_model.line_offsets[li])
self.line_levels.insert (position, self.super_model.line_levels[li])
self.parent_indices.insert (position, super_line_index)
path = (position,)
tree_iter = self.get_iter (path)
self.row_inserted (path, tree_iter)
def replace_line (self, line_index, super_line_index):
li = line_index
self.line_offsets[li] = self.super_model.line_offsets[super_line_index]
self.line_levels[li] = self.super_model.line_levels[super_line_index]
self.parent_indices[li] = super_line_index
path = (line_index,)
tree_iter = self.get_iter (path)
self.row_changed (path, tree_iter)
def remove_line (self, line_index):
for l in (self.line_offsets,
self.line_levels,
self.parent_indices,):
del l[line_index]
path = (line_index,)
self.row_deleted (path)

View file

@ -0,0 +1,780 @@
# -*- 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."""
def _ (s):
return s
import os.path
from bisect import bisect_right, bisect_left
import logging
import gobject
import gtk
from GstDebugViewer import Common, Data, Main
from GstDebugViewer.GUI.columns import LineViewColumnManager, ViewColumnManager
from GstDebugViewer.GUI.filters import (CategoryFilter,
DebugLevelFilter,
FilenameFilter,
ObjectFilter)
from GstDebugViewer.GUI.models import (FilteredLogModel,
LazyLogModel,
LineViewLogModel,
LogModelBase,
RangeFilteredLogModel)
class LineView (object):
def __init__ (self):
self.column_manager = LineViewColumnManager ()
def attach (self, window):
self.clear_action = window.actions.clear_line_view
handler = self.handle_clear_line_view_action_activate
self.clear_action.connect ("activate", handler)
self.line_view = window.widgets.line_view
self.line_view.connect ("row-activated", self.handle_line_view_row_activated)
ui = window.ui_manager
self.popup = ui.get_widget ("/ui/context/LineViewContextMenu").get_submenu ()
Common.GUI.widget_add_popup_menu (self.line_view, self.popup)
self.log_view = log_view = window.log_view
log_view.connect ("row-activated", self.handle_log_view_row_activated)
sel = log_view.get_selection ()
sel.connect ("changed", self.handle_log_view_selection_changed)
self.clear_action.props.sensitive = False
self.column_manager.attach (window)
def clear (self):
model = self.line_view.props.model
if len (model) == 0:
return
for i in range (1, len (model)):
model.remove_line (1)
self.clear_action.props.sensitive = False
def handle_attach_log_file (self, window):
self.line_view.props.model = LineViewLogModel (window.log_model)
def handle_line_view_row_activated (self, view, path, column):
line_index = path[0]
line_model = view.props.model
log_model = self.log_view.props.model
top_index = line_model.line_index_to_top (line_index)
log_index = log_model.line_index_from_top (top_index)
path = (log_index,)
self.log_view.scroll_to_cell (path, use_align = True, row_align = .5)
sel = self.log_view.get_selection ()
sel.select_path (path)
def handle_log_view_row_activated (self, view, path, column):
log_model = view.props.model
line_index = path[0]
top_line_index = log_model.line_index_to_top (line_index)
line_model = self.line_view.props.model
if line_model is None:
return
if len (line_model):
timestamps = [row[line_model.COL_TIME] for row in line_model]
row = log_model[(line_index,)]
position = bisect_right (timestamps, row[line_model.COL_TIME])
else:
position = 0
if len (line_model) > 1:
other_index = line_model.line_index_to_top (position - 1)
else:
other_index = -1
if other_index == top_line_index and position != 1:
# Already have the line.
pass
else:
line_model.insert_line (position, top_line_index)
self.clear_action.props.sensitive = True
def handle_log_view_selection_changed (self, selection):
line_model = self.line_view.props.model
if line_model is None:
return
model, tree_iter = selection.get_selected ()
if tree_iter is None:
return
path = model.get_path (tree_iter)
line_index = model.line_index_to_top (path[0])
if len (line_model) == 0:
line_model.insert_line (0, line_index)
else:
line_model.replace_line (0, line_index)
def handle_clear_line_view_action_activate (self, action):
self.clear ()
class ProgressDialog (object):
def __init__ (self, window, title = ""):
widgets = window.widget_factory.make ("progress-dialog.ui", "progress_dialog")
dialog = widgets.progress_dialog
dialog.connect ("response", self.__handle_dialog_response)
self.__dialog = dialog
self.__progress_bar = widgets.progress_bar
self.__progress_bar.props.text = title
dialog.set_transient_for (window.gtk_window)
dialog.show ()
def __handle_dialog_response (self, dialog, resp):
self.handle_cancel ()
def handle_cancel (self):
pass
def update (self, progress):
if self.__progress_bar is None:
return
self.__progress_bar.props.fraction = progress
def destroy (self):
if self.__dialog is None:
return
self.__dialog.destroy ()
self.__dialog = None
self.__progress_bar = None
class Window (object):
def __init__ (self, app):
self.logger = logging.getLogger ("ui.window")
self.app = app
self.dispatcher = None
self.progress_dialog = None
self.update_progress_id = None
self.window_state = Common.GUI.WindowState ()
self.column_manager = ViewColumnManager (app.state_section)
self.actions = Common.GUI.Actions ()
group = gtk.ActionGroup ("MenuActions")
group.add_actions ([("FileMenuAction", None, _("_File")),
("ViewMenuAction", None, _("_View")),
("ViewColumnsMenuAction", None, _("_Columns")),
("HelpMenuAction", None, _("_Help")),
("LineViewContextMenuAction", None, "")])
self.actions.add_group (group)
group = gtk.ActionGroup ("WindowActions")
group.add_actions ([("new-window", gtk.STOCK_NEW, _("_New Window"), "<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 ())
self.widget_factory = Common.GUI.WidgetFactory (Main.Paths.data_dir)
self.widgets = self.widget_factory.make ("main-window.ui", "main_window")
ui_filename = os.path.join (Main.Paths.data_dir, "menus.ui")
self.ui_factory = Common.GUI.UIFactory (ui_filename, self.actions)
self.ui_manager = ui = self.ui_factory.make ()
menubar = ui.get_widget ("/ui/menubar")
self.widgets.vbox_main.pack_start (menubar, False, False, 0)
self.gtk_window = self.widgets.main_window
self.gtk_window.add_accel_group (ui.get_accel_group ())
self.log_view = self.widgets.log_view
self.log_view.drag_dest_unset ()
self.log_view.set_search_column (-1)
sel = self.log_view.get_selection ()
sel.connect ("changed", self.handle_log_view_selection_changed)
self.view_popup = ui.get_widget ("/ui/context/LogViewContextMenu").get_submenu ()
Common.GUI.widget_add_popup_menu (self.log_view, self.view_popup)
self.line_view = LineView ()
self.attach ()
self.column_manager.attach (self.log_view)
def setup_model (self, model, filter = False):
self.log_model = model
self.log_range = RangeFilteredLogModel (self.log_model)
if filter:
self.log_filter = FilteredLogModel (self.log_range)
self.log_filter.handle_process_finished = self.handle_log_filter_process_finished
else:
self.log_filter = None
def get_top_attach_point (self):
return self.widgets.vbox_main
def get_side_attach_point (self):
return self.widgets.hbox_view
def attach (self):
self.window_state.attach (window = self.gtk_window,
state = self.app.state_section)
self.clipboard = gtk.Clipboard (self.gtk_window.get_display (),
gtk.gdk.SELECTION_CLIPBOARD)
for action_name in ("new-window", "open-file", "reload-file",
"close-window", "cancel-load",
"hide-before-line", "hide-after-line", "show-hidden-lines",
"edit-copy-line", "edit-copy-message",
"hide-log-level", "hide-log-category", "hide-log-object",
"hide-filename", "show-about",):
name = action_name.replace ("-", "_")
action = getattr (self.actions, name)
handler = getattr (self, "handle_%s_action_activate" % (name,))
action.connect ("activate", handler)
self.gtk_window.connect ("delete-event", self.handle_window_delete_event)
self.features = []
for plugin_feature in self.app.iter_plugin_features ():
feature = plugin_feature (self.app)
self.features.append (feature)
for feature in self.features:
feature.handle_attach_window (self)
# FIXME: With multiple selection mode, browsing the list with key
# up/down slows to a crawl! WTF is wrong with this stupid widget???
sel = self.log_view.get_selection ()
sel.set_mode (gtk.SELECTION_BROWSE)
self.line_view.attach (self)
self.gtk_window.show ()
def detach (self):
self.set_log_file (None)
for feature in self.features:
feature.handle_detach_window (self)
self.window_state.detach ()
self.column_manager.detach ()
def get_active_line_index (self):
selection = self.log_view.get_selection ()
model, tree_iter = selection.get_selected ()
if tree_iter is None:
raise ValueError ("no line selected")
path = model.get_path (tree_iter)
return path[0]
def get_active_line (self):
selection = self.log_view.get_selection ()
model, tree_iter = selection.get_selected ()
if tree_iter is None:
raise ValueError ("no line selected")
model = self.log_view.props.model
return model.get (tree_iter, *LogModelBase.column_ids)
def close (self, *a, **kw):
self.logger.debug ("closing window, detaching")
self.detach ()
self.gtk_window.hide ()
self.logger.debug ("requesting close from app")
self.app.close_window (self)
def push_view_state (self):
self.default_index = None
self.default_start_index = None
model = self.log_view.props.model
if model is None:
return
try:
line_index = self.get_active_line_index ()
except ValueError:
super_index = None
self.logger.debug ("no line selected")
else:
super_index = model.line_index_to_top (line_index)
self.logger.debug ("pushing selected line %i (abs %i)",
line_index, super_index)
self.default_index = super_index
vis_range = self.log_view.get_visible_range ()
if vis_range is not None:
start_path, end_path = vis_range
start_index = start_path[0]
self.default_start_index = model.line_index_to_top (start_index)
def update_model (self, model = None):
if model is None:
model = self.log_view.props.model
previous_model = self.log_view.props.model
if previous_model == model:
# Force update.
self.log_view.set_model (None)
self.log_view.props.model = model
def pop_view_state (self, scroll_to_selection = False):
model = self.log_view.props.model
if model is None:
return
selected_index = self.default_index
start_index = self.default_start_index
if selected_index is not None:
try:
select_index = model.line_index_from_top (selected_index)
except IndexError, exc:
self.logger.debug ("abs line index %i filtered out, not reselecting",
selected_index)
else:
assert select_index >= 0
sel = self.log_view.get_selection ()
path = (select_index,)
sel.select_path (path)
if start_index is None or scroll_to_selection:
self.log_view.scroll_to_cell (path, use_align = True, row_align = .5)
if start_index is not None and not scroll_to_selection:
def traverse ():
for i in xrange (start_index, len (model)):
yield i
for i in xrange (start_index - 1, 0, -1):
yield i
for current_index in traverse ():
try:
target_index = model.line_index_from_top (current_index)
except IndexError:
continue
else:
path = (target_index,)
self.log_view.scroll_to_cell (path, use_align = True, row_align = 0.)
break
def update_view (self):
view = self.log_view
model = view.props.model
start_path, end_path = view.get_visible_range ()
start_index, end_index = start_path[0], end_path[0]
for line_index in range (start_index, end_index + 1):
path = (line_index,)
tree_iter = model.get_iter (path)
model.row_changed (path, tree_iter)
def handle_log_view_selection_changed (self, selection):
try:
line_index = self.get_active_line_index ()
except ValueError:
first_selected = True
last_selected = True
else:
first_selected = (line_index == 0)
last_selected = (line_index == len (self.log_view.props.model) - 1)
self.actions.hide_before_line.props.sensitive = not first_selected
self.actions.hide_after_line.props.sensitive = not last_selected
def handle_window_delete_event (self, window, event):
self.actions.close_window.activate ()
def handle_new_window_action_activate (self, action):
self.app.open_window ()
def handle_open_file_action_activate (self, action):
dialog = gtk.FileChooserDialog (None, self.gtk_window,
gtk.FILE_CHOOSER_ACTION_OPEN,
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT,))
response = dialog.run ()
dialog.hide ()
if response == gtk.RESPONSE_ACCEPT:
self.set_log_file (dialog.get_filename ())
dialog.destroy ()
def handle_reload_file_action_activate (self, action):
if self.log_file is None:
return
self.set_log_file (self.log_file.path)
def handle_cancel_load_action_activate (self, action):
self.logger.debug ("cancelling data load")
self.set_log_file (None)
if self.progress_dialog:
self.progress_dialog.destroy ()
self.progress_dialog = None
if self.update_progress_id is not None:
gobject.source_remove (self.update_progress_id)
self.update_progress_id = None
def handle_close_window_action_activate (self, action):
self.close ()
def handle_hide_after_line_action_activate (self, action):
self.hide_range (after = True)
def handle_hide_before_line_action_activate (self, action):
self.hide_range (after = False)
def hide_range (self, after):
model = self.log_view.props.model
try:
filtered_line_index = self.get_active_line_index ()
except ValueError:
return
if after:
first_index = model.line_index_to_top (0)
last_index = model.line_index_to_top (filtered_line_index)
self.logger.info ("hiding lines after %i (abs %i), first line is abs %i",
filtered_line_index,
last_index,
first_index)
else:
first_index = model.line_index_to_top (filtered_line_index)
last_index = model.line_index_to_top (len (model) - 1)
self.logger.info ("hiding lines before %i (abs %i), last line is abs %i",
filtered_line_index,
first_index,
last_index)
self.push_view_state ()
start_index = first_index
stop_index = last_index + 1
self.log_range.set_range (start_index, stop_index)
if self.log_filter:
self.log_filter.super_model_changed_range ()
self.update_model ()
self.pop_view_state ()
self.actions.show_hidden_lines.props.sensitive = True
def handle_show_hidden_lines_action_activate (self, action):
self.logger.info ("restoring model filter to show all lines")
self.push_view_state ()
self.log_range.reset ()
self.log_filter = None
self.update_model (self.log_range)
self.pop_view_state (scroll_to_selection = True)
self.actions.show_hidden_lines.props.sensitive = False
def handle_edit_copy_line_action_activate (self, action):
# TODO: Should probably copy the _exact_ line as taken from the file.
line = self.get_active_line ()
log_line = Data.LogLine (line)
self.clipboard.set_text (log_line.line_string ())
def handle_edit_copy_message_action_activate (self, action):
col_id = LogModelBase.COL_MESSAGE
self.clipboard.set_text (self.get_active_line ()[col_id])
def add_model_filter (self, filter):
self.progress_dialog = ProgressDialog (self, _("Filtering"))
self.progress_dialog.handle_cancel = self.handle_filter_progress_dialog_cancel
dispatcher = Common.Data.GSourceDispatcher ()
self.filter_dispatcher = dispatcher
# FIXME: Unsetting the model to keep e.g. the dispatched timeline
# sentinel from collecting data while we filter idly, which slows
# things down for nothing.
self.push_view_state ()
self.log_view.set_model (None)
if self.log_filter is None:
self.log_filter = FilteredLogModel (self.log_range)
self.log_filter.handle_process_finished = self.handle_log_filter_process_finished
self.log_filter.add_filter (filter, dispatcher = dispatcher)
gobject.timeout_add (250, self.update_filter_progress)
def update_filter_progress (self):
if self.progress_dialog is None:
return False
try:
progress = self.log_filter.get_filter_progress ()
except ValueError:
self.logger.warning ("no filter process running")
return False
self.progress_dialog.update (progress)
return True
def handle_filter_progress_dialog_cancel (self):
self.progress_dialog.destroy ()
self.progress_dialog = None
self.log_filter.abort_process ()
self.log_view.props.model = self.log_filter
self.pop_view_state ()
def handle_log_filter_process_finished (self):
self.progress_dialog.destroy ()
self.progress_dialog = None
# No push_view_state here, did this in add_model_filter.
self.update_model (self.log_filter)
self.pop_view_state ()
self.actions.show_hidden_lines.props.sensitive = True
def handle_hide_log_level_action_activate (self, action):
row = self.get_active_line ()
debug_level = row[LogModelBase.COL_LEVEL]
self.add_model_filter (DebugLevelFilter (debug_level))
def handle_hide_log_category_action_activate (self, action):
row = self.get_active_line ()
category = row[LogModelBase.COL_CATEGORY]
self.add_model_filter (CategoryFilter (category))
def handle_hide_log_object_action_activate (self, action):
row = self.get_active_line ()
object_ = row[LogModelBase.COL_OBJECT]
self.add_model_filter (ObjectFilter (object_))
def handle_hide_filename_action_activate (self, action):
row = self.get_active_line ()
filename = row[LogModelBase.COL_FILENAME]
self.add_model_filter (FilenameFilter (filename))
def handle_show_about_action_activate (self, action):
from GstDebugViewer import version
dialog = self.widget_factory.make_one ("about-dialog.ui", "about_dialog")
dialog.props.version = version
dialog.run ()
dialog.destroy ()
@staticmethod
def _timestamp_cell_data_func (column, renderer, model, tree_iter):
ts = model.get_value (tree_iter, LogModel.COL_TIME)
renderer.props.text = Data.time_args (ts)
def _message_cell_data_func (self, column, renderer, model, tree_iter):
offset = model.get_value (tree_iter, LogModel.COL_MESSAGE_OFFSET)
self.log_file.seek (offset)
renderer.props.text = strip_escape (self.log_file.readline ().strip ())
def set_log_file (self, filename):
if self.log_file is not None:
for feature in self.features:
feature.handle_detach_log_file (self, self.log_file)
if filename is None:
if self.dispatcher is not None:
self.dispatcher.cancel ()
self.dispatcher = None
self.log_file = None
self.actions.groups["RowActions"].props.sensitive = False
else:
self.logger.debug ("setting log file %r", filename)
try:
self.setup_model (LazyLogModel ())
self.dispatcher = Common.Data.GSourceDispatcher ()
self.log_file = Data.LogFile (filename, self.dispatcher)
except EnvironmentError, exc:
try:
file_size = os.path.getsize (filename)
except EnvironmentError:
pass
else:
if file_size == 0:
# Trying to mmap an empty file results in an invalid
# argument error.
self.show_error (_("Could not open file"),
_("The selected file is empty"))
return
self.handle_environment_error (exc, filename)
return
basename = os.path.basename (filename)
self.gtk_window.props.title = _("%s - GStreamer Debug Viewer") % (basename,)
self.log_file.consumers.append (self)
self.log_file.start_loading ()
def handle_environment_error (self, exc, filename):
self.show_error (_("Could not open file"), str (exc))
def show_error (self, message1, message2):
dialog = gtk.MessageDialog (self.gtk_window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
gtk.BUTTONS_OK, message1)
# The property for secondary text is new in 2.10, so we use this clunky
# method instead.
dialog.format_secondary_text (message2)
dialog.set_default_response (0)
dialog.run ()
dialog.destroy ()
def handle_load_started (self):
self.logger.debug ("load has started")
self.progress_dialog = ProgressDialog (self, _("Loading log file"))
self.progress_dialog.handle_cancel = self.handle_load_progress_dialog_cancel
self.update_progress_id = gobject.timeout_add (250, self.update_load_progress)
def handle_load_progress_dialog_cancel (self):
self.actions.cancel_load.activate ()
def update_load_progress (self):
if self.progress_dialog is None:
self.logger.debug ("progress dialog is gone, removing progress update timeout")
self.update_progress_id = None
return False
progress = self.log_file.get_load_progress ()
self.progress_dialog.update (progress)
return True
def handle_load_finished (self):
self.logger.debug ("load has finshed")
self.progress_dialog.destroy ()
self.progress_dialog = None
self.log_model.set_log (self.log_file)
self.log_range.reset ()
self.log_filter = None
self.actions.reload_file.props.sensitive = True
self.actions.groups["RowActions"].props.sensitive = True
self.actions.show_hidden_lines.props.sensitive = False
def idle_set ():
self.log_view.props.model = self.log_range
self.line_view.handle_attach_log_file (self)
for feature in self.features:
feature.handle_attach_log_file (self, self.log_file)
if len (self.log_range):
sel = self.log_view.get_selection ()
sel.select_path ((0,))
return False
gobject.idle_add (idle_set)

View file

@ -36,7 +36,7 @@ class SearchOperation (object):
self.search_forward = search_forward
self.start_position = start_position
col_id = GUI.LogModelBase.COL_MESSAGE
col_id = GUI.models.LogModelBase.COL_MESSAGE
len_search_text = len (search_text)
def match_func (model_row):

View file

@ -21,7 +21,8 @@
import logging
from GstDebugViewer import Common, Data, GUI
from GstDebugViewer import Common, Data
from GstDebugViewer.GUI.colors import LevelColorThemeTango, ThreadColorThemeTango
from GstDebugViewer.Plugins import *
import gobject
@ -264,7 +265,7 @@ class VerticalTimelineWidget (gtk.DrawingArea):
self.logger = logging.getLogger ("ui.vtimeline")
self.log_view = log_view
self.theme = GUI.ThreadColorThemeTango ()
self.theme = ThreadColorThemeTango ()
self.params = None
self.thread_colors = {}
self.next_thread_color = 0
@ -558,7 +559,7 @@ class TimelineWidget (gtk.DrawingArea):
self.logger.debug ("level distribution sentinel has no data yet")
return
colors = GUI.LevelColorThemeTango ().colors
colors = LevelColorThemeTango ().colors
dist_data = self.process.dist_sentinel.data
def cumulative_level_counts (*levels):