gstreamer/debug-viewer/GstDebugViewer/Plugins/Timeline.py

1088 lines
33 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
from GstDebugViewer.GUI.colors import LevelColorThemeTango, ThreadColorThemeTango
from GstDebugViewer.Plugins import *
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gdk
import cairo
def iter_model_reversed (model):
count = model.iter_n_children (None)
for i in xrange (count - 1, 0, -1):
yield model[i]
class LineFrequencySentinel (object):
def __init__ (self, model):
self.model = model
self.clear ()
def clear (self):
self.data = None
self.n_partitions = None
self.partitions = None
self.step = None
self.ts_range = None
def _search_ts (self, target_ts, first_index, last_index):
model_get = self.model.get_value
model_iter_nth_child = self.model.iter_nth_child
col_id = self.model.COL_TIME
# TODO: Rewrite using a lightweight view object + bisect.
while True:
middle = (last_index - first_index) // 2 + first_index
if middle == first_index:
return first_index
ts = model_get (model_iter_nth_child (None, middle), col_id)
if ts < target_ts:
first_index = middle
elif ts > target_ts:
last_index = middle
else:
return middle
def run_for (self, n):
if n == 0:
raise ValueError ("illegal value for n")
self.n_partitions = n
def process (self):
model = self.model
result = []
partitions = []
first_ts = None
for row in self.model:
first_ts = row[model.COL_TIME]
if first_ts is not None:
break
if first_ts is None:
return
last_ts = None
i = 0
UNPARSABLE_LIMIT = 500
for row in iter_model_reversed (self.model):
last_ts = row[model.COL_TIME]
# FIXME: We ignore 0 here (unparsable lines!), this should be
# handled differently!
i += 1
if i == UNPARSABLE_LIMIT:
break
if last_ts:
last_index = row.path[0]
break
if last_ts is None or last_ts < first_ts:
return
step = int (float (last_ts - first_ts) / float (self.n_partitions))
YIELD_LIMIT = 100
limit = YIELD_LIMIT
first_index = 0
target_ts = first_ts + step
old_found = 0
while target_ts < last_ts:
limit -= 1
if limit == 0:
limit = YIELD_LIMIT
yield True
found = self._search_ts (target_ts, first_index, last_index)
result.append (found - old_found)
partitions.append (found)
old_found = found
first_index = found
target_ts += step
if step == 0:
result = []
partitions = []
self.step = step
self.data = result
self.partitions = partitions
self.ts_range = (first_ts, last_ts,)
class LevelDistributionSentinel (object):
def __init__ (self, freq_sentinel, model):
self.freq_sentinel = freq_sentinel
self.model = model
self.data = []
def clear (self):
del self.data[:]
def process (self):
MAX_LEVELS = 8
YIELD_LIMIT = 10000
y = YIELD_LIMIT
model_get = self.model.get_value
model_next = self.model.iter_next
id_time = self.model.COL_TIME
id_level = self.model.COL_LEVEL
del self.data[:]
data = self.data
i = 0
partitions_i = 0
partitions = self.freq_sentinel.partitions
counts = [0] * MAX_LEVELS
tree_iter = self.model.get_iter_first ()
if not partitions:
return
level_index = 0
level_iter = None
finished = False
while tree_iter:
y -= 1
if y == 0:
y = YIELD_LIMIT
yield True
if level_iter is None:
stop_index = level_index + 512
levels = self.model.get_value_range (id_level,
level_index, stop_index)
level_index = stop_index
level_iter = iter (levels)
try:
level = level_iter.next ()
except StopIteration:
level_iter = None
continue
while i > partitions[partitions_i]:
data.append (tuple (counts))
counts = [0] * MAX_LEVELS
partitions_i += 1
if partitions_i == len (partitions):
finished = True
break
if finished:
break
counts[level] += 1
i += 1
# Now handle the last one:
data.append (tuple (counts))
yield False
class UpdateProcess (object):
def __init__ (self, freq_sentinel, dist_sentinel):
self.freq_sentinel = freq_sentinel
self.dist_sentinel = dist_sentinel
self.is_running = False
self.dispatcher = Common.Data.GSourceDispatcher ()
def __process (self):
if self.freq_sentinel is None or self.dist_sentinel is None:
return
self.is_running = True
for x in self.freq_sentinel.process ():
yield True
self.handle_sentinel_finished (self.freq_sentinel)
for x in self.dist_sentinel.process ():
yield True
self.handle_sentinel_progress (self.dist_sentinel)
self.is_running = False
self.handle_sentinel_finished (self.dist_sentinel)
self.handle_process_finished ()
yield False
def run (self):
if self.is_running:
return
self.dispatcher (self.__process ())
def abort (self):
if not self.is_running:
return
self.dispatcher.cancel ()
self.is_running = False
def handle_sentinel_progress (self, sentinel):
pass
def handle_sentinel_finished (self, sentinel):
pass
def handle_process_finished (self):
pass
class VerticalTimelineWidget (Gtk.DrawingArea):
__gtype_name__ = "GstDebugViewerVerticalTimelineWidget"
def __init__ (self, log_view):
GObject.GObject.__init__ (self)
self.logger = logging.getLogger ("ui.vtimeline")
self.log_view = log_view
self.theme = ThreadColorThemeTango ()
self.params = None
self.thread_colors = {}
self.next_thread_color = 0
try:
self.set_tooltip_text (_("Vertical timeline\n"
"Different colors represent different threads"))
except AttributeError:
# Compatibility.
pass
def do_draw (self, ctx):
alloc = self.get_allocation ()
x = alloc.x
y = alloc.y
w = alloc.width
h = alloc.height
# White background rectangle.
ctx.set_line_width (0.)
ctx.rectangle (0, 0, w, h)
ctx.set_source_rgb (1., 1., 1.)
ctx.fill ()
ctx.new_path ()
if self.params is None:
self.__update_params ()
if self.params is None:
return
first_y, cell_height, data = self.params
if len (data) < 2:
return
first_ts, last_ts = data[0][0], data[-1][0]
ts_range = last_ts - first_ts
if ts_range == 0:
return
ctx.set_line_width (1.)
ctx.set_source_rgb (0., 0., 0.)
half_height = cell_height // 2 - .5
quarter_height = cell_height // 4 - .5
first_y += half_height
for i, i_data in enumerate (data):
ts, thread = i_data
if thread in self.thread_colors:
ctx.set_source_rgb (*self.thread_colors[thread])
else:
self.next_thread_color += 1
if self.next_thread_color == len (self.theme.colors):
self.next_thread_color = 0
color = self.theme.colors[self.next_thread_color][0].float_tuple ()
self.thread_colors[thread] = color
ctx.set_source_rgb (*color)
ts_fraction = float (ts - first_ts) / ts_range
ts_offset = ts_fraction * h
row_offset = first_y + i * cell_height
ctx.move_to (-.5, ts_offset)
ctx.line_to (half_height, ts_offset)
ctx.line_to (w - quarter_height, row_offset)
ctx.stroke ()
ctx.line_to (w - quarter_height, row_offset)
ctx.line_to (w + .5, row_offset - half_height)
ctx.line_to (w + .5, row_offset + half_height)
ctx.fill ()
return True
def do_configure_event (self, event):
self.params = None
self.queue_draw ()
return False
def do_get_preferred_width (self):
return 64, 64 # FIXME
def clear (self):
self.params = None
self.thread_colors.clear ()
self.next_thread_color = 0
self.queue_draw ()
def __update_params (self):
# FIXME: Ideally we should take the vertical position difference of the
# view into account (which is 0 with the current UI layout).
view = self.log_view
model = view.get_model ()
visible_range = view.get_visible_range ()
if visible_range is None:
return
start_path, end_path = visible_range
if not start_path or not end_path:
return
column = view.get_column (0)
bg_rect = view.get_background_area (start_path, column)
cell_height = bg_rect.height
cell_rect = view.get_cell_area (start_path, column)
try:
first_y = view.convert_bin_window_to_widget_coords (cell_rect.x, cell_rect.y)[1]
except (AttributeError, SystemError,):
# AttributeError is with PyGTK before 2.12. SystemError is raised
# with PyGTK 2.12.0, pygtk bug #479012.
first_y = cell_rect.y % cell_height
global _warn_tree_view_coords
try:
_warn_tree_view_coords
except NameError:
self.logger.warning ("tree view coordinate conversion method "
"not available, using aproximate offset")
# Only warn once:
_warn_tree_view_coords = True
data = []
tree_iter = model.get_iter (start_path)
if tree_iter is None:
return
while model.get_path (tree_iter) != end_path:
data.append (model.get (tree_iter, model.COL_TIME, model.COL_THREAD))
tree_iter = model.iter_next (tree_iter)
self.params = (first_y, cell_height, data,)
def update (self):
self.params = None
self.queue_draw ()
class TimelineWidget (Gtk.DrawingArea):
__gtype_name__ = "GstDebugViewerTimelineWidget"
__gsignals__ = {"change-position" : (GObject.SignalFlags.RUN_LAST,
None,
(GObject.TYPE_INT,),)}
def __init__ (self):
GObject.GObject.__init__ (self)
self.logger = logging.getLogger ("ui.timeline")
self.add_events (Gdk.EventMask.BUTTON1_MOTION_MASK |
Gdk.EventMask.BUTTON_PRESS_MASK |
Gdk.EventMask.BUTTON_RELEASE_MASK)
self.process = UpdateProcess (None, None)
self.process.handle_sentinel_progress = self.__handle_sentinel_progress
self.process.handle_sentinel_finished = self.__handle_sentinel_finished
self.model = None
self.__offscreen = None
self.__offscreen_size = (0, 0)
self.__offscreen_dirty = (0, 0)
self.__position_ts_range = None
try:
self.set_tooltip_text (_("Log event histogram\n"
"Different colors represent different log-levels"))
except AttributeError:
# Compatibility.
pass
def __handle_sentinel_progress (self, sentinel):
if sentinel == self.process.dist_sentinel:
old_progress = self.__dist_sentinel_progress
new_progress = len (sentinel.data)
if new_progress - old_progress >= 32:
self.__invalidate_offscreen (old_progress, new_progress)
self.__dist_sentinel_progress = new_progress
def __handle_sentinel_finished (self, sentinel):
if sentinel == self.process.freq_sentinel:
self.__invalidate_offscreen (0, -1)
else:
self.__invalidate_offscreen (self.__dist_sentinel_progress, -1)
def __ensure_offscreen (self):
alloc = self.get_allocation ()
if self.__offscreen_size == (alloc.width, alloc.height):
return
self.__offscreen = cairo.ImageSurface (cairo.FORMAT_ARGB32, alloc.width, alloc.height)
self.__offscreen_size = (alloc.width, alloc.height)
self.__offscreen_dirty = (0, alloc.width)
if not self.__offscreen:
self.__offscreen_size = (0, 0)
raise ValueError ("could not obtain offscreen image surface")
def __invalidate_offscreen (self, start, stop):
alloc = self.get_allocation ()
if stop < 0:
stop += alloc.width
dirty_start, dirty_stop = self.__offscreen_dirty
if dirty_start != dirty_stop:
dirty_start = min (dirty_start, start)
dirty_stop = max (dirty_stop, stop)
else:
dirty_start = start
dirty_stop = stop
self.__offscreen_dirty = (dirty_start, dirty_stop)
# Just like in __draw_offscreen. FIXME: Need this in one place!
start -= 8
stop += 8
self.queue_draw_area (start, 0, stop - start, alloc.height)
def __draw_from_offscreen (self, ctx):
if not self.props.visible:
return
alloc = self.get_allocation ()
offscreen_width, offscreen_height = self.__offscreen_size
rect = Gdk.Rectangle () # TODO: damage region
rect.x, rect.y, rect.width, rect.height = 0, 0, alloc.width, alloc.height
# Fill the background (where the offscreen pixmap doesn't fit) with
# white. This happens after enlarging the window, until all sentinels
# have finished running.
if offscreen_width < alloc.width or offscreen_height < alloc.height:
ctx.rectangle (rect.x, rect.y, rect.width, rect.height)
ctx.clip ()
if offscreen_width < alloc.width:
ctx.rectangle (offscreen_width, 0, alloc.width, offscreen_height)
if offscreen_height < alloc.height:
ctx.new_path ()
ctx.rectangle (0, offscreen_height, alloc.width, alloc.height)
ctx.set_line_width (0.)
ctx.set_source_rgb (1., 1., 1.)
ctx.fill ()
ctx.set_source_surface (self.__offscreen)
ctx.rectangle (rect.x, rect.y, rect.width, rect.height)
ctx.paint ()
self.__draw_position (ctx, clip = rect)
def update (self, model):
self.clear ()
self.model = model
if model is not None:
self.__dist_sentinel_progress = 0
self.process.freq_sentinel = LineFrequencySentinel (model)
self.process.dist_sentinel = LevelDistributionSentinel (self.process.freq_sentinel, model)
width = self.get_allocation ().width
self.process.freq_sentinel.run_for (width)
self.process.run ()
def clear (self):
self.model = None
self.process.abort ()
self.process.freq_sentinel = None
self.process.dist_sentinel = None
self.__invalidate_offscreen (0, -1)
def update_position (self, start_ts, end_ts):
if not self.process.freq_sentinel:
return
if not self.process.freq_sentinel.data:
return
alloc = self.get_allocation ()
# Queue old position rectangle for redraw:
if self.__position_ts_range is not None:
start, stop = self.ts_range_to_position (*self.__position_ts_range)
self.queue_draw_area (start - 1, 0, stop - start + 2, alloc.height)
# And the new one:
start, stop = self.ts_range_to_position (start_ts, end_ts)
self.queue_draw_area (start - 1, 0, stop - start + 2, alloc.height)
self.__position_ts_range = (start_ts, end_ts,)
def find_indicative_time_step (self):
MINIMUM_PIXEL_STEP = 32
time_per_pixel = self.process.freq_sentinel.step
return 32 # FIXME use self.freq_sentinel.step and len (self.process.freq_sentinel.data)
def __draw_offscreen (self):
dirty_start, dirty_stop = self.__offscreen_dirty
if dirty_start == dirty_stop:
return
self.__offscreen_dirty = (0, 0)
width, height = self.__offscreen_size
ctx = cairo.Context (self.__offscreen)
# Indicator (triangle) size is 8, so we need to draw surrounding areas
# a bit:
dirty_start -= 8
dirty_stop += 8
dirty_start = max (dirty_start, 0)
dirty_stop = min (dirty_stop, width)
ctx.rectangle (dirty_start, 0., dirty_stop, height)
ctx.clip ()
# White background rectangle.
ctx.set_line_width (0.)
ctx.rectangle (0, 0, width, height)
ctx.set_source_rgb (1., 1., 1.)
ctx.fill ()
ctx.new_path ()
# Horizontal reference lines.
ctx.set_line_width (1.)
ctx.set_source_rgb (.95, .95, .95)
for i in range (height // 16):
y = i * 16 - .5
ctx.move_to (0, y)
ctx.line_to (width, y)
ctx.stroke ()
if self.process.freq_sentinel is None:
return
# Vertical reference lines.
pixel_step = self.find_indicative_time_step ()
ctx.set_source_rgb (.9, .9, .9)
start = dirty_start - dirty_start % pixel_step
for x in xrange (start + pixel_step, dirty_stop, pixel_step):
ctx.move_to (x - .5, 0)
ctx.line_to (x - .5, height)
ctx.stroke ()
if not self.process.freq_sentinel.data:
self.logger.debug ("frequency sentinel has no data yet")
return
ctx.translate (dirty_start, 0.)
maximum = max (self.process.freq_sentinel.data)
ctx.set_source_rgb (0., 0., 0.)
data = self.process.freq_sentinel.data[dirty_start:dirty_stop]
self.__draw_graph (ctx, height, maximum, data)
if not self.process.dist_sentinel.data:
self.logger.debug ("level distribution sentinel has no data yet")
return
colors = LevelColorThemeTango ().colors
dist_data = self.process.dist_sentinel.data[dirty_start:dirty_stop]
def cumulative_level_counts (*levels):
for level_counts in dist_data:
yield sum ((level_counts[level] for level in levels))
level = Data.debug_level_info
levels_prev = (Data.debug_level_trace,
Data.debug_level_fixme,
Data.debug_level_log,
Data.debug_level_debug,)
ctx.set_source_rgb (*(colors[level][1].float_tuple ()))
self.__draw_graph (ctx, height, maximum,
list (cumulative_level_counts (level, *levels_prev)))
level = Data.debug_level_debug
levels_prev = (Data.debug_level_trace,
Data.debug_level_fixme,
Data.debug_level_log,)
ctx.set_source_rgb (*(colors[level][1].float_tuple ()))
self.__draw_graph (ctx, height, maximum,
list (cumulative_level_counts (level, *levels_prev)))
level = Data.debug_level_log
levels_prev = (Data.debug_level_trace,Data.debug_level_fixme,)
ctx.set_source_rgb (*(colors[level][1].float_tuple ()))
self.__draw_graph (ctx, height, maximum,
list (cumulative_level_counts (level, *levels_prev)))
level = Data.debug_level_fixme
levels_prev = (Data.debug_level_trace,)
ctx.set_source_rgb (*(colors[level][1].float_tuple ()))
self.__draw_graph (ctx, height, maximum,
list (cumulative_level_counts (level, *levels_prev)))
level = Data.debug_level_trace
ctx.set_source_rgb (*(colors[level][1].float_tuple ()))
self.__draw_graph (ctx, height, maximum, [counts[level] for counts in dist_data])
# Draw error and warning triangle indicators:
def triangle (ctx, size = 8):
ctx.move_to (-size // 2, 0)
ctx.line_to ((size + 1) // 2, 0)
ctx.line_to (0, size / 1.41)
ctx.close_path ()
for level in (Data.debug_level_warning, Data.debug_level_error,):
ctx.set_source_rgb (*(colors[level][1].float_tuple ()))
for i, counts in enumerate (dist_data):
if counts[level] == 0:
continue
ctx.translate (i, 0.)
triangle (ctx)
ctx.fill ()
ctx.translate (-i, 0.)
def __draw_graph (self, ctx, height, maximum, data):
if not data:
return
heights = [height * float (d) / maximum for d in data]
ctx.move_to (0, height)
for i in range (len (heights)):
ctx.line_to (i - .5, height - heights[i] + .5)
ctx.line_to (i, height)
ctx.close_path ()
ctx.fill ()
def __have_position (self):
if ((self.process is not None) and
(self.process.freq_sentinel is not None) and
(self.process.freq_sentinel.ts_range is not None)):
return True
else:
return False
def ts_range_to_position (self, start_ts, end_ts):
if not self.__have_position ():
return (0, 0)
first_ts, last_ts = self.process.freq_sentinel.ts_range
step = self.process.freq_sentinel.step
if step == 0:
return (0, 0)
position1 = int (float (start_ts - first_ts) / step)
position2 = int (float (end_ts - first_ts) / step)
return (position1, position2)
def __draw_position (self, ctx, clip = None):
if not self.__have_position () or self.__position_ts_range is None:
if not self.__have_position ():
self.logger.debug ("have no positions")
else:
self.logger.debug ("have no positions_ts_range")
return
start_ts, end_ts = self.__position_ts_range
position1, position2 = self.ts_range_to_position (start_ts, end_ts)
if clip:
if clip.x + clip.width < position1 - 1 or clip.x > position2 + 1:
self.logger.debug ("outside of clip range: %d + %d, pos: %d, %d", clip.x, clip.width, position1, position2)
return
ctx.rectangle (clip.x, clip.y, clip.width, clip.height)
ctx.clip ()
height = self.get_allocation ().height
line_width = position2 - position1
if line_width <= 1:
ctx.set_source_rgb (1., 0., 0.)
ctx.set_line_width (1.)
ctx.move_to (position1 + .5, 0)
ctx.line_to (position1 + .5, height)
ctx.stroke ()
else:
ctx.set_source_rgba (1., 0., 0., .5)
ctx.rectangle (position1, 0, line_width, height)
ctx.fill ()
def do_draw (self, cr):
self.__ensure_offscreen ()
self.__draw_offscreen ()
self.__draw_from_offscreen (cr)
return True
def do_configure_event (self, event):
self.logger.debug ("widget size configured to %ix%i",
event.width, event.height)
if event.width < 16:
return False
self.update (self.model)
return False
def do_get_preferred_height (self):
return 64, 64 # FIXME:
def do_button_press_event (self, event):
if event.button != 1:
return False
# TODO: Check if clicked inside a warning/error indicator triangle and
# navigate there.
if not self.has_grab ():
self.grab_add ()
self.props.has_tooltip = False
pos = int (event.x)
self.emit ("change-position", pos)
return True
def do_button_release_event (self, event):
if event.button != 1:
return False
if self.has_grab ():
self.grab_remove ()
self.props.has_tooltip = True
return True
def do_motion_notify_event (self, event):
if event.get_state() & Gdk.ModifierType.BUTTON1_MASK:
self.emit ("change-position", int (event.x))
Gdk.event_request_motions (event)
return True
else:
self._handle_motion (event.x, event.y)
Gdk.event_request_motions (event)
return False
def _handle_motion (self, x, y):
# TODO: Prelight warning and error indicator triangles.
pass
class AttachedWindow (object):
def __init__ (self, feature, window):
self.window = window
ui = window.ui_manager
ui.insert_action_group (feature.action_group, 0)
self.merge_id = ui.new_merge_id ()
ui.add_ui (self.merge_id, "/menubar/ViewMenu/ViewMenuAdditions",
"ViewTimeline", "show-timeline",
Gtk.UIManagerItemType.MENUITEM, False)
ui.add_ui (self.merge_id, "/", "TimelineContextMenu", None,
Gtk.UIManagerItemType.POPUP, False)
# TODO: Make hide before/after operate on the partition that the mouse
# is pointed at instead of the currently selected line.
# ui.add_ui (self.merge_id, "/TimelineContextMenu", "TimelineHideLinesBefore",
# "hide-before-line", Gtk.UIManagerItemType.MENUITEM, False)
# ui.add_ui (self.merge_id, "/TimelineContextMenu", "TimelineHideLinesAfter",
# "hide-after-line", Gtk.UIManagerItemType.MENUITEM, False)
ui.add_ui (self.merge_id, "/TimelineContextMenu", "TimelineShowHiddenLines",
"show-hidden-lines", Gtk.UIManagerItemType.MENUITEM, False)
box = window.get_top_attach_point ()
self.timeline = TimelineWidget ()
self.timeline.connect ("change-position",
self.handle_timeline_change_position)
box.pack_start (self.timeline, False, False, 0)
self.timeline.hide ()
self.popup = ui.get_widget ("/TimelineContextMenu")
Common.GUI.widget_add_popup_menu (self.timeline, self.popup)
box = window.get_side_attach_point ()
self.vtimeline = VerticalTimelineWidget (self.window.log_view)
box.pack_start (self.vtimeline, False, False, 0)
self.vtimeline.hide ()
handler = self.handle_log_view_adjustment_value_changed
adjustment = window.widgets.log_view_scrolled_window.props.vadjustment
adjustment.connect ("value-changed", handler)
handler = self.handle_show_action_toggled
action = feature.action_group.get_action ("show-timeline")
action.connect ("toggled", handler)
handler (action)
handler = self.handle_log_view_notify_model
self.notify_model_id = window.log_view.connect ("notify::model", handler)
self.idle_scroll_path = None
self.idle_scroll_id = None
def detach (self, feature):
self.window.log_view.disconnect (self.notify_model_id)
self.notify_model_id = None
self.window.ui_manager.remove_ui (self.merge_id)
self.merge_id = None
self.window.ui_manager.remove_action_group (feature.action_group)
self.timeline.destroy ()
self.timeline = None
self.idle_scroll_path = None
if self.idle_scroll_id is not None:
GObject.source_remove (self.idle_scroll_id)
self.idle_scroll_id = None
def handle_detach_log_file (self, log_file):
self.timeline.clear ()
self.vtimeline.clear ()
def handle_log_view_notify_model (self, view, gparam):
model = view.get_model ()
if model is None:
self.timeline.clear ()
self.vtimeline.clear ()
return
self.timeline.update (model)
# Need to dispatch these idly with a low priority to avoid triggering a
# warning in treeview.get_visible_range:
def idle_update ():
self.update_timeline_position ()
self.vtimeline.update ()
return False
GObject.idle_add (idle_update, priority = GObject.PRIORITY_LOW)
def handle_log_view_adjustment_value_changed (self, adj):
# FIXME: If not visible, disconnect this handler!
if not self.timeline.props.visible:
return
self.update_timeline_position ()
self.vtimeline.update ()
def update_timeline_position (self):
view = self.window.log_view
model = view.get_model ()
visible_range = view.get_visible_range ()
if visible_range is None:
return
start_path, end_path = visible_range
if not start_path or not end_path:
return
ts1 = model.get_value (model.get_iter (start_path),
model.COL_TIME)
ts2 = model.get_value (model.get_iter (end_path),
model.COL_TIME)
self.timeline.update_position (ts1, ts2)
def handle_show_action_toggled (self, action):
show = action.props.active
if show:
self.timeline.show ()
self.vtimeline.show ()
else:
self.timeline.hide ()
self.vtimeline.hide ()
def handle_timeline_change_position (self, widget, pos):
self.goto_time_position (pos)
def goto_time_position (self, pos):
if not self.timeline.process.freq_sentinel:
return True
data = self.timeline.process.freq_sentinel.data
if not data:
return True
if pos < 0:
pos = 0
elif pos >= len (data):
pos = len (data) - 1
count = sum (data[:pos + 1])
path = (count,)
self.idle_scroll_path = path
if self.idle_scroll_id is None:
self.idle_scroll_id = GObject.idle_add (self.idle_scroll)
return False
def idle_scroll (self):
self.idle_scroll_id = None
if self.idle_scroll_path is None:
return False
path = self.idle_scroll_path
self.idle_scroll_path = None
view = self.window.log_view
view.scroll_to_cell (path, use_align = True, row_align = .5)
return False
class TimelineFeature (FeatureBase):
def __init__ (self, app):
self.logger = logging.getLogger ("ui.timeline")
self.action_group = Gtk.ActionGroup ("TimelineActions")
self.action_group.add_toggle_actions ([("show-timeline",
None, _("_Timeline"),)])
self.state = app.state.sections[TimelineState._name]
self.attached_windows = {}
handler = self.handle_show_action_toggled
action = self.action_group.get_action ("show-timeline")
action.props.active = self.state.shown
action.connect ("toggled", handler)
def handle_show_action_toggled (self, action):
show = action.props.active
if show:
self.state.shown = True
else:
self.state.shown = False
def handle_attach_window (self, window):
self.attached_windows[window] = AttachedWindow (self, window)
def handle_detach_window (self, window):
attached_window = self.attached_windows.pop (window)
attached_window.detach (self)
def handle_attach_log_file (self, window, log_file):
pass
def handle_detach_log_file (self, window, log_file):
attached_window = self.attached_windows[window]
attached_window.handle_detach_log_file (log_file)
class TimelineState (Common.GUI.StateSection):
_name = "timeline"
shown = Common.GUI.StateBool ("shown", default = True)
class Plugin (PluginBase):
features = [TimelineFeature]
def __init__ (self, app):
app.state.add_section_class (TimelineState)
self.state = app.state.sections[TimelineState._name]