mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-14 20:36:32 +00:00
497 lines
16 KiB
Python
497 lines
16 KiB
Python
# -*- coding: utf-8; mode: python; -*-
|
|
#
|
|
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
|
|
#
|
|
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify it
|
|
# under the terms of the GNU General Public License as published by the Free
|
|
# Software Foundation; either version 3 of the License, or (at your option)
|
|
# any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
# more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
"""GStreamer Debug Viewer 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,)
|