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
20 from ... gui import Preferences
21 from ... gui.Constants import \
22 DIR_LEFT, DIR_RIGHT, \
23 SCROLL_PROXIMITY_SENSITIVITY, SCROLL_DISTANCE, \
24 MOTION_DETECT_REDRAWING_SENSITIVITY
25 from ... gui.Actions import \
26 ELEMENT_CREATE, ELEMENT_SELECT, \
27 BLOCK_PARAM_MODIFY, BLOCK_MOVE
31 from ... gui.ParamsDialog import ParamsDialog
32 from Element import Element
33 from .. base import FlowGraph as _FlowGraph
39 from ... gui import Messages
41 class FlowGraph(Element):
43 FlowGraph is the data structure to store graphical signal blocks,
44 graphical inputs and outputs,
45 and the connections between inputs and outputs.
48 def __init__(self, *args, **kwargs):
51 Create a list for signal blocks and connections. Connect mouse handlers.
53 Element.__init__(self)
54 #when is the flow graph selected? (used by keyboard event handler)
55 self.is_selected = lambda: bool(self.get_selected_elements())
56 #important vars dealing with mouse event tracking
57 self.element_moved = False
58 self.mouse_pressed = False
61 self.press_coor = (0, 0)
63 self._old_selected_port = None
64 self._new_selected_port = None
66 ###########################################################################
68 ###########################################################################
69 def get_drawing_area(self): return self.drawing_area
70 def get_gc(self): return self.get_drawing_area().gc
71 def get_pixmap(self): return self.get_drawing_area().pixmap
72 def get_size(self): return self.get_drawing_area().get_size_request()
73 def set_size(self, *args): self.get_drawing_area().set_size_request(*args)
74 def get_window(self): return self.get_drawing_area().window
75 def get_scroll_pane(self): return self.drawing_area.get_parent()
76 def get_ctrl_mask(self): return self.drawing_area.ctrl_mask
78 def add_new_block(self, key):
80 Add a block of the given key to this flow graph.
81 @param key the block key
83 id = self._get_unique_id(key)
84 #calculate the position coordinate
85 h_adj = self.get_scroll_pane().get_hadjustment()
86 v_adj = self.get_scroll_pane().get_vadjustment()
87 x = int(random.uniform(.25, .75)*h_adj.page_size + h_adj.get_value())
88 y = int(random.uniform(.25, .75)*v_adj.page_size + v_adj.get_value())
90 block = self.get_new_block(key)
91 block.set_coordinate((x, y))
93 block.get_param('id').set_value(id)
94 self.handle_states(ELEMENT_CREATE)
96 ###########################################################################
98 ###########################################################################
99 def copy_to_clipboard(self):
101 Copy the selected blocks and connections into the clipboard.
102 @return the clipboard
105 blocks = self.get_selected_blocks()
106 if not blocks: return None
108 x_min, y_min = blocks[0].get_coordinate()
110 x, y = block.get_coordinate()
111 x_min = min(x, x_min)
112 y_min = min(y, y_min)
113 #get connections between selected blocks
114 connections = filter(
115 lambda c: c.get_source().get_parent() in blocks and c.get_sink().get_parent() in blocks,
116 self.get_connections(),
120 [block.export_data() for block in blocks],
121 [connection.export_data() for connection in connections],
125 def paste_from_clipboard(self, clipboard):
127 Paste the blocks and connections from the clipboard.
128 @param clipboard the nested data of blocks, connections
131 (x_min, y_min), blocks_n, connections_n = clipboard
132 old_id2block = dict()
134 h_adj = self.get_scroll_pane().get_hadjustment()
135 v_adj = self.get_scroll_pane().get_vadjustment()
136 x_off = h_adj.get_value() - x_min + h_adj.page_size/4
137 y_off = v_adj.get_value() - y_min + v_adj.page_size/4
139 for block_n in blocks_n:
140 block_key = block_n['key']
141 if block_key == 'options': continue
142 block_id = self._get_unique_id(block_key)
143 block = self.get_new_block(block_key)
146 params_n = utils.listify(block_n, 'param')
147 for param_n in params_n:
148 param_key = param_n['key']
149 param_value = param_n['value']
151 if param_key == 'id':
152 old_id2block[param_value] = block
153 param_value = block_id
155 block.get_param(param_key).set_value(param_value)
156 #move block to offset coordinate
157 block.move((x_off, y_off))
158 #update before creating connections
161 for connection_n in connections_n:
162 source = old_id2block[connection_n['source_block_id']].get_source(connection_n['source_key'])
163 sink = old_id2block[connection_n['sink_block_id']].get_sink(connection_n['sink_key'])
164 self.connect(source, sink)
165 #set all pasted elements selected
166 for block in selected: selected = selected.union(set(block.get_connections()))
167 self._selected_elements = list(selected)
169 ###########################################################################
171 ###########################################################################
172 def type_controller_modify_selected(self, direction):
174 Change the registered type controller for the selected signal blocks.
175 @param direction +1 or -1
176 @return true for change
179 for selected_block in self.get_selected_blocks():
181 for param in filter(lambda p: p.is_enum(), selected_block.get_params()):
182 children = param.get_parent().get_ports() + param.get_parent().get_params()
183 #priority to the type controller
184 if param.get_key() in ' '.join(map(lambda p: p._type, children)): type_param = param
185 #use param if type param is unset
186 if not type_param: type_param = param
188 #try to increment the enum by direction
190 keys = type_param.get_option_keys()
191 old_index = keys.index(type_param.get_value())
192 new_index = (old_index + direction + len(keys))%len(keys)
193 type_param.set_value(keys[new_index])
198 def port_controller_modify_selected(self, direction):
200 Change port controller for the selected signal blocks.
201 @param direction +1 or -1
202 @return true for changed
205 for selected_block in self.get_selected_blocks():
206 for ports in (selected_block.get_sinks(), selected_block.get_sources()):
207 if ports and hasattr(ports[0], 'get_nports') and ports[0].get_nports():
208 #find the param that controls port0
209 for param in selected_block.get_params():
210 if param.get_key() in ports[0]._nports:
211 #try to increment the port controller by direction
213 value = param.evaluate()
214 value = value + direction
216 param.set_value(value)
221 def param_modify_selected(self):
223 Create and show a param modification dialog for the selected block.
224 @return true if parameters were changed
226 if self.get_selected_block():
227 signal_block_params_dialog = ParamsDialog(self.get_selected_block())
228 return signal_block_params_dialog.run()
231 def enable_selected(self, enable):
233 Enable/disable the selected blocks.
234 @param enable true to enable
235 @return true if changed
238 for selected_block in self.get_selected_blocks():
239 if selected_block.get_enabled() != enable:
240 selected_block.set_enabled(enable)
244 def move_selected(self, delta_coordinate):
246 Move the element and by the change in coordinates.
247 @param delta_coordinate the change in coordinates
249 for selected_block in self.get_selected_blocks():
250 selected_block.move(delta_coordinate)
251 self.element_moved = True
253 def rotate_selected(self, direction):
255 Rotate the selected blocks by 90 degrees.
256 @param direction DIR_LEFT or DIR_RIGHT
257 @return true if changed, otherwise false.
259 if not self.get_selected_blocks(): return False
260 #determine the number of degrees to rotate
261 rotation = {DIR_LEFT: 90, DIR_RIGHT:270}[direction]
262 #initialize min and max coordinates
263 min_x, min_y = self.get_selected_block().get_coordinate()
264 max_x, max_y = self.get_selected_block().get_coordinate()
265 #rotate each selected block, and find min/max coordinate
266 for selected_block in self.get_selected_blocks():
267 selected_block.rotate(rotation)
268 #update the min/max coordinate
269 x, y = selected_block.get_coordinate()
270 min_x, min_y = min(min_x, x), min(min_y, y)
271 max_x, max_y = max(max_x, x), max(max_y, y)
272 #calculate center point of slected blocks
273 ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
274 #rotate the blocks around the center point
275 for selected_block in self.get_selected_blocks():
276 x, y = selected_block.get_coordinate()
277 x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
278 selected_block.set_coordinate((x + ctr_x, y + ctr_y))
281 def remove_selected(self):
283 Remove selected elements
284 @return true if changed.
287 for selected_element in self.get_selected_elements():
288 self.remove_element(selected_element)
294 Draw the background and grid if enabled.
295 Draw all of the elements in this flow graph onto the pixmap.
296 Draw the pixmap to the drawable window of this flow graph.
299 W,H = self.get_size()
301 self.get_gc().foreground = Colors.BACKGROUND_COLOR
302 self.get_pixmap().draw_rectangle(self.get_gc(), True, 0, 0, W, H)
303 #draw grid (depends on prefs)
304 if Preferences.show_grid():
305 grid_size = Preferences.get_grid_size()
307 for i in range(W/grid_size):
308 for j in range(H/grid_size):
309 points.append((i*grid_size, j*grid_size))
310 self.get_gc().foreground = Colors.TXT_COLOR
311 self.get_pixmap().draw_points(self.get_gc(), points)
312 #draw multi select rectangle
313 if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()):
315 x1, y1 = self.press_coor
316 x2, y2 = self.get_coordinate()
317 #calculate top-left coordinate and width/height
318 x, y = int(min(x1, x2)), int(min(y1, y2))
319 w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
321 self.get_gc().foreground = Colors.H_COLOR
322 self.get_pixmap().draw_rectangle(self.get_gc(), True, x, y, w, h)
323 self.get_gc().foreground = Colors.TXT_COLOR
324 self.get_pixmap().draw_rectangle(self.get_gc(), False, x, y, w, h)
325 #draw blocks on top of connections
326 for element in self.get_connections() + self.get_blocks():
327 element.draw(self.get_pixmap())
328 #draw selected blocks on top of selected connections
329 for selected_element in self.get_selected_connections() + self.get_selected_blocks():
330 selected_element.draw(self.get_pixmap())
331 self.get_drawing_area().draw()
335 Update highlighting so only the selected is highlighted.
336 Call update on all elements.
337 Resize the window if size changed.
340 map(lambda e: e.set_highlighted(False), self.get_elements())
341 for selected_element in self.get_selected_elements():
342 selected_element.set_highlighted(True)
344 map(lambda e: e.update(), self.get_elements())
345 #set the size of the flow graph area
346 old_x, old_y = self.get_size()
347 try: new_x, new_y = self.get_option('window_size')
348 except: new_x, new_y = old_x, old_y
349 if new_x != old_x or new_y != old_y: self.set_size(new_x, new_y)
351 ##########################################################################
353 ##########################################################################
356 Set selected elements to an empty set.
358 self._selected_elements = []
360 def what_is_selected(self, coor, coor_m=None):
363 At the given coordinate, return the elements found to be selected.
364 If coor_m is unspecified, return a list of only the first element found to be selected:
365 Iterate though the elements backwardssince top elements are at the end of the list.
366 If an element is selected, place it at the end of the list so that is is drawn last,
367 and hence on top. Update the selected port information.
368 @param coor the coordinate of the mouse click
369 @param coor_m the coordinate for multi select
370 @return the selected blocks and connections or an empty list
375 for element in reversed(self.get_elements()):
376 selected_element = element.what_is_selected(coor, coor_m)
377 if not selected_element: continue
378 #update the selected port information
379 if selected_element.is_port():
380 if not coor_m: selected_port = selected_element
381 selected_element = selected_element.get_parent()
382 selected.add(selected_element)
383 #single select mode, break
385 self.get_elements().remove(element)
386 self.get_elements().append(element)
388 #update selected ports
389 self._old_selected_port = self._new_selected_port
390 self._new_selected_port = selected_port
391 return list(selected)
393 def get_selected_connections(self):
395 Get a group of selected connections.
396 @return sub set of connections in this flow graph
399 for selected_element in self.get_selected_elements():
400 if selected_element.is_connection(): selected.add(selected_element)
401 return list(selected)
403 def get_selected_blocks(self):
405 Get a group of selected blocks.
406 @return sub set of blocks in this flow graph
409 for selected_element in self.get_selected_elements():
410 if selected_element.is_block(): selected.add(selected_element)
411 return list(selected)
413 def get_selected_block(self):
415 Get the selected block when a block or port is selected.
416 @return a block or None
418 return self.get_selected_blocks() and self.get_selected_blocks()[0] or None
420 def get_selected_elements(self):
422 Get the group of selected elements.
423 @return sub set of elements in this flow graph
425 return self._selected_elements
427 def get_selected_element(self):
429 Get the selected element.
430 @return a block, port, or connection or None
432 return self.get_selected_elements() and self.get_selected_elements()[0] or None
434 def update_selected_elements(self):
436 Update the selected elements.
437 The update behavior depends on the state of the mouse button.
438 When the mouse button pressed the selection will change when
439 the control mask is set or the new selection is not in the current group.
440 When the mouse button is released the selection will change when
441 the mouse has moved and the control mask is set or the current group is empty.
442 Attempt to make a new connection if the old and ports are filled.
443 If the control mask is set, merge with the current elements.
445 selected_elements = None
446 if self.mouse_pressed:
447 new_selection = self.what_is_selected(self.get_coordinate())
448 #update the selections if the new selection is not in the current selections
449 #allows us to move entire selected groups of elements
450 if self.get_ctrl_mask() or not (
451 new_selection and new_selection[0] in self.get_selected_elements()
452 ): selected_elements = new_selection
453 else: #called from a mouse release
454 if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()):
455 selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor)
456 #this selection and the last were ports, try to connect them
457 if self._old_selected_port and self._new_selected_port and \
458 self._old_selected_port is not self._new_selected_port:
460 self.connect(self._old_selected_port, self._new_selected_port)
461 self.handle_states(ELEMENT_CREATE)
462 except: Messages.send_fail_connection()
463 self._old_selected_port = None
464 self._new_selected_port = None
466 #update selected elements
467 if selected_elements is None: return
468 old_elements = set(self.get_selected_elements())
469 self._selected_elements = list(set(selected_elements))
470 new_elements = set(self.get_selected_elements())
471 #if ctrl, set the selected elements to the union - intersection of old and new
472 if self.get_ctrl_mask():
473 self._selected_elements = list(
474 set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements)
476 self.handle_states(ELEMENT_SELECT)
478 ##########################################################################
480 ##########################################################################
481 def handle_mouse_button_press(self, left_click, double_click, coordinate):
483 A mouse button is pressed, only respond to left clicks.
484 Find the selected element. Attempt a new connection if possible.
485 Open the block params window on a double click.
486 Update the selection state of the flow graph.
488 if not left_click: return
489 self.press_coor = coordinate
490 self.set_coordinate(coordinate)
492 self.mouse_pressed = True
493 self.update_selected_elements()
494 #double click detected, bring up params dialog if possible
495 if double_click and self.get_selected_block():
496 self.mouse_pressed = False
497 self.handle_states(BLOCK_PARAM_MODIFY)
499 def handle_mouse_button_release(self, left_click, coordinate):
501 A mouse button is released, record the state.
503 if not left_click: return
504 self.set_coordinate(coordinate)
506 self.mouse_pressed = False
507 if self.element_moved:
508 if Preferences.snap_to_grid():
509 grid_size = Preferences.get_grid_size()
510 X,Y = self.get_selected_element().get_coordinate()
512 if deltaX < grid_size/2: deltaX = -1 * deltaX
513 else: deltaX = grid_size - deltaX
515 if deltaY < grid_size/2: deltaY = -1 * deltaY
516 else: deltaY = grid_size - deltaY
517 self.move_selected((deltaX, deltaY))
518 self.handle_states(BLOCK_MOVE)
519 self.element_moved = False
520 self.update_selected_elements()
523 def handle_mouse_motion(self, coordinate):
525 The mouse has moved, respond to mouse dragging.
526 Move a selected element to the new coordinate.
527 Auto-scroll the scroll bars at the boundaries.
529 #to perform a movement, the mouse must be pressed, timediff large enough
530 if not self.mouse_pressed: return
531 if time.time() - self.time < MOTION_DETECT_REDRAWING_SENSITIVITY: return
532 #perform autoscrolling
533 width, height = self.get_size()
535 h_adj = self.get_scroll_pane().get_hadjustment()
536 v_adj = self.get_scroll_pane().get_vadjustment()
537 for pos, length, adj, adj_val, adj_len in (
538 (x, width, h_adj, h_adj.get_value(), h_adj.page_size),
539 (y, height, v_adj, v_adj.get_value(), v_adj.page_size),
541 #scroll if we moved near the border
542 if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len:
543 adj.set_value(adj_val+SCROLL_DISTANCE)
545 elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY:
546 adj.set_value(adj_val-SCROLL_DISTANCE)
548 #move the selected element and record the new coordinate
549 X, Y = self.get_coordinate()
550 if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y)))
552 self.set_coordinate((x, y))
554 self.time = time.time()