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
22 ELEMENT_CREATE, ELEMENT_SELECT, \
23 BLOCK_PARAM_MODIFY, BLOCK_MOVE, \
27 from Element import Element
28 from .. base import FlowGraph as _FlowGraph
35 class FlowGraph(Element):
37 FlowGraph is the data structure to store graphical signal blocks,
38 graphical inputs and outputs,
39 and the connections between inputs and outputs.
42 def __init__(self, *args, **kwargs):
45 Create a list for signal blocks and connections. Connect mouse handlers.
47 Element.__init__(self)
48 #when is the flow graph selected? (used by keyboard event handler)
49 self.is_selected = lambda: bool(self.get_selected_elements())
50 #important vars dealing with mouse event tracking
51 self.element_moved = False
52 self.mouse_pressed = False
54 self.press_coor = (0, 0)
56 self._old_selected_port = None
57 self._new_selected_port = None
59 ###########################################################################
61 ###########################################################################
62 def get_drawing_area(self): return self.drawing_area
63 def queue_draw(self): self.get_drawing_area().queue_draw()
64 def get_size(self): return self.get_drawing_area().get_size_request()
65 def set_size(self, *args): self.get_drawing_area().set_size_request(*args)
66 def get_scroll_pane(self): return self.drawing_area.get_parent()
67 def get_ctrl_mask(self): return self.drawing_area.ctrl_mask
68 def new_pixmap(self, *args): return self.get_drawing_area().new_pixmap(*args)
70 def add_new_block(self, key, coor=None):
72 Add a block of the given key to this flow graph.
73 @param key the block key
74 @param coor an optional coordinate or None for random
76 id = self._get_unique_id(key)
77 #calculate the position coordinate
78 h_adj = self.get_scroll_pane().get_hadjustment()
79 v_adj = self.get_scroll_pane().get_vadjustment()
80 if coor is None: coor = (
81 int(random.uniform(.25, .75)*h_adj.page_size + h_adj.get_value()),
82 int(random.uniform(.25, .75)*v_adj.page_size + v_adj.get_value()),
85 block = self.get_new_block(key)
86 block.set_coordinate(coor)
88 block.get_param('id').set_value(id)
89 self.handle_states(ELEMENT_CREATE)
91 ###########################################################################
93 ###########################################################################
94 def copy_to_clipboard(self):
96 Copy the selected blocks and connections into the clipboard.
100 blocks = self.get_selected_blocks()
101 if not blocks: return None
103 x_min, y_min = blocks[0].get_coordinate()
105 x, y = block.get_coordinate()
106 x_min = min(x, x_min)
107 y_min = min(y, y_min)
108 #get connections between selected blocks
109 connections = filter(
110 lambda c: c.get_source().get_parent() in blocks and c.get_sink().get_parent() in blocks,
111 self.get_connections(),
115 [block.export_data() for block in blocks],
116 [connection.export_data() for connection in connections],
120 def paste_from_clipboard(self, clipboard):
122 Paste the blocks and connections from the clipboard.
123 @param clipboard the nested data of blocks, connections
126 (x_min, y_min), blocks_n, connections_n = clipboard
127 old_id2block = dict()
129 h_adj = self.get_scroll_pane().get_hadjustment()
130 v_adj = self.get_scroll_pane().get_vadjustment()
131 x_off = h_adj.get_value() - x_min + h_adj.page_size/4
132 y_off = v_adj.get_value() - y_min + v_adj.page_size/4
134 for block_n in blocks_n:
135 block_key = block_n.find('key')
136 if block_key == 'options': continue
137 block = self.get_new_block(block_key)
140 params_n = block_n.findall('param')
141 for param_n in params_n:
142 param_key = param_n.find('key')
143 param_value = param_n.find('value')
145 if param_key == 'id':
146 old_id2block[param_value] = block
147 #if the block id is not unique, get a new block id
148 if param_value in [block.get_id() for block in self.get_blocks()]:
149 param_value = self._get_unique_id(param_value)
151 block.get_param(param_key).set_value(param_value)
152 #move block to offset coordinate
153 block.move((x_off, y_off))
154 #update before creating connections
157 for connection_n in connections_n:
158 source = old_id2block[connection_n.find('source_block_id')].get_source(connection_n.find('source_key'))
159 sink = old_id2block[connection_n.find('sink_block_id')].get_sink(connection_n.find('sink_key'))
160 self.connect(source, sink)
161 #set all pasted elements selected
162 for block in selected: selected = selected.union(set(block.get_connections()))
163 self._selected_elements = list(selected)
165 ###########################################################################
167 ###########################################################################
168 def type_controller_modify_selected(self, direction):
170 Change the registered type controller for the selected signal blocks.
171 @param direction +1 or -1
172 @return true for change
174 return any([sb.type_controller_modify(direction) for sb in self.get_selected_blocks()])
176 def port_controller_modify_selected(self, direction):
178 Change port controller for the selected signal blocks.
179 @param direction +1 or -1
180 @return true for changed
182 return any([sb.port_controller_modify(direction) for sb in self.get_selected_blocks()])
184 def enable_selected(self, enable):
186 Enable/disable the selected blocks.
187 @param enable true to enable
188 @return true if changed
191 for selected_block in self.get_selected_blocks():
192 if selected_block.get_enabled() != enable:
193 selected_block.set_enabled(enable)
197 def move_selected(self, delta_coordinate):
199 Move the element and by the change in coordinates.
200 @param delta_coordinate the change in coordinates
202 for selected_block in self.get_selected_blocks():
203 selected_block.move(delta_coordinate)
204 self.element_moved = True
206 def rotate_selected(self, rotation):
208 Rotate the selected blocks by multiples of 90 degrees.
209 @param rotation the rotation in degrees
210 @return true if changed, otherwise false.
212 if not self.get_selected_blocks(): return False
213 #initialize min and max coordinates
214 min_x, min_y = self.get_selected_block().get_coordinate()
215 max_x, max_y = self.get_selected_block().get_coordinate()
216 #rotate each selected block, and find min/max coordinate
217 for selected_block in self.get_selected_blocks():
218 selected_block.rotate(rotation)
219 #update the min/max coordinate
220 x, y = selected_block.get_coordinate()
221 min_x, min_y = min(min_x, x), min(min_y, y)
222 max_x, max_y = max(max_x, x), max(max_y, y)
223 #calculate center point of slected blocks
224 ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
225 #rotate the blocks around the center point
226 for selected_block in self.get_selected_blocks():
227 x, y = selected_block.get_coordinate()
228 x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
229 selected_block.set_coordinate((x + ctr_x, y + ctr_y))
232 def remove_selected(self):
234 Remove selected elements
235 @return true if changed.
238 for selected_element in self.get_selected_elements():
239 self.remove_element(selected_element)
243 def draw(self, gc, window):
245 Draw the background and grid if enabled.
246 Draw all of the elements in this flow graph onto the pixmap.
247 Draw the pixmap to the drawable window of this flow graph.
249 W,H = self.get_size()
251 gc.set_foreground(Colors.FLOWGRAPH_BACKGROUND_COLOR)
252 window.draw_rectangle(gc, True, 0, 0, W, H)
253 #draw multi select rectangle
254 if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()):
256 x1, y1 = self.press_coor
257 x2, y2 = self.get_coordinate()
258 #calculate top-left coordinate and width/height
259 x, y = int(min(x1, x2)), int(min(y1, y2))
260 w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
262 gc.set_foreground(Colors.HIGHLIGHT_COLOR)
263 window.draw_rectangle(gc, True, x, y, w, h)
264 gc.set_foreground(Colors.BORDER_COLOR)
265 window.draw_rectangle(gc, False, x, y, w, h)
266 #draw blocks on top of connections
267 for element in self.get_connections() + self.get_blocks():
268 element.draw(gc, window)
269 #draw selected blocks on top of selected connections
270 for selected_element in self.get_selected_connections() + self.get_selected_blocks():
271 selected_element.draw(gc, window)
273 def update_selected(self):
275 Remove deleted elements from the selected elements list.
276 Update highlighting so only the selected are highlighted.
278 selected_elements = self.get_selected_elements()
279 elements = self.get_elements()
280 #remove deleted elements
281 for selected in selected_elements:
282 if selected in elements: continue
283 selected_elements.remove(selected)
284 try: assert self._old_selected_port.get_parent() in elements
285 except: self._old_selected_port = None
286 try: assert self._new_selected_port.get_parent() in elements
287 except: self._new_selected_port = None
289 for element in elements:
290 element.set_highlighted(element in selected_elements)
294 Call update on all elements.
296 1) elements call special rewrite rules that may break validation
297 2) elements should come up with the same results, validation can pass
301 for element in self.get_elements(): element.update()
303 ##########################################################################
305 ##########################################################################
308 Set selected elements to an empty set.
310 self._selected_elements = []
312 def what_is_selected(self, coor, coor_m=None):
315 At the given coordinate, return the elements found to be selected.
316 If coor_m is unspecified, return a list of only the first element found to be selected:
317 Iterate though the elements backwards since top elements are at the end of the list.
318 If an element is selected, place it at the end of the list so that is is drawn last,
319 and hence on top. Update the selected port information.
320 @param coor the coordinate of the mouse click
321 @param coor_m the coordinate for multi select
322 @return the selected blocks and connections or an empty list
327 for element in reversed(self.get_elements()):
328 selected_element = element.what_is_selected(coor, coor_m)
329 if not selected_element: continue
330 #update the selected port information
331 if selected_element.is_port():
332 if not coor_m: selected_port = selected_element
333 selected_element = selected_element.get_parent()
334 selected.add(selected_element)
335 #place at the end of the list
336 self.get_elements().remove(element)
337 self.get_elements().append(element)
338 #single select mode, break
340 #update selected ports
341 self._old_selected_port = self._new_selected_port
342 self._new_selected_port = selected_port
343 return list(selected)
345 def get_selected_connections(self):
347 Get a group of selected connections.
348 @return sub set of connections in this flow graph
351 for selected_element in self.get_selected_elements():
352 if selected_element.is_connection(): selected.add(selected_element)
353 return list(selected)
355 def get_selected_blocks(self):
357 Get a group of selected blocks.
358 @return sub set of blocks in this flow graph
361 for selected_element in self.get_selected_elements():
362 if selected_element.is_block(): selected.add(selected_element)
363 return list(selected)
365 def get_selected_block(self):
367 Get the selected block when a block or port is selected.
368 @return a block or None
370 return self.get_selected_blocks() and self.get_selected_blocks()[0] or None
372 def get_selected_elements(self):
374 Get the group of selected elements.
375 @return sub set of elements in this flow graph
377 return self._selected_elements
379 def get_selected_element(self):
381 Get the selected element.
382 @return a block, port, or connection or None
384 return self.get_selected_elements() and self.get_selected_elements()[0] or None
386 def update_selected_elements(self):
388 Update the selected elements.
389 The update behavior depends on the state of the mouse button.
390 When the mouse button pressed the selection will change when
391 the control mask is set or the new selection is not in the current group.
392 When the mouse button is released the selection will change when
393 the mouse has moved and the control mask is set or the current group is empty.
394 Attempt to make a new connection if the old and ports are filled.
395 If the control mask is set, merge with the current elements.
397 selected_elements = None
398 if self.mouse_pressed:
399 new_selections = self.what_is_selected(self.get_coordinate())
400 #update the selections if the new selection is not in the current selections
401 #allows us to move entire selected groups of elements
402 if self.get_ctrl_mask() or not (
403 new_selections and new_selections[0] in self.get_selected_elements()
404 ): selected_elements = new_selections
405 else: #called from a mouse release
406 if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()):
407 selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor)
408 #this selection and the last were ports, try to connect them
409 if self._old_selected_port and self._new_selected_port and \
410 self._old_selected_port is not self._new_selected_port:
412 self.connect(self._old_selected_port, self._new_selected_port)
413 self.handle_states(ELEMENT_CREATE)
414 except: Messages.send_fail_connection()
415 self._old_selected_port = None
416 self._new_selected_port = None
418 #update selected elements
419 if selected_elements is None: return
420 old_elements = set(self.get_selected_elements())
421 self._selected_elements = list(set(selected_elements))
422 new_elements = set(self.get_selected_elements())
423 #if ctrl, set the selected elements to the union - intersection of old and new
424 if self.get_ctrl_mask():
425 self._selected_elements = list(
426 set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements)
428 self.handle_states(ELEMENT_SELECT)
430 ##########################################################################
432 ##########################################################################
433 def handle_mouse_button_press(self, left_click, double_click, coordinate):
435 A mouse button is pressed, only respond to left clicks.
436 Find the selected element. Attempt a new connection if possible.
437 Open the block params window on a double click.
438 Update the selection state of the flow graph.
440 if not left_click: return
441 self.press_coor = coordinate
442 self.set_coordinate(coordinate)
444 self.mouse_pressed = True
445 if double_click: self.unselect()
446 self.update_selected_elements()
447 #double click detected, bring up params dialog if possible
448 if double_click and self.get_selected_block():
449 self.mouse_pressed = False
450 self.handle_states(BLOCK_PARAM_MODIFY)
452 def handle_mouse_button_release(self, left_click, coordinate):
454 A mouse button is released, record the state.
456 if not left_click: return
457 self.set_coordinate(coordinate)
459 self.mouse_pressed = False
460 if self.element_moved:
461 self.handle_states(BLOCK_MOVE)
462 self.element_moved = False
463 self.update_selected_elements()
465 def handle_mouse_motion(self, coordinate):
467 The mouse has moved, respond to mouse dragging.
468 Move a selected element to the new coordinate.
469 Auto-scroll the scroll bars at the boundaries.
471 #to perform a movement, the mouse must be pressed, no pending events
472 if gtk.events_pending() or not self.mouse_pressed: return
473 #perform autoscrolling
474 width, height = self.get_size()
476 h_adj = self.get_scroll_pane().get_hadjustment()
477 v_adj = self.get_scroll_pane().get_vadjustment()
478 for pos, length, adj, adj_val, adj_len in (
479 (x, width, h_adj, h_adj.get_value(), h_adj.page_size),
480 (y, height, v_adj, v_adj.get_value(), v_adj.page_size),
482 #scroll if we moved near the border
483 if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len:
484 adj.set_value(adj_val+SCROLL_DISTANCE)
486 elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY:
487 adj.set_value(adj_val-SCROLL_DISTANCE)
489 #remove the connection if selected in drag event
490 if len(self.get_selected_elements()) == 1 and self.get_selected_element().is_connection():
491 self.handle_states(ELEMENT_DELETE)
492 #move the selected elements and record the new coordinate
493 X, Y = self.get_coordinate()
494 if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y)))
495 self.set_coordinate((x, y))
496 #queue draw for animation