#!/usr/bin/python """GUI stuff.""" from schedule import * from renderer import * from graph import * from canvas import * from windows import * from format import * import pygtk import gtk import gobject import copy class GraphContextMenu(gtk.Menu): MAX_STR_LEN = 80 def __init__(self, selected, info_win): super(GraphContextMenu, self).__init__() self.info_win = info_win if not selected: item = gtk.MenuItem("(No events selected)") item.set_sensitive(False) self.append(item) item.show() else: for layer in selected: for event in selected[layer]: string = event.str_short(info_win.get_unit()) if len(string) > GraphContextMenu.MAX_STR_LEN - 3: string = string[:GraphContextMenu.MAX_STR_LEN - 3] + '...' item = gtk.MenuItem(string) item.connect('activate', self.update_info_window, event) self.append(item) item.show() def update_info_window(self, widget, data): self.info_win.set_event(data) self.info_win.present() class GraphArea(gtk.DrawingArea): HORIZ_PAGE_SCROLL_FACTOR = 10.8 HORIZ_STEP_SCROLL_FACTOR = 0.8 VERT_PAGE_SCROLL_FACTOR = 3.0 VERT_STEP_SCROLL_FACTOR = 0.5 REFRESH_INFLATION_FACTOR = 4.0 MIN_ZOOM_OUT = 0.25 MAX_ZOOM_IN = 4.0 ZOOM_INCR = 0.25 def __init__(self, renderer): super(GraphArea, self).__init__() self.renderer = renderer self.cur_x = 0 self.cur_y = 0 self.width = 0 self.height = 0 self.scale = 1.0 self.set_set_scroll_adjustments_signal('set-scroll-adjustments') self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.EXPOSURE_MASK | gtk.gdk.LEAVE_NOTIFY_MASK) self.band_rect = None self.ctrl_clicked = False self.cursor_pos = None self.last_selected = {} self.dirtied_regions = [] self.get_graph().update_view(self.cur_x, self.cur_y, self.width, self.height, self.scale) self.connect('expose-event', self.expose) self.connect('size-allocate', self.size_allocate) self.connect('set-scroll-adjustments', self.set_scroll_adjustments) self.connect('button-press-event', self.button_press) self.connect('button-release-event', self.button_release) self.connect('motion-notify-event', self.motion_notify) self.connect('leave-notify-event', self.leave_notify) def output_to_file(self, filename, ftype): """Outputs the entire graph to a file.""" graph = self.renderer.get_graph() surface = None if ftype == 'png': surface = ImageSurface() elif ftype == 'svg': surface = SVGSurface() else: raise ValueError surface.renew(graph.get_width(), graph.get_height()) graph.set_tmp_surface(surface) graph.render_all(self.renderer.get_schedule()) surface.write_out(filename) graph.revert_surface() def expose(self, widget, expose_event, data=None): ctx = widget.window.cairo_create() graph = self.get_graph() graph.load_ctx(ctx) # If X caused the expose event, we need to update the entire area, not just the # changes we might have made. An expose event caused by X needs to take priority # over any expose events caused by updates to the state of the graph because # the areas we marked as dirty only include the state changes, which is completely # unrelated to the area that X indicates must be updated. if expose_event.type == gtk.gdk.EXPOSE: self.dirtied_regions = [(expose_event.area.x, expose_event.area.y, expose_event.area.width, expose_event.area.height)] virt_dirtied_regions = graph.real_to_virt_list(self.dirtied_regions) graph.render_surface(self.renderer.get_schedule(), virt_dirtied_regions) # render dragging band rectangle, if there is one if self.band_rect is not None: x, y, width, height = graph.virt_to_real(self.band_rect[0], self.band_rect[1], self.band_rect[2], self.band_rect[3]) thickness = GraphFormat.BAND_THICKNESS color = GraphFormat.BAND_COLOR ctx.rectangle(x, y, width, height) ctx.set_line_width(thickness) ctx.set_source_rgb(color[0], color[1], color[2]) ctx.stroke() self.dirtied_regions = [] def get_renderer(self): return self.renderer def get_graph(self): return self.renderer.get_graph() def get_schedule(self): return self.renderer.get_schedule() def zoom_in(self): scale = self.scale + GraphArea.ZOOM_INCR if scale > GraphArea.MAX_ZOOM_IN: scale = GraphArea.MAX_ZOOM_IN self.set_scale(scale) self.config_scrollbars(self.cur_x, self.cur_y, self.scale) self._pos_update() def zoom_out(self): scale = self.scale - GraphArea.ZOOM_INCR if scale < GraphArea.MIN_ZOOM_OUT: scale = GraphArea.MIN_ZOOM_OUT self.set_scale(scale) self.config_scrollbars(self.cur_x, self.cur_y, self.scale) self._pos_update() def set_scale(self, scale): if scale == self.scale: return self.scale = scale self._dirty(0, 0, self.width, self.height) def set_hvalue(self, value): if self.horizontal is None: return value = max(value, self.horizontal.get_lower()) value = min(value, self.horizontal.get_upper() - self.horizontal.get_page_size()) self.horizontal.set_value(value) def set_vvalue(self, value): if self.vertical is None: return value = max(value, self.vertical.get_lower()) value = min(value, self.vertical.get_upper() - self.vertical.get_page_size()) self.vertical.set_value(value) def set_scroll_adjustments(self, widget, horizontal, vertical, data=None): graph = self.renderer.get_graph() width = graph.get_width() height = graph.get_height() self.horizontal = horizontal self.vertical = vertical self.config_scrollbars(self.cur_x, self.cur_y, self.scale) if self.horizontal is not None: self.horizontal.connect('value-changed', self.horizontal_value_changed) if self.vertical is not None: self.vertical.connect('value-changed', self.vertical_value_changed) def horizontal_value_changed(self, adjustment): self.cur_x = min(adjustment.value, self.renderer.get_graph().get_width()) self.cur_x = max(adjustment.value, 0.0) self.get_graph().render_surface(self.renderer.get_schedule(), [self.get_graph().real_to_virt(0, 0, self.width, self.height)], True) self._dirty(0, 0, self.width, self.height) self._pos_update() def vertical_value_changed(self, adjustment): self.cur_y = min(adjustment.value, self.renderer.get_graph().get_height()) self.cur_y = max(adjustment.value, 0.0) self.get_graph().render_surface(self.renderer.get_schedule(), [self.get_graph().real_to_virt(0, 0, self.width, self.height)], True) self._dirty(0, 0, self.width, self.height) self._pos_update() def size_allocate(self, widget, allocation): self.width = allocation.width self.height = allocation.height self.config_scrollbars(self.cur_x, self.cur_y, self.scale) self._pos_update() def config_scrollbars(self, hvalue, vvalue, scale): graph = self.renderer.get_graph() width = graph.get_width() height = graph.get_height() if self.horizontal is not None: self.horizontal.set_all(0.0, 0.0, width, graph.get_attrs().maj_sep * GraphArea.HORIZ_STEP_SCROLL_FACTOR / scale, graph.get_attrs().maj_sep * GraphArea.HORIZ_PAGE_SCROLL_FACTOR / scale, self.width / scale) self.set_hvalue(hvalue) if self.vertical is not None: self.vertical.set_all(0.0, 0.0, height, graph.get_attrs().y_item_size * GraphArea.VERT_STEP_SCROLL_FACTOR / scale, graph.get_attrs().y_item_size * GraphArea.VERT_PAGE_SCROLL_FACTOR / scale, self.height / scale) self.set_vvalue(vvalue) def refresh_all(self): """Refreshes the entire visible screen.""" self._dirty(0, 0, self.width, self.height) def refresh_events(self, sender, new, old, replace): """Even if the selected areas change on one graph, they change everywhere, and different events might be in different regions on different graphs. So when an event is selected on one graph its region on a completely different graph needs to be updated. This is why this method is here: given the graph that requested the change, the old set of events selected, and the new set of events selected, it updates the selected regions of this graph, and refreshes the drawing of the graph that requested the change.""" if self is not sender: self.renderer.get_graph().render_events(new, True) self._tag_events(new) if self is sender: self._copy_tags(old) self._dirty_events(new) self._dirty_events(old) if replace: self.renderer.get_schedule().set_selected(new) else: self.renderer.get_schedule().remove_selected(old) self.renderer.get_schedule().add_selected(new) self.emit('update-sel-changes', self.renderer.get_schedule().get_selected()) def _find_max_layer(self, regions): max_layer = -1 for layer in regions: if layer > max_layer: max_layer = layer return max_layer def _dirty_events(self, events): # if an event changed selected status, update the bounding area for layer in events: for event in events[layer]: if not events[layer][event][self].is_dummy(): dim = events[layer][event][self].get_dimensions() x, y, width, height = self.get_graph().virt_to_real(dim[0], dim[1], dim[2], dim[3]) self._dirty_inflate(x, y, width, height, GraphFormat.BORDER_THICKNESS * self.scale) def _tag_events(self, selected): """Some of the events in the collection of selected events might be new. In this case, these events are not yet associated with the region on the graph that they belong to. This method fixes this. """ graph = self.renderer.get_graph() for layer in selected: for event in selected[layer]: # note that each graph has its own region associated # with the event selected[layer][event][self] = graph.get_sel_region(event) def _copy_tags(self, selected): """When we want to specify a collection of selected events to perform an operation on, we usually do not know ahead of time what regions (in which graphs) the events are associated with. But we have this information stored in the collection of all selected events. This method just copies this information over to the selected variable.""" cur_selected = self.renderer.get_schedule().get_selected() for layer in selected: for event in selected[layer]: selected[layer][event] = cur_selected[layer][event] def _select_event(self, coll, event): if event.get_layer() not in coll: coll[event.get_layer()] = {} if event not in coll[event.get_layer()]: coll[event.get_layer()][event] = {} def leave_notify(self, widget, leave_event): self.cursor_pos = None self.emit('update-event-description', None) def motion_notify(self, widget, motion_event, data=None): self.cursor_pos = (motion_event.x, motion_event.y) self._pos_update() def _pos_update(self): self.get_graph().update_view(self.cur_x, self.cur_y, self.width, self.height, self.scale) if self.cursor_pos is None: return graph = self.renderer.get_graph() graph.render_surface(self.renderer.get_schedule(), [graph.real_to_virt(self.cursor_pos[0], self.cursor_pos[1], 0, 0)], True) cur_x_v, cur_y_v, w, h = graph.real_to_virt(self.cursor_pos[0], self.cursor_pos[1], 0, 0) just_selected = graph.get_intersecting_regions(cur_x_v, cur_y_v, 0, 0) was_selected = self.renderer.get_schedule().get_selected() if not just_selected: msg = '' the_event = None else: max_layer = self._find_max_layer(just_selected) for event in just_selected[max_layer]: if event.get_layer() == max_layer: the_event = event break msg = the_event.str_short(graph.get_attrs().unit) self.emit('update-event-description', the_event) virt_x, virt_y, w, h = graph.real_to_virt(self.cursor_pos[0], self.cursor_pos[1], 0, 0) if self.band_rect is not None: remove_selected = {} add_selected = {} # dragging a rectangle x = self.band_rect[0] y = self.band_rect[1] width = virt_x - self.band_rect[0] height = virt_y - self.band_rect[1] old_x, old_y, old_width, old_height = self.band_rect x_p, y_p, width_p, height_p = self._positivify_int(x, y, width, height) old_x_p, old_y_p, old_width_p, old_height_p = self._positivify_int(old_x, old_y, old_width, old_height) new_reg = gtk.gdk.region_rectangle(gtk.gdk.Rectangle(x_p, y_p, width_p, height_p)) old_reg = gtk.gdk.region_rectangle(gtk.gdk.Rectangle(old_x_p, old_y_p, old_width_p, old_height_p)) # To find the events that should be deselected and the new events that should be selected, compute # the set differences between the old and new selection rectangles remove_reg = gtk.gdk.region_rectangle(gtk.gdk.Rectangle(old_x_p, old_y_p, old_width_p, old_height_p)) remove_reg.subtract(new_reg) dirty_list = [] for rect in remove_reg.get_rectangles(): dirty_list.append((rect.x, rect.y, rect.width, rect.height)) graph.render_surface(self.renderer.get_schedule(), graph.real_to_virt_list(dirty_list), True) for rect in dirty_list: rx, ry, rwidth, rheight = rect i_regs = graph.get_intersecting_regions(rx, ry, rwidth, rheight) for layer in i_regs: for event in i_regs[layer]: if event.get_layer() in was_selected and event in was_selected[event.get_layer()]: self._select_event(remove_selected, event) add_reg = gtk.gdk.region_rectangle(gtk.gdk.Rectangle(x_p, y_p, width_p, height_p)) add_reg.subtract(old_reg) dirty_list = [(x_p, y_p, width_p, 0), (x_p, y_p, 0, height_p), (x_p, y_p + height_p, width_p, 0), (x_p + width_p, y_p, 0, height_p)] for rect in add_reg.get_rectangles(): dirty_list.append((rect.x, rect.y, rect.width, rect.height)) graph.render_surface(self.renderer.get_schedule(), dirty_list, True) for rect in dirty_list: rx, ry, rwidth, rheight = rect i_regs = graph.get_intersecting_regions(rx, ry, rwidth, rheight) for layer in i_regs: for event in i_regs[layer]: self._select_event(add_selected, event) self.band_rect = x, y, width, height self.emit('request-refresh-events', self, add_selected, remove_selected, False) x_r, y_r, width_r, height_r = graph.virt_to_real(x, y, width, height) old_x_r, old_y_r, old_width_r, old_height_r = graph.virt_to_real(old_x, old_y, old_width, old_height) self._dirty_rect_border(old_x_r, old_y_r, old_width_r, old_height_r, GraphFormat.BAND_THICKNESS * self.scale) self._dirty_rect_border(x_r, y_r, width_r, height_r, GraphFormat.BAND_THICKNESS * self.scale) def button_press(self, widget, button_event, data=None): graph = self.renderer.get_graph() self.ctrl_clicked = button_event.state & gtk.gdk.CONTROL_MASK if button_event.button == 1: self.left_button_start_coor = (button_event.x, button_event.y) graph.render_surface(self.renderer.get_schedule(), \ [graph.real_to_virt(button_event.x, button_event.y, 0, 0)], True) b_x_r, b_y_r, w, h = graph.real_to_virt(button_event.x, button_event.y, 0, 0) just_selected = graph.get_intersecting_regions(b_x_r, b_y_r, 0, 0) max_layer = self._find_max_layer(just_selected) was_selected = self.renderer.get_schedule().get_selected() if not self.ctrl_clicked: new_now_selected = {} more_than_one = 0 for layer in was_selected: for event in was_selected[layer]: more_than_one += 1 if more_than_one > 1: break # only select those events which were in the top layer (it's # not intuitive to click something and then have something # below it get selected). Also, clicking something that # is selected deselects it, if it's the only thing selected if just_selected: for event in just_selected[max_layer]: if not (more_than_one == 1 and max_layer in was_selected \ and event in was_selected[max_layer]): self._select_event(new_now_selected, event) break # only pick one event when just clicking self.emit('request-refresh-events', self, new_now_selected, was_selected, True) else: remove_selected = {} add_selected = {} if just_selected: for event in just_selected[max_layer]: if max_layer in was_selected and event in was_selected[max_layer]: self._select_event(remove_selected, event) else: self._select_event(add_selected, event) break # again, only pick one event because we are just clicking self.emit('request-refresh-events', self, add_selected, remove_selected, False) if self.band_rect is None: self.band_rect = (b_x_r, b_y_r, 0, 0) elif button_event.button == 3: self._release_band() self.emit('request-context-menu', button_event, self.renderer.get_schedule().get_selected()) def button_release(self, widget, button_event, data=None): self.ctrl_clicked = False if button_event.button == 1: self._release_band() def get_width(self): return self.width def get_height(self): return self.height def _release_band(self): if self.band_rect is not None: x, y, width, height = self.band_rect x_r, y_r, width_r, height_r = self.get_graph().virt_to_real(x, y, width, height) self._dirty_rect_border(x_r, y_r, width_r, height_r, GraphFormat.BAND_THICKNESS * self.scale) self.band_rect = None def _dirty(self, x, y, width, height): x = max(int(math.floor(x)), 0) y = max(int(math.floor(y)), 0) width = min(int(math.ceil(width)), self.width) height = min(int(math.ceil(height)), self.height) self.dirtied_regions.append((x, y, width, height)) rect = gtk.gdk.Rectangle(x, y, width, height) self.window.invalidate_rect(rect, True) def _dirty_inflate(self, x, y, width, height, thickness): t = thickness * GraphArea.REFRESH_INFLATION_FACTOR x -= t / 2.0 y -= t / 2.0 width += t height += t self._dirty(x, y, width, height) def _dirty_rect_border(self, x, y, width, height, thickness): # support rectangles with negative width and height (i.e. -width = width, but going leftwards # instead of rightwards) x, y, width, height = self._positivify(x, y, width, height) self._dirty_inflate(x, y, width, 0, thickness) self._dirty_inflate(x, y, 0, height, thickness) self._dirty_inflate(x, y + height, width, 0, thickness) self._dirty_inflate(x + width, y, 0, height, thickness) def _positivify(self, x, y, width, height): if width < 0: x += width width = -width if height < 0: y += height height = -height return x, y, width, height def _positivify_int(self, x, y, width, height): p = self._positivify(x, y, width, height) return (int(p[0]), int(p[1]), int(p[2]), int(p[3])) class GraphWindow(gtk.ScrolledWindow): def __init__(self, renderer): super(GraphWindow, self).__init__(None, None) self.add_events(gtk.gdk.KEY_PRESS_MASK | gtk.gdk.SCROLL_MASK) self.ctr = 0 self.connect('key-press-event', self.key_press) self.connect('scroll-event', self.scroll) self.garea = GraphArea(renderer) self.add(self.garea) self.garea.show() def key_press(self, widget, key_event): hadj = self.get_hadjustment() vadj = self.get_vadjustment() if hadj is None or vadj is None: return ctrl_clicked = key_event.state & gtk.gdk.CONTROL_MASK keystr = None keymap = {gtk.keysyms.Up : 'up', gtk.keysyms.Down : 'down', gtk.keysyms.Left : 'left', gtk.keysyms.Right : 'right'} if key_event.keyval in keymap: keystr = keymap[key_event.keyval] else: return True if ctrl_clicked: keystr = 'ctrl-' + keystr if keystr is not None: self._scroll_direction(keystr) return True def set_hvalue(self, value): self.get_graph_area().set_hvalue(value) def set_vvalue(self, value): self.get_graph_area().set_vvalue(value) def _scroll_direction(self, keystr): hadj = self.get_hadjustment() vadj = self.get_vadjustment() if hadj is None or vadj is None: return hupper = hadj.get_upper() hlower = hadj.get_lower() hpincr = hadj.get_page_increment() hsincr = hadj.get_step_increment() hpsize = hadj.get_page_size() hval = hadj.get_value() vupper = vadj.get_upper() vlower = vadj.get_lower() vval = vadj.get_value() vpincr = vadj.get_page_increment() vsincr = vadj.get_step_increment() vpsize = vadj.get_page_size() adj_tuple = {'up' : (vadj, -vsincr, 0, vval, max), 'ctrl-up' : (vadj, -vpincr, 0, vval, max), 'down' : (vadj, vsincr, vupper - vpsize, vval, min), 'ctrl-down' : (vadj, vpincr, vupper - vpsize, vval, min), 'left' : (hadj, -hsincr, 0, hval, max), 'ctrl-left' : (hadj, -hpincr, 0, hval, max), 'right' : (hadj, hsincr, hupper - hpsize, hval, min), 'ctrl-right' : (hadj, hpincr, hupper - hpsize, hval, min)} adj, inc, lim, val, extr = adj_tuple[keystr] adj.set_value(extr(val + inc, lim)) def scroll(self, widget, scroll_event): if scroll_event.state & gtk.gdk.CONTROL_MASK: if scroll_event.direction == gtk.gdk.SCROLL_UP: self.emit('request-zoom-in') elif scroll_event.direction == gtk.gdk.SCROLL_DOWN: self.emit('request-zoom-out') else: if scroll_event.direction == gtk.gdk.SCROLL_UP: self._scroll_direction('up') elif scroll_event.direction == gtk.gdk.SCROLL_DOWN: self._scroll_direction('down') return True def get_graph_area(self): return self.garea class MainWindow(gtk.Window): WINDOW_WIDTH_REQ = 500 WINDOW_HEIGHT_REQ = 300 def __init__(self): super(MainWindow, self).__init__(gtk.WINDOW_TOPLEVEL) self.add_events(gtk.gdk.BUTTON_PRESS_MASK) self.connect('delete_event', self.delete_event) self.connect('destroy', self.die) file_menu = gtk.Menu() edit_menu = gtk.Menu() view_menu = gtk.Menu() agr = gtk.AccelGroup() self.add_accel_group(agr) # list of file items that are clickable if and only if at # least one graph is viewable self.sensitive_menu_items = [] save_item = gtk.ImageMenuItem('_Save Graph to File') img = gtk.Image() img.set_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU) save_item.set_image(img) key, mod = gtk.accelerator_parse('S') save_item.add_accelerator('activate', agr, key, mod, gtk.ACCEL_VISIBLE) save_item.connect('activate', self.save_item_activate) save_item.set_sensitive(False) self.sensitive_menu_items.append(save_item) quit_item = gtk.ImageMenuItem(gtk.STOCK_QUIT, agr) key, mod = gtk.accelerator_parse('Q') quit_item.add_accelerator('activate', agr, key, mod, gtk.ACCEL_VISIBLE) quit_item.connect('activate', self.quit_item_activate) file_menu.append(save_item) file_menu.append(quit_item) file_item = gtk.MenuItem('_File', True) file_item.set_submenu(file_menu) move_item = gtk.ImageMenuItem('_Jump to Time') key, mod = gtk.accelerator_parse('J') img = gtk.Image() img.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU) move_item.set_image(img) move_item.add_accelerator('activate', agr, key, mod, gtk.ACCEL_VISIBLE) move_item.set_sensitive(False) self.sensitive_menu_items.append(move_item) move_item.connect('activate', self.move_to_time_activate) self.colors_item = gtk.ImageMenuItem('Co_lors') key, mod = gtk.accelerator_parse('L') img = gtk.Image() img.set_from_stock(gtk.STOCK_SELECT_COLOR, gtk.ICON_SIZE_MENU) self.colors_item.set_image(img) self.colors_item.add_accelerator('activate', agr, key, mod, gtk.ACCEL_VISIBLE) self.colors_item.set_sensitive(False) self.colors_item.connect('activate', self.colors_activate) edit_menu.append(self.colors_item) edit_item = gtk.MenuItem('_Edit', True) edit_item.set_submenu(edit_menu) zoom_in_item = gtk.ImageMenuItem(gtk.STOCK_ZOOM_IN, agr) key, mod = gtk.accelerator_parse('plus') zoom_in_item.add_accelerator('activate', agr, key, mod, gtk.ACCEL_VISIBLE) key, mod = gtk.accelerator_parse('equal') zoom_in_item.add_accelerator('activate', agr, key, mod, 0) zoom_in_item.connect('activate', self.zoom_in_item_activate) zoom_out_item = gtk.ImageMenuItem(gtk.STOCK_ZOOM_OUT, agr) key, mod = gtk.accelerator_parse('minus') zoom_out_item.add_accelerator('activate', agr, key, mod, gtk.ACCEL_VISIBLE) key, mod = gtk.accelerator_parse('underscore') zoom_out_item.add_accelerator('activate', agr, key, mod, 0) zoom_out_item.connect('activate', self.zoom_out_item_activate) view_menu.append(move_item) view_menu.append(zoom_in_item) view_menu.append(zoom_out_item) view_item = gtk.MenuItem('_View', True) view_item.set_submenu(view_menu) menu_bar = gtk.MenuBar() menu_bar.append(file_item) menu_bar.append(edit_item) menu_bar.append(view_item) self.vbox = gtk.VBox(False, 0) self.notebook = gtk.Notebook() self.notebook.last_page = -1 self.notebook.connect('switch-page', self.switch_page) self.event_label = gtk.Label('') self.job_run_label = gtk.Label('') self.job_rd_label = gtk.Label('') self.vbox.pack_start(menu_bar, False, False, 0) self.vbox.pack_start(self.notebook, True, True, 0) self.vbox.pack_start(self.event_label, False, False, 0) self.vbox.pack_start(self.job_run_label, False, False, 0) self.vbox.pack_start(self.job_rd_label, False, False, 0) self.add(self.vbox) self.info_win = InfoWindow() self.text_input_dialog = TextInputDialog('Move to Time', 'how is babby formed?', self) self.color_dialog = gtk.ColorSelectionDialog('Choose Item Color') self.cur_item_no = None self.set_size_request(MainWindow.WINDOW_WIDTH_REQ, MainWindow.WINDOW_HEIGHT_REQ) self.set_title('Unit-Trace Visualizer') self.show_all() def connect_widgets(self, gwindow): gwindow.get_graph_area().connect('update-event-description', self.update_event_description) gwindow.get_graph_area().connect('request-context-menu', self.request_context_menu) gwindow.get_graph_area().connect('request-refresh-events', self.request_refresh_events) gwindow.connect('request-zoom-in', self.zoom_in_item_activate) gwindow.connect('request-zoom-out', self.zoom_out_item_activate) gwindow.get_graph_area().connect('update-sel-changes', self.update_sel_changes) def set_renderers(self, renderers): for i in range(0, self.notebook.get_n_pages()): self.notebook.remove_page(0) for title in renderers: gwindow = GraphWindow(renderers[title]) self.connect_widgets(gwindow) gwindow.show() self.notebook.append_page(gwindow, gtk.Label(title)) if self.notebook.get_n_pages() > 0: self.notebook.get_nth_page(0).grab_focus() if self.notebook.get_n_pages() > 0: for menu_item in self.sensitive_menu_items: menu_item.set_sensitive(True) else: for menu_item in self.sensitive_menu_items: menu_item.set_sensitive(False) def update_sel_changes(self, widget, selected): """Modify the GUI appropriately to reflect a change in selection.""" self.selected = selected self.cur_item_no = self.notebook.get_nth_page(self.notebook.last_page).get_graph_area() \ .get_graph().all_one_item_selected(selected) if self.cur_item_no is not None: self.colors_item.set_sensitive(True) else: self.colors_item.set_sensitive(False) def switch_page(self, widget, page, page_num): if self.notebook.get_nth_page(self.notebook.last_page) is not None: old_value = self.notebook.get_nth_page(self.notebook.last_page).get_hadjustment().get_value() old_ofs = self.notebook.get_nth_page(self.notebook.last_page).get_graph_area().get_graph().get_origin()[0] new_ofs = self.notebook.get_nth_page(page_num).get_graph_area().get_graph().get_origin()[0] new_value = old_value - old_ofs + new_ofs self.notebook.get_nth_page(page_num).get_hadjustment().set_value(new_value) unit = self.notebook.get_nth_page(page_num).get_graph_area().get_graph().get_attrs().unit self.info_win.set_unit(unit) self.notebook.last_page = page_num def update_event_description(self, widget, event): if event is None: self.event_label.set_text('') self.job_run_label.set_text('') self.job_rd_label.set_text('') return unit = self.notebook.get_nth_page(self.notebook.last_page).get_graph_area() \ .get_graph().get_attrs().unit self.event_label.set_text(event.str_short(unit)) job = event.get_job() if job is None: self.job_run_label.set_text('') self.job_rd_label.set_text('') else: self.job_run_label.set_text(job.str_run_short(unit)) self.job_rd_label.set_text(job.str_rd_short(unit)) def request_context_menu(self, widget, gdk_event, selected): button = 0 if hasattr(gdk_event, 'button'): button = gdk_event.button time = gdk_event.time menu = GraphContextMenu(selected, self.info_win) menu.popup(None, None, None, button, time) def request_refresh_events(self, widget, sender, old, new, replace): for i in range(0, self.notebook.get_n_pages()): if self.notebook.get_nth_page(i).get_graph_area() is not sender: self.notebook.get_nth_page(i).get_graph_area().refresh_events(sender, old, new, replace) for i in range(0, self.notebook.get_n_pages()): self.notebook.get_nth_page(i).get_graph_area().renderer.graph if self.notebook.get_nth_page(i).get_graph_area() is sender: self.notebook.get_nth_page(i).get_graph_area().refresh_events(sender, old, new, replace) break def move_to_time_activate(self, widget): unit = self.notebook.get_nth_page(self.notebook.last_page).get_graph_area() \ .get_graph().get_attrs().unit self.text_input_dialog.set_prompt('What time to move to (default ' + str(unit) + ') ?') err = True while err: ret = self.text_input_dialog.run() if ret == gtk.RESPONSE_ACCEPT: err, time = None, None try: time = util.parse_time(self.text_input_dialog.get_input()) start, end = self.notebook.get_nth_page(self.notebook.last_page).get_graph_area() \ .get_schedule().get_time_bounds() if time < start or time > end: err = 'Time out of range!' except ValueError: err = 'Must input a number (with an optional unit)!' if not err: for i in xrange(0, self.notebook.get_n_pages()): garea = self.notebook.get_nth_page(i).get_graph_area() # Center as much as possible pos = garea.get_graph().get_time_xpos(time) - garea.get_width() / 2.0 self.notebook.get_nth_page(i).set_hvalue(pos) else: err_dialog = gtk.MessageDialog(self, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, err) err_dialog.set_title('Input Error') err_dialog.run() err_dialog.hide() else: break self.text_input_dialog.hide() def zoom_in_item_activate(self, widget): for i in range(0, self.notebook.get_n_pages()): self.notebook.get_nth_page(i).get_graph_area().zoom_in() def zoom_out_item_activate(self, widget): for i in range(0, self.notebook.get_n_pages()): self.notebook.get_nth_page(i).get_graph_area().zoom_out() def colors_activate(self, widget): garea = self.notebook.get_nth_page(self.notebook.last_page).get_graph_area() graph = garea.get_renderer().get_graph() pattern = graph.get_bar_pattern(self.cur_item_no) if len(pattern.get_color_list()) > 1: print 'striped patterns not yet supported' r, g, b = pattern.get_color_list()[0] colorsel = self.color_dialog.get_color_selection() cur_color = gtk.gdk.Color(int(r * 65535), int(g * 65535), int(b * 65535)) colorsel.set_previous_color(cur_color) colorsel.set_current_color(cur_color) colorsel.set_has_palette(True) if self.color_dialog.run() == gtk.RESPONSE_OK: color = colorsel.get_current_color() r, g, b = color.red / 65535.0, color.green / 65535.0, color.blue / 65535.0 graph.set_bar_pattern(self.cur_item_no, Pattern([(r, g, b)])) garea.refresh_all() self.color_dialog.hide() def save_item_activate(self, widget): ACCEPTED_TYPES = ('png', 'svg') dialog = gtk.FileChooserDialog('Save to Graphics File', self, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT)) png_filter = gtk.FileFilter() png_filter.add_mime_type('image/png') png_filter.set_name('PNG image (*.png)') dialog.add_filter(png_filter) svg_filter = gtk.FileFilter() svg_filter.add_mime_type('image/svg+xml') svg_filter.set_name('SVG vector image (*.svg)') dialog.add_filter(svg_filter) filename = None ftype = None while True: if dialog.run() == gtk.RESPONSE_ACCEPT: filename = dialog.get_filename() # parse filename for extension parts = filename.split('.') ftype = None if len(parts) == 1: filename += '.' + parts[0] if dialog.get_filter() is png_filter: ftype = 'png' elif dialog.get_filter() is svg_filter: ftype = 'svg' break else: ftype = parts[-1] if ftype in ACCEPTED_TYPES: break else: err_dialog = gtk.MessageDialog(self, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, 'File type "' + ftype + '" not recognized') err_dialog.set_title('File Type Error') err_dialog.run() err_dialog.destroy() else: filename = None ftype = None break if filename is not None: cursor = gtk.gdk.Cursor(gtk.gdk.WATCH) dialog.get_window().set_cursor(cursor) def idle_callback(dialog): self.notebook.get_nth_page(self.notebook.last_page).get_graph_area().output_to_file(filename, ftype) dialog.get_window().set_cursor(None) dialog.destroy() gobject.idle_add(idle_callback, dialog) # needed to get the cursor to display else: dialog.destroy() def quit_item_activate(self, widget): self.destroy() def delete_event(self, widget, event, data=None): return False def die(self, widget, data=None): gtk.main_quit()