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