2 Copyright 2007, 2008, 2009 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 ###########################################################################
57 ###########################################################################
58 def get_drawing_area(self): return self.drawing_area
59 def queue_draw(self): self.get_drawing_area().queue_draw()
60 def get_size(self): return self.get_drawing_area().get_size_request()
61 def set_size(self, *args): self.get_drawing_area().set_size_request(*args)
62 def get_scroll_pane(self): return self.drawing_area.get_parent()
63 def get_ctrl_mask(self): return self.drawing_area.ctrl_mask
64 def new_pixmap(self, *args): return self.get_drawing_area().new_pixmap(*args)
66 def add_new_block(self, key, coor=None):
68 Add a block of the given key to this flow graph.
69 @param key the block key
70 @param coor an optional coordinate or None for random
72 id = self._get_unique_id(key)
73 #calculate the position coordinate
74 h_adj = self.get_scroll_pane().get_hadjustment()
75 v_adj = self.get_scroll_pane().get_vadjustment()
76 if coor is None: coor = (
77 int(random.uniform(.25, .75)*h_adj.page_size + h_adj.get_value()),
78 int(random.uniform(.25, .75)*v_adj.page_size + v_adj.get_value()),
81 block = self.get_new_block(key)
82 block.set_coordinate(coor)
84 block.get_param('id').set_value(id)
85 Actions.ELEMENT_CREATE()
87 ###########################################################################
89 ###########################################################################
90 def copy_to_clipboard(self):
92 Copy the selected blocks and connections into the clipboard.
96 blocks = self.get_selected_blocks()
97 if not blocks: return None
99 x_min, y_min = blocks[0].get_coordinate()
101 x, y = block.get_coordinate()
102 x_min = min(x, x_min)
103 y_min = min(y, y_min)
104 #get connections between selected blocks
105 connections = filter(
106 lambda c: c.get_source().get_parent() in blocks and c.get_sink().get_parent() in blocks,
107 self.get_connections(),
111 [block.export_data() for block in blocks],
112 [connection.export_data() for connection in connections],
116 def paste_from_clipboard(self, clipboard):
118 Paste the blocks and connections from the clipboard.
119 @param clipboard the nested data of blocks, connections
122 (x_min, y_min), blocks_n, connections_n = clipboard
123 old_id2block = dict()
125 h_adj = self.get_scroll_pane().get_hadjustment()
126 v_adj = self.get_scroll_pane().get_vadjustment()
127 x_off = h_adj.get_value() - x_min + h_adj.page_size/4
128 y_off = v_adj.get_value() - y_min + v_adj.page_size/4
130 for block_n in blocks_n:
131 block_key = block_n.find('key')
132 if block_key == 'options': continue
133 block = self.get_new_block(block_key)
136 params_n = block_n.findall('param')
137 for param_n in params_n:
138 param_key = param_n.find('key')
139 param_value = param_n.find('value')
141 if param_key == 'id':
142 old_id2block[param_value] = block
143 #if the block id is not unique, get a new block id
144 if param_value in [block.get_id() for block in self.get_blocks()]:
145 param_value = self._get_unique_id(param_value)
147 block.get_param(param_key).set_value(param_value)
148 #move block to offset coordinate
149 block.move((x_off, y_off))
150 #update before creating connections
153 for connection_n in connections_n:
154 source = old_id2block[connection_n.find('source_block_id')].get_source(connection_n.find('source_key'))
155 sink = old_id2block[connection_n.find('sink_block_id')].get_sink(connection_n.find('sink_key'))
156 self.connect(source, sink)
157 #set all pasted elements selected
158 for block in selected: selected = selected.union(set(block.get_connections()))
159 self._selected_elements = list(selected)
161 ###########################################################################
163 ###########################################################################
164 def type_controller_modify_selected(self, direction):
166 Change the registered type controller for the selected signal blocks.
167 @param direction +1 or -1
168 @return true for change
170 return any([sb.type_controller_modify(direction) for sb in self.get_selected_blocks()])
172 def port_controller_modify_selected(self, direction):
174 Change port controller for the selected signal blocks.
175 @param direction +1 or -1
176 @return true for changed
178 return any([sb.port_controller_modify(direction) for sb in self.get_selected_blocks()])
180 def enable_selected(self, enable):
182 Enable/disable the selected blocks.
183 @param enable true to enable
184 @return true if changed
187 for selected_block in self.get_selected_blocks():
188 if selected_block.get_enabled() != enable:
189 selected_block.set_enabled(enable)
193 def move_selected(self, delta_coordinate):
195 Move the element and by the change in coordinates.
196 @param delta_coordinate the change in coordinates
198 for selected_block in self.get_selected_blocks():
199 selected_block.move(delta_coordinate)
200 self.element_moved = True
202 def rotate_selected(self, rotation):
204 Rotate the selected blocks by multiples of 90 degrees.
205 @param rotation the rotation in degrees
206 @return true if changed, otherwise false.
208 if not self.get_selected_blocks(): return False
209 #initialize min and max coordinates
210 min_x, min_y = self.get_selected_block().get_coordinate()
211 max_x, max_y = self.get_selected_block().get_coordinate()
212 #rotate each selected block, and find min/max coordinate
213 for selected_block in self.get_selected_blocks():
214 selected_block.rotate(rotation)
215 #update the min/max coordinate
216 x, y = selected_block.get_coordinate()
217 min_x, min_y = min(min_x, x), min(min_y, y)
218 max_x, max_y = max(max_x, x), max(max_y, y)
219 #calculate center point of slected blocks
220 ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
221 #rotate the blocks around the center point
222 for selected_block in self.get_selected_blocks():
223 x, y = selected_block.get_coordinate()
224 x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
225 selected_block.set_coordinate((x + ctr_x, y + ctr_y))
228 def remove_selected(self):
230 Remove selected elements
231 @return true if changed.
234 for selected_element in self.get_selected_elements():
235 self.remove_element(selected_element)
239 def draw(self, gc, window):
241 Draw the background and grid if enabled.
242 Draw all of the elements in this flow graph onto the pixmap.
243 Draw the pixmap to the drawable window of this flow graph.
245 W,H = self.get_size()
247 gc.set_foreground(Colors.FLOWGRAPH_BACKGROUND_COLOR)
248 window.draw_rectangle(gc, True, 0, 0, W, H)
249 #draw multi select rectangle
250 if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()):
252 x1, y1 = self.press_coor
253 x2, y2 = self.get_coordinate()
254 #calculate top-left coordinate and width/height
255 x, y = int(min(x1, x2)), int(min(y1, y2))
256 w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
258 gc.set_foreground(Colors.HIGHLIGHT_COLOR)
259 window.draw_rectangle(gc, True, x, y, w, h)
260 gc.set_foreground(Colors.BORDER_COLOR)
261 window.draw_rectangle(gc, False, x, y, w, h)
262 #draw blocks on top of connections
263 for element in self.get_connections() + self.get_blocks():
264 element.draw(gc, window)
265 #draw selected blocks on top of selected connections
266 for selected_element in self.get_selected_connections() + self.get_selected_blocks():
267 selected_element.draw(gc, window)
269 def update_selected(self):
271 Remove deleted elements from the selected elements list.
272 Update highlighting so only the selected are highlighted.
274 selected_elements = self.get_selected_elements()
275 elements = self.get_elements()
276 #remove deleted elements
277 for selected in selected_elements:
278 if selected in elements: continue
279 selected_elements.remove(selected)
280 try: assert self._old_selected_port.get_parent() in elements
281 except: self._old_selected_port = None
282 try: assert self._new_selected_port.get_parent() in elements
283 except: self._new_selected_port = None
285 for element in elements:
286 element.set_highlighted(element in selected_elements)
290 Call the top level rewrite and validate.
291 Call the top level create labels and shapes.
298 ##########################################################################
300 ##########################################################################
303 Set selected elements to an empty set.
305 self._selected_elements = []
307 def what_is_selected(self, coor, coor_m=None):
310 At the given coordinate, return the elements found to be selected.
311 If coor_m is unspecified, return a list of only the first element found to be selected:
312 Iterate though the elements backwards since top elements are at the end of the list.
313 If an element is selected, place it at the end of the list so that is is drawn last,
314 and hence on top. Update the selected port information.
315 @param coor the coordinate of the mouse click
316 @param coor_m the coordinate for multi select
317 @return the selected blocks and connections or an empty list
322 for element in reversed(self.get_elements()):
323 selected_element = element.what_is_selected(coor, coor_m)
324 if not selected_element: continue
325 #update the selected port information
326 if selected_element.is_port():
327 if not coor_m: selected_port = selected_element
328 selected_element = selected_element.get_parent()
329 selected.add(selected_element)
330 #place at the end of the list
331 self.get_elements().remove(element)
332 self.get_elements().append(element)
333 #single select mode, break
335 #update selected ports
336 self._old_selected_port = self._new_selected_port
337 self._new_selected_port = selected_port
338 return list(selected)
340 def get_selected_connections(self):
342 Get a group of selected connections.
343 @return sub set of connections in this flow graph
346 for selected_element in self.get_selected_elements():
347 if selected_element.is_connection(): selected.add(selected_element)
348 return list(selected)
350 def get_selected_blocks(self):
352 Get a group of selected blocks.
353 @return sub set of blocks in this flow graph
356 for selected_element in self.get_selected_elements():
357 if selected_element.is_block(): selected.add(selected_element)
358 return list(selected)
360 def get_selected_block(self):
362 Get the selected block when a block or port is selected.
363 @return a block or None
365 return self.get_selected_blocks() and self.get_selected_blocks()[0] or None
367 def get_selected_elements(self):
369 Get the group of selected elements.
370 @return sub set of elements in this flow graph
372 return self._selected_elements
374 def get_selected_element(self):
376 Get the selected element.
377 @return a block, port, or connection or None
379 return self.get_selected_elements() and self.get_selected_elements()[0] or None
381 def update_selected_elements(self):
383 Update the selected elements.
384 The update behavior depends on the state of the mouse button.
385 When the mouse button pressed the selection will change when
386 the control mask is set or the new selection is not in the current group.
387 When the mouse button is released the selection will change when
388 the mouse has moved and the control mask is set or the current group is empty.
389 Attempt to make a new connection if the old and ports are filled.
390 If the control mask is set, merge with the current elements.
392 selected_elements = None
393 if self.mouse_pressed:
394 new_selections = self.what_is_selected(self.get_coordinate())
395 #update the selections if the new selection is not in the current selections
396 #allows us to move entire selected groups of elements
397 if self.get_ctrl_mask() or not (
398 new_selections and new_selections[0] in self.get_selected_elements()
399 ): selected_elements = new_selections
400 else: #called from a mouse release
401 if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()):
402 selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor)
403 #this selection and the last were ports, try to connect them
404 if self._old_selected_port and self._new_selected_port and \
405 self._old_selected_port is not self._new_selected_port:
407 self.connect(self._old_selected_port, self._new_selected_port)
408 Actions.ELEMENT_CREATE()
409 except: Messages.send_fail_connection()
410 self._old_selected_port = None
411 self._new_selected_port = None
413 #update selected elements
414 if selected_elements is None: return
415 old_elements = set(self.get_selected_elements())
416 self._selected_elements = list(set(selected_elements))
417 new_elements = set(self.get_selected_elements())
418 #if ctrl, set the selected elements to the union - intersection of old and new
419 if self.get_ctrl_mask():
420 self._selected_elements = list(
421 set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements)
423 Actions.ELEMENT_SELECT()
425 ##########################################################################
427 ##########################################################################
428 def handle_mouse_button_press(self, left_click, double_click, coordinate):
430 A mouse button is pressed, only respond to left clicks.
431 Find the selected element. Attempt a new connection if possible.
432 Open the block params window on a double click.
433 Update the selection state of the flow graph.
435 if not left_click: return
436 self.press_coor = coordinate
437 self.set_coordinate(coordinate)
439 self.mouse_pressed = True
440 if double_click: self.unselect()
441 self.update_selected_elements()
442 #double click detected, bring up params dialog if possible
443 if double_click and self.get_selected_block():
444 self.mouse_pressed = False
445 Actions.BLOCK_PARAM_MODIFY()
447 def handle_mouse_button_release(self, left_click, coordinate):
449 A mouse button is released, record the state.
451 if not left_click: return
452 self.set_coordinate(coordinate)
454 self.mouse_pressed = False
455 if self.element_moved:
457 self.element_moved = False
458 self.update_selected_elements()
460 def handle_mouse_motion(self, coordinate):
462 The mouse has moved, respond to mouse dragging.
463 Move a selected element to the new coordinate.
464 Auto-scroll the scroll bars at the boundaries.
466 #to perform a movement, the mouse must be pressed, no pending events
467 if gtk.events_pending() or not self.mouse_pressed: return
468 #perform autoscrolling
469 width, height = self.get_size()
471 h_adj = self.get_scroll_pane().get_hadjustment()
472 v_adj = self.get_scroll_pane().get_vadjustment()
473 for pos, length, adj, adj_val, adj_len in (
474 (x, width, h_adj, h_adj.get_value(), h_adj.page_size),
475 (y, height, v_adj, v_adj.get_value(), v_adj.page_size),
477 #scroll if we moved near the border
478 if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len:
479 adj.set_value(adj_val+SCROLL_DISTANCE)
481 elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY:
482 adj.set_value(adj_val-SCROLL_DISTANCE)
484 #remove the connection if selected in drag event
485 if len(self.get_selected_elements()) == 1 and self.get_selected_element().is_connection():
486 Actions.ELEMENT_DELETE()
487 #move the selected elements and record the new coordinate
488 X, Y = self.get_coordinate()
489 if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y)))
490 self.set_coordinate((x, y))
491 #queue draw for animation