Update
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8; mode: python; -*-
|
||||
#
|
||||
# GStreamer Development Utilities
|
||||
#
|
||||
# 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 Development Utilities Common Data module."""
|
||||
|
||||
import gi
|
||||
|
||||
from gi.repository import GObject
|
||||
|
||||
|
||||
class Dispatcher (object):
|
||||
|
||||
def __call__(self, iterator):
|
||||
|
||||
raise NotImplementedError("derived classes must override this method")
|
||||
|
||||
def cancel(self):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DefaultDispatcher (Dispatcher):
|
||||
|
||||
def __call__(self, iterator):
|
||||
|
||||
for x in iterator:
|
||||
pass
|
||||
|
||||
|
||||
class GSourceDispatcher (Dispatcher):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
Dispatcher.__init__(self)
|
||||
|
||||
self.source_id = None
|
||||
|
||||
def __call__(self, iterator):
|
||||
|
||||
if self.source_id is not None:
|
||||
GObject.source_remove(self.source_id)
|
||||
|
||||
def iteration():
|
||||
r = iterator.__next__()
|
||||
if not r:
|
||||
self.source_id = None
|
||||
return r
|
||||
|
||||
self.source_id = GObject.idle_add(
|
||||
iteration, priority=GObject.PRIORITY_LOW)
|
||||
|
||||
def cancel(self):
|
||||
|
||||
if self.source_id is None:
|
||||
return
|
||||
|
||||
GObject.source_remove(self.source_id)
|
||||
self.source_id = None
|
||||
@@ -0,0 +1,528 @@
|
||||
# -*- coding: utf-8; mode: python; -*-
|
||||
#
|
||||
# GStreamer Development Utilities
|
||||
#
|
||||
# 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 Development Utilities Common GUI module."""
|
||||
|
||||
import os
|
||||
|
||||
import logging
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.types import GObjectMeta
|
||||
|
||||
import GstDebugViewer
|
||||
from GstDebugViewer.Common import utils
|
||||
from .generictreemodel import GenericTreeModel
|
||||
|
||||
|
||||
def widget_add_popup_menu(widget, menu, button=3):
|
||||
|
||||
def popup_callback(widget, event):
|
||||
|
||||
if event.button == button:
|
||||
menu.popup(
|
||||
None, None, None, None, event.button, event.get_time())
|
||||
return False
|
||||
|
||||
widget.connect("button-press-event", popup_callback)
|
||||
|
||||
|
||||
class Actions (dict):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
dict.__init__(self)
|
||||
|
||||
self.groups = {}
|
||||
|
||||
def __getattr__(self, name):
|
||||
|
||||
try:
|
||||
return self[name]
|
||||
except KeyError:
|
||||
if "_" in name:
|
||||
try:
|
||||
return self[name.replace("_", "-")]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
raise AttributeError("no action with name %r" % (name,))
|
||||
|
||||
def add_group(self, group):
|
||||
|
||||
name = group.props.name
|
||||
if name in self.groups:
|
||||
raise ValueError("already have a group named %s", name)
|
||||
self.groups[name] = group
|
||||
for action in group.list_actions():
|
||||
self[action.props.name] = action
|
||||
|
||||
|
||||
class Widgets (dict):
|
||||
|
||||
def __init__(self, builder):
|
||||
|
||||
widgets = (obj for obj in builder.get_objects()
|
||||
if isinstance(obj, Gtk.Buildable))
|
||||
# Gtk.Widget.get_name() shadows out the GtkBuildable interface method
|
||||
# of the same name, hence calling the unbound interface method here:
|
||||
items = ((Gtk.Buildable.get_name(w), w,) for w in widgets)
|
||||
|
||||
dict.__init__(self, items)
|
||||
|
||||
def __getattr__(self, name):
|
||||
|
||||
try:
|
||||
return self[name]
|
||||
except KeyError:
|
||||
if "_" in name:
|
||||
try:
|
||||
return self[name.replace("_", "-")]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
raise AttributeError("no widget with name %r" % (name,))
|
||||
|
||||
|
||||
class WidgetFactory (object):
|
||||
|
||||
def __init__(self, directory):
|
||||
|
||||
self.directory = directory
|
||||
|
||||
def get_builder(self, filename):
|
||||
|
||||
builder_filename = os.path.join(self.directory, filename)
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(GstDebugViewer.GETTEXT_DOMAIN)
|
||||
builder.add_from_file(builder_filename)
|
||||
|
||||
return builder
|
||||
|
||||
def make(self, filename, widget_name, autoconnect=None):
|
||||
|
||||
builder = self.get_builder(filename)
|
||||
|
||||
if autoconnect is not None:
|
||||
builder.connect_signals(autoconnect)
|
||||
|
||||
return Widgets(builder)
|
||||
|
||||
def make_one(self, filename, widget_name):
|
||||
|
||||
builder = self.get_builder(filename)
|
||||
|
||||
return builder.get_object(widget_name)
|
||||
|
||||
|
||||
class UIFactory (object):
|
||||
|
||||
def __init__(self, ui_filename, actions=None):
|
||||
|
||||
self.filename = ui_filename
|
||||
if actions:
|
||||
self.action_groups = actions.groups
|
||||
else:
|
||||
self.action_groups = ()
|
||||
|
||||
def make(self, extra_actions=None):
|
||||
|
||||
ui_manager = Gtk.UIManager()
|
||||
for action_group in list(self.action_groups.values()):
|
||||
ui_manager.insert_action_group(action_group, 0)
|
||||
if extra_actions:
|
||||
for action_group in extra_actions.groups:
|
||||
ui_manager.insert_action_group(action_group, 0)
|
||||
ui_manager.add_ui_from_file(self.filename)
|
||||
ui_manager.ensure_update()
|
||||
|
||||
return ui_manager
|
||||
|
||||
|
||||
class MetaModel (GObjectMeta):
|
||||
|
||||
"""Meta class for easy setup of gtk tree models.
|
||||
|
||||
Looks for a class attribute named `columns' which must be set to a
|
||||
sequence of the form name1, type1, name2, type2, ..., where the
|
||||
names are strings. This metaclass adds the following attributes
|
||||
to created classes:
|
||||
|
||||
cls.column_types = (type1, type2, ...)
|
||||
cls.column_ids = (0, 1, ...)
|
||||
cls.name1 = 0
|
||||
cls.name2 = 1
|
||||
...
|
||||
|
||||
Example: A Gtk.ListStore derived model can use
|
||||
|
||||
columns = ("COL_NAME", str, "COL_VALUE", str)
|
||||
|
||||
and use this in __init__:
|
||||
|
||||
GObject.GObject.__init__ (self, *self.column_types)
|
||||
|
||||
Then insert data like this:
|
||||
|
||||
self.set (self.append (),
|
||||
self.COL_NAME, "spam",
|
||||
self.COL_VALUE, "ham")
|
||||
"""
|
||||
|
||||
def __init__(cls, name, bases, dict):
|
||||
|
||||
super(MetaModel, cls).__init__(name, bases, dict)
|
||||
|
||||
spec = tuple(cls.columns)
|
||||
|
||||
column_names = spec[::2]
|
||||
column_types = spec[1::2]
|
||||
column_indices = list(range(len(column_names)))
|
||||
|
||||
for col_index, col_name, in zip(column_indices, column_names):
|
||||
setattr(cls, col_name, col_index)
|
||||
|
||||
cls.column_types = column_types
|
||||
cls.column_ids = tuple(column_indices)
|
||||
|
||||
|
||||
class Manager (object):
|
||||
|
||||
"""GUI Manager base class."""
|
||||
|
||||
@classmethod
|
||||
def iter_item_classes(cls):
|
||||
|
||||
msg = "%s class does not support manager item class access"
|
||||
raise NotImplementedError(msg % (cls.__name__,))
|
||||
|
||||
@classmethod
|
||||
def find_item_class(self, **kw):
|
||||
|
||||
return self.__find_by_attrs(self.iter_item_classes(), kw)
|
||||
|
||||
def iter_items(self):
|
||||
|
||||
msg = "%s object does not support manager item access"
|
||||
raise NotImplementedError(msg % (type(self).__name__,))
|
||||
|
||||
def find_item(self, **kw):
|
||||
|
||||
return self.__find_by_attrs(self.iter_items(), kw)
|
||||
|
||||
@staticmethod
|
||||
def __find_by_attrs(i, kw):
|
||||
|
||||
from operator import attrgetter
|
||||
|
||||
if len(kw) != 1:
|
||||
raise ValueError("need exactly one keyword argument")
|
||||
|
||||
attr, value = list(kw.items())[0]
|
||||
getter = attrgetter(attr)
|
||||
|
||||
for item in i:
|
||||
if getter(item) == value:
|
||||
return item
|
||||
else:
|
||||
raise KeyError("no item such that item.%s == %r" % (attr, value,))
|
||||
|
||||
|
||||
class StateString (object):
|
||||
|
||||
"""Descriptor for binding to StateSection classes."""
|
||||
|
||||
def __init__(self, option, default=None):
|
||||
|
||||
self.option = option
|
||||
self.default = default
|
||||
|
||||
def __get__(self, section, section_class=None):
|
||||
|
||||
import configparser
|
||||
|
||||
if section is None:
|
||||
return self
|
||||
|
||||
try:
|
||||
return self.get(section)
|
||||
except (configparser.NoSectionError,
|
||||
configparser.NoOptionError,):
|
||||
return self.get_default(section)
|
||||
|
||||
def __set__(self, section, value):
|
||||
|
||||
import configparser
|
||||
|
||||
self.set(section, value)
|
||||
|
||||
def get(self, section):
|
||||
|
||||
return section.get(self)
|
||||
|
||||
def get_default(self, section):
|
||||
|
||||
return self.default
|
||||
|
||||
def set(self, section, value):
|
||||
|
||||
if value is None:
|
||||
value = ""
|
||||
|
||||
section.set(self, str(value))
|
||||
|
||||
|
||||
class StateBool (StateString):
|
||||
|
||||
"""Descriptor for binding to StateSection classes."""
|
||||
|
||||
def get(self, section):
|
||||
|
||||
return section.state._parser.getboolean(section._name, self.option)
|
||||
|
||||
|
||||
class StateInt (StateString):
|
||||
|
||||
"""Descriptor for binding to StateSection classes."""
|
||||
|
||||
def get(self, section):
|
||||
|
||||
return section.state._parser.getint(section._name, self.option)
|
||||
|
||||
|
||||
class StateInt4 (StateString):
|
||||
|
||||
"""Descriptor for binding to StateSection classes. This implements storing
|
||||
a tuple of 4 integers."""
|
||||
|
||||
def get(self, section):
|
||||
|
||||
value = StateString.get(self, section)
|
||||
|
||||
try:
|
||||
l = value.split(",")
|
||||
if len(l) != 4:
|
||||
return None
|
||||
else:
|
||||
return tuple((int(v) for v in l))
|
||||
except (AttributeError, TypeError, ValueError,):
|
||||
return None
|
||||
|
||||
def set(self, section, value):
|
||||
|
||||
if value is None:
|
||||
svalue = ""
|
||||
elif len(value) != 4:
|
||||
raise ValueError("value needs to be a 4-sequence, or None")
|
||||
else:
|
||||
svalue = ", ".join((str(v) for v in value))
|
||||
|
||||
return StateString.set(self, section, svalue)
|
||||
|
||||
|
||||
class StateItem (StateString):
|
||||
|
||||
"""Descriptor for binding to StateSection classes. This implements storing
|
||||
a class controlled by a Manager class."""
|
||||
|
||||
def __init__(self, option, manager_class, default=None):
|
||||
|
||||
StateString.__init__(self, option, default=default)
|
||||
|
||||
self.manager = manager_class
|
||||
|
||||
def get(self, section):
|
||||
|
||||
value = SectionString.get(self, section)
|
||||
|
||||
if not value:
|
||||
return None
|
||||
|
||||
return self.parse_item(value)
|
||||
|
||||
def set(self, section, value):
|
||||
|
||||
if value is None:
|
||||
svalue = ""
|
||||
else:
|
||||
svalue = value.name
|
||||
|
||||
StateString.set(self, section, svalue)
|
||||
|
||||
def parse_item(self, value):
|
||||
|
||||
name = value.strip()
|
||||
|
||||
try:
|
||||
return self.manager.find_item_class(name=name)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
class StateItemList (StateItem):
|
||||
|
||||
"""Descriptor for binding to StateSection classes. This implements storing
|
||||
an ordered set of Manager items."""
|
||||
|
||||
def get(self, section):
|
||||
|
||||
value = StateString.get(self, section)
|
||||
|
||||
if not value:
|
||||
return []
|
||||
|
||||
classes = []
|
||||
for name in value.split(","):
|
||||
item_class = self.parse_item(name)
|
||||
if item_class is None:
|
||||
continue
|
||||
if not item_class in classes:
|
||||
classes.append(item_class)
|
||||
|
||||
return classes
|
||||
|
||||
def get_default(self, section):
|
||||
|
||||
default = StateItem.get_default(self, section)
|
||||
if default is None:
|
||||
return []
|
||||
else:
|
||||
return default
|
||||
|
||||
def set(self, section, value):
|
||||
|
||||
if value is None:
|
||||
svalue = ""
|
||||
else:
|
||||
svalue = ", ".join((v.name for v in value))
|
||||
|
||||
StateString.set(self, section, svalue)
|
||||
|
||||
|
||||
class StateSection (object):
|
||||
|
||||
_name = None
|
||||
|
||||
def __init__(self, state):
|
||||
|
||||
self.state = state
|
||||
|
||||
if self._name is None:
|
||||
raise NotImplementedError(
|
||||
"subclasses must override the _name attribute")
|
||||
|
||||
def get(self, state_string):
|
||||
|
||||
return self.state._parser.get(self._name, state_string.option)
|
||||
|
||||
def set(self, state_string, value):
|
||||
|
||||
import configparser
|
||||
|
||||
parser = self.state._parser
|
||||
|
||||
try:
|
||||
parser.set(self._name, state_string.option, value)
|
||||
except configparser.NoSectionError:
|
||||
parser.add_section(self._name)
|
||||
parser.set(self._name, state_string.option, value)
|
||||
|
||||
|
||||
class State (object):
|
||||
|
||||
def __init__(self, filename, old_filenames=()):
|
||||
|
||||
import configparser
|
||||
|
||||
self.sections = {}
|
||||
|
||||
self._filename = filename
|
||||
self._parser = configparser.RawConfigParser()
|
||||
success = self._parser.read([filename])
|
||||
if not success:
|
||||
for old_filename in old_filenames:
|
||||
success = self._parser.read([old_filename])
|
||||
if success:
|
||||
break
|
||||
|
||||
def add_section_class(self, section_class):
|
||||
|
||||
self.sections[section_class._name] = section_class(self)
|
||||
|
||||
def save(self):
|
||||
|
||||
with utils.SaveWriteFile(self._filename, "wt") as fp:
|
||||
self._parser.write(fp)
|
||||
|
||||
|
||||
class WindowState (object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.logger = logging.getLogger("ui.window-state")
|
||||
|
||||
self.is_maximized = False
|
||||
|
||||
def attach(self, window, state):
|
||||
|
||||
self.window = window
|
||||
self.state = state
|
||||
|
||||
self.window.connect("window-state-event",
|
||||
self.handle_window_state_event)
|
||||
|
||||
geometry = self.state.geometry
|
||||
if geometry:
|
||||
self.window.move(*geometry[:2])
|
||||
self.window.set_default_size(*geometry[2:])
|
||||
|
||||
if self.state.maximized:
|
||||
self.logger.debug("initially maximized")
|
||||
self.window.maximize()
|
||||
|
||||
def detach(self):
|
||||
|
||||
window = self.window
|
||||
|
||||
self.state.maximized = self.is_maximized
|
||||
if not self.is_maximized:
|
||||
position = tuple(window.get_position())
|
||||
size = tuple(window.get_size())
|
||||
self.state.geometry = position + size
|
||||
|
||||
self.window.disconnect_by_func(self.handle_window_state_event)
|
||||
self.window = None
|
||||
|
||||
def handle_window_state_event(self, window, event):
|
||||
|
||||
if not event.changed_mask & Gdk.WindowState.MAXIMIZED:
|
||||
return
|
||||
|
||||
if event.new_window_state & Gdk.WindowState.MAXIMIZED:
|
||||
self.logger.debug("maximized")
|
||||
self.is_maximized = True
|
||||
else:
|
||||
self.logger.debug("unmaximized")
|
||||
self.is_maximized = False
|
||||
@@ -0,0 +1,366 @@
|
||||
# -*- coding: utf-8; mode: python; -*-
|
||||
#
|
||||
# GStreamer Development Utilities
|
||||
#
|
||||
# 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 Development Utilities Common Main module."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
from operator import attrgetter
|
||||
import logging
|
||||
import locale
|
||||
import gettext
|
||||
from gettext import gettext as _, ngettext
|
||||
|
||||
import gi
|
||||
|
||||
from gi.repository import GLib
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class ExceptionHandler (object):
|
||||
|
||||
exc_types = (Exception,)
|
||||
priority = 50
|
||||
inherit_fork = True
|
||||
|
||||
_handling_exception = False
|
||||
|
||||
def __call__(self, exc_type, exc_value, exc_traceback):
|
||||
|
||||
raise NotImplementedError(
|
||||
"derived classes need to override this method")
|
||||
|
||||
|
||||
class DefaultExceptionHandler (ExceptionHandler):
|
||||
exc_types = (BaseException,)
|
||||
priority = 0
|
||||
inherit_fork = True
|
||||
|
||||
def __init__(self, excepthook):
|
||||
|
||||
ExceptionHandler.__init__(self)
|
||||
|
||||
self.excepthook = excepthook
|
||||
|
||||
def __call__(self, *exc_info):
|
||||
|
||||
return self.excepthook(*exc_info)
|
||||
|
||||
|
||||
class ExitOnInterruptExceptionHandler (ExceptionHandler):
|
||||
|
||||
exc_types = (KeyboardInterrupt,)
|
||||
priority = 100
|
||||
inherit_fork = False
|
||||
|
||||
exit_status = 2
|
||||
|
||||
def __call__(self, *args):
|
||||
|
||||
print("Interrupt caught, exiting.", file=sys.stderr)
|
||||
|
||||
sys.exit(self.exit_status)
|
||||
|
||||
|
||||
class MainLoopWrapper (ExceptionHandler):
|
||||
|
||||
priority = 95
|
||||
inherit_fork = False
|
||||
|
||||
def __init__(self, enter, exit):
|
||||
|
||||
ExceptionHandler.__init__(self)
|
||||
|
||||
self.exc_info = (None,) * 3
|
||||
self.enter = enter
|
||||
self.exit = exit
|
||||
|
||||
def __call__(self, *exc_info):
|
||||
|
||||
self.exc_info = exc_info
|
||||
self.exit()
|
||||
|
||||
def run(self):
|
||||
|
||||
ExceptHookManager.register_handler(self)
|
||||
try:
|
||||
self.enter()
|
||||
finally:
|
||||
ExceptHookManager.unregister_handler(self)
|
||||
|
||||
if self.exc_info != (None,) * 3:
|
||||
# Re-raise unhandled exception that occured while running the loop.
|
||||
exc_type, exc_value, exc_tb = self.exc_info
|
||||
raise exc_value
|
||||
|
||||
|
||||
class ExceptHookManagerClass (object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self._in_forked_child = False
|
||||
|
||||
self.handlers = []
|
||||
|
||||
def setup(self):
|
||||
|
||||
if sys.excepthook == self.__excepthook:
|
||||
raise ValueError("already set up")
|
||||
|
||||
hook = sys.excepthook
|
||||
self.__instrument_excepthook()
|
||||
self.__instrument_fork()
|
||||
self.register_handler(DefaultExceptionHandler(hook))
|
||||
|
||||
def shutdown(self):
|
||||
|
||||
if sys.excepthook != self.__excepthook:
|
||||
raise ValueError("not set up")
|
||||
|
||||
self.__restore_excepthook()
|
||||
self.__restore_fork()
|
||||
|
||||
def __instrument_excepthook(self):
|
||||
|
||||
hook = sys.excepthook
|
||||
self._original_excepthook = hook
|
||||
sys.excepthook = self.__excepthook
|
||||
|
||||
def __restore_excepthook(self):
|
||||
|
||||
sys.excepthook = self._original_excepthook
|
||||
|
||||
def __instrument_fork(self):
|
||||
|
||||
try:
|
||||
fork = os.fork
|
||||
except AttributeError:
|
||||
# System has no fork() system call.
|
||||
self._original_fork = None
|
||||
else:
|
||||
self._original_fork = fork
|
||||
os.fork = self.__fork
|
||||
|
||||
def __restore_fork(self):
|
||||
|
||||
if not hasattr(os, "fork"):
|
||||
return
|
||||
|
||||
os.fork = self._original_fork
|
||||
|
||||
def entered_forked_child(self):
|
||||
|
||||
self._in_forked_child = True
|
||||
|
||||
for handler in tuple(self.handlers):
|
||||
if not handler.inherit_fork:
|
||||
self.handlers.remove(handler)
|
||||
|
||||
def register_handler(self, handler):
|
||||
|
||||
if self._in_forked_child and not handler.inherit_fork:
|
||||
return
|
||||
|
||||
self.handlers.append(handler)
|
||||
|
||||
def unregister_handler(self, handler):
|
||||
|
||||
self.handlers.remove(handler)
|
||||
|
||||
def __fork(self):
|
||||
|
||||
pid = self._original_fork()
|
||||
if pid == 0:
|
||||
# Child process.
|
||||
self.entered_forked_child()
|
||||
return pid
|
||||
|
||||
def __excepthook(self, exc_type, exc_value, exc_traceback):
|
||||
|
||||
for handler in sorted(self.handlers,
|
||||
key=attrgetter("priority"),
|
||||
reverse=True):
|
||||
|
||||
if handler._handling_exception:
|
||||
continue
|
||||
|
||||
for type_ in handler.exc_types:
|
||||
if issubclass(exc_type, type_):
|
||||
break
|
||||
else:
|
||||
continue
|
||||
|
||||
handler._handling_exception = True
|
||||
handler(exc_type, exc_value, exc_traceback)
|
||||
# Not using try...finally on purpose here. If the handler itself
|
||||
# fails with an exception, this prevents recursing into it again.
|
||||
handler._handling_exception = False
|
||||
return
|
||||
|
||||
else:
|
||||
from warnings import warn
|
||||
warn("ExceptHookManager: unhandled %r" % (exc_value,),
|
||||
RuntimeWarning,
|
||||
stacklevel=2)
|
||||
|
||||
|
||||
ExceptHookManager = ExceptHookManagerClass()
|
||||
|
||||
|
||||
class PathsBase (object):
|
||||
|
||||
data_dir = None
|
||||
icon_dir = None
|
||||
locale_dir = None
|
||||
|
||||
@classmethod
|
||||
def setup_installed(cls, data_prefix):
|
||||
"""Set up paths for running from a regular installation."""
|
||||
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def setup_devenv(cls, source_dir):
|
||||
"""Set up paths for running the development environment
|
||||
(i.e. directly from the source dist)."""
|
||||
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def ensure_setup(cls):
|
||||
"""If paths are still not set up, try to set from a fallback."""
|
||||
|
||||
if cls.data_dir is None:
|
||||
source_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(__file__)))
|
||||
cls.setup_devenv(source_dir)
|
||||
|
||||
def __new__(cls):
|
||||
|
||||
raise RuntimeError("do not create instances of this class -- "
|
||||
"use the class object directly")
|
||||
|
||||
|
||||
class PathsProgramBase (PathsBase):
|
||||
|
||||
program_name = None
|
||||
|
||||
@classmethod
|
||||
def setup_installed(cls, data_prefix):
|
||||
|
||||
if cls.program_name is None:
|
||||
raise NotImplementedError(
|
||||
"derived classes need to set program_name attribute")
|
||||
|
||||
cls.data_dir = os.path.join(data_prefix, cls.program_name)
|
||||
cls.icon_dir = os.path.join(data_prefix, "icons")
|
||||
cls.locale_dir = os.path.join(data_prefix, "locale")
|
||||
|
||||
@classmethod
|
||||
def setup_devenv(cls, source_dir):
|
||||
"""Set up paths for running the development environment
|
||||
(i.e. directly from the source dist)."""
|
||||
|
||||
# This is essential: The GUI module needs to find the .glade file.
|
||||
cls.data_dir = os.path.join(source_dir, "data")
|
||||
|
||||
# The locale data might be missing if "setup.py build" wasn't run.
|
||||
cls.locale_dir = os.path.join(source_dir, "build", "mo")
|
||||
|
||||
# Not setting icon_dir. It is not useful since we don't employ the
|
||||
# needed directory structure in the source dist.
|
||||
|
||||
|
||||
def _init_excepthooks():
|
||||
|
||||
ExceptHookManager.setup()
|
||||
ExceptHookManager.register_handler(ExitOnInterruptExceptionHandler())
|
||||
|
||||
|
||||
def _init_paths(paths):
|
||||
|
||||
paths.ensure_setup()
|
||||
|
||||
|
||||
def _init_locale(gettext_domain=None):
|
||||
|
||||
if Paths.locale_dir and gettext_domain is not None:
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
except locale.Error as exc:
|
||||
from warnings import warn
|
||||
warn("locale error: %s" % (exc,),
|
||||
RuntimeWarning,
|
||||
stacklevel=2)
|
||||
Paths.locale_dir = None
|
||||
else:
|
||||
gettext.bindtextdomain(gettext_domain, Paths.locale_dir)
|
||||
gettext.textdomain(gettext_domain)
|
||||
gettext.bind_textdomain_codeset(gettext_domain, "UTF-8")
|
||||
|
||||
|
||||
def _init_logging(level):
|
||||
if level == "none":
|
||||
return
|
||||
|
||||
mapping = {"debug": logging.DEBUG,
|
||||
"info": logging.INFO,
|
||||
"warning": logging.WARNING,
|
||||
"error": logging.ERROR,
|
||||
"critical": logging.CRITICAL}
|
||||
logging.basicConfig(level=mapping[level],
|
||||
format='%(asctime)s.%(msecs)03d %(levelname)8s %(name)20s: %(message)s',
|
||||
datefmt='%H:%M:%S')
|
||||
|
||||
logger = logging.getLogger("main")
|
||||
logger.debug("logging at level %s", logging.getLevelName(level))
|
||||
logger.info("using Python %i.%i.%i %s %i", *sys.version_info)
|
||||
|
||||
|
||||
def _init_log_option(parser):
|
||||
choices = ["none", "debug", "info", "warning", "error", "critical"]
|
||||
parser.add_option("--log-level", "-l",
|
||||
type="choice",
|
||||
choices=choices,
|
||||
action="store",
|
||||
dest="log_level",
|
||||
default="none",
|
||||
help=_("Enable logging, possible values: ") + ", ".join(choices))
|
||||
return parser
|
||||
|
||||
|
||||
def main(main_function, option_parser, gettext_domain=None, paths=None):
|
||||
|
||||
# FIXME:
|
||||
global Paths
|
||||
Paths = paths
|
||||
|
||||
_init_excepthooks()
|
||||
_init_paths(paths)
|
||||
_init_locale(gettext_domain)
|
||||
parser = _init_log_option(option_parser)
|
||||
options, args = option_parser.parse_args()
|
||||
_init_logging(options.log_level)
|
||||
|
||||
try:
|
||||
main_function(args)
|
||||
finally:
|
||||
logging.shutdown()
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8; mode: python; -*-
|
||||
#
|
||||
# GStreamer Development Utilities
|
||||
#
|
||||
# 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 Development Utilities Common package."""
|
||||
|
||||
from . import Data
|
||||
from . import GUI
|
||||
from . import Main
|
||||
from . import utils
|
||||
@@ -0,0 +1,420 @@
|
||||
# -*- Mode: Python; py-indent-offset: 4 -*-
|
||||
# generictreemodel - GenericTreeModel implementation for pygtk compatibility.
|
||||
# Copyright (C) 2013 Simon Feltman
|
||||
#
|
||||
# generictreemodel.py: GenericTreeModel implementation for pygtk compatibility
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# System
|
||||
import sys
|
||||
import random
|
||||
import collections
|
||||
import ctypes
|
||||
|
||||
# GObject
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class _CTreeIter(ctypes.Structure):
|
||||
_fields_ = [('stamp', ctypes.c_int),
|
||||
('user_data', ctypes.c_void_p),
|
||||
('user_data2', ctypes.c_void_p),
|
||||
('user_data3', ctypes.c_void_p)]
|
||||
|
||||
@classmethod
|
||||
def from_iter(cls, iter):
|
||||
offset = sys.getsizeof(object()) # size of PyObject_HEAD
|
||||
return ctypes.POINTER(cls).from_address(id(iter) + offset)
|
||||
|
||||
|
||||
def _get_user_data_as_pyobject(iter):
|
||||
citer = _CTreeIter.from_iter(iter)
|
||||
return ctypes.cast(citer.contents.user_data, ctypes.py_object).value
|
||||
|
||||
|
||||
def handle_exception(default_return):
|
||||
"""Returns a function which can act as a decorator for wrapping exceptions and
|
||||
returning "default_return" upon an exception being thrown.
|
||||
|
||||
This is used to wrap Gtk.TreeModel "do_" method implementations so we can return
|
||||
a proper value from the override upon an exception occurring with client code
|
||||
implemented by the "on_" methods.
|
||||
"""
|
||||
def decorator(func):
|
||||
def wrapped_func(*args, **kargs):
|
||||
try:
|
||||
return func(*args, **kargs)
|
||||
except BaseException:
|
||||
# Use excepthook directly to avoid any printing to the screen
|
||||
# if someone installed an except hook.
|
||||
sys.excepthook(*sys.exc_info())
|
||||
return default_return
|
||||
return wrapped_func
|
||||
return decorator
|
||||
|
||||
|
||||
class GenericTreeModel(GObject.GObject, Gtk.TreeModel):
|
||||
|
||||
"""A base implementation of a Gtk.TreeModel for python.
|
||||
|
||||
The GenericTreeModel eases implementing the Gtk.TreeModel interface in Python.
|
||||
The class can be subclassed to provide a TreeModel implementation which works
|
||||
directly with Python objects instead of iterators.
|
||||
|
||||
All of the on_* methods should be overridden by subclasses to provide the
|
||||
underlying implementation a way to access custom model data. For the purposes of
|
||||
this API, all custom model data supplied or handed back through the overridable
|
||||
API will use the argument names: node, parent, and child in regards to user data
|
||||
python objects.
|
||||
|
||||
The create_tree_iter, set_user_data, invalidate_iters, iter_is_valid methods are
|
||||
available to help manage Gtk.TreeIter objects and their Python object references.
|
||||
|
||||
GenericTreeModel manages a pool of user data nodes that have been used with iters.
|
||||
This pool stores a references to user data nodes as a dictionary value with the
|
||||
key being the integer id of the data. This id is what the Gtk.TreeIter objects
|
||||
use to reference data in the pool.
|
||||
References will be removed from the pool when the model is deleted or explicitly
|
||||
by using the optional "node" argument to the "row_deleted" method when notifying
|
||||
the model of row deletion.
|
||||
"""
|
||||
|
||||
leak_references = GObject.Property(default=True, type=bool,
|
||||
blurb="If True, strong references to user data attached to iters are "
|
||||
"stored in a dictionary pool (default). Otherwise the user data is "
|
||||
"stored as a raw pointer to a python object without a reference.")
|
||||
|
||||
#
|
||||
# Methods
|
||||
#
|
||||
def __init__(self):
|
||||
"""Initialize. Make sure to call this from derived classes if overridden."""
|
||||
super(GenericTreeModel, self).__init__()
|
||||
self.stamp = 0
|
||||
|
||||
#: Dictionary of (id(user_data): user_data), used when leak-refernces=False
|
||||
self._held_refs = dict()
|
||||
|
||||
# Set initial stamp
|
||||
self.invalidate_iters()
|
||||
|
||||
def iter_depth_first(self):
|
||||
"""Depth-first iteration of the entire TreeModel yielding the python nodes."""
|
||||
stack = collections.deque([None])
|
||||
while stack:
|
||||
it = stack.popleft()
|
||||
if it is not None:
|
||||
yield self.get_user_data(it)
|
||||
children = [self.iter_nth_child(it, i)
|
||||
for i in range(self.iter_n_children(it))]
|
||||
stack.extendleft(reversed(children))
|
||||
|
||||
def invalidate_iter(self, iter):
|
||||
"""Clear user data and its reference from the iter and this model."""
|
||||
iter.stamp = 0
|
||||
if iter.user_data:
|
||||
if iter.user_data in self._held_refs:
|
||||
del self._held_refs[iter.user_data]
|
||||
iter.user_data = None
|
||||
|
||||
def invalidate_iters(self):
|
||||
"""
|
||||
This method invalidates all TreeIter objects associated with this custom tree model
|
||||
and frees their locally pooled references.
|
||||
"""
|
||||
self.stamp = random.randint(-2147483648, 2147483647)
|
||||
self._held_refs.clear()
|
||||
|
||||
def iter_is_valid(self, iter):
|
||||
"""
|
||||
:Returns:
|
||||
True if the gtk.TreeIter specified by iter is valid for the custom tree model.
|
||||
"""
|
||||
return iter.stamp == self.stamp
|
||||
|
||||
def get_user_data(self, iter):
|
||||
"""Get the user_data associated with the given TreeIter.
|
||||
|
||||
GenericTreeModel stores arbitrary Python objects mapped to instances of Gtk.TreeIter.
|
||||
This method allows to retrieve the Python object held by the given iterator.
|
||||
"""
|
||||
if self.leak_references:
|
||||
return self._held_refs[iter.user_data]
|
||||
else:
|
||||
return _get_user_data_as_pyobject(iter)
|
||||
|
||||
def set_user_data(self, iter, user_data):
|
||||
"""Applies user_data and stamp to the given iter.
|
||||
|
||||
If the models "leak_references" property is set, a reference to the
|
||||
user_data is stored with the model to ensure we don't run into bad
|
||||
memory problems with the TreeIter.
|
||||
"""
|
||||
iter.user_data = id(user_data)
|
||||
|
||||
if user_data is None:
|
||||
self.invalidate_iter(iter)
|
||||
else:
|
||||
iter.stamp = self.stamp
|
||||
if self.leak_references:
|
||||
self._held_refs[iter.user_data] = user_data
|
||||
|
||||
def create_tree_iter(self, user_data):
|
||||
"""Create a Gtk.TreeIter instance with the given user_data specific for this model.
|
||||
|
||||
Use this method to create Gtk.TreeIter instance instead of directly calling
|
||||
Gtk.Treeiter(), this will ensure proper reference managment of wrapped used_data.
|
||||
"""
|
||||
iter = Gtk.TreeIter()
|
||||
self.set_user_data(iter, user_data)
|
||||
return iter
|
||||
|
||||
def _create_tree_iter(self, data):
|
||||
"""Internal creation of a (bool, TreeIter) pair for returning directly
|
||||
back to the view interfacing with this model."""
|
||||
if data is None:
|
||||
return (False, None)
|
||||
else:
|
||||
it = self.create_tree_iter(data)
|
||||
return (True, it)
|
||||
|
||||
def row_deleted(self, path, node=None):
|
||||
"""Notify the model a row has been deleted.
|
||||
|
||||
Use the node parameter to ensure the user_data reference associated
|
||||
with the path is properly freed by this model.
|
||||
|
||||
:Parameters:
|
||||
path : Gtk.TreePath
|
||||
Path to the row that has been deleted.
|
||||
node : object
|
||||
Python object used as the node returned from "on_get_iter". This is
|
||||
optional but ensures the model will not leak references to this object.
|
||||
"""
|
||||
super(GenericTreeModel, self).row_deleted(path)
|
||||
node_id = id(node)
|
||||
if node_id in self._held_refs:
|
||||
del self._held_refs[node_id]
|
||||
|
||||
#
|
||||
# GtkTreeModel Interface Implementation
|
||||
#
|
||||
@handle_exception(0)
|
||||
def do_get_flags(self):
|
||||
"""Internal method."""
|
||||
return self.on_get_flags()
|
||||
|
||||
@handle_exception(0)
|
||||
def do_get_n_columns(self):
|
||||
"""Internal method."""
|
||||
return self.on_get_n_columns()
|
||||
|
||||
@handle_exception(GObject.TYPE_INVALID)
|
||||
def do_get_column_type(self, index):
|
||||
"""Internal method."""
|
||||
return self.on_get_column_type(index)
|
||||
|
||||
@handle_exception((False, None))
|
||||
def do_get_iter(self, path):
|
||||
"""Internal method."""
|
||||
return self._create_tree_iter(self.on_get_iter(path))
|
||||
|
||||
@handle_exception(False)
|
||||
def do_iter_next(self, iter):
|
||||
"""Internal method."""
|
||||
if iter is None:
|
||||
next_data = self.on_iter_next(None)
|
||||
else:
|
||||
next_data = self.on_iter_next(self.get_user_data(iter))
|
||||
|
||||
self.set_user_data(iter, next_data)
|
||||
return next_data is not None
|
||||
|
||||
@handle_exception(None)
|
||||
def do_get_path(self, iter):
|
||||
"""Internal method."""
|
||||
path = self.on_get_path(self.get_user_data(iter))
|
||||
if path is None:
|
||||
return None
|
||||
else:
|
||||
return Gtk.TreePath(path)
|
||||
|
||||
@handle_exception(None)
|
||||
def do_get_value(self, iter, column):
|
||||
"""Internal method."""
|
||||
return self.on_get_value(self.get_user_data(iter), column)
|
||||
|
||||
@handle_exception((False, None))
|
||||
def do_iter_children(self, parent):
|
||||
"""Internal method."""
|
||||
data = self.get_user_data(parent) if parent else None
|
||||
return self._create_tree_iter(self.on_iter_children(data))
|
||||
|
||||
@handle_exception(False)
|
||||
def do_iter_has_child(self, parent):
|
||||
"""Internal method."""
|
||||
return self.on_iter_has_child(self.get_user_data(parent))
|
||||
|
||||
@handle_exception(0)
|
||||
def do_iter_n_children(self, iter):
|
||||
"""Internal method."""
|
||||
if iter is None:
|
||||
return self.on_iter_n_children(None)
|
||||
return self.on_iter_n_children(self.get_user_data(iter))
|
||||
|
||||
@handle_exception((False, None))
|
||||
def do_iter_nth_child(self, parent, n):
|
||||
"""Internal method."""
|
||||
if parent is None:
|
||||
data = self.on_iter_nth_child(None, n)
|
||||
else:
|
||||
data = self.on_iter_nth_child(self.get_user_data(parent), n)
|
||||
return self._create_tree_iter(data)
|
||||
|
||||
@handle_exception((False, None))
|
||||
def do_iter_parent(self, child):
|
||||
"""Internal method."""
|
||||
return self._create_tree_iter(self.on_iter_parent(self.get_user_data(child)))
|
||||
|
||||
@handle_exception(None)
|
||||
def do_ref_node(self, iter):
|
||||
self.on_ref_node(self.get_user_data(iter))
|
||||
|
||||
@handle_exception(None)
|
||||
def do_unref_node(self, iter):
|
||||
self.on_unref_node(self.get_user_data(iter))
|
||||
|
||||
#
|
||||
# Python Subclass Overridables
|
||||
#
|
||||
def on_get_flags(self):
|
||||
"""Overridable.
|
||||
|
||||
:Returns Gtk.TreeModelFlags:
|
||||
The flags for this model. See: Gtk.TreeModelFlags
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_get_n_columns(self):
|
||||
"""Overridable.
|
||||
|
||||
:Returns:
|
||||
The number of columns for this model.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_get_column_type(self, index):
|
||||
"""Overridable.
|
||||
|
||||
:Returns:
|
||||
The column type for the given index.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_get_iter(self, path):
|
||||
"""Overridable.
|
||||
|
||||
:Returns:
|
||||
A python object (node) for the given TreePath.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_iter_next(self, node):
|
||||
"""Overridable.
|
||||
|
||||
:Parameters:
|
||||
node : object
|
||||
Node at current level.
|
||||
|
||||
:Returns:
|
||||
A python object (node) following the given node at the current level.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_get_path(self, node):
|
||||
"""Overridable.
|
||||
|
||||
:Returns:
|
||||
A TreePath for the given node.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_get_value(self, node, column):
|
||||
"""Overridable.
|
||||
|
||||
:Parameters:
|
||||
node : object
|
||||
column : int
|
||||
Column index to get the value from.
|
||||
|
||||
:Returns:
|
||||
The value of the column for the given node."""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_iter_children(self, parent):
|
||||
"""Overridable.
|
||||
|
||||
:Returns:
|
||||
The first child of parent or None if parent has no children.
|
||||
If parent is None, return the first node of the model.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_iter_has_child(self, node):
|
||||
"""Overridable.
|
||||
|
||||
:Returns:
|
||||
True if the given node has children.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_iter_n_children(self, node):
|
||||
"""Overridable.
|
||||
|
||||
:Returns:
|
||||
The number of children for the given node. If node is None,
|
||||
return the number of top level nodes.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_iter_nth_child(self, parent, n):
|
||||
"""Overridable.
|
||||
|
||||
:Parameters:
|
||||
parent : object
|
||||
n : int
|
||||
Index of child within parent.
|
||||
|
||||
:Returns:
|
||||
The child for the given parent index starting at 0. If parent None,
|
||||
return the top level node corresponding to "n".
|
||||
If "n" is larger then available nodes, return None.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_iter_parent(self, child):
|
||||
"""Overridable.
|
||||
|
||||
:Returns:
|
||||
The parent node of child or None if child is a top level node."""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_ref_node(self, node):
|
||||
pass
|
||||
|
||||
def on_unref_node(self, node):
|
||||
pass
|
||||
@@ -0,0 +1,333 @@
|
||||
# -*- coding: utf-8; mode: python; -*-
|
||||
#
|
||||
# GStreamer Development Utilities
|
||||
#
|
||||
# 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 Development Utilities Common utils module."""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import subprocess as _subprocess
|
||||
|
||||
|
||||
class SingletonMeta (type):
|
||||
|
||||
def __init__(cls, name, bases, dict_):
|
||||
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
super(SingletonMeta, cls).__init__(name, bases, dict_)
|
||||
|
||||
cls._singleton_instances = WeakValueDictionary()
|
||||
|
||||
def __call__(cls, *a, **kw):
|
||||
|
||||
kw_key = tuple(sorted(kw.items()))
|
||||
|
||||
try:
|
||||
obj = cls._singleton_instances[a + kw_key]
|
||||
except KeyError:
|
||||
obj = super(SingletonMeta, cls).__call__(*a, **kw)
|
||||
cls._singleton_instances[a + kw_key] = obj
|
||||
return obj
|
||||
|
||||
|
||||
def gettext_cache():
|
||||
"""Return a callable object that operates like gettext.gettext, but is much
|
||||
faster when a string is looked up more than once. This is very useful in
|
||||
loops, where calling gettext.gettext can quickly become a major performance
|
||||
bottleneck."""
|
||||
|
||||
from gettext import gettext
|
||||
|
||||
d = {}
|
||||
|
||||
def gettext_cache_access(s):
|
||||
|
||||
if s not in d:
|
||||
d[s] = gettext(s)
|
||||
return d[s]
|
||||
|
||||
return gettext_cache_access
|
||||
|
||||
|
||||
class ClassProperty (property):
|
||||
|
||||
"Like the property class, but also invokes the getter for class access."
|
||||
|
||||
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
|
||||
|
||||
property.__init__(self, fget, fset, fdel, doc)
|
||||
|
||||
self.__fget = fget
|
||||
|
||||
def __get__(self, obj, obj_class=None):
|
||||
|
||||
ret = property.__get__(self, obj, obj_class)
|
||||
if ret == self:
|
||||
return self.__fget(None)
|
||||
else:
|
||||
return ret
|
||||
|
||||
|
||||
class _XDGClass (object):
|
||||
|
||||
"""Partial implementation of the XDG Base Directory specification v0.6.
|
||||
|
||||
http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self._add_base_dir("DATA_HOME", "~/.local/share")
|
||||
self._add_base_dir("CONFIG_HOME", "~/.config")
|
||||
self._add_base_dir("CACHE_HOME", "~/.cache")
|
||||
|
||||
def _add_base_dir(self, name, default):
|
||||
|
||||
dir = os.environ.get("XDG_%s" % (name,))
|
||||
if not dir:
|
||||
dir = os.path.expanduser(os.path.join(*default.split("/")))
|
||||
|
||||
setattr(self, name, dir)
|
||||
|
||||
|
||||
XDG = _XDGClass()
|
||||
|
||||
|
||||
class SaveWriteFile (object):
|
||||
|
||||
def __init__(self, filename, mode="wt"):
|
||||
|
||||
from tempfile import mkstemp
|
||||
|
||||
self.logger = logging.getLogger("tempfile")
|
||||
|
||||
dir = os.path.dirname(filename)
|
||||
base_name = os.path.basename(filename)
|
||||
temp_prefix = "%s-tmp" % (base_name,)
|
||||
|
||||
if dir:
|
||||
# Destination dir differs from current directory, ensure that it
|
||||
# exists:
|
||||
try:
|
||||
os.makedirs(dir)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self.clean_stale(dir, temp_prefix)
|
||||
|
||||
fd, temp_name = mkstemp(dir=dir, prefix=temp_prefix)
|
||||
|
||||
self.target_name = filename
|
||||
self.temp_name = temp_name
|
||||
self.real_file = os.fdopen(fd, mode)
|
||||
|
||||
def __enter__(self):
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_args):
|
||||
|
||||
if exc_args == (None, None, None,):
|
||||
self.close()
|
||||
else:
|
||||
self.discard()
|
||||
|
||||
def __del__(self):
|
||||
|
||||
try:
|
||||
self.discard()
|
||||
except AttributeError:
|
||||
# If __init__ failed, self has no real_file attribute.
|
||||
pass
|
||||
|
||||
def __close_real(self):
|
||||
|
||||
if self.real_file:
|
||||
self.real_file.close()
|
||||
self.real_file = None
|
||||
|
||||
def clean_stale(self, dir, temp_prefix):
|
||||
|
||||
from time import time
|
||||
from glob import glob
|
||||
|
||||
now = time()
|
||||
pattern = os.path.join(dir, "%s*" % (temp_prefix,))
|
||||
|
||||
for temp_filename in glob(pattern):
|
||||
mtime = os.stat(temp_filename).st_mtime
|
||||
if now - mtime > 3600:
|
||||
self.logger.info("deleting stale temporary file %s",
|
||||
temp_filename)
|
||||
try:
|
||||
os.unlink(temp_filename)
|
||||
except EnvironmentError as exc:
|
||||
self.logger.warning("deleting stale temporary file "
|
||||
"failed: %s", exc)
|
||||
|
||||
def tell(self, *a, **kw):
|
||||
|
||||
return self.real_file.tell(*a, **kw)
|
||||
|
||||
def write(self, *a, **kw):
|
||||
|
||||
return self.real_file.write(*a, **kw)
|
||||
|
||||
def close(self):
|
||||
|
||||
self.__close_real()
|
||||
|
||||
if self.temp_name:
|
||||
try:
|
||||
os.rename(self.temp_name, self.target_name)
|
||||
except OSError as exc:
|
||||
import errno
|
||||
if exc.errno == errno.EEXIST:
|
||||
# We are probably on windows.
|
||||
os.unlink(self.target_name)
|
||||
os.rename(self.temp_name, self.target_name)
|
||||
self.temp_name = None
|
||||
|
||||
def discard(self):
|
||||
|
||||
self.__close_real()
|
||||
|
||||
if self.temp_name:
|
||||
|
||||
try:
|
||||
os.unlink(self.temp_name)
|
||||
except EnvironmentError as exc:
|
||||
self.logger.warning("deleting temporary file failed: %s", exc)
|
||||
self.temp_name = None
|
||||
|
||||
|
||||
class TeeWriteFile (object):
|
||||
|
||||
# TODO Py2.5: Add context manager methods.
|
||||
|
||||
def __init__(self, *file_objects):
|
||||
|
||||
self.files = list(file_objects)
|
||||
|
||||
def close(self):
|
||||
|
||||
for file in self.files:
|
||||
file.close()
|
||||
|
||||
def flush(self):
|
||||
|
||||
for file in self.files:
|
||||
file.flush()
|
||||
|
||||
def write(self, string):
|
||||
|
||||
for file in self.files:
|
||||
file.write(string)
|
||||
|
||||
def writelines(self, lines):
|
||||
|
||||
for file in self.files:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
class FixedPopen (_subprocess.Popen):
|
||||
|
||||
def __init__(self, args, **kw):
|
||||
|
||||
# Unconditionally specify all descriptors as redirected, to
|
||||
# work around Python bug #1358527 (which is triggered for
|
||||
# console-less applications on Windows).
|
||||
|
||||
close = []
|
||||
|
||||
for name in ("stdin", "stdout", "stderr",):
|
||||
target = kw.get(name)
|
||||
if not target:
|
||||
kw[name] = _subprocess.PIPE
|
||||
close.append(name)
|
||||
|
||||
_subprocess.Popen.__init__(self, args, **kw)
|
||||
|
||||
for name in close:
|
||||
fp = getattr(self, name)
|
||||
fp.close()
|
||||
setattr(self, name, None)
|
||||
|
||||
|
||||
class DevhelpError (EnvironmentError):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DevhelpUnavailableError (DevhelpError):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DevhelpClient (object):
|
||||
|
||||
def available(self):
|
||||
|
||||
try:
|
||||
self.version()
|
||||
except DevhelpUnavailableError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def version(self):
|
||||
|
||||
return self._invoke("--version")
|
||||
|
||||
def search(self, entry):
|
||||
|
||||
self._invoke_no_interact("-s", entry)
|
||||
|
||||
def _check_os_error(self, exc):
|
||||
|
||||
import errno
|
||||
if exc.errno == errno.ENOENT:
|
||||
raise DevhelpUnavailableError()
|
||||
|
||||
def _invoke(self, *args):
|
||||
|
||||
from subprocess import PIPE
|
||||
|
||||
try:
|
||||
proc = FixedPopen(("devhelp",) + args,
|
||||
stdout=PIPE)
|
||||
except OSError as exc:
|
||||
self._check_os_error(exc)
|
||||
raise
|
||||
|
||||
out, err = proc.communicate()
|
||||
|
||||
if proc.returncode is not None and proc.returncode != 0:
|
||||
raise DevhelpError("devhelp exited with status %i"
|
||||
% (proc.returncode,))
|
||||
return out
|
||||
|
||||
def _invoke_no_interact(self, *args):
|
||||
|
||||
from subprocess import PIPE
|
||||
|
||||
try:
|
||||
proc = FixedPopen(("devhelp",) + args)
|
||||
except OSError as exc:
|
||||
self._check_os_error(exc)
|
||||
raise
|
||||
@@ -0,0 +1,482 @@
|
||||
# -*- 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 Data module."""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Nanosecond resolution (like Gst.SECOND)
|
||||
SECOND = 1000000000
|
||||
|
||||
|
||||
def time_args(ts):
|
||||
|
||||
secs = ts // SECOND
|
||||
|
||||
return "%i:%02i:%02i.%09i" % (secs // 60 ** 2,
|
||||
secs // 60 % 60,
|
||||
secs % 60,
|
||||
ts % SECOND,)
|
||||
|
||||
|
||||
def time_diff_args(time_diff):
|
||||
|
||||
if time_diff >= 0:
|
||||
sign = "+"
|
||||
else:
|
||||
sign = "-"
|
||||
|
||||
secs = abs(time_diff) // SECOND
|
||||
|
||||
return "%s%02i:%02i.%09i" % (sign,
|
||||
secs // 60,
|
||||
secs % 60,
|
||||
abs(time_diff) % SECOND,)
|
||||
|
||||
|
||||
def time_args_no_hours(ts):
|
||||
|
||||
secs = ts // SECOND
|
||||
|
||||
return "%02i:%02i.%09i" % (secs // 60,
|
||||
secs % 60,
|
||||
ts % SECOND,)
|
||||
|
||||
|
||||
def parse_time(st):
|
||||
"""Parse time strings that look like "0:00:00.0000000"."""
|
||||
|
||||
h, m, s = st.split(":")
|
||||
secs, subsecs = s.split(".")
|
||||
|
||||
return int((int(h) * 60 ** 2 + int(m) * 60) * SECOND) + \
|
||||
int(secs) * SECOND + int(subsecs)
|
||||
|
||||
|
||||
class DebugLevel (int):
|
||||
|
||||
__names = ["NONE", "ERROR", "WARN", "FIXME",
|
||||
"INFO", "DEBUG", "LOG", "TRACE", "MEMDUMP"]
|
||||
__instances = {}
|
||||
|
||||
def __new__(cls, level):
|
||||
|
||||
try:
|
||||
level_int = int(level)
|
||||
except (ValueError, TypeError,):
|
||||
try:
|
||||
level_int = cls.__names.index(level.upper())
|
||||
except ValueError:
|
||||
raise ValueError("no debug level named %r" % (level,))
|
||||
if level_int in cls.__instances:
|
||||
return cls.__instances[level_int]
|
||||
else:
|
||||
new_instance = int.__new__(cls, level_int)
|
||||
new_instance.name = cls.__names[level_int]
|
||||
cls.__instances[level_int] = new_instance
|
||||
return new_instance
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return "<%s %s (%i)>" % (type(self).__name__, self.__names[self], self,)
|
||||
|
||||
def higher_level(self):
|
||||
|
||||
if self == len(self.__names) - 1:
|
||||
raise ValueError("already the highest debug level")
|
||||
|
||||
return DebugLevel(self + 1)
|
||||
|
||||
def lower_level(self):
|
||||
|
||||
if self == 0:
|
||||
raise ValueError("already the lowest debug level")
|
||||
|
||||
return DebugLevel(self - 1)
|
||||
|
||||
|
||||
debug_level_none = DebugLevel("NONE")
|
||||
debug_level_error = DebugLevel("ERROR")
|
||||
debug_level_warning = DebugLevel("WARN")
|
||||
debug_level_info = DebugLevel("INFO")
|
||||
debug_level_debug = DebugLevel("DEBUG")
|
||||
debug_level_log = DebugLevel("LOG")
|
||||
debug_level_fixme = DebugLevel("FIXME")
|
||||
debug_level_trace = DebugLevel("TRACE")
|
||||
debug_level_memdump = DebugLevel("MEMDUMP")
|
||||
debug_levels = [debug_level_none,
|
||||
debug_level_trace,
|
||||
debug_level_fixme,
|
||||
debug_level_log,
|
||||
debug_level_debug,
|
||||
debug_level_info,
|
||||
debug_level_warning,
|
||||
debug_level_error,
|
||||
debug_level_memdump]
|
||||
|
||||
# For stripping color codes:
|
||||
_escape = re.compile(b"\x1b\\[[0-9;]*m")
|
||||
|
||||
|
||||
def strip_escape(s):
|
||||
|
||||
# FIXME: This can be optimized further!
|
||||
|
||||
while b"\x1b" in s:
|
||||
s = _escape.sub(b"", s)
|
||||
return s
|
||||
|
||||
|
||||
def default_log_line_regex_():
|
||||
|
||||
# "DEBUG "
|
||||
LEVEL = "([A-Z]+)\s*"
|
||||
# "0x8165430 "
|
||||
THREAD = r"(0x[0-9a-f]+)\s+" # r"\((0x[0-9a-f]+) - "
|
||||
# "0:00:00.777913000 "
|
||||
TIME = r"(\d+:\d\d:\d\d\.\d+)\s+"
|
||||
CATEGORY = "([A-Za-z0-9_-]+)\s+" # "GST_REFCOUNTING ", "flacdec "
|
||||
# " 3089 "
|
||||
PID = r"(\d+)\s*"
|
||||
FILENAME = r"([^:]*):"
|
||||
LINE = r"(\d+):"
|
||||
FUNCTION = "(~?[A-Za-z0-9_\s\*,\(\)]*):"
|
||||
# FIXME: When non-g(st)object stuff is logged with *_OBJECT (like
|
||||
# buffers!), the address is printed *without* <> brackets!
|
||||
OBJECT = "(?:<([^>]+)>)?"
|
||||
MESSAGE = "(.+)"
|
||||
|
||||
ANSI = "(?:\x1b\\[[0-9;]*m\\s*)*\\s*"
|
||||
|
||||
# New log format:
|
||||
expressions = [TIME, ANSI, PID, ANSI, THREAD, ANSI, LEVEL, ANSI,
|
||||
CATEGORY, FILENAME, LINE, FUNCTION, ANSI,
|
||||
OBJECT, ANSI, MESSAGE]
|
||||
# Old log format:
|
||||
# expressions = [LEVEL, THREAD, TIME, CATEGORY, PID, FILENAME, LINE,
|
||||
# FUNCTION, OBJECT, MESSAGE]
|
||||
|
||||
return expressions
|
||||
|
||||
|
||||
def default_log_line_regex():
|
||||
|
||||
return re.compile("".join(default_log_line_regex_()))
|
||||
|
||||
|
||||
class Producer (object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.consumers = []
|
||||
|
||||
def have_load_started(self):
|
||||
|
||||
for consumer in self.consumers:
|
||||
consumer.handle_load_started()
|
||||
|
||||
def have_load_finished(self):
|
||||
|
||||
for consumer in self.consumers:
|
||||
consumer.handle_load_finished()
|
||||
|
||||
|
||||
class SortHelper (object):
|
||||
|
||||
def __init__(self, fileobj, offsets):
|
||||
|
||||
self._gen = self.__gen(fileobj, offsets)
|
||||
next(self._gen)
|
||||
|
||||
# Override in the instance, for performance (this gets called in an
|
||||
# inner loop):
|
||||
self.find_insert_position = self._gen.send
|
||||
|
||||
@staticmethod
|
||||
def find_insert_position(insert_time_string):
|
||||
|
||||
# Stub for documentary purposes.
|
||||
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def __gen(fileobj, offsets):
|
||||
|
||||
from math import floor
|
||||
tell = fileobj.tell
|
||||
seek = fileobj.seek
|
||||
read = fileobj.read
|
||||
time_len = len(time_args(0))
|
||||
|
||||
# We remember the previous insertion point. This gives a nice speed up
|
||||
# for larger bubbles which are already sorted. TODO: In practice, log
|
||||
# lines only get out of order across threads. Need to check if it pays
|
||||
# to parse the thread here and maintain multiple insertion points for
|
||||
# heavily interleaved parts of the log.
|
||||
pos = 0
|
||||
pos_time_string = ""
|
||||
|
||||
insert_pos = None
|
||||
while True:
|
||||
insert_time_string = (yield insert_pos)
|
||||
|
||||
save_offset = tell()
|
||||
|
||||
if pos_time_string <= insert_time_string:
|
||||
lo = pos
|
||||
hi = len(offsets)
|
||||
else:
|
||||
lo = 0
|
||||
hi = pos
|
||||
|
||||
# This is a bisection search, except we don't cut the range in the
|
||||
# middle each time, but at the 90th percentile. This is because
|
||||
# logs are "mostly sorted", so the insertion point is much more
|
||||
# likely to be at the end anyways:
|
||||
while lo < hi:
|
||||
mid = int(floor(lo * 0.1 + hi * 0.9))
|
||||
seek(offsets[mid])
|
||||
mid_time_string = read(time_len)
|
||||
if insert_time_string.encode('utf8') < mid_time_string:
|
||||
hi = mid
|
||||
else:
|
||||
lo = mid + 1
|
||||
pos = lo
|
||||
# Caller will replace row at pos with the new one, so this is
|
||||
# correct:
|
||||
pos_time_string = insert_time_string
|
||||
|
||||
insert_pos = pos
|
||||
|
||||
seek(save_offset)
|
||||
|
||||
|
||||
class LineCache (Producer):
|
||||
"""
|
||||
offsets: file position for each line
|
||||
levels: the debug level for each line
|
||||
"""
|
||||
|
||||
_lines_per_iteration = 50000
|
||||
|
||||
def __init__(self, fileobj, dispatcher):
|
||||
|
||||
Producer.__init__(self)
|
||||
|
||||
self.logger = logging.getLogger("linecache")
|
||||
self.dispatcher = dispatcher
|
||||
|
||||
self.__fileobj = fileobj
|
||||
self.__fileobj.seek(0, 2)
|
||||
self.__file_size = self.__fileobj.tell()
|
||||
self.__fileobj.seek(0)
|
||||
|
||||
self.offsets = []
|
||||
self.levels = [] # FIXME
|
||||
|
||||
def start_loading(self):
|
||||
|
||||
self.logger.debug("dispatching load process")
|
||||
self.have_load_started()
|
||||
self.dispatcher(self.__process())
|
||||
|
||||
def get_progress(self):
|
||||
|
||||
return float(self.__fileobj.tell()) / self.__file_size
|
||||
|
||||
def __process(self):
|
||||
|
||||
offsets = self.offsets
|
||||
levels = self.levels
|
||||
|
||||
dict_levels = {"T": debug_level_trace, "F": debug_level_fixme,
|
||||
"L": debug_level_log, "D": debug_level_debug,
|
||||
"I": debug_level_info, "W": debug_level_warning,
|
||||
"E": debug_level_error, " ": debug_level_none,
|
||||
"M": debug_level_memdump, }
|
||||
ANSI = "(?:\x1b\\[[0-9;]*m)?"
|
||||
ANSI_PATTERN = r"\d:\d\d:\d\d\.\d+ " + ANSI + \
|
||||
r" *\d+" + ANSI + \
|
||||
r" +0x[0-9a-f]+ +" + ANSI + \
|
||||
r"([TFLDIEWM ])"
|
||||
BARE_PATTERN = ANSI_PATTERN.replace(ANSI, "")
|
||||
rexp_bare = re.compile(BARE_PATTERN)
|
||||
rexp_ansi = re.compile(ANSI_PATTERN)
|
||||
rexp = rexp_bare
|
||||
|
||||
# Moving attribute lookups out of the loop:
|
||||
readline = self.__fileobj.readline
|
||||
tell = self.__fileobj.tell
|
||||
rexp_match = rexp.match
|
||||
levels_append = levels.append
|
||||
offsets_append = offsets.append
|
||||
dict_levels_get = dict_levels.get
|
||||
|
||||
self.__fileobj.seek(0)
|
||||
limit = self._lines_per_iteration
|
||||
last_line = ""
|
||||
i = 0
|
||||
sort_helper = SortHelper(self.__fileobj, offsets)
|
||||
find_insert_position = sort_helper.find_insert_position
|
||||
while True:
|
||||
i += 1
|
||||
if i >= limit:
|
||||
i = 0
|
||||
yield True
|
||||
|
||||
offset = tell()
|
||||
line = readline().decode('utf-8', errors='replace')
|
||||
if not line:
|
||||
break
|
||||
match = rexp_match(line)
|
||||
if match is None:
|
||||
if rexp is rexp_ansi or "\x1b" not in line:
|
||||
continue
|
||||
|
||||
match = rexp_ansi.match(line)
|
||||
if match is None:
|
||||
continue
|
||||
# Switch to slower ANSI parsing:
|
||||
rexp = rexp_ansi
|
||||
rexp_match = rexp.match
|
||||
|
||||
# Timestamp is in the very beginning of the row, and can be sorted
|
||||
# by lexical comparison. That's why we don't bother parsing the
|
||||
# time to integer. We also don't have to take a substring here,
|
||||
# which would be a useless memcpy.
|
||||
if line >= last_line:
|
||||
levels_append(
|
||||
dict_levels_get(match.group(1), debug_level_none))
|
||||
offsets_append(offset)
|
||||
last_line = line
|
||||
else:
|
||||
pos = find_insert_position(line)
|
||||
levels.insert(
|
||||
pos, dict_levels_get(match.group(1), debug_level_none))
|
||||
offsets.insert(pos, offset)
|
||||
|
||||
self.have_load_finished()
|
||||
yield False
|
||||
|
||||
|
||||
class LogLine (list):
|
||||
|
||||
_line_regex = default_log_line_regex()
|
||||
|
||||
@classmethod
|
||||
def parse_full(cls, line_string):
|
||||
match = cls._line_regex.match(line_string.decode('utf8', errors='replace'))
|
||||
if match is None:
|
||||
# raise ValueError ("not a valid log line (%r)" % (line_string,))
|
||||
groups = [0, 0, 0, 0, "", "", 0, "", "", 0]
|
||||
return cls(groups)
|
||||
|
||||
line = cls(match.groups())
|
||||
# Timestamp.
|
||||
line[0] = parse_time(line[0])
|
||||
# PID.
|
||||
line[1] = int(line[1])
|
||||
# Thread.
|
||||
line[2] = int(line[2], 16)
|
||||
# Level (this is handled in LineCache).
|
||||
line[3] = 0
|
||||
# Line.
|
||||
line[6] = int(line[6])
|
||||
# Message start offset.
|
||||
line[9] = match.start(9 + 1)
|
||||
|
||||
for col_id in (4, # COL_CATEGORY
|
||||
5, # COL_FILENAME
|
||||
7, # COL_FUNCTION,
|
||||
8,): # COL_OBJECT
|
||||
line[col_id] = sys.intern(line[col_id] or "")
|
||||
|
||||
return line
|
||||
|
||||
|
||||
class LogLines (object):
|
||||
|
||||
def __init__(self, fileobj, line_cache):
|
||||
|
||||
self.__fileobj = fileobj
|
||||
self.__line_cache = line_cache
|
||||
|
||||
def __len__(self):
|
||||
|
||||
return len(self.__line_cache.offsets)
|
||||
|
||||
def __getitem__(self, line_index):
|
||||
|
||||
offset = self.__line_cache.offsets[line_index]
|
||||
self.__fileobj.seek(offset)
|
||||
line_string = self.__fileobj.readline()
|
||||
line = LogLine.parse_full(line_string)
|
||||
msg = line_string[line[-1]:]
|
||||
line[-1] = msg
|
||||
return line
|
||||
|
||||
def __iter__(self):
|
||||
|
||||
size = len(self)
|
||||
i = 0
|
||||
while i < size:
|
||||
yield self[i]
|
||||
i += 1
|
||||
|
||||
|
||||
class LogFile (Producer):
|
||||
|
||||
def __init__(self, filename, dispatcher):
|
||||
|
||||
import mmap
|
||||
|
||||
Producer.__init__(self)
|
||||
|
||||
self.logger = logging.getLogger("logfile")
|
||||
|
||||
self.path = os.path.normpath(os.path.abspath(filename))
|
||||
self.__real_fileobj = open(filename, "rb")
|
||||
self.fileobj = mmap.mmap(
|
||||
self.__real_fileobj.fileno(), 0, access=mmap.ACCESS_READ)
|
||||
self.line_cache = LineCache(self.fileobj, dispatcher)
|
||||
self.line_cache.consumers.append(self)
|
||||
|
||||
def start_loading(self):
|
||||
|
||||
self.logger.debug("starting load")
|
||||
self.line_cache.start_loading()
|
||||
|
||||
def get_load_progress(self):
|
||||
|
||||
return self.line_cache.get_progress()
|
||||
|
||||
def handle_load_started(self):
|
||||
|
||||
# Chain up to our consumers:
|
||||
self.have_load_started()
|
||||
|
||||
def handle_load_finished(self):
|
||||
self.logger.debug("finish loading")
|
||||
self.lines = LogLines(self.fileobj, self.line_cache)
|
||||
|
||||
# Chain up to our consumers:
|
||||
self.have_load_finished()
|
||||
@@ -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
@@ -0,0 +1,61 @@
|
||||
# -*- 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 Main module."""
|
||||
|
||||
import sys
|
||||
import optparse
|
||||
from gettext import gettext as _, ngettext
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from GstDebugViewer import GUI
|
||||
import GstDebugViewer.Common.Main
|
||||
Common = GstDebugViewer.Common
|
||||
|
||||
GETTEXT_DOMAIN = "gst-debug-viewer"
|
||||
|
||||
|
||||
def main_version(opt, value, parser, *args, **kwargs):
|
||||
|
||||
from GstDebugViewer import version
|
||||
|
||||
print("GStreamer Debug Viewer %s" % (version,))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
class Paths (Common.Main.PathsProgramBase):
|
||||
|
||||
program_name = "gst-debug-viewer"
|
||||
|
||||
|
||||
def main():
|
||||
parser = optparse.OptionParser(
|
||||
_("%prog [OPTION...] [FILENAME]"),
|
||||
description=_("Display and analyze GStreamer debug log files"))
|
||||
parser.add_option("--version", "-v",
|
||||
action="callback",
|
||||
dest="version",
|
||||
callback=main_version,
|
||||
help=_("Display version and exit"))
|
||||
|
||||
Common.Main.main(main_function=GUI.main,
|
||||
option_parser=parser,
|
||||
gettext_domain=GETTEXT_DOMAIN,
|
||||
paths=Paths)
|
||||
@@ -0,0 +1,497 @@
|
||||
# -*- 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 timeline widget plugin."""
|
||||
|
||||
import logging
|
||||
|
||||
from GstDebugViewer import Common, Data, GUI
|
||||
from GstDebugViewer.Plugins import FeatureBase, PluginBase, _N
|
||||
|
||||
from gettext import gettext as _
|
||||
from gi.repository import GObject, GLib
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class SearchOperation (object):
|
||||
|
||||
def __init__(self, model, search_text, search_forward=True, start_position=None):
|
||||
|
||||
self.model = model
|
||||
if isinstance(search_text, str):
|
||||
self.search_text = search_text.encode('utf8')
|
||||
else:
|
||||
self.search_text = search_text
|
||||
self.search_forward = search_forward
|
||||
self.start_position = start_position
|
||||
|
||||
col_id = GUI.models.LogModelBase.COL_MESSAGE
|
||||
len_search_text = len(self.search_text)
|
||||
|
||||
def match_func(model_row):
|
||||
|
||||
message = model_row[col_id]
|
||||
if self.search_text in message:
|
||||
ranges = []
|
||||
start = 0
|
||||
while True:
|
||||
pos = message.find(self.search_text, start)
|
||||
if pos == -1:
|
||||
break
|
||||
ranges.append((pos, pos + len_search_text,))
|
||||
start = pos + len_search_text
|
||||
return ranges
|
||||
else:
|
||||
return ()
|
||||
|
||||
self.match_func = match_func
|
||||
|
||||
|
||||
class SearchSentinel (object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.dispatcher = Common.Data.GSourceDispatcher()
|
||||
self.cancelled = False
|
||||
|
||||
def run_for(self, operation):
|
||||
|
||||
self.dispatcher.cancel()
|
||||
self.dispatcher(self.__process(operation))
|
||||
self.cancelled = False
|
||||
|
||||
def abort(self):
|
||||
|
||||
self.dispatcher.cancel()
|
||||
self.cancelled = True
|
||||
|
||||
def __process(self, operation):
|
||||
|
||||
model = operation.model
|
||||
|
||||
if operation.start_position is not None:
|
||||
start_pos = operation.start_position
|
||||
elif operation.search_forward:
|
||||
start_pos = 0
|
||||
else:
|
||||
start_pos = len(model) - 1
|
||||
|
||||
start_iter = model.iter_nth_child(None, start_pos)
|
||||
|
||||
match_func = operation.match_func
|
||||
if operation.search_forward:
|
||||
iter_next = model.iter_next
|
||||
else:
|
||||
# FIXME: This is really ugly.
|
||||
nth_child = model.iter_nth_child
|
||||
|
||||
def iter_next_():
|
||||
for i in range(start_pos, -1, -1):
|
||||
yield nth_child(None, i)
|
||||
yield None
|
||||
it_ = iter_next_()
|
||||
|
||||
def iter_next(it):
|
||||
return it_.__next__()
|
||||
|
||||
YIELD_LIMIT = 1000
|
||||
i = YIELD_LIMIT
|
||||
tree_iter = start_iter
|
||||
while tree_iter and not self.cancelled:
|
||||
i -= 1
|
||||
if i == 0:
|
||||
yield True
|
||||
i = YIELD_LIMIT
|
||||
row = model[tree_iter]
|
||||
if match_func(row):
|
||||
self.handle_match_found(model, tree_iter)
|
||||
tree_iter = iter_next(tree_iter)
|
||||
|
||||
if not self.cancelled:
|
||||
self.handle_search_complete()
|
||||
yield False
|
||||
|
||||
def handle_match_found(self, model, tree_iter):
|
||||
|
||||
pass
|
||||
|
||||
def handle_search_complete(self):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FindBarWidget (Gtk.HBox):
|
||||
|
||||
__status = {"no-match-found": _N("No match found"),
|
||||
"searching": _N("Searching...")}
|
||||
|
||||
def __init__(self, action_group):
|
||||
|
||||
GObject.GObject.__init__(self)
|
||||
|
||||
label = Gtk.Label(label=_("Find:"))
|
||||
self.pack_start(label, False, False, 2)
|
||||
|
||||
self.entry = Gtk.Entry()
|
||||
self.pack_start(self.entry, True, True, 0)
|
||||
|
||||
prev_action = action_group.get_action("goto-previous-search-result")
|
||||
prev_button = Gtk.Button()
|
||||
prev_button.set_related_action(prev_action)
|
||||
self.pack_start(prev_button, False, False, 0)
|
||||
|
||||
next_action = action_group.get_action("goto-next-search-result")
|
||||
next_button = Gtk.Button()
|
||||
next_button.set_related_action(next_action)
|
||||
self.pack_start(next_button, False, False, 0)
|
||||
|
||||
self.status_label = Gtk.Label()
|
||||
self.status_label.props.xalign = 0.
|
||||
self.status_label.props.use_markup = True
|
||||
self.pack_start(self.status_label, False, False, 6)
|
||||
self.__compute_status_size()
|
||||
self.status_label.connect("notify::style", self.__handle_notify_style)
|
||||
|
||||
self.show_all()
|
||||
|
||||
def __compute_status_size(self):
|
||||
|
||||
label = self.status_label
|
||||
old_markup = label.props.label
|
||||
label.set_size_request(-1, -1)
|
||||
max_width = 0
|
||||
try:
|
||||
for status in self.__status.values():
|
||||
self.__set_status(_(status))
|
||||
req = label.size_request()
|
||||
max_width = max(max_width, req.width)
|
||||
label.set_size_request(max_width, -1)
|
||||
finally:
|
||||
label.props.label = old_markup
|
||||
|
||||
def __handle_notify_style(self, *a, **kw):
|
||||
|
||||
self.__compute_status_size()
|
||||
|
||||
def __set_status(self, text):
|
||||
|
||||
markup = "<b>%s</b>" % (GLib.markup_escape_text(text),)
|
||||
|
||||
self.status_label.props.label = markup
|
||||
|
||||
def status_no_match_found(self):
|
||||
|
||||
self.__set_status(_(self.__status["no-match-found"]))
|
||||
|
||||
def status_searching(self):
|
||||
|
||||
self.__set_status(_(self.__status["searching"]))
|
||||
|
||||
def clear_status(self):
|
||||
|
||||
self.__set_status("")
|
||||
|
||||
|
||||
class FindBarFeature (FeatureBase):
|
||||
|
||||
def __init__(self, app):
|
||||
|
||||
FeatureBase.__init__(self, app)
|
||||
|
||||
self.logger = logging.getLogger("ui.findbar")
|
||||
|
||||
self.action_group = Gtk.ActionGroup("FindBarActions")
|
||||
self.action_group.add_toggle_actions([("show-find-bar",
|
||||
None,
|
||||
_("Find Bar"),
|
||||
"<Ctrl>F")])
|
||||
self.action_group.add_actions([("goto-next-search-result",
|
||||
None, _("Goto Next Match"),
|
||||
"<Ctrl>G"),
|
||||
("goto-previous-search-result",
|
||||
None, _("Goto Previous Match"),
|
||||
"<Ctrl><Shift>G")])
|
||||
|
||||
self.bar = None
|
||||
self.operation = None
|
||||
self.search_state = None
|
||||
self.next_match = None
|
||||
self.prev_match = None
|
||||
self.scroll_match = False
|
||||
|
||||
self.sentinel = SearchSentinel()
|
||||
self.sentinel.handle_match_found = self.handle_match_found
|
||||
self.sentinel.handle_search_complete = self.handle_search_complete
|
||||
|
||||
def scroll_view_to_line(self, line_index):
|
||||
|
||||
view = self.log_view
|
||||
|
||||
path = Gtk.TreePath((line_index,))
|
||||
|
||||
start_path, end_path = view.get_visible_range()
|
||||
|
||||
if path >= start_path and path <= end_path:
|
||||
self.logger.debug(
|
||||
"line index %i already visible, not scrolling", line_index)
|
||||
return
|
||||
|
||||
self.logger.debug("scrolling to line_index %i", line_index)
|
||||
view.scroll_to_cell(path, use_align=True, row_align=.5)
|
||||
|
||||
def handle_attach_window(self, window):
|
||||
|
||||
self.window = window
|
||||
|
||||
ui = window.ui_manager
|
||||
|
||||
ui.insert_action_group(self.action_group, 0)
|
||||
|
||||
self.log_view = window.log_view
|
||||
|
||||
self.merge_id = ui.new_merge_id()
|
||||
for name, action_name in [("ViewFindBar", "show-find-bar",),
|
||||
("ViewNextResult",
|
||||
"goto-next-search-result",),
|
||||
("ViewPrevResult", "goto-previous-search-result",)]:
|
||||
ui.add_ui(self.merge_id, "/menubar/ViewMenu/ViewMenuAdditions",
|
||||
name, action_name, Gtk.UIManagerItemType.MENUITEM, False)
|
||||
|
||||
box = window.widgets.vbox_view
|
||||
self.bar = FindBarWidget(self.action_group)
|
||||
box.pack_end(self.bar, False, False, 0)
|
||||
self.bar.hide()
|
||||
|
||||
action = self.action_group.get_action("show-find-bar")
|
||||
handler = self.handle_show_find_bar_action_toggled
|
||||
action.connect("toggled", handler)
|
||||
|
||||
action = self.action_group.get_action("goto-previous-search-result")
|
||||
handler = self.handle_goto_previous_search_result_action_activate
|
||||
action.props.sensitive = False
|
||||
action.connect("activate", handler)
|
||||
|
||||
action = self.action_group.get_action("goto-next-search-result")
|
||||
handler = self.handle_goto_next_search_result_action_activate
|
||||
action.props.sensitive = False
|
||||
action.connect("activate", handler)
|
||||
|
||||
self.bar.entry.connect("changed", self.handle_entry_changed)
|
||||
|
||||
def handle_detach_window(self, window):
|
||||
|
||||
self.window = None
|
||||
|
||||
window.ui_manager.remove_ui(self.merge_id)
|
||||
self.merge_id = None
|
||||
|
||||
def handle_show_find_bar_action_toggled(self, action):
|
||||
|
||||
if action.props.active:
|
||||
self.bar.show()
|
||||
self.bar.entry.grab_focus()
|
||||
self.update_search()
|
||||
else:
|
||||
try:
|
||||
column = self.window.column_manager.find_item(
|
||||
name="message")
|
||||
del column.highlighters[self]
|
||||
except KeyError:
|
||||
pass
|
||||
self.bar.clear_status()
|
||||
self.bar.hide()
|
||||
for action_name in ["goto-next-search-result",
|
||||
"goto-previous-search-result"]:
|
||||
self.action_group.get_action(
|
||||
action_name).props.sensitive = False
|
||||
|
||||
def handle_goto_previous_search_result_action_activate(self, action):
|
||||
|
||||
if self.prev_match is None:
|
||||
self.logger.warning("inconsistent action sensitivity")
|
||||
return
|
||||
|
||||
self.scroll_view_to_line(self.prev_match)
|
||||
self.prev_match = None
|
||||
|
||||
start_path = self.log_view.get_visible_range()[0]
|
||||
new_position = start_path[0] - 1
|
||||
self.start_search_operation(start_position=new_position,
|
||||
forward=False)
|
||||
|
||||
# FIXME
|
||||
|
||||
def handle_goto_next_search_result_action_activate(self, action):
|
||||
|
||||
if self.next_match is None:
|
||||
self.logger.warning("inconsistent action sensitivity")
|
||||
return
|
||||
|
||||
self.scroll_view_to_line(self.next_match)
|
||||
self.next_match = None
|
||||
|
||||
end_path = self.log_view.get_visible_range()[1]
|
||||
new_position = end_path[0] + 1
|
||||
self.start_search_operation(start_position=new_position,
|
||||
forward=True)
|
||||
# FIXME: Finish.
|
||||
|
||||
# model = self.log_view.get_model ()
|
||||
|
||||
# start_path, end_path = self.log_view.get_visible_range ()
|
||||
# start_index, end_index = start_path[0], end_path[0]
|
||||
|
||||
# for line_index in self.matches:
|
||||
# if line_index > end_index:
|
||||
# break
|
||||
# else:
|
||||
# return
|
||||
|
||||
# self.scroll_view_to_line (line_index)
|
||||
|
||||
def handle_entry_changed(self, entry):
|
||||
|
||||
self.update_search()
|
||||
|
||||
def update_search(self):
|
||||
|
||||
model = self.log_view.get_model()
|
||||
search_text = self.bar.entry.props.text
|
||||
column = self.window.column_manager.find_item(name="message")
|
||||
if search_text == "":
|
||||
self.logger.debug("search string set to '', aborting search")
|
||||
self.search_state = None
|
||||
self.next_match = None
|
||||
self.prev_match = None
|
||||
self.update_sensitivity()
|
||||
self.sentinel.abort()
|
||||
try:
|
||||
del column.highlighters[self]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.logger.debug("starting search for %r", search_text)
|
||||
self.next_match = None
|
||||
self.prev_match = None
|
||||
self.update_sensitivity()
|
||||
self.scroll_match = True
|
||||
|
||||
start_path = self.log_view.get_visible_range()[0]
|
||||
self.start_search_operation(
|
||||
search_text, start_position=start_path[0])
|
||||
self.bar.status_searching()
|
||||
column.highlighters[self] = self.operation.match_func
|
||||
|
||||
self.window.update_view()
|
||||
|
||||
def update_sensitivity(self):
|
||||
|
||||
for name, value in (("goto-next-search-result", self.next_match,),
|
||||
("goto-previous-search-result", self.prev_match,),):
|
||||
action = self.action_group.get_action(name)
|
||||
action.props.sensitive = (value is not None)
|
||||
|
||||
def start_search_operation(self, search_text=None, forward=True, start_position=None):
|
||||
|
||||
model = self.log_view.get_model()
|
||||
|
||||
if forward:
|
||||
self.search_state = "search-forward"
|
||||
if start_position is None:
|
||||
start_position = 0
|
||||
else:
|
||||
self.search_state = "search-backward"
|
||||
if start_position is None:
|
||||
start_position = len(model) - 1
|
||||
|
||||
if search_text is None:
|
||||
operation = self.operation
|
||||
if operation is None:
|
||||
raise ValueError(
|
||||
"search_text not given but have no previous search operation")
|
||||
search_text = operation.search_text
|
||||
|
||||
self.operation = SearchOperation(model, search_text,
|
||||
start_position=start_position,
|
||||
search_forward=forward)
|
||||
self.sentinel.run_for(self.operation)
|
||||
|
||||
def handle_match_found(self, model, tree_iter):
|
||||
|
||||
if self.search_state not in ("search-forward", "search-backward",):
|
||||
self.logger.warning(
|
||||
"inconsistent search state %r", self.search_state)
|
||||
return
|
||||
|
||||
line_index = model.get_path(tree_iter)[0]
|
||||
forward_search = (self.search_state == "search-forward")
|
||||
|
||||
if forward_search:
|
||||
self.logger.debug("forward search for %r matches line %i",
|
||||
self.operation.search_text, line_index)
|
||||
else:
|
||||
self.logger.debug("backward search for %r matches line %i",
|
||||
self.operation.search_text, line_index)
|
||||
|
||||
self.sentinel.abort()
|
||||
|
||||
if self.scroll_match:
|
||||
self.logger.debug("scrolling to matching line")
|
||||
self.scroll_view_to_line(line_index)
|
||||
# Now search for the next one:
|
||||
self.scroll_match = False
|
||||
# FIXME: Start with first line that is outside of the visible
|
||||
# range.
|
||||
self.start_search_operation(start_position=line_index + 1,
|
||||
forward=forward_search)
|
||||
else:
|
||||
if forward_search:
|
||||
self.next_match = line_index
|
||||
|
||||
self.search_state = "search-backward"
|
||||
self.start_search_operation(forward=False,
|
||||
start_position=line_index - 1)
|
||||
else:
|
||||
self.prev_match = line_index
|
||||
self.update_sensitivity()
|
||||
self.bar.clear_status()
|
||||
|
||||
def handle_search_complete(self):
|
||||
|
||||
if self.search_state == "search-forward":
|
||||
self.logger.debug("forward search for %r reached last line",
|
||||
self.operation.search_text)
|
||||
self.next_match = None
|
||||
elif self.search_state == "search-backward":
|
||||
self.logger.debug("backward search for %r reached first line",
|
||||
self.operation.search_text)
|
||||
self.prev_match = None
|
||||
else:
|
||||
self.logger.warning("inconsistent search state %r",
|
||||
self.search_state)
|
||||
return
|
||||
|
||||
self.update_sensitivity()
|
||||
if self.prev_match is None and self.next_match is None:
|
||||
self.bar.status_no_match_found()
|
||||
|
||||
|
||||
class Plugin (PluginBase):
|
||||
|
||||
features = (FindBarFeature,)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
||||
# -*- 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 Plugins package."""
|
||||
|
||||
__all__ = ["_", "_N", "FeatureBase", "PluginBase"]
|
||||
|
||||
import os.path
|
||||
|
||||
|
||||
def _N(s):
|
||||
return s
|
||||
|
||||
|
||||
def load(paths=()):
|
||||
|
||||
for path in paths:
|
||||
for plugin_module in _load_plugins(path):
|
||||
yield plugin_module.Plugin
|
||||
|
||||
|
||||
def _load_plugins(path):
|
||||
|
||||
import imp
|
||||
import glob
|
||||
|
||||
files = glob.glob(os.path.join(path, "*.py"))
|
||||
|
||||
for filename in files:
|
||||
|
||||
name = os.path.basename(os.path.splitext(filename)[0])
|
||||
if name == "__init__":
|
||||
continue
|
||||
fp, pathname, description = imp.find_module(name, [path])
|
||||
module = imp.load_module(name, fp, pathname, description)
|
||||
yield module
|
||||
|
||||
|
||||
class FeatureBase (object):
|
||||
|
||||
def __init__(self, app):
|
||||
|
||||
pass
|
||||
|
||||
def handle_attach_window(self, window):
|
||||
"""
|
||||
window: GstDebugViewer.GUI.window.Window
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def handle_attach_log_file(self, window, log_file):
|
||||
"""
|
||||
window: GstDebugViewer.GUI.window.Window
|
||||
log_file: GstDebugViewer.Data.LogFile
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def handle_detach_log_file(self, window, log_file):
|
||||
"""
|
||||
window: GstDebugViewer.GUI.window.Window
|
||||
log_file: GstDebugViewer.Data.LogFile
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def handle_detach_window(self, window):
|
||||
"""
|
||||
window: GstDebugViewer.GUI.window.Window
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PluginBase (object):
|
||||
"""
|
||||
All plugins must implement a class called Plugin inheriting from PluginBase.
|
||||
They should place a tuple of features they export into 'features'. Each
|
||||
feature should be a subclass of FeatureBase.
|
||||
"""
|
||||
|
||||
features = ()
|
||||
|
||||
def __init__(self, app):
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- 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 package."""
|
||||
|
||||
version = "@VERSION@"
|
||||
|
||||
if version.startswith('@'):
|
||||
version = 'master'
|
||||
|
||||
__version__ = version
|
||||
|
||||
from GstDebugViewer.Main import Paths, GETTEXT_DOMAIN, main as run # noqa
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
|
||||
def line_string(ts, pid, thread, level, category, filename, line, function,
|
||||
object_, message):
|
||||
|
||||
# Replicates gstreamer/gst/gstinfo.c:gst_debug_log_default.
|
||||
|
||||
# FIXME: Regarding object_, this doesn't fully replicate the formatting!
|
||||
return "%s %5d 0x%x %s %20s %s:%d:%s:<%s> %s" % (Data.time_args(ts), pid, thread,
|
||||
level.name.ljust(
|
||||
5), category,
|
||||
filename, line, function,
|
||||
object_, message,)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
import sys
|
||||
import os.path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(sys.argv[0])))
|
||||
|
||||
global Data
|
||||
from GstDebugViewer import Data
|
||||
|
||||
count = 100000
|
||||
|
||||
ts = 0
|
||||
pid = 12345
|
||||
thread = int("89abcdef", 16)
|
||||
level = Data.debug_level_log
|
||||
category = "GST_DUMMY"
|
||||
filename = "gstdummyfilename.c"
|
||||
file_line = 1
|
||||
function = "gst_dummy_function"
|
||||
object_ = "dummyobj0"
|
||||
message = "dummy message with no content"
|
||||
|
||||
levels = (Data.debug_level_log,
|
||||
Data.debug_level_debug,
|
||||
Data.debug_level_info,)
|
||||
|
||||
shift = 0
|
||||
for i in range(count):
|
||||
|
||||
ts = i * 10000
|
||||
shift += i % (count // 100)
|
||||
level = levels[(i + shift) % 3]
|
||||
print(line_string(ts, pid, thread, level, category, filename, file_line,
|
||||
function, object_, message))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8; mode: python; -*-
|
||||
#
|
||||
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
|
||||
#
|
||||
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by the Free
|
||||
# Software Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""GStreamer Debug Viewer performance test program."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import os.path
|
||||
from glob import glob
|
||||
import time
|
||||
|
||||
import gi
|
||||
|
||||
from gi.repository import GObject
|
||||
|
||||
from .. import Common, Data, GUI
|
||||
|
||||
|
||||
class TestParsingPerformance (object):
|
||||
|
||||
def __init__(self, filename):
|
||||
|
||||
self.main_loop = GObject.MainLoop()
|
||||
self.log_file = Data.LogFile(filename, Common.Data.DefaultDispatcher())
|
||||
self.log_file.consumers.append(self)
|
||||
|
||||
def start(self):
|
||||
|
||||
self.log_file.start_loading()
|
||||
|
||||
def handle_load_started(self):
|
||||
|
||||
self.start_time = time.time()
|
||||
|
||||
def handle_load_finished(self):
|
||||
|
||||
diff = time.time() - self.start_time
|
||||
print("line cache built in %0.1f ms" % (diff * 1000.,))
|
||||
|
||||
start_time = time.time()
|
||||
model = GUI.LazyLogModel(self.log_file)
|
||||
for row in model:
|
||||
pass
|
||||
diff = time.time() - start_time
|
||||
print("model iterated in %0.1f ms" % (diff * 1000.,))
|
||||
print("overall time spent: %0.1f s" % (time.time() - self.start_time,))
|
||||
|
||||
import resource
|
||||
rusage = resource.getrusage(resource.RUSAGE_SELF)
|
||||
print("time spent in user mode: %.2f s" % (rusage.ru_utime,))
|
||||
print("time spent in system mode: %.2f s" % (rusage.ru_stime,))
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
test = TestParsingPerformance(sys.argv[1])
|
||||
test.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8; mode: python; -*-
|
||||
#
|
||||
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
|
||||
#
|
||||
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by the Free
|
||||
# Software Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""GStreamer Debug Viewer test suite for the custom tree models."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import os.path
|
||||
from glob import glob
|
||||
|
||||
from unittest import TestCase, main as test_main
|
||||
|
||||
from .. import Common, Data
|
||||
from .. GUI.filters import CategoryFilter, Filter
|
||||
from .. GUI.models import (FilteredLogModel,
|
||||
LogModelBase,
|
||||
SubRange,)
|
||||
|
||||
|
||||
class TestSubRange (TestCase):
|
||||
|
||||
def test_len(self):
|
||||
|
||||
values = list(range(20))
|
||||
|
||||
sr = SubRange(values, 0, 20)
|
||||
self.assertEqual(len(sr), 20)
|
||||
|
||||
sr = SubRange(values, 10, 20)
|
||||
self.assertEqual(len(sr), 10)
|
||||
|
||||
sr = SubRange(values, 0, 10)
|
||||
self.assertEqual(len(sr), 10)
|
||||
|
||||
sr = SubRange(values, 5, 15)
|
||||
self.assertEqual(len(sr), 10)
|
||||
|
||||
def test_iter(self):
|
||||
|
||||
values = list(range(20))
|
||||
|
||||
sr = SubRange(values, 0, 20)
|
||||
self.assertEqual(list(sr), values)
|
||||
|
||||
sr = SubRange(values, 10, 20)
|
||||
self.assertEqual(list(sr), list(range(10, 20)))
|
||||
|
||||
sr = SubRange(values, 0, 10)
|
||||
self.assertEqual(list(sr), list(range(0, 10)))
|
||||
|
||||
sr = SubRange(values, 5, 15)
|
||||
self.assertEqual(list(sr), list(range(5, 15)))
|
||||
|
||||
|
||||
class Model (LogModelBase):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
LogModelBase.__init__(self)
|
||||
|
||||
for i in range(20):
|
||||
self.line_offsets.append(i * 100)
|
||||
self.line_levels.append(Data.debug_level_debug)
|
||||
|
||||
def ensure_cached(self, line_offset):
|
||||
|
||||
pid = line_offset // 100
|
||||
if pid % 2 == 0:
|
||||
category = b"EVEN"
|
||||
else:
|
||||
category = b"ODD"
|
||||
|
||||
line_fmt = (b"0:00:00.000000000 %5i 0x0000000 DEBUG "
|
||||
b"%20s dummy.c:1:dummy: dummy")
|
||||
line_str = line_fmt % (pid, category,)
|
||||
log_line = Data.LogLine.parse_full(line_str)
|
||||
self.line_cache[line_offset] = log_line
|
||||
|
||||
def access_offset(self, line_offset):
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
class IdentityFilter (Filter):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
def filter_func(row):
|
||||
return True
|
||||
self.filter_func = filter_func
|
||||
|
||||
|
||||
class RandomFilter (Filter):
|
||||
|
||||
def __init__(self, seed):
|
||||
|
||||
import random
|
||||
rand = random.Random()
|
||||
rand.seed(seed)
|
||||
|
||||
def filter_func(row):
|
||||
return rand.choice((True, False,))
|
||||
self.filter_func = filter_func
|
||||
|
||||
|
||||
class TestDynamicFilter (TestCase):
|
||||
|
||||
def test_unset_filter_rerange(self):
|
||||
|
||||
full_model = Model()
|
||||
filtered_model = FilteredLogModel(full_model)
|
||||
row_list = self.__row_list
|
||||
|
||||
self.assertEqual(row_list(full_model), list(range(20)))
|
||||
self.assertEqual(row_list(filtered_model), list(range(20)))
|
||||
|
||||
filtered_model.set_range(5, 16)
|
||||
|
||||
self.assertEqual(row_list(filtered_model), list(range(5, 16)))
|
||||
|
||||
def test_identity_filter_rerange(self):
|
||||
|
||||
full_model = Model()
|
||||
filtered_model = FilteredLogModel(full_model)
|
||||
row_list = self.__row_list
|
||||
|
||||
self.assertEqual(row_list(full_model), list(range(20)))
|
||||
self.assertEqual(row_list(filtered_model), list(range(20)))
|
||||
|
||||
filtered_model.add_filter(IdentityFilter(),
|
||||
Common.Data.DefaultDispatcher())
|
||||
filtered_model.set_range(5, 16)
|
||||
|
||||
self.assertEqual(row_list(filtered_model), list(range(5, 16)))
|
||||
|
||||
def test_filtered_range_refilter_skip(self):
|
||||
|
||||
full_model = Model()
|
||||
filtered_model = FilteredLogModel(full_model)
|
||||
|
||||
row_list = self.__row_list
|
||||
|
||||
filtered_model.add_filter(CategoryFilter("EVEN"),
|
||||
Common.Data.DefaultDispatcher())
|
||||
self.__dump_model(filtered_model, "filtered")
|
||||
|
||||
self.assertEqual(row_list(filtered_model), list(range(1, 20, 2)))
|
||||
self.assertEqual([filtered_model.line_index_from_super(i)
|
||||
for i in range(1, 20, 2)],
|
||||
list(range(10)))
|
||||
self.assertEqual([filtered_model.line_index_to_super(i)
|
||||
for i in range(10)],
|
||||
list(range(1, 20, 2)))
|
||||
|
||||
filtered_model.set_range(1, 20)
|
||||
self.__dump_model(filtered_model, "ranged (1, 20)")
|
||||
self.__dump_model(filtered_model, "filtered range")
|
||||
|
||||
self.assertEqual([filtered_model.line_index_from_super(i)
|
||||
for i in range(0, 19, 2)],
|
||||
list(range(10)))
|
||||
self.assertEqual([filtered_model.line_index_to_super(i)
|
||||
for i in range(10)],
|
||||
list(range(1, 20, 2)))
|
||||
|
||||
filtered_model.set_range(2, 20)
|
||||
self.__dump_model(filtered_model, "ranged (2, 20)")
|
||||
|
||||
self.assertEqual(row_list(filtered_model), list(range(3, 20, 2)))
|
||||
|
||||
def test_filtered_range_refilter(self):
|
||||
|
||||
full_model = Model()
|
||||
filtered_model = FilteredLogModel(full_model)
|
||||
|
||||
row_list = self.__row_list
|
||||
rows = row_list(full_model)
|
||||
rows_filtered = row_list(filtered_model)
|
||||
|
||||
self.__dump_model(full_model, "full model")
|
||||
|
||||
self.assertEqual(rows, rows_filtered)
|
||||
|
||||
self.assertEqual([filtered_model.line_index_from_super(i)
|
||||
for i in range(20)],
|
||||
list(range(20)))
|
||||
self.assertEqual([filtered_model.line_index_to_super(i)
|
||||
for i in range(20)],
|
||||
list(range(20)))
|
||||
|
||||
filtered_model.set_range(5, 16)
|
||||
self.__dump_model(filtered_model, "ranged model (5, 16)")
|
||||
|
||||
rows_ranged = row_list(filtered_model)
|
||||
self.assertEqual(rows_ranged, list(range(5, 16)))
|
||||
|
||||
self.__dump_model(filtered_model, "filtered model (nofilter, 5, 15)")
|
||||
|
||||
filtered_model.add_filter(CategoryFilter("EVEN"),
|
||||
Common.Data.DefaultDispatcher())
|
||||
rows_filtered = row_list(filtered_model)
|
||||
self.assertEqual(rows_filtered, list(range(5, 16, 2)))
|
||||
|
||||
self.__dump_model(filtered_model, "filtered model")
|
||||
|
||||
def test_random_filtered_range_refilter(self):
|
||||
|
||||
full_model = Model()
|
||||
filtered_model = FilteredLogModel(full_model)
|
||||
row_list = self.__row_list
|
||||
|
||||
self.assertEqual(row_list(full_model), list(range(20)))
|
||||
self.assertEqual(row_list(filtered_model), list(range(20)))
|
||||
|
||||
filtered_model.add_filter(RandomFilter(538295943),
|
||||
Common.Data.DefaultDispatcher())
|
||||
random_rows = row_list(filtered_model)
|
||||
|
||||
self.__dump_model(filtered_model)
|
||||
|
||||
filtered_model = FilteredLogModel(full_model)
|
||||
filtered_model.add_filter(RandomFilter(538295943),
|
||||
Common.Data.DefaultDispatcher())
|
||||
self.__dump_model(filtered_model, "filtered model")
|
||||
self.assertEqual(row_list(filtered_model), random_rows)
|
||||
|
||||
filtered_model.set_range(1, 10)
|
||||
self.__dump_model(filtered_model)
|
||||
self.assertEqual(row_list(filtered_model), [
|
||||
x for x in range(0, 10) if x in random_rows])
|
||||
|
||||
def __row_list(self, model):
|
||||
|
||||
return [row[Model.COL_PID] for row in model]
|
||||
|
||||
def __dump_model(self, model, comment=None):
|
||||
|
||||
# TODO: Provide a command line option to turn this on and off.
|
||||
|
||||
return
|
||||
|
||||
if not hasattr(model, "super_model"):
|
||||
# Top model.
|
||||
print("\t(%s)" % ("|".join([str(i).rjust(2)
|
||||
for i in self.__row_list(model)]),), end=' ')
|
||||
else:
|
||||
top_model = model.super_model
|
||||
if hasattr(top_model, "super_model"):
|
||||
top_model = top_model.super_model
|
||||
top_indices = self.__row_list(top_model)
|
||||
positions = self.__row_list(model)
|
||||
output = [" "] * len(top_indices)
|
||||
for i, position in enumerate(positions):
|
||||
output[position] = str(i).rjust(2)
|
||||
print("\t(%s)" % ("|".join(output),), end=' ')
|
||||
|
||||
if comment is None:
|
||||
print()
|
||||
else:
|
||||
print(comment)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_main()
|
||||
Reference in New Issue
Block a user