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