moved author attribution out of individual files and put in AUTHORS
[debian/gnuradio] / grc / src / grc / gui / elements / FlowGraph.py
1 """
2 Copyright 2007 Free Software Foundation, Inc.
3 This file is part of GNU Radio
4
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.
9
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.
14
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
18 """
19 ##@package grc.gui.elements.FlowGraph
20 #A flow graph structure for storing signal blocks and their connections.
21
22 from grc import Preferences
23 from grc import Utils
24 from grc.Constants import *
25 from grc.Actions import *
26 import Colors
27 from grc.gui.ParamsDialog import ParamsDialog
28 from Element import Element
29 from grc.elements import FlowGraph as _FlowGraph
30
31 import pygtk
32 pygtk.require('2.0')
33 import gtk
34
35 import random
36 import time
37 from grc import Messages
38
39 class FlowGraph(Element):
40         """
41         FlowGraph is the data structure to store graphical signal blocks,
42         graphical inputs and outputs,
43         and the connections between inputs and outputs.
44         """
45
46         def __init__(self, *args, **kwargs):
47                 """!
48                 FlowGraph contructor.
49                 Create a list for signal blocks and connections. Connect mouse handlers.
50                 """
51                 Element.__init__(self)
52                 #when is the flow graph selected? (used by keyboard event handler)
53                 self.is_selected = lambda: bool(self.get_selected_elements())
54                 #important vars dealing with mouse event tracking
55                 self.element_moved = False
56                 self.mouse_pressed = False
57                 self.unselect()
58                 self.time = 0
59                 self.press_coor = (0, 0)
60                 #selected ports
61                 self._old_selected_port = None
62                 self._new_selected_port = None
63
64         def _get_unique_id(self, base_id=''):
65                 """!
66                 Get a unique id starting with the base id.
67                 @param base_id the id starts with this and appends a count
68                 @return a unique id
69                 """
70                 index = -1
71                 while True:
72                         id = (index < 0) and base_id or '%s%d'%(base_id, index)
73                         index = index + 1
74                         #make sure that the id is not used by another block
75                         if not filter(lambda b: b.get_id() == id, self.get_blocks()): return id
76
77 ###########################################################################
78 # Access Drawing Area
79 ###########################################################################
80         def get_drawing_area(self): return self.drawing_area
81         def get_gc(self): return self.get_drawing_area().gc
82         def get_pixmap(self): return self.get_drawing_area().pixmap
83         def get_size(self): return self.get_drawing_area().get_size_request()
84         def set_size(self, *args): self.get_drawing_area().set_size_request(*args)
85         def get_window(self): return self.get_drawing_area().window
86         def get_scroll_pane(self): return self.drawing_area.get_parent()
87         def get_ctrl_mask(self): return self.drawing_area.ctrl_mask
88
89         def add_new_block(self, key):
90                 """!
91                 Add a block of the given key to this flow graph.
92                 @param key the block key
93                 """
94                 id = self._get_unique_id(key)
95                 #calculate the position coordinate
96                 h_adj = self.get_scroll_pane().get_hadjustment()
97                 v_adj = self.get_scroll_pane().get_vadjustment()
98                 x = int(random.uniform(.25, .75)*h_adj.page_size + h_adj.get_value())
99                 y = int(random.uniform(.25, .75)*v_adj.page_size + v_adj.get_value())
100                 #get the new block
101                 block = self.get_new_block(key)
102                 block.set_coordinate((x, y))
103                 block.set_rotation(0)
104                 block.get_param('id').set_value(id)
105                 self.handle_states(ELEMENT_CREATE)
106
107         ###########################################################################
108         # Copy Paste
109         ###########################################################################
110         def copy_to_clipboard(self):
111                 """!
112                 Copy the selected blocks and connections into the clipboard.
113                 @return the clipboard
114                 """
115                 #get selected blocks
116                 blocks = self.get_selected_blocks()
117                 if not blocks: return None
118                 #calc x and y min
119                 x_min, y_min = blocks[0].get_coordinate()
120                 for block in blocks:
121                         x, y = block.get_coordinate()
122                         x_min = min(x, x_min)
123                         y_min = min(y, y_min)
124                 #get connections between selected blocks
125                 connections = filter(
126                         lambda c: c.get_source().get_parent() in blocks and c.get_sink().get_parent() in blocks,
127                         self.get_connections(),
128                 )
129                 clipboard = (
130                         (x_min, y_min),
131                         [block.export_data() for block in blocks],
132                         [connection.export_data() for connection in connections],
133                 )
134                 return clipboard
135
136         def paste_from_clipboard(self, clipboard):
137                 """!
138                 Paste the blocks and connections from the clipboard.
139                 @param clipboard the nested data of blocks, connections
140                 """
141                 selected = set()
142                 (x_min, y_min), blocks_n, connections_n = clipboard
143                 old_id2block = dict()
144                 #recalc the position
145                 h_adj = self.get_scroll_pane().get_hadjustment()
146                 v_adj = self.get_scroll_pane().get_vadjustment()
147                 x_off = h_adj.get_value() - x_min + h_adj.page_size/4
148                 y_off = v_adj.get_value() - y_min + v_adj.page_size/4
149                 #create blocks
150                 for block_n in blocks_n:
151                         block_key = block_n['key']
152                         if block_key == 'options': continue
153                         block_id = self._get_unique_id(block_key)
154                         block = self.get_new_block(block_key)
155                         selected.add(block)
156                         #set params
157                         params_n = Utils.listify(block_n, 'param')
158                         for param_n in params_n:
159                                 param_key = param_n['key']
160                                 param_value = param_n['value']
161                                 #setup id parameter
162                                 if param_key == 'id':
163                                         old_id2block[param_value] = block
164                                         param_value = block_id
165                                 #set value to key
166                                 block.get_param(param_key).set_value(param_value)
167                         #move block to offset coordinate
168                         block.move((x_off, y_off))
169                 #create connections
170                 for connection_n in connections_n:
171                         source = old_id2block[connection_n['source_block_id']].get_source(connection_n['source_key'])
172                         sink = old_id2block[connection_n['sink_block_id']].get_sink(connection_n['sink_key'])
173                         self.connect(source, sink)
174                 #set all pasted elements selected
175                 for block in selected: selected = selected.union(set(block.get_connections()))
176                 self._selected_elements = list(selected)
177
178         ###########################################################################
179         # Modify Selected
180         ###########################################################################
181         def type_controller_modify_selected(self, direction):
182                 """!
183                 Change the registered type controller for the selected signal blocks.
184                 @param direction +1 or -1
185                 @return true for change
186                 """
187                 changed = False
188                 for selected_block in self.get_selected_blocks():
189                         for child in selected_block.get_params() + selected_block.get_ports():
190                                 #find a param that controls a type
191                                 type_param = None
192                                 for param in selected_block.get_params():
193                                         if not type_param and param.is_enum(): type_param = param
194                                         if param.is_enum() and param.get_key() in child._type: type_param = param
195                                 if type_param:
196                                         #try to increment the enum by direction
197                                         try:
198                                                 keys = type_param.get_option_keys()
199                                                 old_index = keys.index(type_param.get_value())
200                                                 new_index = (old_index + direction + len(keys))%len(keys)
201                                                 type_param.set_value(keys[new_index])
202                                                 changed = True
203                                         except: pass
204                 return changed
205
206         def port_controller_modify_selected(self, direction):
207                 """!
208                 Change port controller for the selected signal blocks.
209                 @param direction +1 or -1
210                 @return true for changed
211                 """
212                 changed = False
213                 for selected_block in self.get_selected_blocks():
214                         for ports in (selected_block.get_sources(), selected_block.get_sinks()):
215                                 if ports and hasattr(ports[0], 'get_nports') and ports[0].get_nports():
216                                         #find the param that controls port0
217                                         for param in selected_block.get_params():
218                                                 if param.get_key() in ports[0]._nports:
219                                                         #try to increment the port controller by direction
220                                                         try:
221                                                                 value = param.evaluate()
222                                                                 value = value + direction
223                                                                 assert(0 < value <= MAX_NUM_PORTS)
224                                                                 param.set_value(value)
225                                                                 changed = True
226                                                         except: pass
227                 return changed
228
229         def param_modify_selected(self):
230                 """!
231                 Create and show a param modification dialog for the selected block.
232                 @return true if parameters were changed
233                 """
234                 if self.get_selected_block():
235                         signal_block_params_dialog = ParamsDialog(self.get_selected_block())
236                         return signal_block_params_dialog.run()
237                 return False
238
239         def enable_selected(self, enable):
240                 """!
241                 Enable/disable the selected blocks.
242                 @param enable true to enable
243                 @return true if changed
244                 """
245                 changed = False
246                 for selected_block in self.get_selected_blocks():
247                         if selected_block.get_enabled() != enable:
248                                 selected_block.set_enabled(enable)
249                                 changed = True
250                 return changed
251
252         def move_selected(self, delta_coordinate):
253                 """!
254                 Move the element and by the change in coordinates.
255                 @param delta_coordinate the change in coordinates
256                 """
257                 for selected_block in self.get_selected_blocks():
258                         selected_block.move(delta_coordinate)
259                         self.element_moved = True
260
261         def rotate_selected(self, direction):
262                 """!
263                 Rotate the selected blocks by 90 degrees.
264                 @param direction DIR_LEFT or DIR_RIGHT
265                 @return true if changed, otherwise false.
266                 """
267                 if not self.get_selected_blocks(): return False
268                 #determine the number of degrees to rotate
269                 direction = {DIR_LEFT: 90, DIR_RIGHT:270}[direction]
270                 cos_r = {0: 1, 90: 0, 180: -1, 270: 0}[direction]
271                 sin_r = {0: 0, 90: 1, 180: 0, 270: -1}[direction]
272                 #initialize min and max coordinates
273                 min_x, min_y = self.get_selected_block().get_coordinate()
274                 max_x, max_y = self.get_selected_block().get_coordinate()
275                 #rotate each selected block, and find min/max coordinate
276                 for selected_block in self.get_selected_blocks():
277                         selected_block.rotate(direction)
278                         #update the min/max coordinate
279                         x, y = selected_block.get_coordinate()
280                         min_x, min_y = min(min_x, x), min(min_y, y)
281                         max_x, max_y = max(max_x, x), max(max_y, y)
282                 #calculate center point of slected blocks
283                 ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
284                 #rotate the blocks around the center point
285                 for selected_block in self.get_selected_blocks():
286                         x, y = selected_block.get_coordinate()
287                         x, y = x - ctr_x, y - ctr_y
288                         x, y = (x*cos_r + y*sin_r + ctr_x, -x*sin_r + y*cos_r + ctr_y)
289                         selected_block.set_coordinate((x, y))
290                 return True
291
292         def remove_selected(self):
293                 """!
294                 Remove selected elements
295                 @return true if changed.
296                 """
297                 changed = False
298                 for selected_element in self.get_selected_elements():
299                         self.remove_element(selected_element)
300                         changed = True
301                 return changed
302
303         def draw(self):
304                 """!
305                 Draw the background and grid if enabled.
306                 Draw all of the elements in this flow graph onto the pixmap.
307                 Draw the pixmap to the drawable window of this flow graph.
308                 """
309                 if self.get_gc():
310                         W,H = self.get_size()
311                         #draw the background
312                         self.get_gc().foreground = Colors.BACKGROUND_COLOR
313                         self.get_pixmap().draw_rectangle(self.get_gc(), True, 0, 0, W, H)
314                         #draw grid (depends on prefs)
315                         if Preferences.show_grid():
316                                 grid_size = Preferences.get_grid_size()
317                                 points = list()
318                                 for i in range(W/grid_size):
319                                         for j in range(H/grid_size):
320                                                 points.append((i*grid_size, j*grid_size))
321                                 self.get_gc().foreground = Colors.TXT_COLOR
322                                 self.get_pixmap().draw_points(self.get_gc(), points)
323                         #draw multi select rectangle
324                         if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()):
325                                 #coordinates
326                                 x1, y1 = self.press_coor
327                                 x2, y2 = self.get_coordinate()
328                                 #calculate top-left coordinate and width/height
329                                 x, y = int(min(x1, x2)), int(min(y1, y2))
330                                 w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
331                                 #draw
332                                 self.get_gc().foreground = Colors.H_COLOR
333                                 self.get_pixmap().draw_rectangle(self.get_gc(), True, x, y, w, h)
334                                 self.get_gc().foreground = Colors.TXT_COLOR
335                                 self.get_pixmap().draw_rectangle(self.get_gc(), False, x, y, w, h)
336                         #draw blocks on top of connections
337                         for element in self.get_connections() + self.get_blocks():
338                                 element.draw(self.get_pixmap())
339                         #draw selected blocks on top of selected connections
340                         for selected_element in self.get_selected_connections() + self.get_selected_blocks():
341                                 selected_element.draw(self.get_pixmap())
342                         self.get_drawing_area().draw()
343
344         def update(self):
345                 """!
346                 Update highlighting so only the selected is highlighted.
347                 Call update on all elements.
348                 Resize the window if size changed.
349                 """
350                 #update highlighting
351                 map(lambda e: e.set_highlighted(False), self.get_elements())
352                 for selected_element in self.get_selected_elements(): 
353                         selected_element.set_highlighted(True)
354                 #update all elements
355                 map(lambda e: e.update(), self.get_elements())
356                 #set the size of the flow graph area
357                 old_x, old_y = self.get_size()
358                 try: new_x, new_y = self.get_option('window_size')
359                 except: new_x, new_y = old_x, old_y
360                 if new_x != old_x or new_y != old_y: self.set_size(new_x, new_y)
361
362         ##########################################################################
363         ## Get Selected
364         ##########################################################################
365         def unselect(self):
366                 """!
367                 Set selected elements to an empty set.
368                 """
369                 self._selected_elements = []
370
371         def what_is_selected(self, coor, coor_m=None):
372                 """!
373                 What is selected?
374                 At the given coordinate, return the elements found to be selected.
375                 If coor_m is unspecified, return a list of only the first element found to be selected:
376                 Iterate though the elements backwardssince top elements are at the end of the list.
377                 If an element is selected, place it at the end of the list so that is is drawn last,
378                 and hence on top. Update the selected port information.
379                 @param coor the coordinate of the mouse click
380                 @param coor_m the coordinate for multi select
381                 @return the selected blocks and connections or an empty list
382                 """
383                 selected_port = None
384                 selected = set()
385                 #check the elements
386                 for element in reversed(self.get_elements()):
387                         selected_element = element.what_is_selected(coor, coor_m)
388                         if not selected_element: continue
389                         #update the selected port information
390                         if selected_element.is_port(): 
391                                 if not coor_m: selected_port = selected_element
392                                 selected_element = selected_element.get_parent()
393                         selected.add(selected_element)
394                         #single select mode, break
395                         if not coor_m:
396                                 self.get_elements().remove(element)
397                                 self.get_elements().append(element)
398                                 break;
399                 #update selected ports
400                 self._old_selected_port = self._new_selected_port
401                 self._new_selected_port = selected_port
402                 return list(selected)
403
404         def get_selected_connections(self):
405                 """!
406                 Get a group of selected connections.
407                 @return sub set of connections in this flow graph
408                 """
409                 selected = set()
410                 for selected_element in self.get_selected_elements():
411                         if selected_element.is_connection(): selected.add(selected_element)
412                 return list(selected)
413
414         def get_selected_blocks(self):
415                 """!
416                 Get a group of selected blocks.
417                 @return sub set of blocks in this flow graph
418                 """
419                 selected = set()
420                 for selected_element in self.get_selected_elements():
421                         if selected_element.is_block(): selected.add(selected_element)
422                 return list(selected)
423
424         def get_selected_block(self):
425                 """!
426                 Get the selected block when a block or port is selected.
427                 @return a block or None
428                 """
429                 return self.get_selected_blocks() and self.get_selected_blocks()[0] or None
430
431         def get_selected_elements(self):
432                 """!
433                 Get the group of selected elements.
434                 @return sub set of elements in this flow graph
435                 """
436                 return self._selected_elements
437
438         def get_selected_element(self):
439                 """!
440                 Get the selected element.
441                 @return a block, port, or connection or None
442                 """
443                 return self.get_selected_elements() and self.get_selected_elements()[0] or None
444
445         def update_selected_elements(self):
446                 """!
447                 Update the selected elements.
448                 The update behavior depends on the state of the mouse button.
449                 When the mouse button pressed the selection will change when
450                 the control mask is set or the new selection is not in the current group.
451                 When the mouse button is released the selection will change when
452                 the mouse has moved and the control mask is set or the current group is empty.
453                 Attempt to make a new connection if the old and ports are filled.
454                 If the control mask is set, merge with the current elements.
455                 """
456                 selected_elements = None
457                 if self.mouse_pressed:
458                         new_selection = self.what_is_selected(self.get_coordinate())
459                         #update the selections if the new selection is not in the current selections
460                         #allows us to move entire selected groups of elements
461                         if self.get_ctrl_mask() or not (
462                                 new_selection and new_selection[0] in self.get_selected_elements()
463                         ): selected_elements = new_selection
464                 else: #called from a mouse release
465                         if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()):
466                                 selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor)
467                 #this selection and the last were ports, try to connect them
468                 if self._old_selected_port and self._new_selected_port and \
469                         self._old_selected_port is not self._new_selected_port:
470                         try:
471                                 self.connect(self._old_selected_port, self._new_selected_port)
472                                 self.handle_states(ELEMENT_CREATE)
473                         except: Messages.send_fail_connection()
474                         self._old_selected_port = None
475                         self._new_selected_port = None
476                         return
477                 #update selected elements
478                 if selected_elements is None: return
479                 old_elements = set(self.get_selected_elements())
480                 self._selected_elements = list(set(selected_elements))
481                 new_elements = set(self.get_selected_elements())
482                 #if ctrl, set the selected elements to the union - intersection of old and new
483                 if self.get_ctrl_mask():
484                         self._selected_elements = list(
485                                 set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements)
486                         )
487                 self.handle_states(ELEMENT_SELECT)
488
489         ##########################################################################
490         ## Event Handlers
491         ##########################################################################
492         def handle_mouse_button_press(self, left_click, double_click, coordinate):
493                 """!
494                 A mouse button is pressed, only respond to left clicks.
495                 Find the selected element. Attempt a new connection if possible.
496                 Open the block params window on a double click.
497                 Update the selection state of the flow graph.
498                 """
499                 if not left_click: return
500                 self.press_coor = coordinate
501                 self.set_coordinate(coordinate)
502                 self.time = 0
503                 self.mouse_pressed = True
504                 self.update_selected_elements()
505                 #double click detected, bring up params dialog if possible
506                 if double_click and self.get_selected_block():
507                         self.mouse_pressed = False
508                         self.handle_states(BLOCK_PARAM_MODIFY)
509
510         def handle_mouse_button_release(self, left_click, coordinate):
511                 """!
512                 A mouse button is released, record the state.
513                 """
514                 if not left_click: return
515                 self.set_coordinate(coordinate)
516                 self.time = 0
517                 self.mouse_pressed = False
518                 if self.element_moved:
519                         if Preferences.snap_to_grid():
520                                 grid_size = Preferences.get_grid_size()
521                                 X,Y = self.get_selected_element().get_coordinate()
522                                 deltaX = X%grid_size
523                                 if deltaX < grid_size/2: deltaX = -1 * deltaX
524                                 else: deltaX = grid_size - deltaX
525                                 deltaY = Y%grid_size
526                                 if deltaY < grid_size/2: deltaY = -1 * deltaY
527                                 else: deltaY = grid_size - deltaY
528                                 self.move_selected((deltaX, deltaY))
529                         self.handle_states(BLOCK_MOVE)
530                         self.element_moved = False
531                 self.update_selected_elements()
532                 self.draw()
533
534         def handle_mouse_motion(self, coordinate):
535                 """!
536                 The mouse has moved, respond to mouse dragging.
537                 Move a selected element to the new coordinate.
538                 Auto-scroll the scroll bars at the boundaries.
539                 """
540                 #to perform a movement, the mouse must be pressed, timediff large enough
541                 if not self.mouse_pressed: return
542                 if time.time() - self.time < MOTION_DETECT_REDRAWING_SENSITIVITY: return
543                 #perform autoscrolling
544                 width, height = self.get_size()
545                 x, y = coordinate
546                 h_adj = self.get_scroll_pane().get_hadjustment()
547                 v_adj = self.get_scroll_pane().get_vadjustment()
548                 for pos, length, adj, adj_val, adj_len in (
549                         (x, width, h_adj, h_adj.get_value(), h_adj.page_size),
550                         (y, height, v_adj, v_adj.get_value(), v_adj.page_size),
551                 ):
552                         #scroll if we moved near the border
553                         if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len:
554                                 adj.set_value(adj_val+SCROLL_DISTANCE)
555                                 adj.emit('changed')
556                         elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY:
557                                 adj.set_value(adj_val-SCROLL_DISTANCE)
558                                 adj.emit('changed')
559                 #move the selected element and record the new coordinate
560                 X, Y = self.get_coordinate()
561                 if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y)))
562                 self.draw()
563                 self.set_coordinate((x, y))
564                 #update time
565                 self.time = time.time()