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