2 Copyright 2007, 2008, 2009, 2010 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 Constants import SCROLL_PROXIMITY_SENSITIVITY, SCROLL_DISTANCE
24 from Element import Element
31 class FlowGraph(Element):
33 FlowGraph is the data structure to store graphical signal blocks,
34 graphical inputs and outputs,
35 and the connections between inputs and outputs.
41 Create a list for signal blocks and connections. Connect mouse handlers.
43 Element.__init__(self)
44 #when is the flow graph selected? (used by keyboard event handler)
45 self.is_selected = lambda: bool(self.get_selected_elements())
46 #important vars dealing with mouse event tracking
47 self.element_moved = False
48 self.mouse_pressed = False
50 self.press_coor = (0, 0)
52 self._old_selected_port = None
53 self._new_selected_port = None
55 self._context_menu = gtk.Menu()
60 Actions.ELEMENT_DELETE,
61 Actions.BLOCK_ROTATE_CCW,
62 Actions.BLOCK_ROTATE_CW,
64 Actions.BLOCK_DISABLE,
65 Actions.BLOCK_PARAM_MODIFY,
66 ]: self._context_menu.append(action.create_menu_item())
68 ###########################################################################
70 ###########################################################################
71 def get_drawing_area(self): return self.drawing_area
72 def queue_draw(self): self.get_drawing_area().queue_draw()
73 def get_size(self): return self.get_drawing_area().get_size_request()
74 def set_size(self, *args): self.get_drawing_area().set_size_request(*args)
75 def get_scroll_pane(self): return self.drawing_area.get_parent()
76 def get_ctrl_mask(self): return self.drawing_area.ctrl_mask
77 def new_pixmap(self, *args): return self.get_drawing_area().new_pixmap(*args)
79 def add_new_block(self, key, coor=None):
81 Add a block of the given key to this flow graph.
82 @param key the block key
83 @param coor an optional coordinate or None for random
85 id = self._get_unique_id(key)
86 #calculate the position coordinate
87 h_adj = self.get_scroll_pane().get_hadjustment()
88 v_adj = self.get_scroll_pane().get_vadjustment()
89 if coor is None: coor = (
90 int(random.uniform(.25, .75)*h_adj.page_size + h_adj.get_value()),
91 int(random.uniform(.25, .75)*v_adj.page_size + v_adj.get_value()),
94 block = self.get_new_block(key)
95 block.set_coordinate(coor)
97 block.get_param('id').set_value(id)
98 Actions.ELEMENT_CREATE()
100 ###########################################################################
102 ###########################################################################
103 def copy_to_clipboard(self):
105 Copy the selected blocks and connections into the clipboard.
106 @return the clipboard
109 blocks = self.get_selected_blocks()
110 if not blocks: return None
112 x_min, y_min = blocks[0].get_coordinate()
114 x, y = block.get_coordinate()
115 x_min = min(x, x_min)
116 y_min = min(y, y_min)
117 #get connections between selected blocks
118 connections = filter(
119 lambda c: c.get_source().get_parent() in blocks and c.get_sink().get_parent() in blocks,
120 self.get_connections(),
124 [block.export_data() for block in blocks],
125 [connection.export_data() for connection in connections],
129 def paste_from_clipboard(self, clipboard):
131 Paste the blocks and connections from the clipboard.
132 @param clipboard the nested data of blocks, connections
135 (x_min, y_min), blocks_n, connections_n = clipboard
136 old_id2block = dict()
138 h_adj = self.get_scroll_pane().get_hadjustment()
139 v_adj = self.get_scroll_pane().get_vadjustment()
140 x_off = h_adj.get_value() - x_min + h_adj.page_size/4
141 y_off = v_adj.get_value() - y_min + v_adj.page_size/4
143 for block_n in blocks_n:
144 block_key = block_n.find('key')
145 if block_key == 'options': continue
146 block = self.get_new_block(block_key)
149 params_n = block_n.findall('param')
150 for param_n in params_n:
151 param_key = param_n.find('key')
152 param_value = param_n.find('value')
154 if param_key == 'id':
155 old_id2block[param_value] = block
156 #if the block id is not unique, get a new block id
157 if param_value in [block.get_id() for block in self.get_blocks()]:
158 param_value = self._get_unique_id(param_value)
160 block.get_param(param_key).set_value(param_value)
161 #move block to offset coordinate
162 block.move((x_off, y_off))
163 #update before creating connections
166 for connection_n in connections_n:
167 source = old_id2block[connection_n.find('source_block_id')].get_source(connection_n.find('source_key'))
168 sink = old_id2block[connection_n.find('sink_block_id')].get_sink(connection_n.find('sink_key'))
169 self.connect(source, sink)
170 #set all pasted elements selected
171 for block in selected: selected = selected.union(set(block.get_connections()))
172 self._selected_elements = list(selected)
174 ###########################################################################
176 ###########################################################################
177 def type_controller_modify_selected(self, direction):
179 Change the registered type controller for the selected signal blocks.
180 @param direction +1 or -1
181 @return true for change
183 return any([sb.type_controller_modify(direction) for sb in self.get_selected_blocks()])
185 def port_controller_modify_selected(self, direction):
187 Change port controller for the selected signal blocks.
188 @param direction +1 or -1
189 @return true for changed
191 return any([sb.port_controller_modify(direction) for sb in self.get_selected_blocks()])
193 def enable_selected(self, enable):
195 Enable/disable the selected blocks.
196 @param enable true to enable
197 @return true if changed
200 for selected_block in self.get_selected_blocks():
201 if selected_block.get_enabled() != enable:
202 selected_block.set_enabled(enable)
206 def move_selected(self, delta_coordinate):
208 Move the element and by the change in coordinates.
209 @param delta_coordinate the change in coordinates
211 for selected_block in self.get_selected_blocks():
212 selected_block.move(delta_coordinate)
213 self.element_moved = True
215 def rotate_selected(self, rotation):
217 Rotate the selected blocks by multiples of 90 degrees.
218 @param rotation the rotation in degrees
219 @return true if changed, otherwise false.
221 if not self.get_selected_blocks(): return False
222 #initialize min and max coordinates
223 min_x, min_y = self.get_selected_block().get_coordinate()
224 max_x, max_y = self.get_selected_block().get_coordinate()
225 #rotate each selected block, and find min/max coordinate
226 for selected_block in self.get_selected_blocks():
227 selected_block.rotate(rotation)
228 #update the min/max coordinate
229 x, y = selected_block.get_coordinate()
230 min_x, min_y = min(min_x, x), min(min_y, y)
231 max_x, max_y = max(max_x, x), max(max_y, y)
232 #calculate center point of slected blocks
233 ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
234 #rotate the blocks around the center point
235 for selected_block in self.get_selected_blocks():
236 x, y = selected_block.get_coordinate()
237 x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
238 selected_block.set_coordinate((x + ctr_x, y + ctr_y))
241 def remove_selected(self):
243 Remove selected elements
244 @return true if changed.
247 for selected_element in self.get_selected_elements():
248 self.remove_element(selected_element)
252 def draw(self, gc, window):
254 Draw the background and grid if enabled.
255 Draw all of the elements in this flow graph onto the pixmap.
256 Draw the pixmap to the drawable window of this flow graph.
258 W,H = self.get_size()
260 gc.set_foreground(Colors.FLOWGRAPH_BACKGROUND_COLOR)
261 window.draw_rectangle(gc, True, 0, 0, W, H)
262 #draw multi select rectangle
263 if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()):
265 x1, y1 = self.press_coor
266 x2, y2 = self.get_coordinate()
267 #calculate top-left coordinate and width/height
268 x, y = int(min(x1, x2)), int(min(y1, y2))
269 w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
271 gc.set_foreground(Colors.HIGHLIGHT_COLOR)
272 window.draw_rectangle(gc, True, x, y, w, h)
273 gc.set_foreground(Colors.BORDER_COLOR)
274 window.draw_rectangle(gc, False, x, y, w, h)
275 #draw blocks on top of connections
276 for element in self.get_connections() + self.get_blocks():
277 element.draw(gc, window)
278 #draw selected blocks on top of selected connections
279 for selected_element in self.get_selected_connections() + self.get_selected_blocks():
280 selected_element.draw(gc, window)
282 def update_selected(self):
284 Remove deleted elements from the selected elements list.
285 Update highlighting so only the selected are highlighted.
287 selected_elements = self.get_selected_elements()
288 elements = self.get_elements()
289 #remove deleted elements
290 for selected in selected_elements:
291 if selected in elements: continue
292 selected_elements.remove(selected)
293 try: assert self._old_selected_port.get_parent() in elements
294 except: self._old_selected_port = None
295 try: assert self._new_selected_port.get_parent() in elements
296 except: self._new_selected_port = None
298 for element in elements:
299 element.set_highlighted(element in selected_elements)
303 Call the top level rewrite and validate.
304 Call the top level create labels and shapes.
311 ##########################################################################
313 ##########################################################################
316 Set selected elements to an empty set.
318 self._selected_elements = []
320 def what_is_selected(self, coor, coor_m=None):
323 At the given coordinate, return the elements found to be selected.
324 If coor_m is unspecified, return a list of only the first element found to be selected:
325 Iterate though the elements backwards since top elements are at the end of the list.
326 If an element is selected, place it at the end of the list so that is is drawn last,
327 and hence on top. Update the selected port information.
328 @param coor the coordinate of the mouse click
329 @param coor_m the coordinate for multi select
330 @return the selected blocks and connections or an empty list
335 for element in reversed(self.get_elements()):
336 selected_element = element.what_is_selected(coor, coor_m)
337 if not selected_element: continue
338 #update the selected port information
339 if selected_element.is_port():
340 if not coor_m: selected_port = selected_element
341 selected_element = selected_element.get_parent()
342 selected.add(selected_element)
343 #place at the end of the list
344 self.get_elements().remove(element)
345 self.get_elements().append(element)
346 #single select mode, break
348 #update selected ports
349 self._old_selected_port = self._new_selected_port
350 self._new_selected_port = selected_port
351 return list(selected)
353 def get_selected_connections(self):
355 Get a group of selected connections.
356 @return sub set of connections in this flow graph
359 for selected_element in self.get_selected_elements():
360 if selected_element.is_connection(): selected.add(selected_element)
361 return list(selected)
363 def get_selected_blocks(self):
365 Get a group of selected blocks.
366 @return sub set of blocks in this flow graph
369 for selected_element in self.get_selected_elements():
370 if selected_element.is_block(): selected.add(selected_element)
371 return list(selected)
373 def get_selected_block(self):
375 Get the selected block when a block or port is selected.
376 @return a block or None
378 return self.get_selected_blocks() and self.get_selected_blocks()[0] or None
380 def get_selected_elements(self):
382 Get the group of selected elements.
383 @return sub set of elements in this flow graph
385 return self._selected_elements
387 def get_selected_element(self):
389 Get the selected element.
390 @return a block, port, or connection or None
392 return self.get_selected_elements() and self.get_selected_elements()[0] or None
394 def update_selected_elements(self):
396 Update the selected elements.
397 The update behavior depends on the state of the mouse button.
398 When the mouse button pressed the selection will change when
399 the control mask is set or the new selection is not in the current group.
400 When the mouse button is released the selection will change when
401 the mouse has moved and the control mask is set or the current group is empty.
402 Attempt to make a new connection if the old and ports are filled.
403 If the control mask is set, merge with the current elements.
405 selected_elements = None
406 if self.mouse_pressed:
407 new_selections = self.what_is_selected(self.get_coordinate())
408 #update the selections if the new selection is not in the current selections
409 #allows us to move entire selected groups of elements
410 if self.get_ctrl_mask() or not (
411 new_selections and new_selections[0] in self.get_selected_elements()
412 ): selected_elements = new_selections
413 else: #called from a mouse release
414 if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()):
415 selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor)
416 #this selection and the last were ports, try to connect them
417 if self._old_selected_port and self._new_selected_port and \
418 self._old_selected_port is not self._new_selected_port:
420 self.connect(self._old_selected_port, self._new_selected_port)
421 Actions.ELEMENT_CREATE()
422 except: Messages.send_fail_connection()
423 self._old_selected_port = None
424 self._new_selected_port = None
426 #update selected elements
427 if selected_elements is None: return
428 old_elements = set(self.get_selected_elements())
429 self._selected_elements = list(set(selected_elements))
430 new_elements = set(self.get_selected_elements())
431 #if ctrl, set the selected elements to the union - intersection of old and new
432 if self.get_ctrl_mask():
433 self._selected_elements = list(
434 set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements)
436 Actions.ELEMENT_SELECT()
438 ##########################################################################
440 ##########################################################################
441 def handle_mouse_context_press(self, coordinate, event):
443 The context mouse button was pressed:
444 If no elements were selected, perform re-selection at this coordinate.
445 Then, show the context menu at the mouse click location.
447 selections = self.what_is_selected(coordinate)
448 if not set(selections).intersection(self.get_selected_elements()):
449 self.set_coordinate(coordinate)
450 self.mouse_pressed = True
451 self.update_selected_elements()
452 self.mouse_pressed = False
453 self._context_menu.popup(None, None, None, event.button, event.time)
455 def handle_mouse_selector_press(self, double_click, coordinate):
457 The selector mouse button was pressed:
458 Find the selected element. Attempt a new connection if possible.
459 Open the block params window on a double click.
460 Update the selection state of the flow graph.
462 self.press_coor = coordinate
463 self.set_coordinate(coordinate)
465 self.mouse_pressed = True
466 if double_click: self.unselect()
467 self.update_selected_elements()
468 #double click detected, bring up params dialog if possible
469 if double_click and self.get_selected_block():
470 self.mouse_pressed = False
471 Actions.BLOCK_PARAM_MODIFY()
473 def handle_mouse_selector_release(self, coordinate):
475 The selector mouse button was released:
476 Update the state, handle motion (dragging).
477 And update the selected flowgraph elements.
479 self.set_coordinate(coordinate)
481 self.mouse_pressed = False
482 if self.element_moved:
484 self.element_moved = False
485 self.update_selected_elements()
487 def handle_mouse_motion(self, coordinate):
489 The mouse has moved, respond to mouse dragging.
490 Move a selected element to the new coordinate.
491 Auto-scroll the scroll bars at the boundaries.
493 #to perform a movement, the mouse must be pressed, no pending events
494 if gtk.events_pending() or not self.mouse_pressed: return
495 #perform autoscrolling
496 width, height = self.get_size()
498 h_adj = self.get_scroll_pane().get_hadjustment()
499 v_adj = self.get_scroll_pane().get_vadjustment()
500 for pos, length, adj, adj_val, adj_len in (
501 (x, width, h_adj, h_adj.get_value(), h_adj.page_size),
502 (y, height, v_adj, v_adj.get_value(), v_adj.page_size),
504 #scroll if we moved near the border
505 if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len:
506 adj.set_value(adj_val+SCROLL_DISTANCE)
508 elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY:
509 adj.set_value(adj_val-SCROLL_DISTANCE)
511 #remove the connection if selected in drag event
512 if len(self.get_selected_elements()) == 1 and self.get_selected_element().is_connection():
513 Actions.ELEMENT_DELETE()
514 #move the selected elements and record the new coordinate
515 X, Y = self.get_coordinate()
516 if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y)))
517 self.set_coordinate((x, y))
518 #queue draw for animation