diff options
Diffstat (limited to 'grc/gui/FlowGraph.py')
-rw-r--r-- | grc/gui/FlowGraph.py | 1079 |
1 files changed, 534 insertions, 545 deletions
diff --git a/grc/gui/FlowGraph.py b/grc/gui/FlowGraph.py index a238ed166a..4dff675afb 100644 --- a/grc/gui/FlowGraph.py +++ b/grc/gui/FlowGraph.py @@ -29,548 +29,537 @@ import random import Messages class FlowGraph(Element): - """ - FlowGraph is the data structure to store graphical signal blocks, - graphical inputs and outputs, - and the connections between inputs and outputs. - """ - - def __init__(self): - """ - FlowGraph contructor. - Create a list for signal blocks and connections. Connect mouse handlers. - """ - Element.__init__(self) - #when is the flow graph selected? (used by keyboard event handler) - self.is_selected = lambda: bool(self.get_selected_elements()) - #important vars dealing with mouse event tracking - self.element_moved = False - self.mouse_pressed = False - self.unselect() - self.press_coor = (0, 0) - #selected ports - self._old_selected_port = None - self._new_selected_port = None - #context menu - self._context_menu = gtk.Menu() - for action in [ - Actions.BLOCK_CUT, - Actions.BLOCK_COPY, - Actions.BLOCK_PASTE, - Actions.ELEMENT_DELETE, - Actions.BLOCK_ROTATE_CCW, - Actions.BLOCK_ROTATE_CW, - Actions.BLOCK_ENABLE, - Actions.BLOCK_DISABLE, - Actions.BLOCK_PARAM_MODIFY, - Actions.BLOCK_CREATE_HIER, - Actions.OPEN_HIER, - Actions.BUSSIFY_SOURCES, - Actions.BUSSIFY_SINKS, - ]: self._context_menu.append(action.create_menu_item()) - - ########################################################################### - # Access Drawing Area - ########################################################################### - def get_drawing_area(self): return self.drawing_area - def queue_draw(self): self.get_drawing_area().queue_draw() - def get_size(self): return self.get_drawing_area().get_size_request() - def set_size(self, *args): self.get_drawing_area().set_size_request(*args) - def get_scroll_pane(self): return self.drawing_area.get_parent() - def get_ctrl_mask(self): return self.drawing_area.ctrl_mask - def new_pixmap(self, *args): return self.get_drawing_area().new_pixmap(*args) - - def add_new_block(self, key, coor=None): - """ - Add a block of the given key to this flow graph. - - Args: - key: the block key - coor: an optional coordinate or None for random - """ - id = self._get_unique_id(key) - #calculate the position coordinate - h_adj = self.get_scroll_pane().get_hadjustment() - v_adj = self.get_scroll_pane().get_vadjustment() - if coor is None: coor = ( - int(random.uniform(.25, .75)*h_adj.page_size + h_adj.get_value()), - int(random.uniform(.25, .75)*v_adj.page_size + v_adj.get_value()), - ) - #get the new block - block = self.get_new_block(key) - block.set_coordinate(coor) - block.set_rotation(0) - block.get_param('id').set_value(id) - Actions.ELEMENT_CREATE() - - return id - - ########################################################################### - # Copy Paste - ########################################################################### - def copy_to_clipboard(self): - """ - Copy the selected blocks and connections into the clipboard. - - Returns: - the clipboard - """ - #get selected blocks - blocks = self.get_selected_blocks() - if not blocks: return None - #calc x and y min - x_min, y_min = blocks[0].get_coordinate() - for block in blocks: - x, y = block.get_coordinate() - x_min = min(x, x_min) - y_min = min(y, y_min) - #get connections between selected blocks - connections = filter( - lambda c: c.get_source().get_parent() in blocks and c.get_sink().get_parent() in blocks, - self.get_connections(), - ) - clipboard = ( - (x_min, y_min), - [block.export_data() for block in blocks], - [connection.export_data() for connection in connections], - ) - return clipboard - - def paste_from_clipboard(self, clipboard): - """ - Paste the blocks and connections from the clipboard. - - Args: - clipboard: the nested data of blocks, connections - """ - - selected = set() - (x_min, y_min), blocks_n, connections_n = clipboard - old_id2block = dict() - #recalc the position - h_adj = self.get_scroll_pane().get_hadjustment() - v_adj = self.get_scroll_pane().get_vadjustment() - x_off = h_adj.get_value() - x_min + h_adj.page_size/4 - y_off = v_adj.get_value() - y_min + v_adj.page_size/4 - #create blocks - for block_n in blocks_n: - block_key = block_n.find('key') - if block_key == 'options': continue - block = self.get_new_block(block_key) - selected.add(block) - #set params - params_n = block_n.findall('param') - for param_n in params_n: - - param_key = param_n.find('key') - param_value = param_n.find('value') - #setup id parameter - if param_key == 'id': - old_id2block[param_value] = block - #if the block id is not unique, get a new block id - if param_value in [bluck.get_id() for bluck in self.get_blocks()]: - param_value = self._get_unique_id(param_value) - - - #set value to key - - block.get_param(param_key).set_value(param_value) - #move block to offset coordinate - block.move((x_off, y_off)) - - #update before creating connections - self.update() - #create connections - for connection_n in connections_n: - source = old_id2block[connection_n.find('source_block_id')].get_source(connection_n.find('source_key')) - sink = old_id2block[connection_n.find('sink_block_id')].get_sink(connection_n.find('sink_key')) - self.connect(source, sink) - #set all pasted elements selected - for block in selected: selected = selected.union(set(block.get_connections())) - self._selected_elements = list(selected) - - ########################################################################### - # Modify Selected - ########################################################################### - def type_controller_modify_selected(self, direction): - """ - Change the registered type controller for the selected signal blocks. - - Args: - direction: +1 or -1 - - Returns: - true for change - """ - return any([sb.type_controller_modify(direction) for sb in self.get_selected_blocks()]) - - def port_controller_modify_selected(self, direction): - """ - Change port controller for the selected signal blocks. - - Args: - direction: +1 or -1 - - Returns: - true for changed - """ - return any([sb.port_controller_modify(direction) for sb in self.get_selected_blocks()]) - - def enable_selected(self, enable): - """ - Enable/disable the selected blocks. - - Args: - enable: true to enable - - Returns: - true if changed - """ - changed = False - for selected_block in self.get_selected_blocks(): - if selected_block.get_enabled() != enable: - selected_block.set_enabled(enable) - changed = True - return changed - - def move_selected(self, delta_coordinate): - """ - Move the element and by the change in coordinates. - - Args: - delta_coordinate: the change in coordinates - """ - for selected_block in self.get_selected_blocks(): - selected_block.move(delta_coordinate) - self.element_moved = True - - def rotate_selected(self, rotation): - """ - Rotate the selected blocks by multiples of 90 degrees. - - Args: - rotation: the rotation in degrees - - Returns: - true if changed, otherwise false. - """ - if not self.get_selected_blocks(): return False - #initialize min and max coordinates - min_x, min_y = self.get_selected_block().get_coordinate() - max_x, max_y = self.get_selected_block().get_coordinate() - #rotate each selected block, and find min/max coordinate - for selected_block in self.get_selected_blocks(): - selected_block.rotate(rotation) - #update the min/max coordinate - x, y = selected_block.get_coordinate() - min_x, min_y = min(min_x, x), min(min_y, y) - max_x, max_y = max(max_x, x), max(max_y, y) - #calculate center point of slected blocks - ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2 - #rotate the blocks around the center point - for selected_block in self.get_selected_blocks(): - x, y = selected_block.get_coordinate() - x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation) - selected_block.set_coordinate((x + ctr_x, y + ctr_y)) - return True - - def remove_selected(self): - """ - Remove selected elements - - Returns: - true if changed. - """ - changed = False - for selected_element in self.get_selected_elements(): - self.remove_element(selected_element) - changed = True - return changed - - def draw(self, gc, window): - """ - Draw the background and grid if enabled. - Draw all of the elements in this flow graph onto the pixmap. - Draw the pixmap to the drawable window of this flow graph. - """ - W,H = self.get_size() - #draw the background - gc.set_foreground(Colors.FLOWGRAPH_BACKGROUND_COLOR) - window.draw_rectangle(gc, True, 0, 0, W, H) - #draw multi select rectangle - if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()): - #coordinates - x1, y1 = self.press_coor - x2, y2 = self.get_coordinate() - #calculate top-left coordinate and width/height - x, y = int(min(x1, x2)), int(min(y1, y2)) - w, h = int(abs(x1 - x2)), int(abs(y1 - y2)) - #draw - gc.set_foreground(Colors.HIGHLIGHT_COLOR) - window.draw_rectangle(gc, True, x, y, w, h) - gc.set_foreground(Colors.BORDER_COLOR) - window.draw_rectangle(gc, False, x, y, w, h) - #draw blocks on top of connections - for element in self.get_connections() + self.get_blocks(): - element.draw(gc, window) - #draw selected blocks on top of selected connections - for selected_element in self.get_selected_connections() + self.get_selected_blocks(): - selected_element.draw(gc, window) - - def update_selected(self): - """ - Remove deleted elements from the selected elements list. - Update highlighting so only the selected are highlighted. - """ - selected_elements = self.get_selected_elements() - elements = self.get_elements() - #remove deleted elements - for selected in selected_elements: - if selected in elements: continue - selected_elements.remove(selected) - if self._old_selected_port and self._old_selected_port.get_parent() not in elements: - self._old_selected_port = None - if self._new_selected_port and self._new_selected_port.get_parent() not in elements: - self._new_selected_port = None - #update highlighting - for element in elements: - element.set_highlighted(element in selected_elements) - - def update(self): - """ - Call the top level rewrite and validate. - Call the top level create labels and shapes. - """ - - self.rewrite() - - self.validate() - - self.create_labels() - - self.create_shapes() - - ########################################################################## - ## Get Selected - ########################################################################## - def unselect(self): - """ - Set selected elements to an empty set. - """ - self._selected_elements = [] - - def what_is_selected(self, coor, coor_m=None): - """ - What is selected? - At the given coordinate, return the elements found to be selected. - If coor_m is unspecified, return a list of only the first element found to be selected: - Iterate though the elements backwards since top elements are at the end of the list. - If an element is selected, place it at the end of the list so that is is drawn last, - and hence on top. Update the selected port information. - - Args: - coor: the coordinate of the mouse click - coor_m: the coordinate for multi select - - Returns: - the selected blocks and connections or an empty list - """ - selected_port = None - selected = set() - #check the elements - for element in reversed(self.get_elements()): - selected_element = element.what_is_selected(coor, coor_m) - if not selected_element: continue - #update the selected port information - if selected_element.is_port(): - if not coor_m: selected_port = selected_element - selected_element = selected_element.get_parent() - selected.add(selected_element) - #place at the end of the list - self.get_elements().remove(element) - self.get_elements().append(element) - #single select mode, break - if not coor_m: break - #update selected ports - self._old_selected_port = self._new_selected_port - self._new_selected_port = selected_port - return list(selected) - - def get_selected_connections(self): - """ - Get a group of selected connections. - - Returns: - sub set of connections in this flow graph - """ - selected = set() - for selected_element in self.get_selected_elements(): - if selected_element.is_connection(): selected.add(selected_element) - return list(selected) - - def get_selected_blocks(self): - """ - Get a group of selected blocks. - - Returns: - sub set of blocks in this flow graph - """ - selected = set() - for selected_element in self.get_selected_elements(): - if selected_element.is_block(): selected.add(selected_element) - return list(selected) - - def get_selected_block(self): - """ - Get the selected block when a block or port is selected. - - Returns: - a block or None - """ - return self.get_selected_blocks() and self.get_selected_blocks()[0] or None - - def get_selected_elements(self): - """ - Get the group of selected elements. - - Returns: - sub set of elements in this flow graph - """ - return self._selected_elements - - def get_selected_element(self): - """ - Get the selected element. - - Returns: - a block, port, or connection or None - """ - return self.get_selected_elements() and self.get_selected_elements()[0] or None - - def update_selected_elements(self): - """ - Update the selected elements. - The update behavior depends on the state of the mouse button. - When the mouse button pressed the selection will change when - the control mask is set or the new selection is not in the current group. - When the mouse button is released the selection will change when - the mouse has moved and the control mask is set or the current group is empty. - Attempt to make a new connection if the old and ports are filled. - If the control mask is set, merge with the current elements. - """ - selected_elements = None - if self.mouse_pressed: - new_selections = self.what_is_selected(self.get_coordinate()) - #update the selections if the new selection is not in the current selections - #allows us to move entire selected groups of elements - if self.get_ctrl_mask() or not ( - new_selections and new_selections[0] in self.get_selected_elements() - ): selected_elements = new_selections - else: #called from a mouse release - if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()): - selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor) - #this selection and the last were ports, try to connect them - if self._old_selected_port and self._new_selected_port and \ - self._old_selected_port is not self._new_selected_port: - try: - self.connect(self._old_selected_port, self._new_selected_port) - Actions.ELEMENT_CREATE() - except: Messages.send_fail_connection() - self._old_selected_port = None - self._new_selected_port = None - return - #update selected elements - if selected_elements is None: return - old_elements = set(self.get_selected_elements()) - self._selected_elements = list(set(selected_elements)) - new_elements = set(self.get_selected_elements()) - #if ctrl, set the selected elements to the union - intersection of old and new - if self.get_ctrl_mask(): - self._selected_elements = list( - set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements) - ) - Actions.ELEMENT_SELECT() - - ########################################################################## - ## Event Handlers - ########################################################################## - def handle_mouse_context_press(self, coordinate, event): - """ - The context mouse button was pressed: - If no elements were selected, perform re-selection at this coordinate. - Then, show the context menu at the mouse click location. - """ - selections = self.what_is_selected(coordinate) - if not set(selections).intersection(self.get_selected_elements()): - self.set_coordinate(coordinate) - self.mouse_pressed = True - self.update_selected_elements() - self.mouse_pressed = False - self._context_menu.popup(None, None, None, event.button, event.time) - - def handle_mouse_selector_press(self, double_click, coordinate): - """ - The selector mouse button was pressed: - Find the selected element. Attempt a new connection if possible. - Open the block params window on a double click. - Update the selection state of the flow graph. - """ - self.press_coor = coordinate - self.set_coordinate(coordinate) - self.time = 0 - self.mouse_pressed = True - if double_click: self.unselect() - self.update_selected_elements() - #double click detected, bring up params dialog if possible - if double_click and self.get_selected_block(): - self.mouse_pressed = False - Actions.BLOCK_PARAM_MODIFY() - - def handle_mouse_selector_release(self, coordinate): - """ - The selector mouse button was released: - Update the state, handle motion (dragging). - And update the selected flowgraph elements. - """ - self.set_coordinate(coordinate) - self.time = 0 - self.mouse_pressed = False - if self.element_moved: - Actions.BLOCK_MOVE() - self.element_moved = False - self.update_selected_elements() - - def handle_mouse_motion(self, coordinate): - """ - The mouse has moved, respond to mouse dragging. - Move a selected element to the new coordinate. - Auto-scroll the scroll bars at the boundaries. - """ - #to perform a movement, the mouse must be pressed - # (no longer checking pending events via gtk.events_pending() - always true in Windows) - if not self.mouse_pressed: return - #perform autoscrolling - width, height = self.get_size() - x, y = coordinate - h_adj = self.get_scroll_pane().get_hadjustment() - v_adj = self.get_scroll_pane().get_vadjustment() - for pos, length, adj, adj_val, adj_len in ( - (x, width, h_adj, h_adj.get_value(), h_adj.page_size), - (y, height, v_adj, v_adj.get_value(), v_adj.page_size), - ): - #scroll if we moved near the border - if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len: - adj.set_value(adj_val+SCROLL_DISTANCE) - adj.emit('changed') - elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY: - adj.set_value(adj_val-SCROLL_DISTANCE) - adj.emit('changed') - #remove the connection if selected in drag event - if len(self.get_selected_elements()) == 1 and self.get_selected_element().is_connection(): - Actions.ELEMENT_DELETE() - #move the selected elements and record the new coordinate - X, Y = self.get_coordinate() - if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y))) - self.set_coordinate((x, y)) - #queue draw for animation - self.queue_draw() + """ + FlowGraph is the data structure to store graphical signal blocks, + graphical inputs and outputs, + and the connections between inputs and outputs. + """ + + def __init__(self): + """ + FlowGraph contructor. + Create a list for signal blocks and connections. Connect mouse handlers. + """ + Element.__init__(self) + #when is the flow graph selected? (used by keyboard event handler) + self.is_selected = lambda: bool(self.get_selected_elements()) + #important vars dealing with mouse event tracking + self.element_moved = False + self.mouse_pressed = False + self.unselect() + self.press_coor = (0, 0) + #selected ports + self._old_selected_port = None + self._new_selected_port = None + #context menu + self._context_menu = gtk.Menu() + for action in [ + Actions.BLOCK_CUT, + Actions.BLOCK_COPY, + Actions.BLOCK_PASTE, + Actions.ELEMENT_DELETE, + Actions.BLOCK_ROTATE_CCW, + Actions.BLOCK_ROTATE_CW, + Actions.BLOCK_ENABLE, + Actions.BLOCK_DISABLE, + Actions.BLOCK_PARAM_MODIFY, + Actions.BLOCK_CREATE_HIER, + Actions.OPEN_HIER, + Actions.BUSSIFY_SOURCES, + Actions.BUSSIFY_SINKS, + ]: self._context_menu.append(action.create_menu_item()) + + ########################################################################### + # Access Drawing Area + ########################################################################### + def get_drawing_area(self): return self.drawing_area + def queue_draw(self): self.get_drawing_area().queue_draw() + def get_size(self): return self.get_drawing_area().get_size_request() + def set_size(self, *args): self.get_drawing_area().set_size_request(*args) + def get_scroll_pane(self): return self.drawing_area.get_parent() + def get_ctrl_mask(self): return self.drawing_area.ctrl_mask + def new_pixmap(self, *args): return self.get_drawing_area().new_pixmap(*args) + + def add_new_block(self, key, coor=None): + """ + Add a block of the given key to this flow graph. + + Args: + key: the block key + coor: an optional coordinate or None for random + """ + id = self._get_unique_id(key) + #calculate the position coordinate + h_adj = self.get_scroll_pane().get_hadjustment() + v_adj = self.get_scroll_pane().get_vadjustment() + if coor is None: coor = ( + int(random.uniform(.25, .75)*h_adj.page_size + h_adj.get_value()), + int(random.uniform(.25, .75)*v_adj.page_size + v_adj.get_value()), + ) + #get the new block + block = self.get_new_block(key) + block.set_coordinate(coor) + block.set_rotation(0) + block.get_param('id').set_value(id) + Actions.ELEMENT_CREATE() + return id + + ########################################################################### + # Copy Paste + ########################################################################### + def copy_to_clipboard(self): + """ + Copy the selected blocks and connections into the clipboard. + + Returns: + the clipboard + """ + #get selected blocks + blocks = self.get_selected_blocks() + if not blocks: return None + #calc x and y min + x_min, y_min = blocks[0].get_coordinate() + for block in blocks: + x, y = block.get_coordinate() + x_min = min(x, x_min) + y_min = min(y, y_min) + #get connections between selected blocks + connections = filter( + lambda c: c.get_source().get_parent() in blocks and c.get_sink().get_parent() in blocks, + self.get_connections(), + ) + clipboard = ( + (x_min, y_min), + [block.export_data() for block in blocks], + [connection.export_data() for connection in connections], + ) + return clipboard + + def paste_from_clipboard(self, clipboard): + """ + Paste the blocks and connections from the clipboard. + + Args: + clipboard: the nested data of blocks, connections + """ + selected = set() + (x_min, y_min), blocks_n, connections_n = clipboard + old_id2block = dict() + #recalc the position + h_adj = self.get_scroll_pane().get_hadjustment() + v_adj = self.get_scroll_pane().get_vadjustment() + x_off = h_adj.get_value() - x_min + h_adj.page_size/4 + y_off = v_adj.get_value() - y_min + v_adj.page_size/4 + #create blocks + for block_n in blocks_n: + block_key = block_n.find('key') + if block_key == 'options': continue + block = self.get_new_block(block_key) + selected.add(block) + #set params + params_n = block_n.findall('param') + for param_n in params_n: + param_key = param_n.find('key') + param_value = param_n.find('value') + #setup id parameter + if param_key == 'id': + old_id2block[param_value] = block + #if the block id is not unique, get a new block id + if param_value in [bluck.get_id() for bluck in self.get_blocks()]: + param_value = self._get_unique_id(param_value) + #set value to key + block.get_param(param_key).set_value(param_value) + #move block to offset coordinate + block.move((x_off, y_off)) + #update before creating connections + self.update() + #create connections + for connection_n in connections_n: + source = old_id2block[connection_n.find('source_block_id')].get_source(connection_n.find('source_key')) + sink = old_id2block[connection_n.find('sink_block_id')].get_sink(connection_n.find('sink_key')) + self.connect(source, sink) + #set all pasted elements selected + for block in selected: selected = selected.union(set(block.get_connections())) + self._selected_elements = list(selected) + + ########################################################################### + # Modify Selected + ########################################################################### + def type_controller_modify_selected(self, direction): + """ + Change the registered type controller for the selected signal blocks. + + Args: + direction: +1 or -1 + + Returns: + true for change + """ + return any([sb.type_controller_modify(direction) for sb in self.get_selected_blocks()]) + + def port_controller_modify_selected(self, direction): + """ + Change port controller for the selected signal blocks. + + Args: + direction: +1 or -1 + + Returns: + true for changed + """ + return any([sb.port_controller_modify(direction) for sb in self.get_selected_blocks()]) + + def enable_selected(self, enable): + """ + Enable/disable the selected blocks. + + Args: + enable: true to enable + + Returns: + true if changed + """ + changed = False + for selected_block in self.get_selected_blocks(): + if selected_block.get_enabled() != enable: + selected_block.set_enabled(enable) + changed = True + return changed + + def move_selected(self, delta_coordinate): + """ + Move the element and by the change in coordinates. + + Args: + delta_coordinate: the change in coordinates + """ + for selected_block in self.get_selected_blocks(): + selected_block.move(delta_coordinate) + self.element_moved = True + + def rotate_selected(self, rotation): + """ + Rotate the selected blocks by multiples of 90 degrees. + + Args: + rotation: the rotation in degrees + + Returns: + true if changed, otherwise false. + """ + if not self.get_selected_blocks(): return False + #initialize min and max coordinates + min_x, min_y = self.get_selected_block().get_coordinate() + max_x, max_y = self.get_selected_block().get_coordinate() + #rotate each selected block, and find min/max coordinate + for selected_block in self.get_selected_blocks(): + selected_block.rotate(rotation) + #update the min/max coordinate + x, y = selected_block.get_coordinate() + min_x, min_y = min(min_x, x), min(min_y, y) + max_x, max_y = max(max_x, x), max(max_y, y) + #calculate center point of slected blocks + ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2 + #rotate the blocks around the center point + for selected_block in self.get_selected_blocks(): + x, y = selected_block.get_coordinate() + x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation) + selected_block.set_coordinate((x + ctr_x, y + ctr_y)) + return True + + def remove_selected(self): + """ + Remove selected elements + + Returns: + true if changed. + """ + changed = False + for selected_element in self.get_selected_elements(): + self.remove_element(selected_element) + changed = True + return changed + + def draw(self, gc, window): + """ + Draw the background and grid if enabled. + Draw all of the elements in this flow graph onto the pixmap. + Draw the pixmap to the drawable window of this flow graph. + """ + W,H = self.get_size() + #draw the background + gc.set_foreground(Colors.FLOWGRAPH_BACKGROUND_COLOR) + window.draw_rectangle(gc, True, 0, 0, W, H) + #draw multi select rectangle + if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()): + #coordinates + x1, y1 = self.press_coor + x2, y2 = self.get_coordinate() + #calculate top-left coordinate and width/height + x, y = int(min(x1, x2)), int(min(y1, y2)) + w, h = int(abs(x1 - x2)), int(abs(y1 - y2)) + #draw + gc.set_foreground(Colors.HIGHLIGHT_COLOR) + window.draw_rectangle(gc, True, x, y, w, h) + gc.set_foreground(Colors.BORDER_COLOR) + window.draw_rectangle(gc, False, x, y, w, h) + #draw blocks on top of connections + for element in self.get_connections() + self.get_blocks(): + element.draw(gc, window) + #draw selected blocks on top of selected connections + for selected_element in self.get_selected_connections() + self.get_selected_blocks(): + selected_element.draw(gc, window) + + def update_selected(self): + """ + Remove deleted elements from the selected elements list. + Update highlighting so only the selected are highlighted. + """ + selected_elements = self.get_selected_elements() + elements = self.get_elements() + #remove deleted elements + for selected in selected_elements: + if selected in elements: continue + selected_elements.remove(selected) + if self._old_selected_port and self._old_selected_port.get_parent() not in elements: + self._old_selected_port = None + if self._new_selected_port and self._new_selected_port.get_parent() not in elements: + self._new_selected_port = None + #update highlighting + for element in elements: + element.set_highlighted(element in selected_elements) + + def update(self): + """ + Call the top level rewrite and validate. + Call the top level create labels and shapes. + """ + self.rewrite() + self.validate() + self.create_labels() + self.create_shapes() + + ########################################################################## + ## Get Selected + ########################################################################## + def unselect(self): + """ + Set selected elements to an empty set. + """ + self._selected_elements = [] + + def what_is_selected(self, coor, coor_m=None): + """ + What is selected? + At the given coordinate, return the elements found to be selected. + If coor_m is unspecified, return a list of only the first element found to be selected: + Iterate though the elements backwards since top elements are at the end of the list. + If an element is selected, place it at the end of the list so that is is drawn last, + and hence on top. Update the selected port information. + + Args: + coor: the coordinate of the mouse click + coor_m: the coordinate for multi select + + Returns: + the selected blocks and connections or an empty list + """ + selected_port = None + selected = set() + #check the elements + for element in reversed(self.get_elements()): + selected_element = element.what_is_selected(coor, coor_m) + if not selected_element: continue + #update the selected port information + if selected_element.is_port(): + if not coor_m: selected_port = selected_element + selected_element = selected_element.get_parent() + selected.add(selected_element) + #place at the end of the list + self.get_elements().remove(element) + self.get_elements().append(element) + #single select mode, break + if not coor_m: break + #update selected ports + self._old_selected_port = self._new_selected_port + self._new_selected_port = selected_port + return list(selected) + + def get_selected_connections(self): + """ + Get a group of selected connections. + + Returns: + sub set of connections in this flow graph + """ + selected = set() + for selected_element in self.get_selected_elements(): + if selected_element.is_connection(): selected.add(selected_element) + return list(selected) + + def get_selected_blocks(self): + """ + Get a group of selected blocks. + + Returns: + sub set of blocks in this flow graph + """ + selected = set() + for selected_element in self.get_selected_elements(): + if selected_element.is_block(): selected.add(selected_element) + return list(selected) + + def get_selected_block(self): + """ + Get the selected block when a block or port is selected. + + Returns: + a block or None + """ + return self.get_selected_blocks() and self.get_selected_blocks()[0] or None + + def get_selected_elements(self): + """ + Get the group of selected elements. + + Returns: + sub set of elements in this flow graph + """ + return self._selected_elements + + def get_selected_element(self): + """ + Get the selected element. + + Returns: + a block, port, or connection or None + """ + return self.get_selected_elements() and self.get_selected_elements()[0] or None + + def update_selected_elements(self): + """ + Update the selected elements. + The update behavior depends on the state of the mouse button. + When the mouse button pressed the selection will change when + the control mask is set or the new selection is not in the current group. + When the mouse button is released the selection will change when + the mouse has moved and the control mask is set or the current group is empty. + Attempt to make a new connection if the old and ports are filled. + If the control mask is set, merge with the current elements. + """ + selected_elements = None + if self.mouse_pressed: + new_selections = self.what_is_selected(self.get_coordinate()) + #update the selections if the new selection is not in the current selections + #allows us to move entire selected groups of elements + if self.get_ctrl_mask() or not ( + new_selections and new_selections[0] in self.get_selected_elements() + ): selected_elements = new_selections + else: #called from a mouse release + if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()): + selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor) + #this selection and the last were ports, try to connect them + if self._old_selected_port and self._new_selected_port and \ + self._old_selected_port is not self._new_selected_port: + try: + self.connect(self._old_selected_port, self._new_selected_port) + Actions.ELEMENT_CREATE() + except: Messages.send_fail_connection() + self._old_selected_port = None + self._new_selected_port = None + return + #update selected elements + if selected_elements is None: return + old_elements = set(self.get_selected_elements()) + self._selected_elements = list(set(selected_elements)) + new_elements = set(self.get_selected_elements()) + #if ctrl, set the selected elements to the union - intersection of old and new + if self.get_ctrl_mask(): + self._selected_elements = list( + set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements) + ) + Actions.ELEMENT_SELECT() + + ########################################################################## + ## Event Handlers + ########################################################################## + def handle_mouse_context_press(self, coordinate, event): + """ + The context mouse button was pressed: + If no elements were selected, perform re-selection at this coordinate. + Then, show the context menu at the mouse click location. + """ + selections = self.what_is_selected(coordinate) + if not set(selections).intersection(self.get_selected_elements()): + self.set_coordinate(coordinate) + self.mouse_pressed = True + self.update_selected_elements() + self.mouse_pressed = False + self._context_menu.popup(None, None, None, event.button, event.time) + + def handle_mouse_selector_press(self, double_click, coordinate): + """ + The selector mouse button was pressed: + Find the selected element. Attempt a new connection if possible. + Open the block params window on a double click. + Update the selection state of the flow graph. + """ + self.press_coor = coordinate + self.set_coordinate(coordinate) + self.time = 0 + self.mouse_pressed = True + if double_click: self.unselect() + self.update_selected_elements() + #double click detected, bring up params dialog if possible + if double_click and self.get_selected_block(): + self.mouse_pressed = False + Actions.BLOCK_PARAM_MODIFY() + + def handle_mouse_selector_release(self, coordinate): + """ + The selector mouse button was released: + Update the state, handle motion (dragging). + And update the selected flowgraph elements. + """ + self.set_coordinate(coordinate) + self.time = 0 + self.mouse_pressed = False + if self.element_moved: + Actions.BLOCK_MOVE() + self.element_moved = False + self.update_selected_elements() + + def handle_mouse_motion(self, coordinate): + """ + The mouse has moved, respond to mouse dragging. + Move a selected element to the new coordinate. + Auto-scroll the scroll bars at the boundaries. + """ + #to perform a movement, the mouse must be pressed + # (no longer checking pending events via gtk.events_pending() - always true in Windows) + if not self.mouse_pressed: return + #perform autoscrolling + width, height = self.get_size() + x, y = coordinate + h_adj = self.get_scroll_pane().get_hadjustment() + v_adj = self.get_scroll_pane().get_vadjustment() + for pos, length, adj, adj_val, adj_len in ( + (x, width, h_adj, h_adj.get_value(), h_adj.page_size), + (y, height, v_adj, v_adj.get_value(), v_adj.page_size), + ): + #scroll if we moved near the border + if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len: + adj.set_value(adj_val+SCROLL_DISTANCE) + adj.emit('changed') + elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY: + adj.set_value(adj_val-SCROLL_DISTANCE) + adj.emit('changed') + #remove the connection if selected in drag event + if len(self.get_selected_elements()) == 1 and self.get_selected_element().is_connection(): + Actions.ELEMENT_DELETE() + #move the selected elements and record the new coordinate + X, Y = self.get_coordinate() + if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y))) + self.set_coordinate((x, y)) + #queue draw for animation + self.queue_draw() |