# -*- coding: utf-8; mode: python; -*- # # GStreamer Debug Viewer - View and analyze GStreamer debug log files # # Copyright (C) 2007 René Stadler # # 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 . """GStreamer Debug Viewer timeline widget plugin.""" import logging from GstDebugViewer import Common, Data, GUI from GstDebugViewer.Plugins import * import pango import gtk class SearchOperation (object): def __init__ (self, model, search_text, search_forward = True, start_position = None): self.model = model self.search_text = search_text self.search_forward = search_forward self.start_position = start_position col_id = GUI.LogModelBase.COL_MESSAGE len_search_text = len (search_text) def match_func (model_row): message = model_row[col_id] if search_text in message: # TODO: Return all match ranges here. pos = message.find (search_text) return ((pos, pos + len_search_text,),) 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 xrange (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): gtk.HBox.__init__ (self) label = gtk.Label (_("Find:")) self.pack_start (label, False, False, 2) self.entry = gtk.Entry () self.pack_start (self.entry) prev_action = action_group.get_action ("goto-previous-search-result") prev_button = gtk.Button () prev_action.connect_proxy (prev_button) self.pack_start (prev_button, False, False, 0) next_action = action_group.get_action ("goto-next-search-result") next_button = gtk.Button () next_action.connect_proxy (next_button) self.pack_start (next_button, False, False, 0) self.status_label = gtk.Label ("") self.status_label.props.xalign = 0. attrs = pango.AttrList () attrs.insert (pango.AttrWeight (pango.WEIGHT_BOLD, 0, -1)) self.status_label.props.attributes = attrs 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_text = label.props.label label.set_size_request (-1, -1) max_width = 0 try: for status in self.__status.values (): self.__set_status (_(status)) width, height = label.size_request () max_width = max (max_width, width) label.set_size_request (max_width, -1) finally: label.props.label = old_text def __handle_notify_style (self, *a, **kw): self.__compute_status_size () def __set_status (self, text): self.status_label.props.label = text 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"), "F")]) self.action_group.add_actions ([("goto-next-search-result", None, _("Goto Next Match"), "G"), ("goto-previous-search-result", None, _("Goto Previous Match"), "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 = (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.UI_MANAGER_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 () 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.props.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): model = self.log_view.props.model search_text = 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.props.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 not self.search_state 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]