This commit is contained in:
Akkariin Meiko
2022-03-12 03:16:09 +08:00
Unverified
parent 12b76e0c7a
commit 27c4ec74a1
10075 changed files with 5122287 additions and 1 deletions
@@ -0,0 +1,44 @@
# -*- 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 gi
from GstDebugViewer.GUI.app import App
def main(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()
@@ -0,0 +1,158 @@
# -*- 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 gi
gi.require_version('Gdk', '3.0')
gi.require_version('Gtk', '3.0')
from gi.repository import GObject
from gi.repository import Gdk
from gi.repository 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)
zoom_level = Common.GUI.StateInt("zoom-level")
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 = []
# Apply custom widget stying
# TODO: check for dark theme
css = b"""
@define-color normal_bg_color #FFFFFF;
@define-color shade_bg_color shade(@normal_bg_color, 0.95);
#log_view row:nth-child(even) {
background-color: @normal_bg_color;
}
#log_view row:nth-child(odd) {
background-color: @shade_bg_color;
}
#log_view row:selected {
background-color: #4488FF;
}
#log_view {
-GtkTreeView-horizontal-separator: 0;
-GtkTreeView-vertical-separator: 1;
outline-width: 0;
outline-offset: 0;
}
"""
style_provider = Gtk.CssProvider()
style_provider.load_from_data(css)
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
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 BaseException:
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()
@@ -0,0 +1,162 @@
# -*- 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 gi.repository import Gtk
from gi.repository import Gdk
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 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_trace, p.black, p.aluminium2)
self.add_color(Data.debug_level_fixme, p.black, p.butter3)
self.add_color(Data.debug_level_log, p.black, p.plum1)
self.add_color(Data.debug_level_debug, p.black, p.skyblue1)
self.add_color(Data.debug_level_info, p.black, p.chameleon1)
self.add_color(Data.debug_level_warning, p.black, p.orange1)
self.add_color(Data.debug_level_error, p.white, p.scarletred1)
self.add_color(Data.debug_level_memdump, p.white, p.aluminium3)
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)
@@ -0,0 +1,741 @@
# -*- 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 logging
from gi.repository import Gtk, GLib
from GstDebugViewer import Common, Data
from GstDebugViewer.GUI.colors import LevelColorThemeTango
from GstDebugViewer.GUI.models import LazyLogModel, LogModelBase
def _(s):
return s
# 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_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):
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, True)
cell.props.yalign = 0.
cell.props.ypad = 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, user_data):
data_func(cell.props, model.get_value(tree_iter, id_))
else:
cell_data_func = data_func
column.set_cell_data_func(cell, cell_data_func)
elif not self.get_modify_func:
column.add_attribute(cell, "text", self.id)
else:
self.update_modify_func(column, cell)
column.props.resizable = True
def update_modify_func(self, column, cell):
modify_func = self.get_modify_func()
id_ = self.id
def cell_data_func(column, cell, model, tree_iter, user_data):
cell.props.text = modify_func(model.get_value(tree_iter, id_))
column.set_cell_data_func(cell, cell_data_func)
def compute_default_size(self):
values = self.get_values_for_size()
if not values:
return SizedColumn.compute_default_size(self)
cell = self.view_column.get_cells()[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)
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"
def __init__(self, *a, **kw):
self.base_time = 0
TextColumn.__init__(self, *a, **kw)
def get_modify_func(self):
if self.base_time:
time_diff_args = Data.time_diff_args
base_time = self.base_time
def format_time(value):
return time_diff_args(value - base_time)
else:
time_args = Data.time_args
def format_time(value):
# TODO: This is hard coded to omit hours.
return time_args(value)[2:]
return format_time
def get_values_for_size(self):
values = [0]
return values
def set_base_time(self, base_time):
self.base_time = base_time
column = self.view_column
cell = column.get_cells()[0]
self.update_modify_func(column, cell)
class LevelColumn (TextColumn):
name = "level"
label_header = _("L")
id = LazyLogModel.COL_LEVEL
def __init__(self):
TextColumn.__init__(self)
cell = self.view_column.get_cells()[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):
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]
cell_props.background_gdk = cell_colors[1]
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, Data.debug_level_memdump]
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, user_data):
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 = None
def __init__(self, *a, **kw):
self.highlighters = {}
TextColumn.__init__(self, *a, **kw)
def get_data_func(self):
highlighters = self.highlighters
id_ = LazyLogModel.COL_MESSAGE
def message_data_func(column, cell, model, tree_iter, user_data):
msg = model.get_value(tree_iter, id_).decode("utf8", errors="replace")
if not highlighters:
cell.props.text = msg
return
if len(highlighters) > 1:
raise NotImplementedError("FIXME: Support more than one...")
highlighter = list(highlighters.values())[0]
row = model[tree_iter]
ranges = highlighter(row)
if not ranges:
cell.props.text = msg
else:
tags = []
prev_end = 0
end = None
for start, end in ranges:
if prev_end < start:
tags.append(
GLib.markup_escape_text(msg[prev_end:start]))
msg_escape = GLib.markup_escape_text(msg[start:end])
tags.append("<span foreground=\'#FFFFFF\'"
" background=\'#0000FF\'>%s</span>" % (msg_escape,))
prev_end = end
if end is not None:
tags.append(GLib.markup_escape_text(msg[end:]))
cell.props.markup = "".join(tags)
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.zoom = 1.0
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.get_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.get_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.get_model()
self.default_sort = tree_sortable_get_sort_column_id(sort_model)
sort_model.set_sort_column_id(TREE_SORTABLE_UNSORTED_COLUMN_ID,
Gtk.SortType.ASCENDING)
def set_zoom(self, scale):
for column in self.columns:
cell = column.view_column.get_cells()[0]
cell.props.scale = scale
column.view_column.queue_resize()
self.zoom = scale
def set_base_time(self, base_time):
try:
time_column = self.find_item(name=TimeColumn.name)
except KeyError:
return
time_column.set_base_time(base_time)
self.size_column(time_column)
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.TreeViewColumnSizing.FIXED
cell = column.view_column.get_cells()[0]
cell.props.scale = self.zoom
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 col_class.name not 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,)
default_column_classes = (
TimeColumn, LevelColumn, 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.default_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 set_zoom(self, scale):
ColumnManager.set_zoom(self, scale)
if self.view is None:
return
# Timestamp and log level columns are pretty much fixed size, so resize
# them back to default on zoom change:
names = (TimeColumn.name,
LevelColumn.name,
PidColumn.name,
ThreadColumn.name)
for column in self.columns:
if column.name in names:
self.size_column(column)
def size_column(self, column):
if column.default_size is None:
default_size = column.compute_default_size()
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)
self.size_column(column)
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.get_model()
if model is None:
return
self.logger.debug("model changed, sizing columns")
for column in self.iter_items():
self.size_column(column)
self.columns_sized = True
class WrappingMessageColumn (MessageColumn):
def wrap_to_width(self, width):
col = self.view_column
col.props.max_width = width
col.get_cells()[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()
@@ -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."""
from GstDebugViewer.GUI.models import LogModelBase
def get_comparison_function(all_but_this):
if (all_but_this):
return lambda x, y: x == y
else:
return lambda x, y: x != y
class Filter (object):
pass
class DebugLevelFilter (Filter):
only_this, all_but_this, this_and_above = range(3)
def __init__(self, debug_level, mode=0):
col_id = LogModelBase.COL_LEVEL
if mode == self.this_and_above:
def comparison_function(x, y):
return x < y
else:
comparison_function = get_comparison_function(
mode == self.all_but_this)
def filter_func(row):
return comparison_function(row[col_id], debug_level)
self.filter_func = filter_func
class CategoryFilter (Filter):
def __init__(self, category, all_but_this=False):
col_id = LogModelBase.COL_CATEGORY
comparison_function = get_comparison_function(all_but_this)
def category_filter_func(row):
return comparison_function(row[col_id], category)
self.filter_func = category_filter_func
class ObjectFilter (Filter):
def __init__(self, object_, all_but_this=False):
col_id = LogModelBase.COL_OBJECT
comparison_function = get_comparison_function(all_but_this)
def object_filter_func(row):
return comparison_function(row[col_id], object_)
self.filter_func = object_filter_func
class FunctionFilter (Filter):
def __init__(self, function_, all_but_this=False):
col_id = LogModelBase.COL_FUNCTION
comparison_function = get_comparison_function(all_but_this)
def function_filter_func(row):
return comparison_function(row[col_id], function_)
self.filter_func = function_filter_func
class ThreadFilter (Filter):
def __init__(self, thread_, all_but_this=False):
col_id = LogModelBase.COL_THREAD
comparison_function = get_comparison_function(all_but_this)
def thread_filter_func(row):
return comparison_function(row[col_id], thread_)
self.filter_func = thread_filter_func
class FilenameFilter (Filter):
def __init__(self, filename, all_but_this=False):
col_id = LogModelBase.COL_FILENAME
comparison_function = get_comparison_function(all_but_this)
def filename_filter_func(row):
return comparison_function(row[col_id], filename)
self.filter_func = filename_filter_func
@@ -0,0 +1,498 @@
# -*- 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 array import array
from bisect import bisect_left
import logging
from gi.repository import GObject
from gi.repository import Gtk
from GstDebugViewer import Common, Data
class LogModelBase (Common.GUI.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):
Common.GUI.GenericTreeModel.__init__(self)
# self.props.leak_references = False
self.line_offsets = array("I")
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
COL_MESSAGE = self.COL_MESSAGE
access_offset = self.access_offset
for i, offset in enumerate(self.line_offsets):
ensure_cached(offset)
row = line_cache[offset]
# adjust special rows
row[COL_LEVEL] = line_levels[i]
msg_offset = row[COL_MESSAGE]
row[COL_MESSAGE] = access_offset(offset + msg_offset)
yield (row, offset,)
row[COL_MESSAGE] = msg_offset
def on_get_flags(self):
flags = Gtk.TreeModelFlags.LIST_ONLY | Gtk.TreeModelFlags.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:
# strip whitespace + newline
value = self.access_offset(line_offset + value).strip()
elif col_id in (self.COL_TIME, self.COL_THREAD):
value = GObject.Value(GObject.TYPE_UINT64, value)
return value
def get_value_range(self, col_id, start, stop):
if col_id != self.COL_LEVEL:
raise NotImplementedError("XXX FIXME")
return self.line_levels[start:stop]
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")
class FilteredLogModel (FilteredLogModelBase):
def __init__(self, super_model):
FilteredLogModelBase.__init__(self, super_model)
self.logger = logging.getLogger("filtered-log-model")
self.filters = []
self.reset()
self.__active_process = None
self.__filter_progress = 0.
def reset(self):
self.logger.debug("reset filter")
self.line_offsets = self.super_model.line_offsets
self.line_levels = self.super_model.line_levels
self.super_index = range(len(self.line_offsets))
del self.filters[:]
def __filter_process(self, filter):
YIELD_LIMIT = 10000
self.logger.debug("preparing new filter")
new_line_offsets = array("I")
new_line_levels = []
new_super_index = array("I")
level_id = self.COL_LEVEL
func = filter.filter_func
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)
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.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.logger.debug("adding filter")
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):
return bisect_left(self.super_index, super_line_index)
def line_index_to_super(self, line_index):
return self.super_index[line_index]
def set_range(self, super_start, super_stop):
old_super_start = self.line_index_to_super(0)
old_super_stop = self.line_index_to_super(
len(self.super_index) - 1) + 1
self.logger.debug("set range (%i, %i), current (%i, %i)",
super_start, super_stop, old_super_start, old_super_stop)
if len(self.filters) == 0:
# Identity.
self.super_index = range(super_start, super_stop)
self.line_offsets = SubRange(self.super_model.line_offsets,
super_start, super_stop)
self.line_levels = SubRange(self.super_model.line_levels,
super_start, super_stop)
return
if super_start < old_super_start:
# TODO:
raise NotImplementedError("Only handling further restriction of the range"
" (start offset = %i)" % (super_start,))
if super_stop > old_super_stop:
# TODO:
raise NotImplementedError("Only handling further restriction of the range"
" (end offset = %i)" % (super_stop,))
start = self.line_index_from_super(super_start)
stop = self.line_index_from_super(super_stop)
self.super_index = SubRange(self.super_index, start, stop)
self.line_offsets = SubRange(self.line_offsets, start, stop)
self.line_levels = SubRange(self.line_levels, start, stop)
class SubRange (object):
__slots__ = ("size", "start", "stop",)
def __init__(self, size, start, stop):
if start > stop:
raise ValueError(
"need start <= stop (got %r, %r)" % (start, stop,))
if isinstance(size, type(self)):
# Another SubRange, don't stack:
start += size.start
stop += size.start
size = size.size
self.size = size
self.start = start
self.stop = stop
def __getitem__(self, i):
if isinstance(i, slice):
stop = i.stop
if stop >= 0:
stop += self.start
else:
stop += self.stop
return self.size[i.start + self.start:stop]
else:
return self.size[i + self.start]
def __len__(self):
return self.stop - self.start
def __iter__(self):
size = self.size
for i in range(self.start, self.stop):
yield size[i]
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)
File diff suppressed because it is too large Load Diff