#!/usr/bin/env python ## -*- Mode: python; py-indent-offset: 4; indent-tabs-mode: nil; coding: utf-8; -*- import cairo import sys import re import gtk class DataRange: def __init__(self, start = 0, end = 0, value = ''): self.start = start self.end = end self.value = value class EventString: def __init__(self, at = 0, value = ''): self.at = at self.value = value class EventFloat: def __init__(self, at = 0, value = 0.0): self.at = at self.value = value class EventInt: def __init__(self, at = 0, value = 0.0): self.at = at self.value = value def ranges_cmp(a, b): diff = a.start - b.start if diff < 0: return -1 elif diff > 0: return +1 else: return 0 def events_cmp(a, b): diff = a.at - b.at if diff < 0: return -1 elif diff > 0: return +1 else: return 0 class TimelineDataRange: def __init__(self, name = ''): self.name = name self.ranges = [] return def __search(self, key): l = 0 u = len(self.ranges)-1 while l <= u: i = int((l + u) / 2) if key >= self.ranges[i].start and key <= self.ranges[i].end: return i elif key < self.ranges[i].start: u = i - 1 else: # key > self.ranges[i].end l = i + 1 return - 1 def add_range(self, range): self.ranges.append(range) def get_all(self): return self.ranges def get_ranges(self, start, end): s = self.__search(start) e = self.__search(end) if s == -1 and e == -1: return [] elif s == -1: return self.ranges[0:e + 1] elif e == -1: return self.ranges[s:len(self.ranges)] else: return self.ranges[s:e + 1] def get_ranges_bounds(self, start, end): s = self.__search(start) e = self.__search(end) if s == -1 and e == -1: return(0, 0) elif s == -1: return(0, e + 1) elif e == -1: return(s, len(self.ranges)) else: return(s, e + 1) def sort(self): self.ranges.sort(ranges_cmp) def get_bounds(self): if len(self.ranges) > 0: lo = self.ranges[0].start hi = self.ranges[len(self.ranges)-1].end return(lo, hi) else: return(0, 0) class TimelineEvent: def __init__(self, name = ''): self.name = name self.events = [] def __search(self, key): l = 0 u = len(self.events)-1 while l <= u: i = int((l + u) / 2) if key == self.events[i].at: return i elif key < self.events[i].at: u = i - 1 else: # key > self.events[i].at l = i + 1 return l def add_event(self, event): self.events.append(event) def get_events(self, start, end): s = self.__search(start) e = self.__search(end) return self.events[s:e + 1] def get_events_bounds(self, start, end): s = self.__search(start) e = self.__search(end) return(s, e + 1) def sort(self): self.events.sort(events_cmp) def get_bounds(self): if len(self.events) > 0: lo = self.events[0].at hi = self.events[-1].at return(lo, hi) else: return(0, 0) class Timeline: def __init__(self, name = ''): self.ranges = [] self.event_str = [] self.event_int = [] self.name = name def get_range(self, name): for range in self.ranges: if range.name == name: return range timeline = TimelineDataRange(name) self.ranges.append(timeline) return timeline def get_event_str(self, name): for event_str in self.event_str: if event_str.name == name: return event_str timeline = TimelineEvent(name) self.event_str.append(timeline) return timeline def get_event_int(self, name): for event_int in self.event_int: if event_int.name == name: return event_int timeline = TimelineEvent(name) self.event_int.append(timeline) return timeline def get_ranges(self): return self.ranges def get_events_str(self): return self.event_str def get_events_int(self): return self.event_int def sort(self): for range in self.ranges: range.sort() for event in self.event_int: event.sort() for event in self.event_str: event.sort() def get_bounds(self): lo = 0 hi = 0 for range in self.ranges: (range_lo, range_hi) = range.get_bounds() if range_lo < lo: lo = range_lo if range_hi > hi: hi = range_hi for event_str in self.event_str: (ev_lo, ev_hi) = event_str.get_bounds() if ev_lo < lo: lo = ev_lo if ev_hi > hi: hi = ev_hi for event_int in self.event_int: (ev_lo, ev_hi) = event_int.get_bounds() if ev_lo < lo: lo = ev_lo if ev_hi > hi: hi = ev_hi return(lo, hi) class Timelines: def __init__(self): self.timelines = [] def get(self, name): for timeline in self.timelines: if timeline.name == name: return timeline timeline = Timeline(name) self.timelines.append(timeline) return timeline def get_all(self): return self.timelines def sort(self): for timeline in self.timelines: timeline.sort() def get_bounds(self): lo = 0 hi = 0 for timeline in self.timelines: (t_lo, t_hi) = timeline.get_bounds() if t_lo < lo: lo = t_lo if t_hi > hi: hi = t_hi return(lo, hi) def get_all_range_values(self): range_values = {} for timeline in self.timelines: for ranges in timeline.get_ranges(): for ran in ranges.get_all(): range_values[ran.value] = 1 return range_values.keys() class Color: def __init__(self, r = 0.0, g = 0.0, b = 0.0): self.r = r self.g = g self.b = b def set(self, r, g, b): self.r = r self.g = g self.b = b class Colors: # XXX add more default_colors = [Color(1, 0, 0), Color(0, 1, 0), Color(0, 0, 1), Color(1, 1, 0), Color(1, 0, 1), Color(0, 1, 1)] def __init__(self): self.__colors = {} def add(self, name, color): self.__colors[name] = color def lookup(self, name): if not self.__colors.has_key(name): self.add(name, self.default_colors.pop()) return self.__colors.get(name) class TopLegendRenderer: def __init__(self): self.__padding = 10 def set_padding(self, padding): self.__padding = padding def set_legends(self, legends, colors): self.__legends = legends self.__colors = colors def layout(self, width): self.__width = width surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1) ctx = cairo.Context(surface) line_height = 0 total_height = self.__padding line_used = self.__padding for legend in self.__legends: (t_width, t_height) = ctx.text_extents(legend)[2:4] item_width = self.__padding + self.__padding + t_width + self.__padding item_height = t_height + self.__padding if item_height > line_height: line_height = item_height if line_used + item_width > self.__width: line_used = self.__padding + item_width total_height += line_height else: line_used += item_width x = line_used - item_width total_height += line_height self.__height = total_height def get_height(self): return self.__height def draw(self, ctx): i = 0 line_height = 0 total_height = self.__padding line_used = self.__padding for legend in self.__legends: (t_width, t_height) = ctx.text_extents(legend)[2:4] item_width = self.__padding + self.__padding + t_width + self.__padding item_height = t_height + self.__padding if item_height > line_height: line_height = item_height if line_used + item_width > self.__width: line_used = self.__padding + item_width total_height += line_height else: line_used += item_width x = line_used - item_width ctx.rectangle(x, total_height, self.__padding, self.__padding) ctx.set_source_rgb(0, 0, 0) ctx.set_line_width(2) ctx.stroke_preserve() ctx.set_source_rgb(self.__colors[i].r, self.__colors[i].g, self.__colors[i].b) ctx.fill() ctx.move_to(x + self.__padding*2, total_height + t_height) ctx.set_source_rgb(0, 0, 0) ctx.show_text(legend) i += 1 return class TimelinesRenderer: def __init__(self): self.padding = 10 return def get_height(self): return self.height def set_timelines(self, timelines, colors): self.timelines = timelines self.colors = colors def set_render_range(self, start, end): self.start = start self.end = end def get_data_x_start(self): return self.padding / 2 + self.left_width + self.padding + self.right_width + self.padding / 2 def layout(self, width): surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1) ctx = cairo.Context(surface) max_text_height = ctx.text_extents("ABCDEFGHIJKLMNOPQRSTUVWXYZabcedefghijklmnopqrstuvwxyz0123456789")[3] left_width = 0 right_width = 0 left_n_lines = 0 range_n = 0 eventint_n = 0 eventstr_n = 0 for timeline in self.timelines.get_all(): left_n_lines += 1 t_width = ctx.text_extents(timeline.name)[2] left_width = max(left_width, t_width) for rang in timeline.get_ranges(): t_width = ctx.text_extents(rang.name)[2] right_width = max(right_width, t_width) range_n += 1 for events_int in timeline.get_events_int(): t_width = ctx.text_extents(events_int.name)[2] right_width = max(right_width, t_width) eventint_n += 1 for events_str in timeline.get_events_str(): t_width = ctx.text_extents(events_str.name)[2] right_width = max(right_width, t_width) eventstr_n += 1 left_height = left_n_lines * max_text_height + (left_n_lines - 1) * self.padding right_n_lines = range_n + eventint_n + eventstr_n right_height = (right_n_lines - 1) * self.padding + right_n_lines * max_text_height right_data_height = (eventint_n + eventstr_n) * (max_text_height + 5) + range_n * 10 right_data_height += (right_n_lines - 1) * self.padding height = max(left_height, right_height) height = max(height, right_data_height) self.left_width = left_width self.right_width = right_width self.max_text_height = max_text_height self.width = width self.height = height + self.padding def draw_line(self, ctx, x, y, width, height): ctx.move_to(x, y) ctx.rel_line_to(width, height) ctx.close_path() ctx.set_operator(cairo.OPERATOR_SOURCE) ctx.set_line_width(1.0) ctx.set_source_rgb(0, 0, 0) ctx.stroke() def draw_events(self, ctx, events, x, y, width, height): if (self.grey_background % 2) == 0: ctx.rectangle(x, y - self.padding / 2, width, height + self.padding) ctx.set_source_rgb(0.9, 0.9, 0.9) ctx.fill() last_x_drawn = int(x) (lo, hi) = events.get_events_bounds(self.start, self.end) for event in events.events[lo:hi]: real_x = int(x + (event.at - self.start) * width / (self.end - self.start)) if real_x > last_x_drawn + 2: ctx.rectangle(real_x, y, 1, 1) ctx.set_source_rgb(1, 0, 0) ctx.stroke() ctx.move_to(real_x, y + self.max_text_height) ctx.set_source_rgb(0, 0, 0) ctx.show_text(str(event.value)) last_x_drawn = real_x self.grey_background += 1 def draw_ranges(self, ctx, ranges, x, y, width, height): if (self.grey_background % 2) == 0: ctx.rectangle(x, y - self.padding / 2, width, height + self.padding) ctx.set_source_rgb(0.9, 0.9, 0.9) ctx.fill() last_x_drawn = int(x - 1) (lo, hi) = ranges.get_ranges_bounds(self.start, self.end) for data_range in ranges.ranges[lo:hi]: s = max(data_range.start, self.start) e = min(data_range.end, self.end) x_start = int(x + (s - self.start) * width / (self.end - self.start)) x_end = int(x + (e - self.start) * width / (self.end - self.start)) if x_end > last_x_drawn: ctx.rectangle(x_start, y, x_end - x_start, 10) ctx.set_source_rgb(0, 0, 0) ctx.stroke_preserve() color = self.colors.lookup(data_range.value) ctx.set_source_rgb(color.r, color.g, color.b) ctx.fill() last_x_drawn = x_end self.grey_background += 1 def draw(self, ctx): timeline_top = 0 top_y = self.padding / 2 left_x_start = self.padding / 2 left_x_end = left_x_start + self.left_width right_x_start = left_x_end + self.padding right_x_end = right_x_start + self.right_width data_x_start = right_x_end + self.padding / 2 data_x_end = self.width data_width = data_x_end - data_x_start cur_y = top_y self.draw_line(ctx, 0, 0, self.width, 0) self.grey_background = 1 for timeline in self.timelines.get_all(): (y_bearing, t_width, t_height) = ctx.text_extents(timeline.name)[1:4] ctx.move_to(left_x_start, cur_y + self.max_text_height - (t_height + y_bearing)) ctx.show_text(timeline.name); for events_int in timeline.get_events_int(): (y_bearing, t_width, t_height) = ctx.text_extents(events_int.name)[1:4] ctx.move_to(right_x_start, cur_y + self.max_text_height - (t_height + y_bearing)) ctx.show_text(events_int.name) self.draw_events(ctx, events_int, data_x_start, cur_y, data_width, self.max_text_height + 5) cur_y += self.max_text_height + 5 + self.padding self.draw_line(ctx, right_x_start - self.padding / 2, cur_y - self.padding / 2, self.right_width + self.padding, 0) for events_str in timeline.get_events_str(): (y_bearing, t_width, t_height) = ctx.text_extents(events_str.name)[1:4] ctx.move_to(right_x_start, cur_y + self.max_text_height - (t_height + y_bearing)) ctx.show_text(events_str.name) self.draw_events(ctx, events_str, data_x_start, cur_y, data_width, self.max_text_height + 5) cur_y += self.max_text_height + 5 + self.padding self.draw_line(ctx, right_x_start - self.padding / 2, cur_y - self.padding / 2, self.right_width + self.padding, 0) for ranges in timeline.get_ranges(): (y_bearing, t_width, t_height) = ctx.text_extents(ranges.name)[1:4] ctx.move_to(right_x_start, cur_y + self.max_text_height - (t_height + y_bearing)) ctx.show_text(ranges.name) self.draw_ranges(ctx, ranges, data_x_start, cur_y, data_width, 10) cur_y += self.max_text_height + self.padding self.draw_line(ctx, right_x_start - self.padding / 2, cur_y - self.padding / 2, self.right_width + self.padding, 0) self.draw_line(ctx, 0, cur_y - self.padding / 2, self.width, 0) bot_y = cur_y - self.padding / 2 self.draw_line(ctx, left_x_end + self.padding / 2, 0, 0, bot_y) self.draw_line(ctx, right_x_end + self.padding / 2, 0, 0, bot_y) return class ScaleRenderer: def __init__(self): self.__top = 0 return def set_bounds(self, lo, hi): self.__lo = lo self.__hi = hi def get_position(self, x): real_x = (x - self.__lo ) * self.__width / (self.__hi - self.__lo) return real_x def set_top(self): self.__top = 1 def set_bot(self): self.__top = 0 def layout(self, width): surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1) ctx = cairo.Context(surface) # calculate scale delta data_delta = self.__hi - self.__lo closest = 1 while (closest*10) < data_delta: closest *= 10 if (data_delta / closest) == 0: delta = closest elif(data_delta / closest) == 1: delta = closest / 10 else: delta = closest start = self.__lo - (self.__lo % delta) + delta end = self.__hi - (self.__hi % delta) self.__delta = delta self.__width = width # calculate text height max_text_height = ctx.text_extents("ABCDEFGHIJKLMNOPQRSTUVWXYZabcedefghijklmnopqrstuvwxyz0123456789")[3] self.max_text_height = max_text_height height = max_text_height + 10 self.__height = height def get_height(self): return self.__height def draw(self, ctx): delta = self.__delta start = self.__lo - (self.__lo % delta) + delta end = self.__hi - (self.__hi % delta) if self.__top == 1: s = -1 else: s = 1 # print scale points ctx.set_source_rgb(0, 0, 0) ctx.set_line_width(1.0) ticks = range(int(start), int(end + delta), int(delta)) for x in ticks: real_x = (x - self.__lo ) * self.__width / (self.__hi - self.__lo) ctx.move_to(real_x, 0) ctx.line_to(real_x, 5*s) ctx.close_path() ctx.stroke() (t_y_bearing, t_width, t_height) = ctx.text_extents(str(x))[1:4] if self.__top: text_delta = t_height + t_y_bearing else: text_delta = -t_y_bearing ctx.move_to(real_x - t_width / 2, (5 + 5 + text_delta)*s) ctx.show_text(str(x)) # draw subticks delta /= 10 if delta > 0: start = self.__lo - (self.__lo % delta) + delta end = self.__hi - (self.__hi % delta) for x in range(int(start), int(end + delta), int(delta)): real_x = (x - self.__lo ) * self.__width / (self.__hi - self.__lo) ctx.move_to(real_x, 0) ctx.line_to(real_x, 3*s) ctx.close_path() ctx.stroke() class GraphicRenderer: def __init__(self, start, end): self.__start = float(start) self.__end = float(end) self.__mid_scale = ScaleRenderer() self.__mid_scale.set_top() self.__bot_scale = ScaleRenderer() self.__bot_scale.set_bounds(start, end) self.__bot_scale.set_bot() self.__width = 1 self.__height = 1 def get_width(self): return self.__width def get_height(self): return self.__height # return x, y, width, height def get_data_rectangle(self): y_start = self.__top_legend.get_height() x_start = self.__data.get_data_x_start() return(x_start, y_start, self.__width - x_start, self.__data.get_height()) def scale_data(self, x): x_start = self.__data.get_data_x_start() x_scaled = x / (self.__width - x_start) * (self.__r_end - self.__r_start) return x_scaled # return x, y, width, height def get_selection_rectangle(self): y_start = self.__top_legend.get_height() + self.__data.get_height() + self.__mid_scale.get_height() + 20 y_height = self.__bot_scale.get_height() + 20 x_start = self.__bot_scale.get_position(self.__r_start) x_end = self.__bot_scale.get_position(self.__r_end) return(x_start, y_start, x_end - x_start, y_height) def scale_selection(self, x): x_scaled = x / self.__width * (self.__end - self.__start) return x_scaled def set_range(self, start, end): s = min(start, end) e = max(start, end) start = max(self.__start, s) end = min(self.__end, e) self.__r_start = start self.__r_end = end self.__data.set_render_range(start, end) self.__mid_scale.set_bounds(start, end) self.layout(self.__width, self.__height) def get_range(self): return(self.__r_start, self.__r_end) def set_data(self, data): self.__data = data def set_top_legend(self, top_legend): self.__top_legend = top_legend def layout(self, width, height): self.__width = width self.__height = height self.__top_legend.layout(width) top_legend_height = self.__top_legend.get_height() self.__data.layout(width) self.__mid_scale.layout(width - self.__data.get_data_x_start()) self.__bot_scale.layout(width) return def __x_pixel(self, x, width): new_x = (x - self.__start) * width / (self.__end - self.__start) return new_x def draw(self, ctx): # default background is white ctx.save() ctx.set_source_rgb(1, 1, 1) ctx.set_operator(cairo.OPERATOR_SOURCE) ctx.rectangle(0, 0, self.__width, self.__height) ctx.fill() # top legend ctx.save() self.__top_legend.draw(ctx) top_legend_height = self.__top_legend.get_height() ctx.restore() # separation line ctx.move_to(0, top_legend_height) ctx.line_to(self.__width, top_legend_height) ctx.close_path() ctx.set_line_width(2) ctx.set_source_rgb(0, 0, 0) ctx.stroke() # data ctx.save() ctx.translate(0, top_legend_height) self.__data.draw(ctx) ctx.restore() # scale below data ctx.save() ctx.translate(self.__data.get_data_x_start(), top_legend_height + self.__data.get_height() + self.__mid_scale.get_height()) self.__mid_scale.draw(ctx) ctx.restore() height_used = top_legend_height + self.__data.get_height() + self.__mid_scale.get_height() # separation between scale and left pane ctx.move_to(self.__data.get_data_x_start(), height_used) ctx.rel_line_to(0, -self.__mid_scale.get_height()) ctx.close_path() ctx.set_source_rgb(0, 0, 0) ctx.set_line_width(2) ctx.stroke() # separation below scale ctx.move_to(0, height_used) ctx.line_to(self.__width, height_used) ctx.close_path() ctx.set_line_width(2) ctx.set_source_rgb(0, 0, 0) ctx.stroke() select_start = self.__bot_scale.get_position(self.__r_start) select_end = self.__bot_scale.get_position(self.__r_end) # left connection between top scale and bottom scale ctx.move_to(0, height_used); ctx.line_to(self.__data.get_data_x_start(), height_used) ctx.line_to(select_start, height_used + 20) ctx.line_to(0, height_used + 20) ctx.line_to(0, height_used) ctx.set_source_rgb(0, 0, 0) ctx.set_line_width(1) ctx.stroke_preserve() ctx.set_source_rgb(0.9, 0.9, 0.9) ctx.fill() # right connection between top scale and bottom scale ctx.move_to(self.__width, height_used) ctx.line_to(self.__width, height_used + 20) ctx.line_to(select_end, height_used + 20) ctx.line_to(self.__width, height_used) ctx.set_source_rgb(0, 0, 0) ctx.set_line_width(1) ctx.stroke_preserve() ctx.set_source_rgb(0.9, 0.9, 0.9) ctx.fill() height_used += 20 # unused area background unused_start = self.__bot_scale.get_position(self.__r_start) unused_end = self.__bot_scale.get_position(self.__r_end) unused_height = self.__bot_scale.get_height() + 20 ctx.rectangle(0, height_used, unused_start, unused_height) ctx.rectangle(unused_end, height_used, self.__width - unused_end, unused_height) ctx.set_source_rgb(0.9, 0.9, 0.9) ctx.fill() # border line around bottom scale ctx.move_to(unused_end, height_used) ctx.line_to(self.__width, height_used) ctx.line_to(self.__width, height_used + unused_height) ctx.line_to(0, height_used + unused_height) ctx.line_to(0, height_used) ctx.line_to(unused_start, height_used) ctx.close_path() ctx.set_line_width(2) ctx.set_source_rgb(0, 0, 0) ctx.stroke() ctx.move_to(unused_start, height_used) ctx.line_to(unused_end, height_used) ctx.close_path() ctx.set_line_width(1) ctx.set_source_rgb(0.9, 0.9, 0.9) ctx.stroke() # unused area dot borders ctx.save() ctx.move_to(max(unused_start, 2), height_used) ctx.rel_line_to(0, unused_height) ctx.move_to(min(unused_end, self.__width - 2), height_used) ctx.rel_line_to(0, unused_height) ctx.set_dash([5], 0) ctx.set_source_rgb(0, 0, 0) ctx.set_line_width(1) ctx.stroke() ctx.restore() # bottom scale ctx.save() ctx.translate(0, height_used) self.__bot_scale.draw(ctx) ctx.restore() class GtkGraphicRenderer(gtk.DrawingArea): def __init__(self, data): super(GtkGraphicRenderer, self).__init__() self.__data = data self.__moving_left = False self.__moving_right = False self.__moving_both = False self.__moving_top = False self.__force_full_redraw = True self.add_events(gtk.gdk.POINTER_MOTION_MASK) self.add_events(gtk.gdk.BUTTON_PRESS_MASK) self.add_events(gtk.gdk.BUTTON_RELEASE_MASK) self.connect("expose_event", self.expose) self.connect('size-allocate', self.size_allocate) self.connect('motion-notify-event', self.motion_notify) self.connect('button-press-event', self.button_press) self.connect('button-release-event', self.button_release) def set_smaller_zoom(self): (start, end) = self.__data.get_range() self.__data.set_range(start, start + (end - start)*2) self.__force_full_redraw = True self.queue_draw() def set_bigger_zoom(self): (start, end) = self.__data.get_range() self.__data.set_range(start, start + (end - start) / 2) self.__force_full_redraw = True self.queue_draw() def output_png(self, filename): surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.__data.get_width(), self.__data.get_height()) ctx = cairo.Context(self.__buffer_surface) self.__data.draw(ctx) surface.write_to_png(filename) def button_press(self, widget, event): (x, y, width, height) = self.__data.get_selection_rectangle() (d_x, d_y, d_width, d_height) = self.__data.get_data_rectangle() if event.y > y and event.y < y + height: if abs(event.x - x) < 5: self.__moving_left = True return True if abs(event.x - (x + width)) < 5: self.__moving_right = True return True if event.x > x and event.x < x + width: self.__moving_both = True self.__moving_both_start = event.x self.__moving_both_cur = event.x return True if event.y > d_y and event.y < (d_y + d_height): if event.x > d_x and event.x < (d_x + d_width): self.__moving_top = True self.__moving_top_start = event.x self.__moving_top_cur = event.x return True return False def button_release(self, widget, event): if self.__moving_left: self.__moving_left = False left = self.__data.scale_selection(self.__moving_left_cur) right = self.__data.get_range()[1] self.__data.set_range(left, right) self.__force_full_redraw = True self.queue_draw() return True if self.__moving_right: self.__moving_right = False right = self.__data.scale_selection(self.__moving_right_cur) left = self.__data.get_range()[0] self.__data.set_range(left, right) self.__force_full_redraw = True self.queue_draw() return True if self.__moving_both: self.__moving_both = False delta = self.__data.scale_selection(self.__moving_both_cur - self.__moving_both_start) (left, right) = self.__data.get_range() self.__data.set_range(left + delta, right + delta) self.__force_full_redraw = True self.queue_draw() return True if self.__moving_top: self.__moving_top = False return False def motion_notify(self, widget, event): (x, y, width, height) = self.__data.get_selection_rectangle() if self.__moving_left: if event.x <= 0: self.__moving_left_cur = 0 elif event.x >= x + width: self.__moving_left_cur = x + width else: self.__moving_left_cur = event.x self.queue_draw_area(0, int(y), int(self.__width), int(height)) return True if self.__moving_right: if event.x >= self.__width: self.__moving_right = self.__width elif event.x < x: self.__moving_right_cur = x else: self.__moving_right_cur = event.x self.queue_draw_area(0, int(y), int(self.__width), int(height)) return True if self.__moving_both: cur_e = self.__width - (x + width - self.__moving_both_start) cur_s = (self.__moving_both_start - x) if event.x < cur_s: self.__moving_both_cur = cur_s elif event.x > cur_e: self.__moving_both_cur = cur_e else: self.__moving_both_cur = event.x self.queue_draw_area(0, int(y), int(self.__width), int(height)) return True if self.__moving_top: self.__moving_top_cur = event.x delta = self.__data.scale_data(self.__moving_top_start - self.__moving_top_cur) (left, right) = self.__data.get_range() self.__data.set_range(left + delta, right + delta) self.__force_full_redraw = True self.__moving_top_start = event.x self.queue_draw() return True (d_x, d_y, d_width, d_height) = self.__data.get_data_rectangle() if event.y > y and event.y < y + height: if abs(event.x - x) < 5 or abs(event.x - (x + width)) < 5: widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.SB_H_DOUBLE_ARROW)) return True if event.x > x and event.x < x + width: widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR)) return True if event.y > d_y and event.y < (d_y + d_height): if event.x > d_x and event.x < (d_x + d_width): widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR)) return True widget.window.set_cursor(None) return False def size_allocate(self, widget, allocation): self.__width = allocation.width self.__height = allocation.height self.__data.layout(allocation.width, allocation.height) self.__force_full_redraw = True self.queue_draw() def expose(self, widget, event): if self.__force_full_redraw: self.__buffer_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.__data.get_width(), self.__data.get_height()) ctx = cairo.Context(self.__buffer_surface) self.__data.draw(ctx) self.__force_full_redraw = False ctx = widget.window.cairo_create() ctx.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) ctx.clip() ctx.set_source_surface(self.__buffer_surface) ctx.paint() (x, y, width, height) = self.__data.get_selection_rectangle() if self.__moving_left: ctx.move_to(max(self.__moving_left_cur, 2), y) ctx.rel_line_to(0, height) ctx.close_path() ctx.set_line_width(1) ctx.set_source_rgb(0, 0, 0) ctx.stroke() if self.__moving_right: ctx.move_to(min(self.__moving_right_cur, self.__width - 2), y) ctx.rel_line_to(0, height) ctx.close_path() ctx.set_line_width(1) ctx.set_source_rgb(0, 0, 0) ctx.stroke() if self.__moving_both: delta_x = self.__moving_both_cur - self.__moving_both_start left_x = x + delta_x ctx.move_to(x + delta_x, y) ctx.rel_line_to(0, height) ctx.close_path() ctx.move_to(x + width + delta_x, y) ctx.rel_line_to(0, height) ctx.close_path() ctx.set_source_rgb(0, 0, 0) ctx.set_line_width(1) ctx.stroke() return False class MainWindow: def __init__(self): return def run(self, graphic): window = gtk.Window() self.__window = window window.set_default_size(200, 200) vbox = gtk.VBox() window.add(vbox) render = GtkGraphicRenderer(graphic) self.__render = render vbox.pack_end(render, True, True, 0) hbox = gtk.HBox() vbox.pack_start(hbox, False, False, 0) smaller_zoom = gtk.Button("Zoom Out") smaller_zoom.connect("clicked", self.__set_smaller_cb) hbox.pack_start(smaller_zoom) bigger_zoom = gtk.Button("Zoom In") bigger_zoom.connect("clicked", self.__set_bigger_cb) hbox.pack_start(bigger_zoom) output_png = gtk.Button("Output Png") output_png.connect("clicked", self.__output_png_cb) hbox.pack_start(output_png) window.connect('destroy', gtk.main_quit) window.show_all() #gtk.bindings_activate(gtk.main_quit, 'q', 0) gtk.main() def __set_smaller_cb(self, widget): self.__render.set_smaller_zoom() def __set_bigger_cb(self, widget): self.__render.set_bigger_zoom() def __output_png_cb(self, widget): dialog = gtk.FileChooserDialog("Output Png", self.__window, gtk.FILE_CHOOSER_ACTION_SAVE, ("Save", 1)) self.__dialog = dialog dialog.set_default_response(1) dialog.connect("response", self.__dialog_response_cb) dialog.show() return def __dialog_response_cb(self, widget, response): if response == 1: filename = self.__dialog.get_filename() self.__render.output_png(filename) widget.hide() return def read_data(filename): timelines = Timelines() colors = Colors() fh = open(filename) m1 = re.compile('range ([^ ]+) ([^ ]+) ([^ ]+) ([0-9]+) ([0-9]+)') m2 = re.compile('event-str ([^ ]+) ([^ ]+) ([^ ]+) ([0-9]+)') m3 = re.compile('event-int ([^ ]+) ([^ ]+) ([0-9]+) ([0-9]+)') m4 = re.compile('color ([^ ]+) #([a-fA-F0-9]{2,2})([a-fA-F0-9]{2,2})([a-fA-F0-9]{2,2})') for line in fh.readlines(): m = m1.match(line) if m: line_name = m.group(1) timeline = timelines.get(m.group(1)) rang = timeline.get_range(m.group(2)) data_range = DataRange() data_range.value = m.group(3) data_range.start = int(m.group(4)) data_range.end = int(m.group(5)) rang.add_range(data_range) continue m = m2.match(line) if m: line_name = m.group(1) timeline = timelines.get(m.group(1)) ev = timeline.get_event_str(m.group(2)) event = EventString() event.value = m.group(3) event.at = int(m.group(4)) ev.add_event(event) continue m = m3.match(line) if m: line_name = m.group(1) timeline = timelines.get(m.group(1)) ev = timeline.get_event_int(m.group(2)) event = EventInt() event.value = int(m.group(3)) event.at = int(m.group(4)) ev.add_event(event) continue m = m4.match(line) if m: r = int(m.group(2), 16) g = int(m.group(3), 16) b = int(m.group(4), 16) color = Color(r / 255, g / 255, b / 255) colors.add(m.group(1), color) continue timelines.sort() return (colors, timelines) def main(): (colors, timelines) = read_data(sys.argv[1]) (lower_bound, upper_bound) = timelines.get_bounds() graphic = GraphicRenderer(lower_bound, upper_bound) top_legend = TopLegendRenderer() range_values = timelines.get_all_range_values() range_colors = [] for range_value in range_values: range_colors.append(colors.lookup(range_value)) top_legend.set_legends(range_values, range_colors) graphic.set_top_legend(top_legend) data = TimelinesRenderer() data.set_timelines(timelines, colors) graphic.set_data(data) # default range range_mid = (upper_bound - lower_bound) / 2 range_width = (upper_bound - lower_bound) / 10 range_lo = range_mid - range_width / 2 range_hi = range_mid + range_width / 2 graphic.set_range(range_lo, range_hi) main_window = MainWindow() main_window.run(graphic) main()