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.Constants import \
21 DIR_LEFT, DIR_RIGHT, \
22 SCROLL_PROXIMITY_SENSITIVITY, SCROLL_DISTANCE, \
23 MOTION_DETECT_REDRAWING_SENSITIVITY
24 from ... gui.Actions import \
25 ELEMENT_CREATE, ELEMENT_SELECT, \
26 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 = self.get_new_block(block_key)
145 params_n = utils.listify(block_n, 'param')
146 for param_n in params_n:
147 param_key = param_n['key']
148 param_value = param_n['value']
150 if param_key == 'id':
151 old_id2block[param_value] = block
152 #if the block id is not unique, get a new block id
153 if param_value in [block.get_id() for block in self.get_blocks()]:
154 param_value = self._get_unique_id(param_value)
156 block.get_param(param_key).set_value(param_value)
157 #move block to offset coordinate
158 block.move((x_off, y_off))
159 #update before creating connections
162 for connection_n in connections_n:
163 source = old_id2block[connection_n['source_block_id']].get_source(connection_n['source_key'])
164 sink = old_id2block[connection_n['sink_block_id']].get_sink(connection_n['sink_key'])
165 self.connect(source, sink)
166 #set all pasted elements selected
167 for block in selected: selected = selected.union(set(block.get_connections()))
168 self._selected_elements = list(selected)
170 ###########################################################################
172 ###########################################################################
173 def type_controller_modify_selected(self, direction):
175 Change the registered type controller for the selected signal blocks.
176 @param direction +1 or -1
177 @return true for change
180 for selected_block in self.get_selected_blocks():
182 for param in filter(lambda p: p.is_enum(), selected_block.get_params()):
183 children = param.get_parent().get_ports() + param.get_parent().get_params()
184 #priority to the type controller
185 if param.get_key() in ' '.join(map(lambda p: p._type, children)): type_param = param
186 #use param if type param is unset
187 if not type_param: type_param = param
189 #try to increment the enum by direction
191 keys = type_param.get_option_keys()
192 old_index = keys.index(type_param.get_value())
193 new_index = (old_index + direction + len(keys))%len(keys)
194 type_param.set_value(keys[new_index])
199 def port_controller_modify_selected(self, direction):
201 Change port controller for the selected signal blocks.
202 @param direction +1 or -1
203 @return true for changed
206 for selected_block in self.get_selected_blocks():
207 for ports in (selected_block.get_sinks(), selected_block.get_sources()):
208 if ports and hasattr(ports[0], 'get_nports') and ports[0].get_nports():
209 #find the param that controls port0
210 for param in selected_block.get_params():
211 if param.get_key() in ports[0]._nports:
212 #try to increment the port controller by direction
214 value = param.evaluate()
215 value = value + direction
217 param.set_value(value)
222 def param_modify_selected(self):
224 Create and show a param modification dialog for the selected block.
225 @return true if parameters were changed
227 if self.get_selected_block():
228 signal_block_params_dialog = ParamsDialog(self.get_selected_block())
229 return signal_block_params_dialog.run()
232 def enable_selected(self, enable):
234 Enable/disable the selected blocks.
235 @param enable true to enable
236 @return true if changed
239 for selected_block in self.get_selected_blocks():
240 if selected_block.get_enabled() != enable:
241 selected_block.set_enabled(enable)
245 def move_selected(self, delta_coordinate):
247 Move the element and by the change in coordinates.
248 @param delta_coordinate the change in coordinates
250 for selected_block in self.get_selected_blocks():
251 selected_block.move(delta_coordinate)
252 self.element_moved = True
254 def rotate_selected(self, direction):
256 Rotate the selected blocks by 90 degrees.
257 @param direction DIR_LEFT or DIR_RIGHT
258 @return true if changed, otherwise false.
260 if not self.get_selected_blocks(): return False
261 #determine the number of degrees to rotate
262 rotation = {DIR_LEFT: 90, DIR_RIGHT:270}[direction]
263 #initialize min and max coordinates
264 min_x, min_y = self.get_selected_block().get_coordinate()
265 max_x, max_y = self.get_selected_block().get_coordinate()
266 #rotate each selected block, and find min/max coordinate
267 for selected_block in self.get_selected_blocks():
268 selected_block.rotate(rotation)
269 #update the min/max coordinate
270 x, y = selected_block.get_coordinate()
271 min_x, min_y = min(min_x, x), min(min_y, y)
272 max_x, max_y = max(max_x, x), max(max_y, y)
273 #calculate center point of slected blocks
274 ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
275 #rotate the blocks around the center point
276 for selected_block in self.get_selected_blocks():
277 x, y = selected_block.get_coordinate()
278 x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
279 selected_block.set_coordinate((x + ctr_x, y + ctr_y))
282 def remove_selected(self):
284 Remove selected elements
285 @return true if changed.
288 for selected_element in self.get_selected_elements():
289 self.remove_element(selected_element)
295 Draw the background and grid if enabled.
296 Draw all of the elements in this flow graph onto the pixmap.
297 Draw the pixmap to the drawable window of this flow graph.
300 W,H = self.get_size()
302 self.get_gc().foreground = Colors.BACKGROUND_COLOR
303 self.get_pixmap().draw_rectangle(self.get_gc(), True, 0, 0, W, H)
304 #draw multi select rectangle
305 if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()):
307 x1, y1 = self.press_coor
308 x2, y2 = self.get_coordinate()
309 #calculate top-left coordinate and width/height
310 x, y = int(min(x1, x2)), int(min(y1, y2))
311 w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
313 self.get_gc().foreground = Colors.H_COLOR
314 self.get_pixmap().draw_rectangle(self.get_gc(), True, x, y, w, h)
315 self.get_gc().foreground = Colors.TXT_COLOR
316 self.get_pixmap().draw_rectangle(self.get_gc(), False, x, y, w, h)
317 #draw blocks on top of connections
318 for element in self.get_connections() + self.get_blocks():
319 element.draw(self.get_pixmap())
320 #draw selected blocks on top of selected connections
321 for selected_element in self.get_selected_connections() + self.get_selected_blocks():
322 selected_element.draw(self.get_pixmap())
323 self.get_drawing_area().draw()
327 Update highlighting so only the selected is highlighted.
328 Call update on all elements.
329 Resize the window if size changed.
332 map(lambda e: e.set_highlighted(False), self.get_elements())
333 for selected_element in self.get_selected_elements():
334 selected_element.set_highlighted(True)
336 map(lambda e: e.update(), self.get_elements())
337 #set the size of the flow graph area
338 old_x, old_y = self.get_size()
339 try: new_x, new_y = self.get_option('window_size')
340 except: new_x, new_y = old_x, old_y
341 if new_x != old_x or new_y != old_y: self.set_size(new_x, new_y)
343 ##########################################################################
345 ##########################################################################
348 Set selected elements to an empty set.
350 self._selected_elements = []
352 def what_is_selected(self, coor, coor_m=None):
355 At the given coordinate, return the elements found to be selected.
356 If coor_m is unspecified, return a list of only the first element found to be selected:
357 Iterate though the elements backwardssince top elements are at the end of the list.
358 If an element is selected, place it at the end of the list so that is is drawn last,
359 and hence on top. Update the selected port information.
360 @param coor the coordinate of the mouse click
361 @param coor_m the coordinate for multi select
362 @return the selected blocks and connections or an empty list
367 for element in reversed(self.get_elements()):
368 selected_element = element.what_is_selected(coor, coor_m)
369 if not selected_element: continue
370 #update the selected port information
371 if selected_element.is_port():
372 if not coor_m: selected_port = selected_element
373 selected_element = selected_element.get_parent()
374 selected.add(selected_element)
375 #single select mode, break
377 self.get_elements().remove(element)
378 self.get_elements().append(element)
380 #update selected ports
381 self._old_selected_port = self._new_selected_port
382 self._new_selected_port = selected_port
383 return list(selected)
385 def get_selected_connections(self):
387 Get a group of selected connections.
388 @return sub set of connections in this flow graph
391 for selected_element in self.get_selected_elements():
392 if selected_element.is_connection(): selected.add(selected_element)
393 return list(selected)
395 def get_selected_blocks(self):
397 Get a group of selected blocks.
398 @return sub set of blocks in this flow graph
401 for selected_element in self.get_selected_elements():
402 if selected_element.is_block(): selected.add(selected_element)
403 return list(selected)
405 def get_selected_block(self):
407 Get the selected block when a block or port is selected.
408 @return a block or None
410 return self.get_selected_blocks() and self.get_selected_blocks()[0] or None
412 def get_selected_elements(self):
414 Get the group of selected elements.
415 @return sub set of elements in this flow graph
417 return self._selected_elements
419 def get_selected_element(self):
421 Get the selected element.
422 @return a block, port, or connection or None
424 return self.get_selected_elements() and self.get_selected_elements()[0] or None
426 def update_selected_elements(self):
428 Update the selected elements.
429 The update behavior depends on the state of the mouse button.
430 When the mouse button pressed the selection will change when
431 the control mask is set or the new selection is not in the current group.
432 When the mouse button is released the selection will change when
433 the mouse has moved and the control mask is set or the current group is empty.
434 Attempt to make a new connection if the old and ports are filled.
435 If the control mask is set, merge with the current elements.
437 selected_elements = None
438 if self.mouse_pressed:
439 new_selection = self.what_is_selected(self.get_coordinate())
440 #update the selections if the new selection is not in the current selections
441 #allows us to move entire selected groups of elements
442 if self.get_ctrl_mask() or not (
443 new_selection and new_selection[0] in self.get_selected_elements()
444 ): selected_elements = new_selection
445 else: #called from a mouse release
446 if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()):
447 selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor)
448 #this selection and the last were ports, try to connect them
449 if self._old_selected_port and self._new_selected_port and \
450 self._old_selected_port is not self._new_selected_port:
452 self.connect(self._old_selected_port, self._new_selected_port)
453 self.handle_states(ELEMENT_CREATE)
454 except: Messages.send_fail_connection()
455 self._old_selected_port = None
456 self._new_selected_port = None
458 #update selected elements
459 if selected_elements is None: return
460 old_elements = set(self.get_selected_elements())
461 self._selected_elements = list(set(selected_elements))
462 new_elements = set(self.get_selected_elements())
463 #if ctrl, set the selected elements to the union - intersection of old and new
464 if self.get_ctrl_mask():
465 self._selected_elements = list(
466 set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements)
468 self.handle_states(ELEMENT_SELECT)
470 ##########################################################################
472 ##########################################################################
473 def handle_mouse_button_press(self, left_click, double_click, coordinate):
475 A mouse button is pressed, only respond to left clicks.
476 Find the selected element. Attempt a new connection if possible.
477 Open the block params window on a double click.
478 Update the selection state of the flow graph.
480 if not left_click: return
481 self.press_coor = coordinate
482 self.set_coordinate(coordinate)
484 self.mouse_pressed = True
485 self.update_selected_elements()
486 #double click detected, bring up params dialog if possible
487 if double_click and self.get_selected_block():
488 self.mouse_pressed = False
489 self.handle_states(BLOCK_PARAM_MODIFY)
491 def handle_mouse_button_release(self, left_click, coordinate):
493 A mouse button is released, record the state.
495 if not left_click: return
496 self.set_coordinate(coordinate)
498 self.mouse_pressed = False
499 if self.element_moved:
500 self.handle_states(BLOCK_MOVE)
501 self.element_moved = False
502 self.update_selected_elements()
505 def handle_mouse_motion(self, coordinate):
507 The mouse has moved, respond to mouse dragging.
508 Move a selected element to the new coordinate.
509 Auto-scroll the scroll bars at the boundaries.
511 #to perform a movement, the mouse must be pressed, timediff large enough
512 if not self.mouse_pressed: return
513 if time.time() - self.time < MOTION_DETECT_REDRAWING_SENSITIVITY: return
514 #perform autoscrolling
515 width, height = self.get_size()
517 h_adj = self.get_scroll_pane().get_hadjustment()
518 v_adj = self.get_scroll_pane().get_vadjustment()
519 for pos, length, adj, adj_val, adj_len in (
520 (x, width, h_adj, h_adj.get_value(), h_adj.page_size),
521 (y, height, v_adj, v_adj.get_value(), v_adj.page_size),
523 #scroll if we moved near the border
524 if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len:
525 adj.set_value(adj_val+SCROLL_DISTANCE)
527 elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY:
528 adj.set_value(adj_val-SCROLL_DISTANCE)
530 #remove the connection if selected in drag event
531 if len(self.get_selected_elements()) == 1 and self.get_selected_element().is_connection():
532 self.handle_states(ELEMENT_DELETE)
533 #move the selected elements and record the new coordinate
534 X, Y = self.get_coordinate()
535 if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y)))
537 self.set_coordinate((x, y))
539 self.time = time.time()