another distcheck fix
[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 import Utils
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                 rotation = {DIR_LEFT: 90, DIR_RIGHT:270}[direction]
271                 #initialize min and max coordinates
272                 min_x, min_y = self.get_selected_block().get_coordinate()
273                 max_x, max_y = self.get_selected_block().get_coordinate()
274                 #rotate each selected block, and find min/max coordinate
275                 for selected_block in self.get_selected_blocks():
276                         selected_block.rotate(rotation)
277                         #update the min/max coordinate
278                         x, y = selected_block.get_coordinate()
279                         min_x, min_y = min(min_x, x), min(min_y, y)
280                         max_x, max_y = max(max_x, x), max(max_y, y)
281                 #calculate center point of slected blocks
282                 ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
283                 #rotate the blocks around the center point
284                 for selected_block in self.get_selected_blocks():
285                         x, y = selected_block.get_coordinate()
286                         x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
287                         selected_block.set_coordinate((x + ctr_x, y + ctr_y))
288                 return True
289
290         def remove_selected(self):
291                 """!
292                 Remove selected elements
293                 @return true if changed.
294                 """
295                 changed = False
296                 for selected_element in self.get_selected_elements():
297                         self.remove_element(selected_element)
298                         changed = True
299                 return changed
300
301         def draw(self):
302                 """!
303                 Draw the background and grid if enabled.
304                 Draw all of the elements in this flow graph onto the pixmap.
305                 Draw the pixmap to the drawable window of this flow graph.
306                 """
307                 if self.get_gc():
308                         W,H = self.get_size()
309                         #draw the background
310                         self.get_gc().foreground = Colors.BACKGROUND_COLOR
311                         self.get_pixmap().draw_rectangle(self.get_gc(), True, 0, 0, W, H)
312                         #draw grid (depends on prefs)
313                         if Preferences.show_grid():
314                                 grid_size = Preferences.get_grid_size()
315                                 points = list()
316                                 for i in range(W/grid_size):
317                                         for j in range(H/grid_size):
318                                                 points.append((i*grid_size, j*grid_size))
319                                 self.get_gc().foreground = Colors.TXT_COLOR
320                                 self.get_pixmap().draw_points(self.get_gc(), points)
321                         #draw multi select rectangle
322                         if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()):
323                                 #coordinates
324                                 x1, y1 = self.press_coor
325                                 x2, y2 = self.get_coordinate()
326                                 #calculate top-left coordinate and width/height
327                                 x, y = int(min(x1, x2)), int(min(y1, y2))
328                                 w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
329                                 #draw
330                                 self.get_gc().foreground = Colors.H_COLOR
331                                 self.get_pixmap().draw_rectangle(self.get_gc(), True, x, y, w, h)
332                                 self.get_gc().foreground = Colors.TXT_COLOR
333                                 self.get_pixmap().draw_rectangle(self.get_gc(), False, x, y, w, h)
334                         #draw blocks on top of connections
335                         for element in self.get_connections() + self.get_blocks():
336                                 element.draw(self.get_pixmap())
337                         #draw selected blocks on top of selected connections
338                         for selected_element in self.get_selected_connections() + self.get_selected_blocks():
339                                 selected_element.draw(self.get_pixmap())
340                         self.get_drawing_area().draw()
341
342         def update(self):
343                 """!
344                 Update highlighting so only the selected is highlighted.
345                 Call update on all elements.
346                 Resize the window if size changed.
347                 """
348                 #update highlighting
349                 map(lambda e: e.set_highlighted(False), self.get_elements())
350                 for selected_element in self.get_selected_elements(): 
351                         selected_element.set_highlighted(True)
352                 #update all elements
353                 map(lambda e: e.update(), self.get_elements())
354                 #set the size of the flow graph area
355                 old_x, old_y = self.get_size()
356                 try: new_x, new_y = self.get_option('window_size')
357                 except: new_x, new_y = old_x, old_y
358                 if new_x != old_x or new_y != old_y: self.set_size(new_x, new_y)
359
360         ##########################################################################
361         ## Get Selected
362         ##########################################################################
363         def unselect(self):
364                 """!
365                 Set selected elements to an empty set.
366                 """
367                 self._selected_elements = []
368
369         def what_is_selected(self, coor, coor_m=None):
370                 """!
371                 What is selected?
372                 At the given coordinate, return the elements found to be selected.
373                 If coor_m is unspecified, return a list of only the first element found to be selected:
374                 Iterate though the elements backwardssince top elements are at the end of the list.
375                 If an element is selected, place it at the end of the list so that is is drawn last,
376                 and hence on top. Update the selected port information.
377                 @param coor the coordinate of the mouse click
378                 @param coor_m the coordinate for multi select
379                 @return the selected blocks and connections or an empty list
380                 """
381                 selected_port = None
382                 selected = set()
383                 #check the elements
384                 for element in reversed(self.get_elements()):
385                         selected_element = element.what_is_selected(coor, coor_m)
386                         if not selected_element: continue
387                         #update the selected port information
388                         if selected_element.is_port(): 
389                                 if not coor_m: selected_port = selected_element
390                                 selected_element = selected_element.get_parent()
391                         selected.add(selected_element)
392                         #single select mode, break
393                         if not coor_m:
394                                 self.get_elements().remove(element)
395                                 self.get_elements().append(element)
396                                 break;
397                 #update selected ports
398                 self._old_selected_port = self._new_selected_port
399                 self._new_selected_port = selected_port
400                 return list(selected)
401
402         def get_selected_connections(self):
403                 """!
404                 Get a group of selected connections.
405                 @return sub set of connections in this flow graph
406                 """
407                 selected = set()
408                 for selected_element in self.get_selected_elements():
409                         if selected_element.is_connection(): selected.add(selected_element)
410                 return list(selected)
411
412         def get_selected_blocks(self):
413                 """!
414                 Get a group of selected blocks.
415                 @return sub set of blocks in this flow graph
416                 """
417                 selected = set()
418                 for selected_element in self.get_selected_elements():
419                         if selected_element.is_block(): selected.add(selected_element)
420                 return list(selected)
421
422         def get_selected_block(self):
423                 """!
424                 Get the selected block when a block or port is selected.
425                 @return a block or None
426                 """
427                 return self.get_selected_blocks() and self.get_selected_blocks()[0] or None
428
429         def get_selected_elements(self):
430                 """!
431                 Get the group of selected elements.
432                 @return sub set of elements in this flow graph
433                 """
434                 return self._selected_elements
435
436         def get_selected_element(self):
437                 """!
438                 Get the selected element.
439                 @return a block, port, or connection or None
440                 """
441                 return self.get_selected_elements() and self.get_selected_elements()[0] or None
442
443         def update_selected_elements(self):
444                 """!
445                 Update the selected elements.
446                 The update behavior depends on the state of the mouse button.
447                 When the mouse button pressed the selection will change when
448                 the control mask is set or the new selection is not in the current group.
449                 When the mouse button is released the selection will change when
450                 the mouse has moved and the control mask is set or the current group is empty.
451                 Attempt to make a new connection if the old and ports are filled.
452                 If the control mask is set, merge with the current elements.
453                 """
454                 selected_elements = None
455                 if self.mouse_pressed:
456                         new_selection = self.what_is_selected(self.get_coordinate())
457                         #update the selections if the new selection is not in the current selections
458                         #allows us to move entire selected groups of elements
459                         if self.get_ctrl_mask() or not (
460                                 new_selection and new_selection[0] in self.get_selected_elements()
461                         ): selected_elements = new_selection
462                 else: #called from a mouse release
463                         if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()):
464                                 selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor)
465                 #this selection and the last were ports, try to connect them
466                 if self._old_selected_port and self._new_selected_port and \
467                         self._old_selected_port is not self._new_selected_port:
468                         try:
469                                 self.connect(self._old_selected_port, self._new_selected_port)
470                                 self.handle_states(ELEMENT_CREATE)
471                         except: Messages.send_fail_connection()
472                         self._old_selected_port = None
473                         self._new_selected_port = None
474                         return
475                 #update selected elements
476                 if selected_elements is None: return
477                 old_elements = set(self.get_selected_elements())
478                 self._selected_elements = list(set(selected_elements))
479                 new_elements = set(self.get_selected_elements())
480                 #if ctrl, set the selected elements to the union - intersection of old and new
481                 if self.get_ctrl_mask():
482                         self._selected_elements = list(
483                                 set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements)
484                         )
485                 self.handle_states(ELEMENT_SELECT)
486
487         ##########################################################################
488         ## Event Handlers
489         ##########################################################################
490         def handle_mouse_button_press(self, left_click, double_click, coordinate):
491                 """!
492                 A mouse button is pressed, only respond to left clicks.
493                 Find the selected element. Attempt a new connection if possible.
494                 Open the block params window on a double click.
495                 Update the selection state of the flow graph.
496                 """
497                 if not left_click: return
498                 self.press_coor = coordinate
499                 self.set_coordinate(coordinate)
500                 self.time = 0
501                 self.mouse_pressed = True
502                 self.update_selected_elements()
503                 #double click detected, bring up params dialog if possible
504                 if double_click and self.get_selected_block():
505                         self.mouse_pressed = False
506                         self.handle_states(BLOCK_PARAM_MODIFY)
507
508         def handle_mouse_button_release(self, left_click, coordinate):
509                 """!
510                 A mouse button is released, record the state.
511                 """
512                 if not left_click: return
513                 self.set_coordinate(coordinate)
514                 self.time = 0
515                 self.mouse_pressed = False
516                 if self.element_moved:
517                         if Preferences.snap_to_grid():
518                                 grid_size = Preferences.get_grid_size()
519                                 X,Y = self.get_selected_element().get_coordinate()
520                                 deltaX = X%grid_size
521                                 if deltaX < grid_size/2: deltaX = -1 * deltaX
522                                 else: deltaX = grid_size - deltaX
523                                 deltaY = Y%grid_size
524                                 if deltaY < grid_size/2: deltaY = -1 * deltaY
525                                 else: deltaY = grid_size - deltaY
526                                 self.move_selected((deltaX, deltaY))
527                         self.handle_states(BLOCK_MOVE)
528                         self.element_moved = False
529                 self.update_selected_elements()
530                 self.draw()
531
532         def handle_mouse_motion(self, coordinate):
533                 """!
534                 The mouse has moved, respond to mouse dragging.
535                 Move a selected element to the new coordinate.
536                 Auto-scroll the scroll bars at the boundaries.
537                 """
538                 #to perform a movement, the mouse must be pressed, timediff large enough
539                 if not self.mouse_pressed: return
540                 if time.time() - self.time < MOTION_DETECT_REDRAWING_SENSITIVITY: return
541                 #perform autoscrolling
542                 width, height = self.get_size()
543                 x, y = coordinate
544                 h_adj = self.get_scroll_pane().get_hadjustment()
545                 v_adj = self.get_scroll_pane().get_vadjustment()
546                 for pos, length, adj, adj_val, adj_len in (
547                         (x, width, h_adj, h_adj.get_value(), h_adj.page_size),
548                         (y, height, v_adj, v_adj.get_value(), v_adj.page_size),
549                 ):
550                         #scroll if we moved near the border
551                         if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len:
552                                 adj.set_value(adj_val+SCROLL_DISTANCE)
553                                 adj.emit('changed')
554                         elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY:
555                                 adj.set_value(adj_val-SCROLL_DISTANCE)
556                                 adj.emit('changed')
557                 #move the selected element and record the new coordinate
558                 X, Y = self.get_coordinate()
559                 if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y)))
560                 self.draw()
561                 self.set_coordinate((x, y))
562                 #update time
563                 self.time = time.time()