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