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 *
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 rotation = {DIR_LEFT: 90, DIR_RIGHT:270}[direction]
271 #initialize min and max coordinates
272 min_x, min_y = self.get_selected_block().get_coordinate()
273 max_x, max_y = self.get_selected_block().get_coordinate()
274 #rotate each selected block, and find min/max coordinate
275 for selected_block in self.get_selected_blocks():
276 selected_block.rotate(rotation)
277 #update the min/max coordinate
278 x, y = selected_block.get_coordinate()
279 min_x, min_y = min(min_x, x), min(min_y, y)
280 max_x, max_y = max(max_x, x), max(max_y, y)
281 #calculate center point of slected blocks
282 ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
283 #rotate the blocks around the center point
284 for selected_block in self.get_selected_blocks():
285 x, y = selected_block.get_coordinate()
286 x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
287 selected_block.set_coordinate((x + ctr_x, y + ctr_y))
290 def remove_selected(self):
292 Remove selected elements
293 @return true if changed.
296 for selected_element in self.get_selected_elements():
297 self.remove_element(selected_element)
303 Draw the background and grid if enabled.
304 Draw all of the elements in this flow graph onto the pixmap.
305 Draw the pixmap to the drawable window of this flow graph.
308 W,H = self.get_size()
310 self.get_gc().foreground = Colors.BACKGROUND_COLOR
311 self.get_pixmap().draw_rectangle(self.get_gc(), True, 0, 0, W, H)
312 #draw grid (depends on prefs)
313 if Preferences.show_grid():
314 grid_size = Preferences.get_grid_size()
316 for i in range(W/grid_size):
317 for j in range(H/grid_size):
318 points.append((i*grid_size, j*grid_size))
319 self.get_gc().foreground = Colors.TXT_COLOR
320 self.get_pixmap().draw_points(self.get_gc(), points)
321 #draw multi select rectangle
322 if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()):
324 x1, y1 = self.press_coor
325 x2, y2 = self.get_coordinate()
326 #calculate top-left coordinate and width/height
327 x, y = int(min(x1, x2)), int(min(y1, y2))
328 w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
330 self.get_gc().foreground = Colors.H_COLOR
331 self.get_pixmap().draw_rectangle(self.get_gc(), True, x, y, w, h)
332 self.get_gc().foreground = Colors.TXT_COLOR
333 self.get_pixmap().draw_rectangle(self.get_gc(), False, x, y, w, h)
334 #draw blocks on top of connections
335 for element in self.get_connections() + self.get_blocks():
336 element.draw(self.get_pixmap())
337 #draw selected blocks on top of selected connections
338 for selected_element in self.get_selected_connections() + self.get_selected_blocks():
339 selected_element.draw(self.get_pixmap())
340 self.get_drawing_area().draw()
344 Update highlighting so only the selected is highlighted.
345 Call update on all elements.
346 Resize the window if size changed.
349 map(lambda e: e.set_highlighted(False), self.get_elements())
350 for selected_element in self.get_selected_elements():
351 selected_element.set_highlighted(True)
353 map(lambda e: e.update(), self.get_elements())
354 #set the size of the flow graph area
355 old_x, old_y = self.get_size()
356 try: new_x, new_y = self.get_option('window_size')
357 except: new_x, new_y = old_x, old_y
358 if new_x != old_x or new_y != old_y: self.set_size(new_x, new_y)
360 ##########################################################################
362 ##########################################################################
365 Set selected elements to an empty set.
367 self._selected_elements = []
369 def what_is_selected(self, coor, coor_m=None):
372 At the given coordinate, return the elements found to be selected.
373 If coor_m is unspecified, return a list of only the first element found to be selected:
374 Iterate though the elements backwardssince top elements are at the end of the list.
375 If an element is selected, place it at the end of the list so that is is drawn last,
376 and hence on top. Update the selected port information.
377 @param coor the coordinate of the mouse click
378 @param coor_m the coordinate for multi select
379 @return the selected blocks and connections or an empty list
384 for element in reversed(self.get_elements()):
385 selected_element = element.what_is_selected(coor, coor_m)
386 if not selected_element: continue
387 #update the selected port information
388 if selected_element.is_port():
389 if not coor_m: selected_port = selected_element
390 selected_element = selected_element.get_parent()
391 selected.add(selected_element)
392 #single select mode, break
394 self.get_elements().remove(element)
395 self.get_elements().append(element)
397 #update selected ports
398 self._old_selected_port = self._new_selected_port
399 self._new_selected_port = selected_port
400 return list(selected)
402 def get_selected_connections(self):
404 Get a group of selected connections.
405 @return sub set of connections in this flow graph
408 for selected_element in self.get_selected_elements():
409 if selected_element.is_connection(): selected.add(selected_element)
410 return list(selected)
412 def get_selected_blocks(self):
414 Get a group of selected blocks.
415 @return sub set of blocks in this flow graph
418 for selected_element in self.get_selected_elements():
419 if selected_element.is_block(): selected.add(selected_element)
420 return list(selected)
422 def get_selected_block(self):
424 Get the selected block when a block or port is selected.
425 @return a block or None
427 return self.get_selected_blocks() and self.get_selected_blocks()[0] or None
429 def get_selected_elements(self):
431 Get the group of selected elements.
432 @return sub set of elements in this flow graph
434 return self._selected_elements
436 def get_selected_element(self):
438 Get the selected element.
439 @return a block, port, or connection or None
441 return self.get_selected_elements() and self.get_selected_elements()[0] or None
443 def update_selected_elements(self):
445 Update the selected elements.
446 The update behavior depends on the state of the mouse button.
447 When the mouse button pressed the selection will change when
448 the control mask is set or the new selection is not in the current group.
449 When the mouse button is released the selection will change when
450 the mouse has moved and the control mask is set or the current group is empty.
451 Attempt to make a new connection if the old and ports are filled.
452 If the control mask is set, merge with the current elements.
454 selected_elements = None
455 if self.mouse_pressed:
456 new_selection = self.what_is_selected(self.get_coordinate())
457 #update the selections if the new selection is not in the current selections
458 #allows us to move entire selected groups of elements
459 if self.get_ctrl_mask() or not (
460 new_selection and new_selection[0] in self.get_selected_elements()
461 ): selected_elements = new_selection
462 else: #called from a mouse release
463 if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()):
464 selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor)
465 #this selection and the last were ports, try to connect them
466 if self._old_selected_port and self._new_selected_port and \
467 self._old_selected_port is not self._new_selected_port:
469 self.connect(self._old_selected_port, self._new_selected_port)
470 self.handle_states(ELEMENT_CREATE)
471 except: Messages.send_fail_connection()
472 self._old_selected_port = None
473 self._new_selected_port = None
475 #update selected elements
476 if selected_elements is None: return
477 old_elements = set(self.get_selected_elements())
478 self._selected_elements = list(set(selected_elements))
479 new_elements = set(self.get_selected_elements())
480 #if ctrl, set the selected elements to the union - intersection of old and new
481 if self.get_ctrl_mask():
482 self._selected_elements = list(
483 set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements)
485 self.handle_states(ELEMENT_SELECT)
487 ##########################################################################
489 ##########################################################################
490 def handle_mouse_button_press(self, left_click, double_click, coordinate):
492 A mouse button is pressed, only respond to left clicks.
493 Find the selected element. Attempt a new connection if possible.
494 Open the block params window on a double click.
495 Update the selection state of the flow graph.
497 if not left_click: return
498 self.press_coor = coordinate
499 self.set_coordinate(coordinate)
501 self.mouse_pressed = True
502 self.update_selected_elements()
503 #double click detected, bring up params dialog if possible
504 if double_click and self.get_selected_block():
505 self.mouse_pressed = False
506 self.handle_states(BLOCK_PARAM_MODIFY)
508 def handle_mouse_button_release(self, left_click, coordinate):
510 A mouse button is released, record the state.
512 if not left_click: return
513 self.set_coordinate(coordinate)
515 self.mouse_pressed = False
516 if self.element_moved:
517 if Preferences.snap_to_grid():
518 grid_size = Preferences.get_grid_size()
519 X,Y = self.get_selected_element().get_coordinate()
521 if deltaX < grid_size/2: deltaX = -1 * deltaX
522 else: deltaX = grid_size - deltaX
524 if deltaY < grid_size/2: deltaY = -1 * deltaY
525 else: deltaY = grid_size - deltaY
526 self.move_selected((deltaX, deltaY))
527 self.handle_states(BLOCK_MOVE)
528 self.element_moved = False
529 self.update_selected_elements()
532 def handle_mouse_motion(self, coordinate):
534 The mouse has moved, respond to mouse dragging.
535 Move a selected element to the new coordinate.
536 Auto-scroll the scroll bars at the boundaries.
538 #to perform a movement, the mouse must be pressed, timediff large enough
539 if not self.mouse_pressed: return
540 if time.time() - self.time < MOTION_DETECT_REDRAWING_SENSITIVITY: return
541 #perform autoscrolling
542 width, height = self.get_size()
544 h_adj = self.get_scroll_pane().get_hadjustment()
545 v_adj = self.get_scroll_pane().get_vadjustment()
546 for pos, length, adj, adj_val, adj_len in (
547 (x, width, h_adj, h_adj.get_value(), h_adj.page_size),
548 (y, height, v_adj, v_adj.get_value(), v_adj.page_size),
550 #scroll if we moved near the border
551 if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len:
552 adj.set_value(adj_val+SCROLL_DISTANCE)
554 elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY:
555 adj.set_value(adj_val-SCROLL_DISTANCE)
557 #move the selected element and record the new coordinate
558 X, Y = self.get_coordinate()
559 if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y)))
561 self.set_coordinate((x, y))
563 self.time = time.time()