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
25 from .. base import FlowGraph as _FlowGraph
32 class FlowGraph(Element):
34 FlowGraph is the data structure to store graphical signal blocks,
35 graphical inputs and outputs,
36 and the connections between inputs and outputs.
42 Create a list for signal blocks and connections. Connect mouse handlers.
44 Element.__init__(self)
45 #when is the flow graph selected? (used by keyboard event handler)
46 self.is_selected = lambda: bool(self.get_selected_elements())
47 #important vars dealing with mouse event tracking
48 self.element_moved = False
49 self.mouse_pressed = False
51 self.press_coor = (0, 0)
53 self._old_selected_port = None
54 self._new_selected_port = None
56 ###########################################################################
58 ###########################################################################
59 def get_drawing_area(self): return self.drawing_area
60 def queue_draw(self): self.get_drawing_area().queue_draw()
61 def get_size(self): return self.get_drawing_area().get_size_request()
62 def set_size(self, *args): self.get_drawing_area().set_size_request(*args)
63 def get_scroll_pane(self): return self.drawing_area.get_parent()
64 def get_ctrl_mask(self): return self.drawing_area.ctrl_mask
65 def new_pixmap(self, *args): return self.get_drawing_area().new_pixmap(*args)
67 def add_new_block(self, key, coor=None):
69 Add a block of the given key to this flow graph.
70 @param key the block key
71 @param coor an optional coordinate or None for random
73 id = self._get_unique_id(key)
74 #calculate the position coordinate
75 h_adj = self.get_scroll_pane().get_hadjustment()
76 v_adj = self.get_scroll_pane().get_vadjustment()
77 if coor is None: coor = (
78 int(random.uniform(.25, .75)*h_adj.page_size + h_adj.get_value()),
79 int(random.uniform(.25, .75)*v_adj.page_size + v_adj.get_value()),
82 block = self.get_new_block(key)
83 block.set_coordinate(coor)
85 block.get_param('id').set_value(id)
86 Actions.ELEMENT_CREATE()
88 ###########################################################################
90 ###########################################################################
91 def copy_to_clipboard(self):
93 Copy the selected blocks and connections into the clipboard.
97 blocks = self.get_selected_blocks()
98 if not blocks: return None
100 x_min, y_min = blocks[0].get_coordinate()
102 x, y = block.get_coordinate()
103 x_min = min(x, x_min)
104 y_min = min(y, y_min)
105 #get connections between selected blocks
106 connections = filter(
107 lambda c: c.get_source().get_parent() in blocks and c.get_sink().get_parent() in blocks,
108 self.get_connections(),
112 [block.export_data() for block in blocks],
113 [connection.export_data() for connection in connections],
117 def paste_from_clipboard(self, clipboard):
119 Paste the blocks and connections from the clipboard.
120 @param clipboard the nested data of blocks, connections
123 (x_min, y_min), blocks_n, connections_n = clipboard
124 old_id2block = dict()
126 h_adj = self.get_scroll_pane().get_hadjustment()
127 v_adj = self.get_scroll_pane().get_vadjustment()
128 x_off = h_adj.get_value() - x_min + h_adj.page_size/4
129 y_off = v_adj.get_value() - y_min + v_adj.page_size/4
131 for block_n in blocks_n:
132 block_key = block_n.find('key')
133 if block_key == 'options': continue
134 block = self.get_new_block(block_key)
137 params_n = block_n.findall('param')
138 for param_n in params_n:
139 param_key = param_n.find('key')
140 param_value = param_n.find('value')
142 if param_key == 'id':
143 old_id2block[param_value] = block
144 #if the block id is not unique, get a new block id
145 if param_value in [block.get_id() for block in self.get_blocks()]:
146 param_value = self._get_unique_id(param_value)
148 block.get_param(param_key).set_value(param_value)
149 #move block to offset coordinate
150 block.move((x_off, y_off))
151 #update before creating connections
154 for connection_n in connections_n:
155 source = old_id2block[connection_n.find('source_block_id')].get_source(connection_n.find('source_key'))
156 sink = old_id2block[connection_n.find('sink_block_id')].get_sink(connection_n.find('sink_key'))
157 self.connect(source, sink)
158 #set all pasted elements selected
159 for block in selected: selected = selected.union(set(block.get_connections()))
160 self._selected_elements = list(selected)
162 ###########################################################################
164 ###########################################################################
165 def type_controller_modify_selected(self, direction):
167 Change the registered type controller for the selected signal blocks.
168 @param direction +1 or -1
169 @return true for change
171 return any([sb.type_controller_modify(direction) for sb in self.get_selected_blocks()])
173 def port_controller_modify_selected(self, direction):
175 Change port controller for the selected signal blocks.
176 @param direction +1 or -1
177 @return true for changed
179 return any([sb.port_controller_modify(direction) for sb in self.get_selected_blocks()])
181 def enable_selected(self, enable):
183 Enable/disable the selected blocks.
184 @param enable true to enable
185 @return true if changed
188 for selected_block in self.get_selected_blocks():
189 if selected_block.get_enabled() != enable:
190 selected_block.set_enabled(enable)
194 def move_selected(self, delta_coordinate):
196 Move the element and by the change in coordinates.
197 @param delta_coordinate the change in coordinates
199 for selected_block in self.get_selected_blocks():
200 selected_block.move(delta_coordinate)
201 self.element_moved = True
203 def rotate_selected(self, rotation):
205 Rotate the selected blocks by multiples of 90 degrees.
206 @param rotation the rotation in degrees
207 @return true if changed, otherwise false.
209 if not self.get_selected_blocks(): return False
210 #initialize min and max coordinates
211 min_x, min_y = self.get_selected_block().get_coordinate()
212 max_x, max_y = self.get_selected_block().get_coordinate()
213 #rotate each selected block, and find min/max coordinate
214 for selected_block in self.get_selected_blocks():
215 selected_block.rotate(rotation)
216 #update the min/max coordinate
217 x, y = selected_block.get_coordinate()
218 min_x, min_y = min(min_x, x), min(min_y, y)
219 max_x, max_y = max(max_x, x), max(max_y, y)
220 #calculate center point of slected blocks
221 ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
222 #rotate the blocks around the center point
223 for selected_block in self.get_selected_blocks():
224 x, y = selected_block.get_coordinate()
225 x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
226 selected_block.set_coordinate((x + ctr_x, y + ctr_y))
229 def remove_selected(self):
231 Remove selected elements
232 @return true if changed.
235 for selected_element in self.get_selected_elements():
236 self.remove_element(selected_element)
240 def draw(self, gc, window):
242 Draw the background and grid if enabled.
243 Draw all of the elements in this flow graph onto the pixmap.
244 Draw the pixmap to the drawable window of this flow graph.
246 W,H = self.get_size()
248 gc.set_foreground(Colors.FLOWGRAPH_BACKGROUND_COLOR)
249 window.draw_rectangle(gc, True, 0, 0, W, H)
250 #draw multi select rectangle
251 if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()):
253 x1, y1 = self.press_coor
254 x2, y2 = self.get_coordinate()
255 #calculate top-left coordinate and width/height
256 x, y = int(min(x1, x2)), int(min(y1, y2))
257 w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
259 gc.set_foreground(Colors.HIGHLIGHT_COLOR)
260 window.draw_rectangle(gc, True, x, y, w, h)
261 gc.set_foreground(Colors.BORDER_COLOR)
262 window.draw_rectangle(gc, False, x, y, w, h)
263 #draw blocks on top of connections
264 for element in self.get_connections() + self.get_blocks():
265 element.draw(gc, window)
266 #draw selected blocks on top of selected connections
267 for selected_element in self.get_selected_connections() + self.get_selected_blocks():
268 selected_element.draw(gc, window)
270 def update_selected(self):
272 Remove deleted elements from the selected elements list.
273 Update highlighting so only the selected are highlighted.
275 selected_elements = self.get_selected_elements()
276 elements = self.get_elements()
277 #remove deleted elements
278 for selected in selected_elements:
279 if selected in elements: continue
280 selected_elements.remove(selected)
281 try: assert self._old_selected_port.get_parent() in elements
282 except: self._old_selected_port = None
283 try: assert self._new_selected_port.get_parent() in elements
284 except: self._new_selected_port = None
286 for element in elements:
287 element.set_highlighted(element in selected_elements)
291 Call the top level rewrite and validate.
292 Call the top level create labels and shapes.
299 ##########################################################################
301 ##########################################################################
304 Set selected elements to an empty set.
306 self._selected_elements = []
308 def what_is_selected(self, coor, coor_m=None):
311 At the given coordinate, return the elements found to be selected.
312 If coor_m is unspecified, return a list of only the first element found to be selected:
313 Iterate though the elements backwards since top elements are at the end of the list.
314 If an element is selected, place it at the end of the list so that is is drawn last,
315 and hence on top. Update the selected port information.
316 @param coor the coordinate of the mouse click
317 @param coor_m the coordinate for multi select
318 @return the selected blocks and connections or an empty list
323 for element in reversed(self.get_elements()):
324 selected_element = element.what_is_selected(coor, coor_m)
325 if not selected_element: continue
326 #update the selected port information
327 if selected_element.is_port():
328 if not coor_m: selected_port = selected_element
329 selected_element = selected_element.get_parent()
330 selected.add(selected_element)
331 #place at the end of the list
332 self.get_elements().remove(element)
333 self.get_elements().append(element)
334 #single select mode, break
336 #update selected ports
337 self._old_selected_port = self._new_selected_port
338 self._new_selected_port = selected_port
339 return list(selected)
341 def get_selected_connections(self):
343 Get a group of selected connections.
344 @return sub set of connections in this flow graph
347 for selected_element in self.get_selected_elements():
348 if selected_element.is_connection(): selected.add(selected_element)
349 return list(selected)
351 def get_selected_blocks(self):
353 Get a group of selected blocks.
354 @return sub set of blocks in this flow graph
357 for selected_element in self.get_selected_elements():
358 if selected_element.is_block(): selected.add(selected_element)
359 return list(selected)
361 def get_selected_block(self):
363 Get the selected block when a block or port is selected.
364 @return a block or None
366 return self.get_selected_blocks() and self.get_selected_blocks()[0] or None
368 def get_selected_elements(self):
370 Get the group of selected elements.
371 @return sub set of elements in this flow graph
373 return self._selected_elements
375 def get_selected_element(self):
377 Get the selected element.
378 @return a block, port, or connection or None
380 return self.get_selected_elements() and self.get_selected_elements()[0] or None
382 def update_selected_elements(self):
384 Update the selected elements.
385 The update behavior depends on the state of the mouse button.
386 When the mouse button pressed the selection will change when
387 the control mask is set or the new selection is not in the current group.
388 When the mouse button is released the selection will change when
389 the mouse has moved and the control mask is set or the current group is empty.
390 Attempt to make a new connection if the old and ports are filled.
391 If the control mask is set, merge with the current elements.
393 selected_elements = None
394 if self.mouse_pressed:
395 new_selections = self.what_is_selected(self.get_coordinate())
396 #update the selections if the new selection is not in the current selections
397 #allows us to move entire selected groups of elements
398 if self.get_ctrl_mask() or not (
399 new_selections and new_selections[0] in self.get_selected_elements()
400 ): selected_elements = new_selections
401 else: #called from a mouse release
402 if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()):
403 selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor)
404 #this selection and the last were ports, try to connect them
405 if self._old_selected_port and self._new_selected_port and \
406 self._old_selected_port is not self._new_selected_port:
408 self.connect(self._old_selected_port, self._new_selected_port)
409 Actions.ELEMENT_CREATE()
410 except: Messages.send_fail_connection()
411 self._old_selected_port = None
412 self._new_selected_port = None
414 #update selected elements
415 if selected_elements is None: return
416 old_elements = set(self.get_selected_elements())
417 self._selected_elements = list(set(selected_elements))
418 new_elements = set(self.get_selected_elements())
419 #if ctrl, set the selected elements to the union - intersection of old and new
420 if self.get_ctrl_mask():
421 self._selected_elements = list(
422 set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements)
424 Actions.ELEMENT_SELECT()
426 ##########################################################################
428 ##########################################################################
429 def handle_mouse_button_press(self, left_click, double_click, coordinate):
431 A mouse button is pressed, only respond to left clicks.
432 Find the selected element. Attempt a new connection if possible.
433 Open the block params window on a double click.
434 Update the selection state of the flow graph.
436 if not left_click: return
437 self.press_coor = coordinate
438 self.set_coordinate(coordinate)
440 self.mouse_pressed = True
441 if double_click: self.unselect()
442 self.update_selected_elements()
443 #double click detected, bring up params dialog if possible
444 if double_click and self.get_selected_block():
445 self.mouse_pressed = False
446 Actions.BLOCK_PARAM_MODIFY()
448 def handle_mouse_button_release(self, left_click, coordinate):
450 A mouse button is released, record the state.
452 if not left_click: return
453 self.set_coordinate(coordinate)
455 self.mouse_pressed = False
456 if self.element_moved:
458 self.element_moved = False
459 self.update_selected_elements()
461 def handle_mouse_motion(self, coordinate):
463 The mouse has moved, respond to mouse dragging.
464 Move a selected element to the new coordinate.
465 Auto-scroll the scroll bars at the boundaries.
467 #to perform a movement, the mouse must be pressed, no pending events
468 if gtk.events_pending() or not self.mouse_pressed: return
469 #perform autoscrolling
470 width, height = self.get_size()
472 h_adj = self.get_scroll_pane().get_hadjustment()
473 v_adj = self.get_scroll_pane().get_vadjustment()
474 for pos, length, adj, adj_val, adj_len in (
475 (x, width, h_adj, h_adj.get_value(), h_adj.page_size),
476 (y, height, v_adj, v_adj.get_value(), v_adj.page_size),
478 #scroll if we moved near the border
479 if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len:
480 adj.set_value(adj_val+SCROLL_DISTANCE)
482 elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY:
483 adj.set_value(adj_val-SCROLL_DISTANCE)
485 #remove the connection if selected in drag event
486 if len(self.get_selected_elements()) == 1 and self.get_selected_element().is_connection():
487 Actions.ELEMENT_DELETE()
488 #move the selected elements and record the new coordinate
489 X, Y = self.get_coordinate()
490 if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y)))
491 self.set_coordinate((x, y))
492 #queue draw for animation