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