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