diff options
Diffstat (limited to 'grc/src/platforms/gui/FlowGraph.py')
-rw-r--r-- | grc/src/platforms/gui/FlowGraph.py | 563 |
1 files changed, 563 insertions, 0 deletions
diff --git a/grc/src/platforms/gui/FlowGraph.py b/grc/src/platforms/gui/FlowGraph.py new file mode 100644 index 000000000..1e654e1bf --- /dev/null +++ b/grc/src/platforms/gui/FlowGraph.py @@ -0,0 +1,563 @@ +""" +Copyright 2007 Free Software Foundation, Inc. +This file is part of GNU Radio + +GNU Radio Companion is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +GNU Radio Companion is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +""" + +from ... gui import Preferences +from ... gui.Constants import \ + DIR_LEFT, DIR_RIGHT, \ + SCROLL_PROXIMITY_SENSITIVITY, SCROLL_DISTANCE, \ + MOTION_DETECT_REDRAWING_SENSITIVITY +from ... gui.Actions import \ + ELEMENT_CREATE, ELEMENT_SELECT, \ + BLOCK_PARAM_MODIFY, BLOCK_MOVE +import Colors +import Utils +from ... gui.ParamsDialog import ParamsDialog +from Element import Element +from .. base import FlowGraph as _FlowGraph +import pygtk +pygtk.require('2.0') +import gtk +import random +import time +from ... gui 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, *args, **kwargs): + """ + 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.time = 0 + self.press_coor = (0, 0) + #selected ports + self._old_selected_port = None + self._new_selected_port = None + + def _get_unique_id(self, base_id=''): + """ + Get a unique id starting with the base id. + @param base_id the id starts with this and appends a count + @return a unique id + """ + index = -1 + while True: + id = (index < 0) and base_id or '%s%d'%(base_id, index) + index = index + 1 + #make sure that the id is not used by another block + if not filter(lambda b: b.get_id() == id, self.get_blocks()): return id + +########################################################################### +# Access Drawing Area +########################################################################### + def get_drawing_area(self): return self.drawing_area + def get_gc(self): return self.get_drawing_area().gc + def get_pixmap(self): return self.get_drawing_area().pixmap + 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_window(self): return self.get_drawing_area().window + def get_scroll_pane(self): return self.drawing_area.get_parent() + def get_ctrl_mask(self): return self.drawing_area.ctrl_mask + + def add_new_block(self, key): + """ + Add a block of the given key to this flow graph. + @param key the block key + """ + 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() + x = int(random.uniform(.25, .75)*h_adj.page_size + h_adj.get_value()) + y = 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((x, y)) + block.set_rotation(0) + block.get_param('id').set_value(id) + self.handle_states(ELEMENT_CREATE) + + ########################################################################### + # Copy Paste + ########################################################################### + def copy_to_clipboard(self): + """ + Copy the selected blocks and connections into the clipboard. + @return 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. + @param 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['key'] + if block_key == 'options': continue + block_id = self._get_unique_id(block_key) + block = self.get_new_block(block_key) + selected.add(block) + #set params + params_n = Utils.listify(block_n, 'param') + for param_n in params_n: + param_key = param_n['key'] + param_value = param_n['value'] + #setup id parameter + if param_key == 'id': + old_id2block[param_value] = block + param_value = block_id + #set value to key + block.get_param(param_key).set_value(param_value) + #move block to offset coordinate + block.move((x_off, y_off)) + #create connections + for connection_n in connections_n: + source = old_id2block[connection_n['source_block_id']].get_source(connection_n['source_key']) + sink = old_id2block[connection_n['sink_block_id']].get_sink(connection_n['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. + @param direction +1 or -1 + @return true for change + """ + changed = False + for selected_block in self.get_selected_blocks(): + for child in selected_block.get_params() + selected_block.get_ports(): + #find a param that controls a type + type_param = None + for param in selected_block.get_params(): + if not type_param and param.is_enum(): type_param = param + if param.is_enum() and param.get_key() in child._type: type_param = param + if type_param: + #try to increment the enum by direction + try: + keys = type_param.get_option_keys() + old_index = keys.index(type_param.get_value()) + new_index = (old_index + direction + len(keys))%len(keys) + type_param.set_value(keys[new_index]) + changed = True + except: pass + return changed + + def port_controller_modify_selected(self, direction): + """ + Change port controller for the selected signal blocks. + @param direction +1 or -1 + @return true for changed + """ + changed = False + for selected_block in self.get_selected_blocks(): + for ports in (selected_block.get_sources(), selected_block.get_sinks()): + if ports and hasattr(ports[0], 'get_nports') and ports[0].get_nports(): + #find the param that controls port0 + for param in selected_block.get_params(): + if param.get_key() in ports[0]._nports: + #try to increment the port controller by direction + try: + value = param.evaluate() + value = value + direction + assert 0 < value + param.set_value(value) + changed = True + except: pass + return changed + + def param_modify_selected(self): + """ + Create and show a param modification dialog for the selected block. + @return true if parameters were changed + """ + if self.get_selected_block(): + signal_block_params_dialog = ParamsDialog(self.get_selected_block()) + return signal_block_params_dialog.run() + return False + + def enable_selected(self, enable): + """ + Enable/disable the selected blocks. + @param enable true to enable + @return 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. + @param 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, direction): + """ + Rotate the selected blocks by 90 degrees. + @param direction DIR_LEFT or DIR_RIGHT + @return true if changed, otherwise false. + """ + if not self.get_selected_blocks(): return False + #determine the number of degrees to rotate + rotation = {DIR_LEFT: 90, DIR_RIGHT:270}[direction] + #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 + @return 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): + """ + 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. + """ + if self.get_gc(): + W,H = self.get_size() + #draw the background + self.get_gc().foreground = Colors.BACKGROUND_COLOR + self.get_pixmap().draw_rectangle(self.get_gc(), True, 0, 0, W, H) + #draw grid (depends on prefs) + if Preferences.show_grid(): + grid_size = Preferences.get_grid_size() + points = list() + for i in range(W/grid_size): + for j in range(H/grid_size): + points.append((i*grid_size, j*grid_size)) + self.get_gc().foreground = Colors.TXT_COLOR + self.get_pixmap().draw_points(self.get_gc(), points) + #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 + self.get_gc().foreground = Colors.H_COLOR + self.get_pixmap().draw_rectangle(self.get_gc(), True, x, y, w, h) + self.get_gc().foreground = Colors.TXT_COLOR + self.get_pixmap().draw_rectangle(self.get_gc(), False, x, y, w, h) + #draw blocks on top of connections + for element in self.get_connections() + self.get_blocks(): + element.draw(self.get_pixmap()) + #draw selected blocks on top of selected connections + for selected_element in self.get_selected_connections() + self.get_selected_blocks(): + selected_element.draw(self.get_pixmap()) + self.get_drawing_area().draw() + + def update(self): + """ + Update highlighting so only the selected is highlighted. + Call update on all elements. + Resize the window if size changed. + """ + #update highlighting + map(lambda e: e.set_highlighted(False), self.get_elements()) + for selected_element in self.get_selected_elements(): + selected_element.set_highlighted(True) + #update all elements + map(lambda e: e.update(), self.get_elements()) + #set the size of the flow graph area + old_x, old_y = self.get_size() + try: new_x, new_y = self.get_option('window_size') + except: new_x, new_y = old_x, old_y + if new_x != old_x or new_y != old_y: self.set_size(new_x, new_y) + + ########################################################################## + ## 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 backwardssince 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. + @param coor the coordinate of the mouse click + @param coor_m the coordinate for multi select + @return 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) + #single select mode, break + if not coor_m: + self.get_elements().remove(element) + self.get_elements().append(element) + 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. + @return 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. + @return 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. + @return 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. + @return sub set of elements in this flow graph + """ + return self._selected_elements + + def get_selected_element(self): + """ + Get the selected element. + @return 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_selection = 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_selection and new_selection[0] in self.get_selected_elements() + ): selected_elements = new_selection + 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) + self.handle_states(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) + ) + self.handle_states(ELEMENT_SELECT) + + ########################################################################## + ## Event Handlers + ########################################################################## + def handle_mouse_button_press(self, left_click, double_click, coordinate): + """ + A mouse button is pressed, only respond to left clicks. + 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. + """ + if not left_click: return + self.press_coor = coordinate + self.set_coordinate(coordinate) + self.time = 0 + self.mouse_pressed = True + 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 + self.handle_states(BLOCK_PARAM_MODIFY) + + def handle_mouse_button_release(self, left_click, coordinate): + """ + A mouse button is released, record the state. + """ + if not left_click: return + self.set_coordinate(coordinate) + self.time = 0 + self.mouse_pressed = False + if self.element_moved: + if Preferences.snap_to_grid(): + grid_size = Preferences.get_grid_size() + X,Y = self.get_selected_element().get_coordinate() + deltaX = X%grid_size + if deltaX < grid_size/2: deltaX = -1 * deltaX + else: deltaX = grid_size - deltaX + deltaY = Y%grid_size + if deltaY < grid_size/2: deltaY = -1 * deltaY + else: deltaY = grid_size - deltaY + self.move_selected((deltaX, deltaY)) + self.handle_states(BLOCK_MOVE) + self.element_moved = False + self.update_selected_elements() + self.draw() + + 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, timediff large enough + if not self.mouse_pressed: return + if time.time() - self.time < MOTION_DETECT_REDRAWING_SENSITIVITY: 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') + #move the selected element 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.draw() + self.set_coordinate((x, y)) + #update time + self.time = time.time() |