2 Copyright 2007 Free Software Foundation, Inc.
3 This file is part of GNU Radio
5 GNU Radio Companion is free software; you can redistribute it and/or
6 modify it under the terms of the GNU General Public License
7 as published by the Free Software Foundation; either version 2
8 of the License, or (at your option) any later version.
10 GNU Radio Companion is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
19 ##@package grc.gui.elements.FlowGraph
20 #A flow graph structure for storing signal blocks and their connections.
22 from grc import Preferences
24 from grc.Constants import *
25 from grc.Actions import *
27 from grc.gui.ParamsDialog import ParamsDialog
28 from Element import Element
29 from grc.elements import FlowGraph as _FlowGraph
37 from grc import Messages
39 class FlowGraph(Element):
41 FlowGraph is the data structure to store graphical signal blocks,
42 graphical inputs and outputs,
43 and the connections between inputs and outputs.
46 def __init__(self, *args, **kwargs):
49 Create a list for signal blocks and connections. Connect mouse handlers.
51 Element.__init__(self)
52 #when is the flow graph selected? (used by keyboard event handler)
53 self.is_selected = lambda: bool(self.get_selected_elements())
54 #important vars dealing with mouse event tracking
55 self.element_moved = False
56 self.mouse_pressed = False
59 self.press_coor = (0, 0)
61 self._old_selected_port = None
62 self._new_selected_port = None
64 def _get_unique_id(self, base_id=''):
66 Get a unique id starting with the base id.
67 @param base_id the id starts with this and appends a count
72 id = (index < 0) and base_id or '%s%d'%(base_id, index)
74 #make sure that the id is not used by another block
75 if not filter(lambda b: b.get_id() == id, self.get_blocks()): return id
77 ###########################################################################
79 ###########################################################################
80 def get_drawing_area(self): return self.drawing_area
81 def get_gc(self): return self.get_drawing_area().gc
82 def get_pixmap(self): return self.get_drawing_area().pixmap
83 def get_size(self): return self.get_drawing_area().get_size_request()
84 def set_size(self, *args): self.get_drawing_area().set_size_request(*args)
85 def get_window(self): return self.get_drawing_area().window
86 def get_scroll_pane(self): return self.drawing_area.get_parent()
87 def get_ctrl_mask(self): return self.drawing_area.ctrl_mask
89 def add_new_block(self, key):
91 Add a block of the given key to this flow graph.
92 @param key the block key
94 id = self._get_unique_id(key)
95 #calculate the position coordinate
96 h_adj = self.get_scroll_pane().get_hadjustment()
97 v_adj = self.get_scroll_pane().get_vadjustment()
98 x = int(random.uniform(.25, .75)*h_adj.page_size + h_adj.get_value())
99 y = int(random.uniform(.25, .75)*v_adj.page_size + v_adj.get_value())
101 block = self.get_new_block(key)
102 block.set_coordinate((x, y))
103 block.set_rotation(0)
104 block.get_param('id').set_value(id)
105 self.handle_states(ELEMENT_CREATE)
107 ###########################################################################
109 ###########################################################################
110 def copy_to_clipboard(self):
112 Copy the selected blocks and connections into the clipboard.
113 @return the clipboard
116 blocks = self.get_selected_blocks()
117 if not blocks: return None
119 x_min, y_min = blocks[0].get_coordinate()
121 x, y = block.get_coordinate()
122 x_min = min(x, x_min)
123 y_min = min(y, y_min)
124 #get connections between selected blocks
125 connections = filter(
126 lambda c: c.get_source().get_parent() in blocks and c.get_sink().get_parent() in blocks,
127 self.get_connections(),
131 [block.export_data() for block in blocks],
132 [connection.export_data() for connection in connections],
136 def paste_from_clipboard(self, clipboard):
138 Paste the blocks and connections from the clipboard.
139 @param clipboard the nested data of blocks, connections
142 (x_min, y_min), blocks_n, connections_n = clipboard
143 old_id2block = dict()
145 h_adj = self.get_scroll_pane().get_hadjustment()
146 v_adj = self.get_scroll_pane().get_vadjustment()
147 x_off = h_adj.get_value() - x_min + h_adj.page_size/4
148 y_off = v_adj.get_value() - y_min + v_adj.page_size/4
150 for block_n in blocks_n:
151 block_key = block_n['key']
152 if block_key == 'options': continue
153 block_id = self._get_unique_id(block_key)
154 block = self.get_new_block(block_key)
157 params_n = Utils.listify(block_n, 'param')
158 for param_n in params_n:
159 param_key = param_n['key']
160 param_value = param_n['value']
162 if param_key == 'id':
163 old_id2block[param_value] = block
164 param_value = block_id
166 block.get_param(param_key).set_value(param_value)
167 #move block to offset coordinate
168 block.move((x_off, y_off))
170 for connection_n in connections_n:
171 source = old_id2block[connection_n['source_block_id']].get_source(connection_n['source_key'])
172 sink = old_id2block[connection_n['sink_block_id']].get_sink(connection_n['sink_key'])
173 self.connect(source, sink)
174 #set all pasted elements selected
175 for block in selected: selected = selected.union(set(block.get_connections()))
176 self._selected_elements = list(selected)
178 ###########################################################################
180 ###########################################################################
181 def type_controller_modify_selected(self, direction):
183 Change the registered type controller for the selected signal blocks.
184 @param direction +1 or -1
185 @return true for change
188 for selected_block in self.get_selected_blocks():
189 for child in selected_block.get_params() + selected_block.get_ports():
190 #find a param that controls a type
192 for param in selected_block.get_params():
193 if not type_param and param.is_enum(): type_param = param
194 if param.is_enum() and param.get_key() in child._type: type_param = param
196 #try to increment the enum by direction
198 keys = type_param.get_option_keys()
199 old_index = keys.index(type_param.get_value())
200 new_index = (old_index + direction + len(keys))%len(keys)
201 type_param.set_value(keys[new_index])
206 def port_controller_modify_selected(self, direction):
208 Change port controller for the selected signal blocks.
209 @param direction +1 or -1
210 @return true for changed
213 for selected_block in self.get_selected_blocks():
214 for ports in (selected_block.get_sources(), selected_block.get_sinks()):
215 if ports and hasattr(ports[0], 'get_nports') and ports[0].get_nports():
216 #find the param that controls port0
217 for param in selected_block.get_params():
218 if param.get_key() in ports[0]._nports:
219 #try to increment the port controller by direction
221 value = param.evaluate()
222 value = value + direction
223 assert(0 < value <= MAX_NUM_PORTS)
224 param.set_value(value)
229 def param_modify_selected(self):
231 Create and show a param modification dialog for the selected block.
232 @return true if parameters were changed
234 if self.get_selected_block():
235 signal_block_params_dialog = ParamsDialog(self.get_selected_block())
236 return signal_block_params_dialog.run()
239 def enable_selected(self, enable):
241 Enable/disable the selected blocks.
242 @param enable true to enable
243 @return true if changed
246 for selected_block in self.get_selected_blocks():
247 if selected_block.get_enabled() != enable:
248 selected_block.set_enabled(enable)
252 def move_selected(self, delta_coordinate):
254 Move the element and by the change in coordinates.
255 @param delta_coordinate the change in coordinates
257 for selected_block in self.get_selected_blocks():
258 selected_block.move(delta_coordinate)
259 self.element_moved = True
261 def rotate_selected(self, direction):
263 Rotate the selected blocks by 90 degrees.
264 @param direction DIR_LEFT or DIR_RIGHT
265 @return true if changed, otherwise false.
267 if not self.get_selected_blocks(): return False
268 #determine the number of degrees to rotate
269 direction = {DIR_LEFT: 90, DIR_RIGHT:270}[direction]
270 cos_r = {0: 1, 90: 0, 180: -1, 270: 0}[direction]
271 sin_r = {0: 0, 90: 1, 180: 0, 270: -1}[direction]
272 #initialize min and max coordinates
273 min_x, min_y = self.get_selected_block().get_coordinate()
274 max_x, max_y = self.get_selected_block().get_coordinate()
275 #rotate each selected block, and find min/max coordinate
276 for selected_block in self.get_selected_blocks():
277 selected_block.rotate(direction)
278 #update the min/max coordinate
279 x, y = selected_block.get_coordinate()
280 min_x, min_y = min(min_x, x), min(min_y, y)
281 max_x, max_y = max(max_x, x), max(max_y, y)
282 #calculate center point of slected blocks
283 ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
284 #rotate the blocks around the center point
285 for selected_block in self.get_selected_blocks():
286 x, y = selected_block.get_coordinate()
287 x, y = x - ctr_x, y - ctr_y
288 x, y = (x*cos_r + y*sin_r + ctr_x, -x*sin_r + y*cos_r + ctr_y)
289 selected_block.set_coordinate((x, y))
292 def remove_selected(self):
294 Remove selected elements
295 @return true if changed.
298 for selected_element in self.get_selected_elements():
299 self.remove_element(selected_element)
305 Draw the background and grid if enabled.
306 Draw all of the elements in this flow graph onto the pixmap.
307 Draw the pixmap to the drawable window of this flow graph.
310 W,H = self.get_size()
312 self.get_gc().foreground = Colors.BACKGROUND_COLOR
313 self.get_pixmap().draw_rectangle(self.get_gc(), True, 0, 0, W, H)
314 #draw grid (depends on prefs)
315 if Preferences.show_grid():
316 grid_size = Preferences.get_grid_size()
318 for i in range(W/grid_size):
319 for j in range(H/grid_size):
320 points.append((i*grid_size, j*grid_size))
321 self.get_gc().foreground = Colors.TXT_COLOR
322 self.get_pixmap().draw_points(self.get_gc(), points)
323 #draw multi select rectangle
324 if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()):
326 x1, y1 = self.press_coor
327 x2, y2 = self.get_coordinate()
328 #calculate top-left coordinate and width/height
329 x, y = int(min(x1, x2)), int(min(y1, y2))
330 w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
332 self.get_gc().foreground = Colors.H_COLOR
333 self.get_pixmap().draw_rectangle(self.get_gc(), True, x, y, w, h)
334 self.get_gc().foreground = Colors.TXT_COLOR
335 self.get_pixmap().draw_rectangle(self.get_gc(), False, x, y, w, h)
336 #draw blocks on top of connections
337 for element in self.get_connections() + self.get_blocks():
338 element.draw(self.get_pixmap())
339 #draw selected blocks on top of selected connections
340 for selected_element in self.get_selected_connections() + self.get_selected_blocks():
341 selected_element.draw(self.get_pixmap())
342 self.get_drawing_area().draw()
346 Update highlighting so only the selected is highlighted.
347 Call update on all elements.
348 Resize the window if size changed.
351 map(lambda e: e.set_highlighted(False), self.get_elements())
352 for selected_element in self.get_selected_elements():
353 selected_element.set_highlighted(True)
355 map(lambda e: e.update(), self.get_elements())
356 #set the size of the flow graph area
357 old_x, old_y = self.get_size()
358 try: new_x, new_y = self.get_option('window_size')
359 except: new_x, new_y = old_x, old_y
360 if new_x != old_x or new_y != old_y: self.set_size(new_x, new_y)
362 ##########################################################################
364 ##########################################################################
367 Set selected elements to an empty set.
369 self._selected_elements = []
371 def what_is_selected(self, coor, coor_m=None):
374 At the given coordinate, return the elements found to be selected.
375 If coor_m is unspecified, return a list of only the first element found to be selected:
376 Iterate though the elements backwardssince top elements are at the end of the list.
377 If an element is selected, place it at the end of the list so that is is drawn last,
378 and hence on top. Update the selected port information.
379 @param coor the coordinate of the mouse click
380 @param coor_m the coordinate for multi select
381 @return the selected blocks and connections or an empty list
386 for element in reversed(self.get_elements()):
387 selected_element = element.what_is_selected(coor, coor_m)
388 if not selected_element: continue
389 #update the selected port information
390 if selected_element.is_port():
391 if not coor_m: selected_port = selected_element
392 selected_element = selected_element.get_parent()
393 selected.add(selected_element)
394 #single select mode, break
396 self.get_elements().remove(element)
397 self.get_elements().append(element)
399 #update selected ports
400 self._old_selected_port = self._new_selected_port
401 self._new_selected_port = selected_port
402 return list(selected)
404 def get_selected_connections(self):
406 Get a group of selected connections.
407 @return sub set of connections in this flow graph
410 for selected_element in self.get_selected_elements():
411 if selected_element.is_connection(): selected.add(selected_element)
412 return list(selected)
414 def get_selected_blocks(self):
416 Get a group of selected blocks.
417 @return sub set of blocks in this flow graph
420 for selected_element in self.get_selected_elements():
421 if selected_element.is_block(): selected.add(selected_element)
422 return list(selected)
424 def get_selected_block(self):
426 Get the selected block when a block or port is selected.
427 @return a block or None
429 return self.get_selected_blocks() and self.get_selected_blocks()[0] or None
431 def get_selected_elements(self):
433 Get the group of selected elements.
434 @return sub set of elements in this flow graph
436 return self._selected_elements
438 def get_selected_element(self):
440 Get the selected element.
441 @return a block, port, or connection or None
443 return self.get_selected_elements() and self.get_selected_elements()[0] or None
445 def update_selected_elements(self):
447 Update the selected elements.
448 The update behavior depends on the state of the mouse button.
449 When the mouse button pressed the selection will change when
450 the control mask is set or the new selection is not in the current group.
451 When the mouse button is released the selection will change when
452 the mouse has moved and the control mask is set or the current group is empty.
453 Attempt to make a new connection if the old and ports are filled.
454 If the control mask is set, merge with the current elements.
456 selected_elements = None
457 if self.mouse_pressed:
458 new_selection = self.what_is_selected(self.get_coordinate())
459 #update the selections if the new selection is not in the current selections
460 #allows us to move entire selected groups of elements
461 if self.get_ctrl_mask() or not (
462 new_selection and new_selection[0] in self.get_selected_elements()
463 ): selected_elements = new_selection
464 else: #called from a mouse release
465 if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()):
466 selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor)
467 #this selection and the last were ports, try to connect them
468 if self._old_selected_port and self._new_selected_port and \
469 self._old_selected_port is not self._new_selected_port:
471 self.connect(self._old_selected_port, self._new_selected_port)
472 self.handle_states(ELEMENT_CREATE)
473 except: Messages.send_fail_connection()
474 self._old_selected_port = None
475 self._new_selected_port = None
477 #update selected elements
478 if selected_elements is None: return
479 old_elements = set(self.get_selected_elements())
480 self._selected_elements = list(set(selected_elements))
481 new_elements = set(self.get_selected_elements())
482 #if ctrl, set the selected elements to the union - intersection of old and new
483 if self.get_ctrl_mask():
484 self._selected_elements = list(
485 set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements)
487 self.handle_states(ELEMENT_SELECT)
489 ##########################################################################
491 ##########################################################################
492 def handle_mouse_button_press(self, left_click, double_click, coordinate):
494 A mouse button is pressed, only respond to left clicks.
495 Find the selected element. Attempt a new connection if possible.
496 Open the block params window on a double click.
497 Update the selection state of the flow graph.
499 if not left_click: return
500 self.press_coor = coordinate
501 self.set_coordinate(coordinate)
503 self.mouse_pressed = True
504 self.update_selected_elements()
505 #double click detected, bring up params dialog if possible
506 if double_click and self.get_selected_block():
507 self.mouse_pressed = False
508 self.handle_states(BLOCK_PARAM_MODIFY)
510 def handle_mouse_button_release(self, left_click, coordinate):
512 A mouse button is released, record the state.
514 if not left_click: return
515 self.set_coordinate(coordinate)
517 self.mouse_pressed = False
518 if self.element_moved:
519 if Preferences.snap_to_grid():
520 grid_size = Preferences.get_grid_size()
521 X,Y = self.get_selected_element().get_coordinate()
523 if deltaX < grid_size/2: deltaX = -1 * deltaX
524 else: deltaX = grid_size - deltaX
526 if deltaY < grid_size/2: deltaY = -1 * deltaY
527 else: deltaY = grid_size - deltaY
528 self.move_selected((deltaX, deltaY))
529 self.handle_states(BLOCK_MOVE)
530 self.element_moved = False
531 self.update_selected_elements()
534 def handle_mouse_motion(self, coordinate):
536 The mouse has moved, respond to mouse dragging.
537 Move a selected element to the new coordinate.
538 Auto-scroll the scroll bars at the boundaries.
540 #to perform a movement, the mouse must be pressed, timediff large enough
541 if not self.mouse_pressed: return
542 if time.time() - self.time < MOTION_DETECT_REDRAWING_SENSITIVITY: return
543 #perform autoscrolling
544 width, height = self.get_size()
546 h_adj = self.get_scroll_pane().get_hadjustment()
547 v_adj = self.get_scroll_pane().get_vadjustment()
548 for pos, length, adj, adj_val, adj_len in (
549 (x, width, h_adj, h_adj.get_value(), h_adj.page_size),
550 (y, height, v_adj, v_adj.get_value(), v_adj.page_size),
552 #scroll if we moved near the border
553 if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len:
554 adj.set_value(adj_val+SCROLL_DISTANCE)
556 elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY:
557 adj.set_value(adj_val-SCROLL_DISTANCE)
559 #move the selected element and record the new coordinate
560 X, Y = self.get_coordinate()
561 if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y)))
563 self.set_coordinate((x, y))
565 self.time = time.time()