#!/usr/bin/python """GUI stuff.""" from schedule import * from renderer import * import pygtk import gtk import gobject import copy class GraphContextMenu(gtk.Menu): MAX_STR_LEN = 80 def __init__(self, selected): super(GraphContextMenu, self).__init__() for event in selected: string = str(event) if len(string) > GraphContextMenu.MAX_STR_LEN - 3: string = string[:GraphContextMenu.MAX_STR_LEN - 3] + '...' item = gtk.MenuItem(string) self.append(item) item.show() 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 = 20.0 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.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) self.band_rect = None self.ctrl_clicked = False self.dirtied_regions = [] 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) def expose(self, widget, expose_event, data=None): ctx = widget.window.cairo_create() graph = self.renderer.get_graph() graph.update_view(self.cur_x, self.cur_y, self.width, self.height, ctx) # We ourselves didn't update dirtied_regions, so this means that X or the # window manager must have caused the expose event. So just update the # expose_event's bounding area. if not self.dirtied_regions or expose_event.send_event: self.dirtied_regions = [(expose_event.area.x, expose_event.area.y, expose_event.area.width, expose_event.area.height)] graph.render_surface(self.renderer.get_schedule(), self.dirtied_regions) # render dragging band rectangle, if there is one if self.band_rect is not None: x, y, width, height = self.band_rect 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 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) 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._dirty(0, 0, self.width, self.height) 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._dirty(0, 0, self.width, self.height) def size_allocate(self, widget, allocation): self.width = allocation.width self.height = allocation.height self.config_scrollbars(self.cur_x, self.cur_y) def config_scrollbars(self, hvalue, vvalue): graph = self.renderer.get_graph() width = graph.get_width() height = graph.get_height() if self.horizontal is not None: self.horizontal.set_all(hvalue, 0.0, width, graph.get_attrs().maj_sep * GraphArea.HORIZ_STEP_SCROLL_FACTOR, graph.get_attrs().maj_sep * GraphArea.HORIZ_PAGE_SCROLL_FACTOR, self.width) if self.vertical is not None: self.vertical.set_all(vvalue, 0.0, height, graph.get_attrs().y_item_size * GraphArea.VERT_STEP_SCROLL_FACTOR, graph.get_attrs().y_item_size * GraphArea.VERT_PAGE_SCROLL_FACTOR, self.height) def _find_max_layer(self, regions): max_layer = Canvas.BOTTOM_LAYER for event in regions: if event.get_layer() > max_layer: max_layer = event.get_layer() return max_layer def _update_event_changes(self, list1, list2): # if an event changed selected status, update the bounding area for event in list1: if event not in list2: x, y, width, height = list1[event].get_dimensions() self._dirty_inflate(x - self.cur_x, y - self.cur_y, width, height, GraphFormat.BORDER_THICKNESS) for event in list2: if event not in list1: x, y, width, height = list2[event].get_dimensions() self._dirty_inflate(x - self.cur_x, y - self.cur_y, width, height, GraphFormat.BORDER_THICKNESS) def motion_notify(self, widget, motion_event, data=None): msg = None graph = self.renderer.get_graph() graph.render_surface(self.renderer.get_schedule(), [(motion_event.x, motion_event.y, 0, 0)], True) just_selected = graph.get_selected_regions(motion_event.x, motion_event.y, 0, 0) if not just_selected: msg = '' the_event = None else: max_layer = self._find_max_layer(just_selected) for event in just_selected: if event.get_layer() == max_layer: the_event = event break msg = str(the_event) self.emit('update-event-description', the_event, msg) if self.band_rect is not None: selected = {} was_selected = self.renderer.get_schedule().get_selected() if self.ctrl_clicked: selected = copy.copy(was_selected) # dragging a rectangle x = self.band_rect[0] y = self.band_rect[1] width = motion_event.x - self.band_rect[0] height = motion_event.y - self.band_rect[1] x_p, y_p, width_p, height_p = self._positivify(x, y, width, height) graph.render_surface(self.renderer.get_schedule(), [(x_p, y_p, width_p, height_p)], True) selected.update(graph.get_selected_regions(x_p, y_p, width_p, height_p)) self.renderer.get_schedule().set_selected(selected) old_x, old_y, old_width, old_height = self.band_rect self.band_rect = (x, y, width, height) self._dirty_rect_border(old_x, old_y, old_width, old_height, GraphFormat.BAND_THICKNESS) self._dirty_rect_border(x, y, width, height, GraphFormat.BAND_THICKNESS) self._update_event_changes(was_selected, selected) 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(), \ [(button_event.x, button_event.y, 0, 0)], True) just_selected = graph.get_selected_regions(button_event.x, button_event.y, 0, 0) max_layer = self._find_max_layer(just_selected) new_now_selected = None was_selected = self.renderer.get_schedule().get_selected() if self.ctrl_clicked: new_now_selected = copy.copy(was_selected) else: new_now_selected = {} # 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 for event in just_selected: if event.get_layer() == max_layer: val = True now_selected = self.renderer.get_schedule().get_selected() if (len(now_selected) == 1 or self.ctrl_clicked) and event in now_selected: val = not event.is_selected if val: new_now_selected[event] = graph.get_sel_region(event) elif event in new_now_selected: del new_now_selected[event] break # only pick one event when just clicking self.renderer.get_schedule().set_selected(new_now_selected) #self.last_selected = new_now_selected self._update_event_changes(new_now_selected, was_selected) if self.band_rect is None: self.band_rect = (button_event.x, button_event.y, 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 #self.last_selected = copy.copy(self.renderer.get_schedule().get_selected()) if button_event.button == 1: self._release_band() def _release_band(self): if self.band_rect is not None: x, y, width, height = self.band_rect self._dirty_rect_border(x, y, width, height, GraphFormat.BAND_THICKNESS) 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 class GraphWindow(gtk.ScrolledWindow): def __init__(self, renderer): super(GraphWindow, self).__init__(None, None) self.add_events(gtk.gdk.KEY_PRESS_MASK) self.ctr = 0 self.connect('key-press-event', self.key_press) 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 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() ctrl_clicked = key_event.state & gtk.gdk.CONTROL_MASK 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)} 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] if ctrl_clicked: keystr = 'ctrl-' + keystr if keystr is not None: adj, inc, lim, val, extr = adj_tuple[keystr] adj.set_value(extr(val + inc, lim)) 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.connect('delete_event', self.delete_event) self.connect('destroy', self.die) self.file_menu = gtk.Menu() self.quit_item = gtk.MenuItem('_Quit', True) self.quit_item.connect('activate', self.quit_item_activate) self.quit_item.show() self.file_menu.append(self.quit_item) self.file_item = gtk.MenuItem('_File', True) self.file_item.set_submenu(self.file_menu) self.file_item.show() self.menu_bar = gtk.MenuBar() self.menu_bar.append(self.file_item) self.menu_bar.show() self.vbox = gtk.VBox(False, 0) self.notebook = gtk.Notebook() self.notebook.last_page = -1 self.notebook.connect('switch-page', self.switch_page) self.notebook.show() self.desc_label = gtk.Label('') self.desc_label.set_justify(gtk.JUSTIFY_LEFT) self.desc_label.show() self.vbox.pack_start(self.menu_bar, False, False, 0) self.vbox.pack_start(self.notebook, True, True, 0) self.vbox.pack_start(self.desc_label, False, False, 0) self.vbox.show() self.add(self.vbox) self.set_size_request(MainWindow.WINDOW_WIDTH_REQ, MainWindow.WINDOW_HEIGHT_REQ) self.show() def connect_widgets(self, garea): garea.connect('update-event-description', self.update_event_description) garea.connect('request-context-menu', self.request_context_menu) 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.get_graph_area()) gwindow.show() self.notebook.append_page(gwindow, gtk.Label(title)) 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) self.notebook.last_page = page_num def update_event_description(self, widget, event, msg): self.desc_label.set_text(msg) 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) menu.popup(None, None, None, button, time) 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()