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.
23 from grc import Preferences
25 from grc.Constants import *
26 from grc.Actions import *
28 from grc.gui.ParamsDialog import ParamsDialog
29 from Element import Element
30 from grc.elements import FlowGraph as _FlowGraph
38 from grc import Messages
40 class FlowGraph(Element):
42 FlowGraph is the data structure to store graphical signal blocks,
43 graphical inputs and outputs,
44 and the connections between inputs and outputs.
47 def __init__(self, *args, **kwargs):
50 Create a list for signal blocks and connections. Connect mouse handlers.
52 Element.__init__(self)
53 #when is the flow graph selected? (used by keyboard event handler)
54 self.is_selected = lambda: bool(self.get_selected_elements())
55 #important vars dealing with mouse event tracking
56 self.element_moved = False
57 self.mouse_pressed = False
60 self.press_coor = (0, 0)
62 self._old_selected_port = None
63 self._new_selected_port = None
65 def _get_unique_id(self, base_id=''):
67 Get a unique id starting with the base id.
68 @param base_id the id starts with this and appends a count
73 id = (index < 0) and base_id or '%s%d'%(base_id, index)
75 #make sure that the id is not used by another block
76 if not filter(lambda b: b.get_id() == id, self.get_blocks()): return id
78 ###########################################################################
80 ###########################################################################
81 def get_drawing_area(self): return self.drawing_area
82 def get_gc(self): return self.get_drawing_area().gc
83 def get_pixmap(self): return self.get_drawing_area().pixmap
84 def get_size(self): return self.get_drawing_area().get_size_request()
85 def set_size(self, *args): self.get_drawing_area().set_size_request(*args)
86 def get_window(self): return self.get_drawing_area().window
87 def get_scroll_pane(self): return self.drawing_area.get_parent()
88 def get_ctrl_mask(self): return self.drawing_area.ctrl_mask
90 def add_new_block(self, key):
92 Add a block of the given key to this flow graph.
93 @param key the block key
95 id = self._get_unique_id(key)
96 #calculate the position coordinate
97 h_adj = self.get_scroll_pane().get_hadjustment()
98 v_adj = self.get_scroll_pane().get_vadjustment()
99 x = int(random.uniform(.25, .75)*h_adj.page_size + h_adj.get_value())
100 y = int(random.uniform(.25, .75)*v_adj.page_size + v_adj.get_value())
102 block = self.get_new_block(key)
103 block.set_coordinate((x, y))
104 block.set_rotation(0)
105 block.get_param('id').set_value(id)
106 self.handle_states(ELEMENT_CREATE)
108 ###########################################################################
110 ###########################################################################
111 def copy_to_clipboard(self):
113 Copy the selected blocks and connections into the clipboard.
114 @return the clipboard
117 blocks = self.get_selected_blocks()
118 if not blocks: return None
120 x_min, y_min = blocks[0].get_coordinate()
122 x, y = block.get_coordinate()
123 x_min = min(x, x_min)
124 y_min = min(y, y_min)
125 #get connections between selected blocks
126 connections = filter(
127 lambda c: c.get_source().get_parent() in blocks and c.get_sink().get_parent() in blocks,
128 self.get_connections(),
132 [block.export_data() for block in blocks],
133 [connection.export_data() for connection in connections],
137 def paste_from_clipboard(self, clipboard):
139 Paste the blocks and connections from the clipboard.
140 @param clipboard the nested data of blocks, connections
143 (x_min, y_min), blocks_n, connections_n = clipboard
144 old_id2block = dict()
146 h_adj = self.get_scroll_pane().get_hadjustment()
147 v_adj = self.get_scroll_pane().get_vadjustment()
148 x_off = h_adj.get_value() - x_min + h_adj.page_size/4
149 y_off = v_adj.get_value() - y_min + v_adj.page_size/4
151 for block_n in blocks_n:
152 block_key = block_n['key']
153 if block_key == 'options': continue
154 block_id = self._get_unique_id(block_key)
155 block = self.get_new_block(block_key)
158 params_n = Utils.listify(block_n, 'param')
159 for param_n in params_n:
160 param_key = param_n['key']
161 param_value = param_n['value']
163 if param_key == 'id':
164 old_id2block[param_value] = block
165 param_value = block_id
167 block.get_param(param_key).set_value(param_value)
168 #move block to offset coordinate
169 block.move((x_off, y_off))
171 for connection_n in connections_n:
172 source = old_id2block[connection_n['source_block_id']].get_source(connection_n['source_key'])
173 sink = old_id2block[connection_n['sink_block_id']].get_sink(connection_n['sink_key'])
174 self.connect(source, sink)
175 #set all pasted elements selected
176 for block in selected: selected = selected.union(set(block.get_connections()))
177 self._selected_elements = list(selected)
179 ###########################################################################
181 ###########################################################################
182 def type_controller_modify_selected(self, direction):
184 Change the registered type controller for the selected signal blocks.
185 @param direction +1 or -1
186 @return true for change
189 for selected_block in self.get_selected_blocks():
190 for child in selected_block.get_params() + selected_block.get_ports():
191 #find a param that controls a type
193 for param in selected_block.get_params():
194 if not type_param and param.is_enum(): type_param = param
195 if param.is_enum() and param.get_key() in child._type: type_param = param
197 #try to increment the enum by direction
199 keys = type_param.get_option_keys()
200 old_index = keys.index(type_param.get_value())
201 new_index = (old_index + direction + len(keys))%len(keys)
202 type_param.set_value(keys[new_index])
207 def port_controller_modify_selected(self, direction):
209 Change port controller for the selected signal blocks.
210 @param direction +1 or -1
211 @return true for changed
214 for selected_block in self.get_selected_blocks():
215 for ports in (selected_block.get_sources(), selected_block.get_sinks()):
216 if ports and hasattr(ports[0], 'get_nports') and ports[0].get_nports():
217 #find the param that controls port0
218 for param in selected_block.get_params():
219 if param.get_key() in ports[0]._nports:
220 #try to increment the port controller by direction
222 value = param.evaluate()
223 value = value + direction
224 assert(0 < value <= MAX_NUM_PORTS)
225 param.set_value(value)
230 def param_modify_selected(self):
232 Create and show a param modification dialog for the selected block.
233 @return true if parameters were changed
235 if self.get_selected_block():
236 signal_block_params_dialog = ParamsDialog(self.get_selected_block())
237 return signal_block_params_dialog.run()
240 def enable_selected(self, enable):
242 Enable/disable the selected blocks.
243 @param enable true to enable
244 @return true if changed
247 for selected_block in self.get_selected_blocks():
248 if selected_block.get_enabled() != enable:
249 selected_block.set_enabled(enable)
253 def move_selected(self, delta_coordinate):
255 Move the element and by the change in coordinates.
256 @param delta_coordinate the change in coordinates
258 for selected_block in self.get_selected_blocks():
259 selected_block.move(delta_coordinate)
260 self.element_moved = True
262 def rotate_selected(self, direction):
264 Rotate the selected blocks by 90 degrees.
265 @param direction DIR_LEFT or DIR_RIGHT
266 @return true if changed, otherwise false.
268 if not self.get_selected_blocks(): return False
269 #determine the number of degrees to rotate
270 direction = {DIR_LEFT: 90, DIR_RIGHT:270}[direction]
271 cos_r = {0: 1, 90: 0, 180: -1, 270: 0}[direction]
272 sin_r = {0: 0, 90: 1, 180: 0, 270: -1}[direction]
273 #initialize min and max coordinates
274 min_x, min_y = self.get_selected_block().get_coordinate()
275 max_x, max_y = self.get_selected_block().get_coordinate()
276 #rotate each selected block, and find min/max coordinate
277 for selected_block in self.get_selected_blocks():
278 selected_block.rotate(direction)
279 #update the min/max coordinate
280 x, y = selected_block.get_coordinate()
281 min_x, min_y = min(min_x, x), min(min_y, y)
282 max_x, max_y = max(max_x, x), max(max_y, y)
283 #calculate center point of slected blocks
284 ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
285 #rotate the blocks around the center point
286 for selected_block in self.get_selected_blocks():
287 x, y = selected_block.get_coordinate()
288 x, y = x - ctr_x, y - ctr_y
289 x, y = (x*cos_r + y*sin_r + ctr_x, -x*sin_r + y*cos_r + ctr_y)
290 selected_block.set_coordinate((x, y))
293 def remove_selected(self):
295 Remove selected elements
296 @return true if changed.
299 for selected_element in self.get_selected_elements():
300 self.remove_element(selected_element)
306 Draw the background and grid if enabled.
307 Draw all of the elements in this flow graph onto the pixmap.
308 Draw the pixmap to the drawable window of this flow graph.
311 W,H = self.get_size()
313 self.get_gc().foreground = Colors.BACKGROUND_COLOR
314 self.get_pixmap().draw_rectangle(self.get_gc(), True, 0, 0, W, H)
315 #draw grid (depends on prefs)
316 if Preferences.show_grid():
317 grid_size = Preferences.get_grid_size()
319 for i in range(W/grid_size):
320 for j in range(H/grid_size):
321 points.append((i*grid_size, j*grid_size))
322 self.get_gc().foreground = Colors.TXT_COLOR
323 self.get_pixmap().draw_points(self.get_gc(), points)
324 #draw multi select rectangle
325 if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()):
327 x1, y1 = self.press_coor
328 x2, y2 = self.get_coordinate()
329 #calculate top-left coordinate and width/height
330 x, y = int(min(x1, x2)), int(min(y1, y2))
331 w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
333 self.get_gc().foreground = Colors.H_COLOR
334 self.get_pixmap().draw_rectangle(self.get_gc(), True, x, y, w, h)
335 self.get_gc().foreground = Colors.TXT_COLOR
336 self.get_pixmap().draw_rectangle(self.get_gc(), False, x, y, w, h)
337 #draw blocks on top of connections
338 for element in self.get_connections() + self.get_blocks():
339 element.draw(self.get_pixmap())
340 #draw selected blocks on top of selected connections
341 for selected_element in self.get_selected_connections() + self.get_selected_blocks():
342 selected_element.draw(self.get_pixmap())
343 self.get_drawing_area().draw()
347 Update highlighting so only the selected is highlighted.
348 Call update on all elements.
349 Resize the window if size changed.
352 map(lambda e: e.set_highlighted(False), self.get_elements())
353 for selected_element in self.get_selected_elements():
354 selected_element.set_highlighted(True)
356 map(lambda e: e.update(), self.get_elements())
357 #set the size of the flow graph area
358 old_x, old_y = self.get_size()
359 try: new_x, new_y = self.get_option('window_size')
360 except: new_x, new_y = old_x, old_y
361 if new_x != old_x or new_y != old_y: self.set_size(new_x, new_y)
363 ##########################################################################
365 ##########################################################################
368 Set selected elements to an empty set.
370 self._selected_elements = []
372 def what_is_selected(self, coor, coor_m=None):
375 At the given coordinate, return the elements found to be selected.
376 If coor_m is unspecified, return a list of only the first element found to be selected:
377 Iterate though the elements backwardssince top elements are at the end of the list.
378 If an element is selected, place it at the end of the list so that is is drawn last,
379 and hence on top. Update the selected port information.
380 @param coor the coordinate of the mouse click
381 @param coor_m the coordinate for multi select
382 @return the selected blocks and connections or an empty list
387 for element in reversed(self.get_elements()):
388 selected_element = element.what_is_selected(coor, coor_m)
389 if not selected_element: continue
390 #update the selected port information
391 if selected_element.is_port():
392 if not coor_m: selected_port = selected_element
393 selected_element = selected_element.get_parent()
394 selected.add(selected_element)
395 #single select mode, break
397 self.get_elements().remove(element)
398 self.get_elements().append(element)
400 #update selected ports
401 self._old_selected_port = self._new_selected_port
402 self._new_selected_port = selected_port
403 return list(selected)
405 def get_selected_connections(self):
407 Get a group of selected connections.
408 @return sub set of connections in this flow graph
411 for selected_element in self.get_selected_elements():
412 if selected_element.is_connection(): selected.add(selected_element)
413 return list(selected)
415 def get_selected_blocks(self):
417 Get a group of selected blocks.
418 @return sub set of blocks in this flow graph
421 for selected_element in self.get_selected_elements():
422 if selected_element.is_block(): selected.add(selected_element)
423 return list(selected)
425 def get_selected_block(self):
427 Get the selected block when a block or port is selected.
428 @return a block or None
430 return self.get_selected_blocks() and self.get_selected_blocks()[0] or None
432 def get_selected_elements(self):
434 Get the group of selected elements.
435 @return sub set of elements in this flow graph
437 return self._selected_elements
439 def get_selected_element(self):
441 Get the selected element.
442 @return a block, port, or connection or None
444 return self.get_selected_elements() and self.get_selected_elements()[0] or None
446 def update_selected_elements(self):
448 Update the selected elements.
449 The update behavior depends on the state of the mouse button.
450 When the mouse button pressed the selection will change when
451 the control mask is set or the new selection is not in the current group.
452 When the mouse button is released the selection will change when
453 the mouse has moved and the control mask is set or the current group is empty.
454 Attempt to make a new connection if the old and ports are filled.
455 If the control mask is set, merge with the current elements.
457 selected_elements = None
458 if self.mouse_pressed:
459 new_selection = self.what_is_selected(self.get_coordinate())
460 #update the selections if the new selection is not in the current selections
461 #allows us to move entire selected groups of elements
462 if self.get_ctrl_mask() or not (
463 new_selection and new_selection[0] in self.get_selected_elements()
464 ): selected_elements = new_selection
465 else: #called from a mouse release
466 if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()):
467 selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor)
468 #this selection and the last were ports, try to connect them
469 if self._old_selected_port and self._new_selected_port and \
470 self._old_selected_port is not self._new_selected_port:
472 self.connect(self._old_selected_port, self._new_selected_port)
473 self.handle_states(ELEMENT_CREATE)
474 except: Messages.send_fail_connection()
475 self._old_selected_port = None
476 self._new_selected_port = None
478 #update selected elements
479 if selected_elements is None: return
480 old_elements = set(self.get_selected_elements())
481 self._selected_elements = list(set(selected_elements))
482 new_elements = set(self.get_selected_elements())
483 #if ctrl, set the selected elements to the union - intersection of old and new
484 if self.get_ctrl_mask():
485 self._selected_elements = list(
486 set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements)
488 self.handle_states(ELEMENT_SELECT)
490 ##########################################################################
492 ##########################################################################
493 def handle_mouse_button_press(self, left_click, double_click, coordinate):
495 A mouse button is pressed, only respond to left clicks.
496 Find the selected element. Attempt a new connection if possible.
497 Open the block params window on a double click.
498 Update the selection state of the flow graph.
500 if not left_click: return
501 self.press_coor = coordinate
502 self.set_coordinate(coordinate)
504 self.mouse_pressed = True
505 self.update_selected_elements()
506 #double click detected, bring up params dialog if possible
507 if double_click and self.get_selected_block():
508 self.mouse_pressed = False
509 self.handle_states(BLOCK_PARAM_MODIFY)
511 def handle_mouse_button_release(self, left_click, coordinate):
513 A mouse button is released, record the state.
515 if not left_click: return
516 self.set_coordinate(coordinate)
518 self.mouse_pressed = False
519 if self.element_moved:
520 if Preferences.snap_to_grid():
521 grid_size = Preferences.get_grid_size()
522 X,Y = self.get_selected_element().get_coordinate()
524 if deltaX < grid_size/2: deltaX = -1 * deltaX
525 else: deltaX = grid_size - deltaX
527 if deltaY < grid_size/2: deltaY = -1 * deltaY
528 else: deltaY = grid_size - deltaY
529 self.move_selected((deltaX, deltaY))
530 self.handle_states(BLOCK_MOVE)
531 self.element_moved = False
532 self.update_selected_elements()
535 def handle_mouse_motion(self, coordinate):
537 The mouse has moved, respond to mouse dragging.
538 Move a selected element to the new coordinate.
539 Auto-scroll the scroll bars at the boundaries.
541 #to perform a movement, the mouse must be pressed, timediff large enough
542 if not self.mouse_pressed: return
543 if time.time() - self.time < MOTION_DETECT_REDRAWING_SENSITIVITY: return
544 #perform autoscrolling
545 width, height = self.get_size()
547 h_adj = self.get_scroll_pane().get_hadjustment()
548 v_adj = self.get_scroll_pane().get_vadjustment()
549 for pos, length, adj, adj_val, adj_len in (
550 (x, width, h_adj, h_adj.get_value(), h_adj.page_size),
551 (y, height, v_adj, v_adj.get_value(), v_adj.page_size),
553 #scroll if we moved near the border
554 if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len:
555 adj.set_value(adj_val+SCROLL_DISTANCE)
557 elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY:
558 adj.set_value(adj_val-SCROLL_DISTANCE)
560 #move the selected element and record the new coordinate
561 X, Y = self.get_coordinate()
562 if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y)))
564 self.set_coordinate((x, y))
566 self.time = time.time()