1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
|
"""
Copyright 2015 Free Software Foundation, Inc.
This file is part of GNU Radio
GNU Radio Companion is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
GNU Radio Companion is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
from operator import attrgetter
import pygtk
pygtk.require('2.0')
import gtk
import gobject
from . import Actions
from . import Preferences
from . import Utils
from .Constants import DEFAULT_BLOCKS_WINDOW_WIDTH
BLOCK_INDEX = 0
ID_INDEX = 1
class VariableEditorContextMenu(gtk.Menu):
""" A simple context menu for our variable editor """
def __init__(self, var_edit):
gtk.Menu.__init__(self)
self.imports = gtk.MenuItem("Add _Import")
self.imports.connect('activate', var_edit.handle_action, var_edit.ADD_IMPORT)
self.add(self.imports)
self.variables = gtk.MenuItem("Add _Variable")
self.variables.connect('activate', var_edit.handle_action, var_edit.ADD_VARIABLE)
self.add(self.variables)
self.add(gtk.SeparatorMenuItem())
self.enable = gtk.MenuItem("_Enable")
self.enable.connect('activate', var_edit.handle_action, var_edit.ENABLE_BLOCK)
self.disable = gtk.MenuItem("_Disable")
self.disable.connect('activate', var_edit.handle_action, var_edit.DISABLE_BLOCK)
self.add(self.enable)
self.add(self.disable)
self.add(gtk.SeparatorMenuItem())
self.delete = gtk.MenuItem("_Delete")
self.delete.connect('activate', var_edit.handle_action, var_edit.DELETE_BLOCK)
self.add(self.delete)
self.add(gtk.SeparatorMenuItem())
self.properties = gtk.MenuItem("_Properties...")
self.properties.connect('activate', var_edit.handle_action, var_edit.OPEN_PROPERTIES)
self.add(self.properties)
self.show_all()
def update_sensitive(self, selected, enabled=False):
self.delete.set_sensitive(selected)
self.properties.set_sensitive(selected)
self.enable.set_sensitive(selected and not enabled)
self.disable.set_sensitive(selected and enabled)
class VariableEditor(gtk.VBox):
# Actions that are handled by the editor
ADD_IMPORT = 0
ADD_VARIABLE = 1
OPEN_PROPERTIES = 2
DELETE_BLOCK = 3
DELETE_CONFIRM = 4
ENABLE_BLOCK = 5
DISABLE_BLOCK = 6
def __init__(self, platform, get_flow_graph):
gtk.VBox.__init__(self)
self.platform = platform
self.get_flow_graph = get_flow_graph
self._block = None
self._mouse_button_pressed = False
# Only use the model to store the block reference and name.
# Generate everything else dynamically
self.treestore = gtk.TreeStore(gobject.TYPE_PYOBJECT, # Block reference
gobject.TYPE_STRING) # Category and block name
self.treeview = gtk.TreeView(self.treestore)
self.treeview.set_enable_search(False)
self.treeview.set_search_column(-1)
#self.treeview.set_enable_search(True)
#self.treeview.set_search_column(ID_INDEX)
self.treeview.get_selection().set_mode('single')
self.treeview.set_headers_visible(True)
self.treeview.connect('button-press-event', self._handle_mouse_button_press)
self.treeview.connect('button-release-event', self._handle_mouse_button_release)
self.treeview.connect('motion-notify-event', self._handle_motion_notify)
self.treeview.connect('key-press-event', self._handle_key_button_press)
# Block Name or Category
self.id_cell = gtk.CellRendererText()
self.id_cell.connect('edited', self._handle_name_edited_cb)
id_column = gtk.TreeViewColumn("Id", self.id_cell, text=ID_INDEX)
id_column.set_name("id")
id_column.set_resizable(True)
id_column.set_max_width(Utils.scale_scalar(300))
id_column.set_min_width(Utils.scale_scalar(80))
id_column.set_fixed_width(Utils.scale_scalar(100))
id_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
id_column.set_cell_data_func(self.id_cell, self.set_properties)
self.id_column = id_column
self.treeview.append_column(id_column)
self.treestore.set_sort_column_id(ID_INDEX, gtk.SORT_ASCENDING)
# For forcing resize
self._col_width = 0
# Block Value
self.value_cell = gtk.CellRendererText()
self.value_cell.connect('edited', self._handle_value_edited_cb)
value_column = gtk.TreeViewColumn("Value", self.value_cell)
value_column.set_name("value")
value_column.set_resizable(False)
value_column.set_expand(True)
value_column.set_min_width(Utils.scale_scalar(100))
value_column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
value_column.set_cell_data_func(self.value_cell, self.set_value)
self.value_column = value_column
self.treeview.append_column(value_column)
# Block Actions (Add, Remove)
self.action_cell = gtk.CellRendererPixbuf()
value_column.pack_start(self.action_cell, False)
value_column.set_cell_data_func(self.action_cell, self.set_icon)
# Make the scrolled window to hold the tree view
scrolled_window = gtk.ScrolledWindow()
scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
scrolled_window.add_with_viewport(self.treeview)
scrolled_window.set_size_request(DEFAULT_BLOCKS_WINDOW_WIDTH, -1)
self.pack_start(scrolled_window)
# Context menus
self._context_menu = VariableEditorContextMenu(self)
self._confirm_delete = Preferences.variable_editor_confirm_delete()
# Sets cell contents
def set_icon(self, col, cell, model, iter):
block = model.get_value(iter, BLOCK_INDEX)
if block:
pb = self.treeview.render_icon(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU, None)
else:
pb = self.treeview.render_icon(gtk.STOCK_ADD, gtk.ICON_SIZE_MENU, None)
cell.set_property('pixbuf', pb)
def set_value(self, col, cell, model, iter):
sp = cell.set_property
block = model.get_value(iter, BLOCK_INDEX)
# Set the default properties for this column first.
# Some set in set_properties() may be overridden (editable for advanced variable blocks)
self.set_properties(col, cell, model, iter)
# Set defaults
value = None
self.set_tooltip_text(None)
# Block specific values
if block:
if block.get_key() == 'import':
value = block.get_param('import').get_value()
elif block.get_key() != "variable":
value = "<Open Properties>"
sp('editable', False)
sp('foreground', '#0D47A1')
else:
value = block.get_param('value').get_value()
# Check if there are errors in the blocks.
# Show the block error as a tooltip
error_message = block.get_error_messages()
if len(error_message) > 0:
# Set the error message to the last error in the list.
# This should be the first message generated
self.set_tooltip_text(error_message[-1])
else:
# Evaluate and show the value (if it is a variable)
if block.get_key() == "variable":
evaluated = str(block.get_param('value').evaluate())
self.set_tooltip_text(evaluated)
# Always set the text value.
sp('text', value)
def set_properties(self, col, cell, model, iter):
sp = cell.set_property
block = model.get_value(iter, BLOCK_INDEX)
# Set defaults
sp('sensitive', True)
sp('editable', False)
sp('foreground', None)
# Block specific changes
if block:
if not block.get_enabled():
# Disabled block. But, this should still be editable
sp('editable', True)
sp('foreground', 'gray')
else:
sp('editable', True)
if block.get_error_messages():
sp('foreground', 'red')
def update_gui(self):
if not self.get_flow_graph():
return
self._update_blocks()
self._rebuild()
self.treeview.expand_all()
def _update_blocks(self):
self._imports = filter(attrgetter('is_import'),
self.get_flow_graph().blocks)
self._variables = filter(attrgetter('is_variable'),
self.get_flow_graph().blocks)
def _rebuild(self, *args):
self.treestore.clear()
imports = self.treestore.append(None, [None, 'Imports'])
variables = self.treestore.append(None, [None, 'Variables'])
for block in self._imports:
self.treestore.append(imports, [block, block.get_param('id').get_value()])
for block in sorted(self._variables, key=lambda v: v.get_id()):
self.treestore.append(variables, [block, block.get_param('id').get_value()])
def _handle_name_edited_cb(self, cell, path, new_text):
block = self.treestore[path][BLOCK_INDEX]
block.get_param('id').set_value(new_text)
Actions.VARIABLE_EDITOR_UPDATE()
def _handle_value_edited_cb(self, cell, path, new_text):
block = self.treestore[path][BLOCK_INDEX]
if block.is_import:
block.get_param('import').set_value(new_text)
else:
block.get_param('value').set_value(new_text)
Actions.VARIABLE_EDITOR_UPDATE()
def handle_action(self, item, key, event=None):
"""
Single handler for the different actions that can be triggered by the context menu,
key presses or mouse clicks. Also triggers an update of the flow graph and editor.
"""
if key == self.ADD_IMPORT:
self.get_flow_graph().add_new_block('import')
elif key == self.ADD_VARIABLE:
self.get_flow_graph().add_new_block('variable')
elif key == self.OPEN_PROPERTIES:
Actions.BLOCK_PARAM_MODIFY(self._block)
elif key == self.DELETE_BLOCK:
self.get_flow_graph().remove_element(self._block)
elif key == self.DELETE_CONFIRM:
if self._confirm_delete:
# Create a context menu to confirm the delete operation
confirmation_menu = gtk.Menu()
block_id = self._block.get_param('id').get_value().replace("_", "__")
confirm = gtk.MenuItem("Delete {}".format(block_id))
confirm.connect('activate', self.handle_action, self.DELETE_BLOCK)
confirmation_menu.add(confirm)
confirmation_menu.show_all()
confirmation_menu.popup(None, None, None, event.button, event.time)
else:
self.handle_action(None, self.DELETE_BLOCK, None)
elif key == self.ENABLE_BLOCK:
self._block.set_enabled(True)
elif key == self.DISABLE_BLOCK:
self._block.set_enabled(False)
Actions.VARIABLE_EDITOR_UPDATE()
def _handle_mouse_button_press(self, widget, event):
"""
Handles mouse button for several different events:
- Double Click to open properties for advanced blocks
- Click to add/remove blocks
"""
# Save the column width to see if it changes on button_release
self._mouse_button_pressed = True
self._col_width = self.id_column.get_width()
path = widget.get_path_at_pos(int(event.x), int(event.y))
if path:
# If there is a valid path, then get the row, column and block selected.
row = self.treestore[path[0]]
col = path[1]
self._block = row[BLOCK_INDEX]
if event.button == 1 and col.get_name() == "value":
# Make sure this has a block (not the import/variable rows)
if self._block and event.type == gtk.gdk._2BUTTON_PRESS:
# Open the advanced dialog if it is a gui variable
if self._block.get_key() not in ("variable", "import"):
self.handle_action(None, self.OPEN_PROPERTIES, event=event)
return True
if event.type == gtk.gdk.BUTTON_PRESS:
# User is adding/removing blocks
# Make sure this is the action cell (Add/Remove Icons)
if path[2] > col.cell_get_position(self.action_cell)[0]:
if row[1] == "Imports":
# Add a new import block.
self.handle_action(None, self.ADD_IMPORT, event=event)
elif row[1] == "Variables":
# Add a new variable block
self.handle_action(None, self.ADD_VARIABLE, event=event)
else:
self.handle_action(None, self.DELETE_CONFIRM, event=event)
return True
elif event.button == 3 and event.type == gtk.gdk.BUTTON_PRESS:
if self._block:
self._context_menu.update_sensitive(True, enabled=self._block.get_enabled())
else:
self._context_menu.update_sensitive(False)
self._context_menu.popup(None, None, None, event.button, event.time)
# Null handler. Stops the treeview from handling double click events.
if event.type == gtk.gdk._2BUTTON_PRESS:
return True
return False
def _handle_mouse_button_release(self, widget, event):
self._mouse_button_pressed = False
return False
def _handle_motion_notify(self, widget, event):
# Check to see if the column size has changed
if self._mouse_button_pressed and self.id_column.get_width() != self._col_width:
self.value_column.queue_resize()
return False
def _handle_key_button_press(self, widget, event):
model, path = self.treeview.get_selection().get_selected_rows()
if path and self._block:
if self._block.get_enabled() and event.string == "d":
self.handle_action(None, self.DISABLE_BLOCK, None)
return True
elif not self._block.get_enabled() and event.string == "e":
self.handle_action(None, self.ENABLE_BLOCK, None)
return True
return False
|