new preferences
[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.Constants import \
21         DIR_LEFT, DIR_RIGHT, \
22         SCROLL_PROXIMITY_SENSITIVITY, SCROLL_DISTANCE, \
23         MOTION_DETECT_REDRAWING_SENSITIVITY
24 from ... gui.Actions import \
25         ELEMENT_CREATE, ELEMENT_SELECT, \
26         BLOCK_PARAM_MODIFY, BLOCK_MOVE, \
27         ELEMENT_DELETE
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 = self.get_new_block(block_key)
143                         selected.add(block)
144                         #set params
145                         params_n = utils.listify(block_n, 'param')
146                         for param_n in params_n:
147                                 param_key = param_n['key']
148                                 param_value = param_n['value']
149                                 #setup id parameter
150                                 if param_key == 'id':
151                                         old_id2block[param_value] = block
152                                         #if the block id is not unique, get a new block id
153                                         if param_value in [block.get_id() for block in self.get_blocks()]:
154                                                 param_value = self._get_unique_id(param_value)
155                                 #set value to key
156                                 block.get_param(param_key).set_value(param_value)
157                         #move block to offset coordinate
158                         block.move((x_off, y_off))
159                 #update before creating connections
160                 self.update()
161                 #create connections
162                 for connection_n in connections_n:
163                         source = old_id2block[connection_n['source_block_id']].get_source(connection_n['source_key'])
164                         sink = old_id2block[connection_n['sink_block_id']].get_sink(connection_n['sink_key'])
165                         self.connect(source, sink)
166                 #set all pasted elements selected
167                 for block in selected: selected = selected.union(set(block.get_connections()))
168                 self._selected_elements = list(selected)
169
170         ###########################################################################
171         # Modify Selected
172         ###########################################################################
173         def type_controller_modify_selected(self, direction):
174                 """
175                 Change the registered type controller for the selected signal blocks.
176                 @param direction +1 or -1
177                 @return true for change
178                 """
179                 changed = False
180                 for selected_block in self.get_selected_blocks():
181                         type_param = None
182                         for param in filter(lambda p: p.is_enum(), selected_block.get_params()):
183                                 children = param.get_parent().get_ports() + param.get_parent().get_params()
184                                 #priority to the type controller
185                                 if param.get_key() in ' '.join(map(lambda p: p._type, children)): type_param = param
186                                 #use param if type param is unset
187                                 if not type_param: type_param = param
188                         if type_param:
189                                 #try to increment the enum by direction
190                                 try:
191                                         keys = type_param.get_option_keys()
192                                         old_index = keys.index(type_param.get_value())
193                                         new_index = (old_index + direction + len(keys))%len(keys)
194                                         type_param.set_value(keys[new_index])
195                                         changed = True
196                                 except: pass
197                 return changed
198
199         def port_controller_modify_selected(self, direction):
200                 """
201                 Change port controller for the selected signal blocks.
202                 @param direction +1 or -1
203                 @return true for changed
204                 """
205                 changed = False
206                 for selected_block in self.get_selected_blocks():
207                         for ports in (selected_block.get_sinks(), selected_block.get_sources()):
208                                 if ports and hasattr(ports[0], 'get_nports') and ports[0].get_nports():
209                                         #find the param that controls port0
210                                         for param in selected_block.get_params():
211                                                 if param.get_key() in ports[0]._nports:
212                                                         #try to increment the port controller by direction
213                                                         try:
214                                                                 value = param.evaluate()
215                                                                 value = value + direction
216                                                                 assert 0 < value
217                                                                 param.set_value(value)
218                                                                 changed = True
219                                                         except: pass
220                 return changed
221
222         def param_modify_selected(self):
223                 """
224                 Create and show a param modification dialog for the selected block.
225                 @return true if parameters were changed
226                 """
227                 if self.get_selected_block():
228                         signal_block_params_dialog = ParamsDialog(self.get_selected_block())
229                         return signal_block_params_dialog.run()
230                 return False
231
232         def enable_selected(self, enable):
233                 """
234                 Enable/disable the selected blocks.
235                 @param enable true to enable
236                 @return true if changed
237                 """
238                 changed = False
239                 for selected_block in self.get_selected_blocks():
240                         if selected_block.get_enabled() != enable:
241                                 selected_block.set_enabled(enable)
242                                 changed = True
243                 return changed
244
245         def move_selected(self, delta_coordinate):
246                 """
247                 Move the element and by the change in coordinates.
248                 @param delta_coordinate the change in coordinates
249                 """
250                 for selected_block in self.get_selected_blocks():
251                         selected_block.move(delta_coordinate)
252                         self.element_moved = True
253
254         def rotate_selected(self, direction):
255                 """
256                 Rotate the selected blocks by 90 degrees.
257                 @param direction DIR_LEFT or DIR_RIGHT
258                 @return true if changed, otherwise false.
259                 """
260                 if not self.get_selected_blocks(): return False
261                 #determine the number of degrees to rotate
262                 rotation = {DIR_LEFT: 90, DIR_RIGHT:270}[direction]
263                 #initialize min and max coordinates
264                 min_x, min_y = self.get_selected_block().get_coordinate()
265                 max_x, max_y = self.get_selected_block().get_coordinate()
266                 #rotate each selected block, and find min/max coordinate
267                 for selected_block in self.get_selected_blocks():
268                         selected_block.rotate(rotation)
269                         #update the min/max coordinate
270                         x, y = selected_block.get_coordinate()
271                         min_x, min_y = min(min_x, x), min(min_y, y)
272                         max_x, max_y = max(max_x, x), max(max_y, y)
273                 #calculate center point of slected blocks
274                 ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
275                 #rotate the blocks around the center point
276                 for selected_block in self.get_selected_blocks():
277                         x, y = selected_block.get_coordinate()
278                         x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
279                         selected_block.set_coordinate((x + ctr_x, y + ctr_y))
280                 return True
281
282         def remove_selected(self):
283                 """
284                 Remove selected elements
285                 @return true if changed.
286                 """
287                 changed = False
288                 for selected_element in self.get_selected_elements():
289                         self.remove_element(selected_element)
290                         changed = True
291                 return changed
292
293         def draw(self):
294                 """
295                 Draw the background and grid if enabled.
296                 Draw all of the elements in this flow graph onto the pixmap.
297                 Draw the pixmap to the drawable window of this flow graph.
298                 """
299                 if self.get_gc():
300                         W,H = self.get_size()
301                         #draw the background
302                         self.get_gc().foreground = Colors.BACKGROUND_COLOR
303                         self.get_pixmap().draw_rectangle(self.get_gc(), True, 0, 0, W, H)
304                         #draw multi select rectangle
305                         if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()):
306                                 #coordinates
307                                 x1, y1 = self.press_coor
308                                 x2, y2 = self.get_coordinate()
309                                 #calculate top-left coordinate and width/height
310                                 x, y = int(min(x1, x2)), int(min(y1, y2))
311                                 w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
312                                 #draw
313                                 self.get_gc().foreground = Colors.H_COLOR
314                                 self.get_pixmap().draw_rectangle(self.get_gc(), True, x, y, w, h)
315                                 self.get_gc().foreground = Colors.TXT_COLOR
316                                 self.get_pixmap().draw_rectangle(self.get_gc(), False, x, y, w, h)
317                         #draw blocks on top of connections
318                         for element in self.get_connections() + self.get_blocks():
319                                 element.draw(self.get_pixmap())
320                         #draw selected blocks on top of selected connections
321                         for selected_element in self.get_selected_connections() + self.get_selected_blocks():
322                                 selected_element.draw(self.get_pixmap())
323                         self.get_drawing_area().draw()
324
325         def update(self):
326                 """
327                 Update highlighting so only the selected is highlighted.
328                 Call update on all elements.
329                 Resize the window if size changed.
330                 """
331                 #update highlighting
332                 map(lambda e: e.set_highlighted(False), self.get_elements())
333                 for selected_element in self.get_selected_elements():
334                         selected_element.set_highlighted(True)
335                 #update all elements
336                 map(lambda e: e.update(), self.get_elements())
337                 #set the size of the flow graph area
338                 old_x, old_y = self.get_size()
339                 try: new_x, new_y = self.get_option('window_size')
340                 except: new_x, new_y = old_x, old_y
341                 if new_x != old_x or new_y != old_y: self.set_size(new_x, new_y)
342
343         ##########################################################################
344         ## Get Selected
345         ##########################################################################
346         def unselect(self):
347                 """
348                 Set selected elements to an empty set.
349                 """
350                 self._selected_elements = []
351
352         def what_is_selected(self, coor, coor_m=None):
353                 """
354                 What is selected?
355                 At the given coordinate, return the elements found to be selected.
356                 If coor_m is unspecified, return a list of only the first element found to be selected:
357                 Iterate though the elements backwardssince top elements are at the end of the list.
358                 If an element is selected, place it at the end of the list so that is is drawn last,
359                 and hence on top. Update the selected port information.
360                 @param coor the coordinate of the mouse click
361                 @param coor_m the coordinate for multi select
362                 @return the selected blocks and connections or an empty list
363                 """
364                 selected_port = None
365                 selected = set()
366                 #check the elements
367                 for element in reversed(self.get_elements()):
368                         selected_element = element.what_is_selected(coor, coor_m)
369                         if not selected_element: continue
370                         #update the selected port information
371                         if selected_element.is_port():
372                                 if not coor_m: selected_port = selected_element
373                                 selected_element = selected_element.get_parent()
374                         selected.add(selected_element)
375                         #single select mode, break
376                         if not coor_m:
377                                 self.get_elements().remove(element)
378                                 self.get_elements().append(element)
379                                 break;
380                 #update selected ports
381                 self._old_selected_port = self._new_selected_port
382                 self._new_selected_port = selected_port
383                 return list(selected)
384
385         def get_selected_connections(self):
386                 """
387                 Get a group of selected connections.
388                 @return sub set of connections in this flow graph
389                 """
390                 selected = set()
391                 for selected_element in self.get_selected_elements():
392                         if selected_element.is_connection(): selected.add(selected_element)
393                 return list(selected)
394
395         def get_selected_blocks(self):
396                 """
397                 Get a group of selected blocks.
398                 @return sub set of blocks in this flow graph
399                 """
400                 selected = set()
401                 for selected_element in self.get_selected_elements():
402                         if selected_element.is_block(): selected.add(selected_element)
403                 return list(selected)
404
405         def get_selected_block(self):
406                 """
407                 Get the selected block when a block or port is selected.
408                 @return a block or None
409                 """
410                 return self.get_selected_blocks() and self.get_selected_blocks()[0] or None
411
412         def get_selected_elements(self):
413                 """
414                 Get the group of selected elements.
415                 @return sub set of elements in this flow graph
416                 """
417                 return self._selected_elements
418
419         def get_selected_element(self):
420                 """
421                 Get the selected element.
422                 @return a block, port, or connection or None
423                 """
424                 return self.get_selected_elements() and self.get_selected_elements()[0] or None
425
426         def update_selected_elements(self):
427                 """
428                 Update the selected elements.
429                 The update behavior depends on the state of the mouse button.
430                 When the mouse button pressed the selection will change when
431                 the control mask is set or the new selection is not in the current group.
432                 When the mouse button is released the selection will change when
433                 the mouse has moved and the control mask is set or the current group is empty.
434                 Attempt to make a new connection if the old and ports are filled.
435                 If the control mask is set, merge with the current elements.
436                 """
437                 selected_elements = None
438                 if self.mouse_pressed:
439                         new_selection = self.what_is_selected(self.get_coordinate())
440                         #update the selections if the new selection is not in the current selections
441                         #allows us to move entire selected groups of elements
442                         if self.get_ctrl_mask() or not (
443                                 new_selection and new_selection[0] in self.get_selected_elements()
444                         ): selected_elements = new_selection
445                 else: #called from a mouse release
446                         if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()):
447                                 selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor)
448                 #this selection and the last were ports, try to connect them
449                 if self._old_selected_port and self._new_selected_port and \
450                         self._old_selected_port is not self._new_selected_port:
451                         try:
452                                 self.connect(self._old_selected_port, self._new_selected_port)
453                                 self.handle_states(ELEMENT_CREATE)
454                         except: Messages.send_fail_connection()
455                         self._old_selected_port = None
456                         self._new_selected_port = None
457                         return
458                 #update selected elements
459                 if selected_elements is None: return
460                 old_elements = set(self.get_selected_elements())
461                 self._selected_elements = list(set(selected_elements))
462                 new_elements = set(self.get_selected_elements())
463                 #if ctrl, set the selected elements to the union - intersection of old and new
464                 if self.get_ctrl_mask():
465                         self._selected_elements = list(
466                                 set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements)
467                         )
468                 self.handle_states(ELEMENT_SELECT)
469
470         ##########################################################################
471         ## Event Handlers
472         ##########################################################################
473         def handle_mouse_button_press(self, left_click, double_click, coordinate):
474                 """
475                 A mouse button is pressed, only respond to left clicks.
476                 Find the selected element. Attempt a new connection if possible.
477                 Open the block params window on a double click.
478                 Update the selection state of the flow graph.
479                 """
480                 if not left_click: return
481                 self.press_coor = coordinate
482                 self.set_coordinate(coordinate)
483                 self.time = 0
484                 self.mouse_pressed = True
485                 self.update_selected_elements()
486                 #double click detected, bring up params dialog if possible
487                 if double_click and self.get_selected_block():
488                         self.mouse_pressed = False
489                         self.handle_states(BLOCK_PARAM_MODIFY)
490
491         def handle_mouse_button_release(self, left_click, coordinate):
492                 """
493                 A mouse button is released, record the state.
494                 """
495                 if not left_click: return
496                 self.set_coordinate(coordinate)
497                 self.time = 0
498                 self.mouse_pressed = False
499                 if self.element_moved:
500                         self.handle_states(BLOCK_MOVE)
501                         self.element_moved = False
502                 self.update_selected_elements()
503                 self.draw()
504
505         def handle_mouse_motion(self, coordinate):
506                 """
507                 The mouse has moved, respond to mouse dragging.
508                 Move a selected element to the new coordinate.
509                 Auto-scroll the scroll bars at the boundaries.
510                 """
511                 #to perform a movement, the mouse must be pressed, timediff large enough
512                 if not self.mouse_pressed: return
513                 if time.time() - self.time < MOTION_DETECT_REDRAWING_SENSITIVITY: return
514                 #perform autoscrolling
515                 width, height = self.get_size()
516                 x, y = coordinate
517                 h_adj = self.get_scroll_pane().get_hadjustment()
518                 v_adj = self.get_scroll_pane().get_vadjustment()
519                 for pos, length, adj, adj_val, adj_len in (
520                         (x, width, h_adj, h_adj.get_value(), h_adj.page_size),
521                         (y, height, v_adj, v_adj.get_value(), v_adj.page_size),
522                 ):
523                         #scroll if we moved near the border
524                         if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len:
525                                 adj.set_value(adj_val+SCROLL_DISTANCE)
526                                 adj.emit('changed')
527                         elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY:
528                                 adj.set_value(adj_val-SCROLL_DISTANCE)
529                                 adj.emit('changed')
530                 #remove the connection if selected in drag event
531                 if len(self.get_selected_elements()) == 1 and self.get_selected_element().is_connection():
532                         self.handle_states(ELEMENT_DELETE)
533                 #move the selected elements and record the new coordinate
534                 X, Y = self.get_coordinate()
535                 if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y)))
536                 self.draw()
537                 self.set_coordinate((x, y))
538                 #update time
539                 self.time = time.time()