2 # Copyright 2009 Free Software Foundation, Inc.
4 # This file is part of GNU Radio
6 # GNU Radio is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3, or (at your option)
11 # GNU Radio is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with GNU Radio; see the file COPYING. If not, write to
18 # the Free Software Foundation, Inc., 51 Franklin Street,
19 # Boston, MA 02110-1301, USA.
23 The forms module contains general purpose wx-gui forms for gnuradio apps.
25 The forms follow a layered model:
27 * deals with the wxgui objects directly
28 * implemented in event handler and update methods
30 * translates the between the external and internal layers
31 * handles parsing errors between layers
33 * provided external access to the user
34 * set_value, get_value, and optional callback
35 * set and get through optional pubsub and key
38 * An empty label in the radio box still consumes space.
39 * The static text cannot resize the parent at runtime.
47 from gnuradio.gr.pubsub import pubsub
50 EVT_DATA = wx.PyEventBinder(wx.NewEventType())
51 class DataEvent(wx.PyEvent):
52 def __init__(self, data):
53 wx.PyEvent.__init__(self, wx.NewId(), EVT_DATA.typeId)
56 def make_bold(widget):
57 font = widget.GetFont()
58 font.SetWeight(wx.FONTWEIGHT_BOLD)
61 ########################################################################
63 ########################################################################
64 class _form_base(pubsub, wx.BoxSizer):
65 def __init__(self, parent=None, sizer=None, proportion=0, flag=wx.EXPAND, ps=None, key='', value=None, callback=None, converter=converters.identity_converter()):
67 wx.BoxSizer.__init__(self, wx.HORIZONTAL)
70 self._converter = converter
71 self._callback = callback
72 self._widgets = list()
73 #add to the sizer if provided
74 if sizer: sizer.Add(self, proportion, flag)
75 #proxy the pubsub and key into this form
78 self.proxy(EXT_KEY, ps, key)
79 #no pubsub passed, must set initial value
80 else: self.set_value(value)
83 return "Form: %s -> %s"%(self.__class__, self._key)
85 def _add_widget(self, widget, label='', flag=0, label_prop=0, widget_prop=1):
87 Add the main widget to this object sizer.
88 If label is passed, add a label as well.
89 Register the widget and the label in the widgets list (for enable/disable).
90 Bind the update handler to the widget for data events.
91 This ensures that the gui thread handles updating widgets.
92 Setup the pusub triggers for external and internal.
93 @param widget the main widget
94 @param label the optional label
95 @param flag additional flags for widget
96 @param label_prop the proportion for the label
97 @param widget_prop the proportion for the widget
100 widget.Bind(EVT_DATA, lambda x: self._update(x.data))
101 update = lambda x: wx.PostEvent(widget, DataEvent(x))
103 self._widgets.append(widget)
104 #create optional label
105 if not label: self.Add(widget, widget_prop, wx.ALIGN_CENTER_VERTICAL | flag)
107 label_text = wx.StaticText(self._parent, label='%s: '%label)
108 self._widgets.append(label_text)
109 self.Add(label_text, label_prop, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT)
110 self.Add(widget, widget_prop, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | flag)
111 #initialize without triggering pubsubs
112 self._translate_external_to_internal(self[EXT_KEY])
113 update(self[INT_KEY])
114 #subscribe all the functions
115 self.subscribe(INT_KEY, update)
116 self.subscribe(INT_KEY, self._translate_internal_to_external)
117 self.subscribe(EXT_KEY, self._translate_external_to_internal)
119 def _translate_external_to_internal(self, external):
121 internal = self._converter.external_to_internal(external)
122 #prevent infinite loop between internal and external pubsub keys by only setting if changed
123 if self[INT_KEY] != internal: self[INT_KEY] = internal
125 self._err_msg(external, e)
126 self[INT_KEY] = self[INT_KEY] #reset to last good setting
128 def _translate_internal_to_external(self, internal):
130 external = self._converter.internal_to_external(internal)
131 #prevent infinite loop between internal and external pubsub keys by only setting if changed
132 if self[EXT_KEY] != external: self[EXT_KEY] = external
134 self._err_msg(internal, e)
135 self[EXT_KEY] = self[EXT_KEY] #reset to last good setting
136 if self._callback: self._callback(self[EXT_KEY])
138 def _err_msg(self, value, e):
139 print >> sys.stderr, self, 'Error translating value: "%s"\n\t%s\n\t%s'%(value, e, self._converter.help())
141 #override in subclasses to handle the wxgui object
142 def _update(self, value): raise NotImplementedError
143 def _handle(self, event): raise NotImplementedError
145 #provide a set/get interface for this form
146 def get_value(self): return self[EXT_KEY]
147 def set_value(self, value): self[EXT_KEY] = value
149 def Disable(self, disable=True): self.Enable(not disable)
150 def Enable(self, enable=True):
152 for widget in self._widgets: widget.Enable()
154 for widget in self._widgets: widget.Disable()
156 ########################################################################
157 # Base Class Chooser Form
158 ########################################################################
159 class _chooser_base(_form_base):
160 def __init__(self, choices=[], labels=None, **kwargs):
161 _form_base.__init__(self, converter=converters.chooser_converter(choices), **kwargs)
162 self._choices = choices
163 self._labels = map(str, labels or choices)
165 ########################################################################
166 # Base Class Slider Form
167 ########################################################################
168 class _slider_base(_form_base):
169 def __init__(self, label='', length=-1, converter=None, num_steps=100, style=wx.SL_HORIZONTAL, **kwargs):
170 _form_base.__init__(self, converter=converter, **kwargs)
171 if style & wx.SL_HORIZONTAL: slider_size = wx.Size(length, -1)
172 elif style & wx.SL_VERTICAL: slider_size = wx.Size(-1, length)
173 else: raise NotImplementedError
174 self._slider = wx.Slider(self._parent, minValue=0, maxValue=num_steps, size=slider_size, style=style)
175 self._slider.Bind(wx.EVT_SCROLL, self._handle)
176 self._add_widget(self._slider, label, flag=wx.EXPAND)
178 def _handle(self, event): self[INT_KEY] = self._slider.GetValue()
179 def _update(self, value): self._slider.SetValue(value)
181 ########################################################################
183 ########################################################################
184 class static_text(_form_base):
187 @param parent the parent widget
188 @param sizer add this widget to sizer if provided (optional)
189 @param proportion the proportion when added to the sizer (default=0)
190 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
191 @param ps the pubsub object (optional)
192 @param key the pubsub key (optional)
193 @param value the default value (optional)
194 @param label title label for this widget (optional)
195 @param width the width of the form in px
196 @param bold true to bold-ify the text (default=False)
197 @param converter forms.str_converter(), int_converter(), float_converter()...
199 def __init__(self, label='', width=-1, bold=False, converter=converters.str_converter(), **kwargs):
200 _form_base.__init__(self, converter=converter, **kwargs)
201 self._static_text = wx.StaticText(self._parent, size=wx.Size(width, -1))
202 if bold: make_bold(self._static_text)
203 self._add_widget(self._static_text, label)
205 def _update(self, label): self._static_text.SetLabel(label); self._parent.Layout()
207 ########################################################################
209 ########################################################################
210 class text_box(_form_base):
213 @param parent the parent widget
214 @param sizer add this widget to sizer if provided (optional)
215 @param proportion the proportion when added to the sizer (default=0)
216 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
217 @param ps the pubsub object (optional)
218 @param key the pubsub key (optional)
219 @param value the default value (optional)
220 @param label title label for this widget (optional)
221 @param width the width of the form in px
222 @param converter forms.str_converter(), int_converter(), float_converter()...
224 def __init__(self, label='', width=-1, converter=converters.eval_converter(), **kwargs):
225 _form_base.__init__(self, converter=converter, **kwargs)
226 self._text_box = wx.TextCtrl(self._parent, size=wx.Size(width, -1), style=wx.TE_PROCESS_ENTER)
227 self._text_box.Bind(wx.EVT_TEXT_ENTER, self._handle)
228 self._add_widget(self._text_box, label)
230 def _handle(self, event): self[INT_KEY] = self._text_box.GetValue()
231 def _update(self, value): self._text_box.SetValue(value)
233 ########################################################################
237 ########################################################################
238 class slider(_slider_base):
240 A generic linear slider.
241 @param parent the parent widget
242 @param sizer add this widget to sizer if provided (optional)
243 @param proportion the proportion when added to the sizer (default=0)
244 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
245 @param ps the pubsub object (optional)
246 @param key the pubsub key (optional)
247 @param value the default value (optional)
248 @param label title label for this widget (optional)
249 @param length the length of the slider in px (optional)
250 @param style wx.SL_HORIZONTAL or wx.SL_VERTICAL (default=horizontal)
251 @param minimum the minimum value
252 @param maximum the maximum value
253 @param num_steps the number of slider steps (or specify step_size)
254 @param step_size the step between slider jumps (or specify num_steps)
255 @param cast a cast function, int, or float (default=float)
257 def __init__(self, minimum=-100, maximum=100, num_steps=100, step_size=None, cast=float, **kwargs):
258 assert step_size or num_steps
259 if step_size is not None: num_steps = (maximum - minimum)/step_size
260 converter = converters.slider_converter(minimum=minimum, maximum=maximum, num_steps=num_steps, cast=cast)
261 _slider_base.__init__(self, converter=converter, num_steps=num_steps, **kwargs)
263 class log_slider(_slider_base):
265 A generic logarithmic slider.
266 The sliders min and max values are base**min_exp and base**max_exp.
267 @param parent the parent widget
268 @param sizer add this widget to sizer if provided (optional)
269 @param proportion the proportion when added to the sizer (default=0)
270 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
271 @param ps the pubsub object (optional)
272 @param key the pubsub key (optional)
273 @param value the default value (optional)
274 @param label title label for this widget (optional)
275 @param length the length of the slider in px (optional)
276 @param style wx.SL_HORIZONTAL or wx.SL_VERTICAL (default=horizontal)
277 @param min_exp the minimum exponent
278 @param max_exp the maximum exponent
279 @param base the exponent base in base**exp
280 @param num_steps the number of slider steps (or specify step_size)
281 @param step_size the exponent step size (or specify num_steps)
283 def __init__(self, min_exp=0, max_exp=1, base=10, num_steps=100, step_size=None, **kwargs):
284 assert step_size or num_steps
285 if step_size is not None: num_steps = (max_exp - min_exp)/step_size
286 converter = converters.log_slider_converter(min_exp=min_exp, max_exp=max_exp, num_steps=num_steps, base=base)
287 _slider_base.__init__(self, converter=converter, num_steps=num_steps, **kwargs)
289 ########################################################################
291 ########################################################################
292 class gauge(_form_base):
295 The gauge displays floating point values between the minimum and maximum.
296 @param parent the parent widget
297 @param sizer add this widget to sizer if provided (optional)
298 @param proportion the proportion when added to the sizer (default=0)
299 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
300 @param ps the pubsub object (optional)
301 @param key the pubsub key (optional)
302 @param value the default value (optional)
303 @param label title label for this widget (optional)
304 @param length the length of the slider in px (optional)
305 @param style wx.GA_HORIZONTAL or wx.GA_VERTICAL (default=horizontal)
306 @param minimum the minimum value
307 @param maximum the maximum value
308 @param num_steps the number of slider steps (or specify step_size)
309 @param step_size the step between slider jumps (or specify num_steps)
311 def __init__(self, label='', length=-1, minimum=-100, maximum=100, num_steps=100, step_size=None, style=wx.GA_HORIZONTAL, **kwargs):
312 assert step_size or num_steps
313 if step_size is not None: num_steps = (maximum - minimum)/step_size
314 converter = converters.slider_converter(minimum=minimum, maximum=maximum, num_steps=num_steps, cast=float)
315 _form_base.__init__(self, converter=converter, **kwargs)
316 if style & wx.SL_HORIZONTAL: gauge_size = wx.Size(length, -1)
317 elif style & wx.SL_VERTICAL: gauge_size = wx.Size(-1, length)
318 else: raise NotImplementedError
319 self._gauge = wx.Gauge(self._parent, range=num_steps, size=gauge_size, style=style)
320 self._add_widget(self._gauge, label, flag=wx.EXPAND)
322 def _update(self, value): self._gauge.SetValue(value)
324 ########################################################################
326 ########################################################################
327 class check_box(_form_base):
329 Create a check box form.
330 @param parent the parent widget
331 @param sizer add this widget to sizer if provided (optional)
332 @param proportion the proportion when added to the sizer (default=0)
333 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
334 @param ps the pubsub object (optional)
335 @param key the pubsub key (optional)
336 @param value the default value (optional)
337 @param true the value for form when checked (default=True)
338 @param false the value for form when unchecked (default=False)
339 @param label title label for this widget (optional)
341 def __init__(self, label='', true=True, false=False, **kwargs):
342 _form_base.__init__(self, converter=converters.bool_converter(true=true, false=false), **kwargs)
343 self._check_box = wx.CheckBox(self._parent, style=wx.CHK_2STATE, label=label)
344 self._check_box.Bind(wx.EVT_CHECKBOX, self._handle)
345 self._add_widget(self._check_box)
347 def _handle(self, event): self[INT_KEY] = self._check_box.IsChecked()
348 def _update(self, checked): self._check_box.SetValue(checked)
350 ########################################################################
351 # Drop Down Chooser Form
352 ########################################################################
353 class drop_down(_chooser_base):
355 Create a drop down menu form.
356 @param parent the parent widget
357 @param sizer add this widget to sizer if provided (optional)
358 @param proportion the proportion when added to the sizer (default=0)
359 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
360 @param ps the pubsub object (optional)
361 @param key the pubsub key (optional)
362 @param value the default value (optional)
363 @param choices list of possible values
364 @param labels list of labels for each choice (default=choices)
365 @param label title label for this widget (optional)
366 @param width the form width in px (optional)
368 def __init__(self, label='', width=-1, **kwargs):
369 _chooser_base.__init__(self, **kwargs)
370 self._drop_down = wx.Choice(self._parent, choices=self._labels, size=wx.Size(width, -1))
371 self._drop_down.Bind(wx.EVT_CHOICE, self._handle)
372 self._add_widget(self._drop_down, label, widget_prop=0, label_prop=1)
374 def _handle(self, event): self[INT_KEY] = self._drop_down.GetSelection()
375 def _update(self, i): self._drop_down.SetSelection(i)
377 ########################################################################
378 # Button Chooser Form
379 # Circularly move through the choices with each click.
380 # Can be a single-click button with one choice.
381 # Can be a 2-state button with two choices.
382 ########################################################################
383 class button(_chooser_base):
385 Create a multi-state button.
386 @param parent the parent widget
387 @param sizer add this widget to sizer if provided (optional)
388 @param proportion the proportion when added to the sizer (default=0)
389 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
390 @param ps the pubsub object (optional)
391 @param key the pubsub key (optional)
392 @param value the default value (optional)
393 @param choices list of possible values
394 @param labels list of labels for each choice (default=choices)
395 @param width the width of the button in pixels (optional)
396 @param style style arguments (optional)
397 @param label title label for this widget (optional)
399 def __init__(self, label='', style=0, width=-1, **kwargs):
400 _chooser_base.__init__(self, **kwargs)
401 self._button = wx.Button(self._parent, size=wx.Size(width, -1), style=style)
402 self._button.Bind(wx.EVT_BUTTON, self._handle)
403 self._add_widget(self._button, label, widget_prop=((not style&wx.BU_EXACTFIT) and 1 or 0))
405 def _handle(self, event): self[INT_KEY] = (self[INT_KEY] + 1)%len(self._choices) #circularly increment index
406 def _update(self, i): self._button.SetLabel(self._labels[i]); self.Layout()
408 class toggle_button(button):
410 Create a dual-state button.
411 This button will alternate between True and False when clicked.
412 @param parent the parent widget
413 @param sizer add this widget to sizer if provided (optional)
414 @param proportion the proportion when added to the sizer (default=0)
415 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
416 @param ps the pubsub object (optional)
417 @param key the pubsub key (optional)
418 @param value the default value (optional)
419 @param width the width of the button in pixels (optional)
420 @param style style arguments (optional)
421 @param true_label the button's label in the true state
422 @param false_label the button's label in the false state
424 def __init__(self, true_label='On (click to stop)', false_label='Off (click to start)', **kwargs):
425 button.__init__(self, choices=[True, False], labels=[true_label, false_label], **kwargs)
427 class single_button(toggle_button):
429 Create a single state button.
430 This button will callback() when clicked.
431 For use when state holding is not important.
432 @param parent the parent widget
433 @param sizer add this widget to sizer if provided (optional)
434 @param proportion the proportion when added to the sizer (default=0)
435 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
436 @param ps the pubsub object (optional)
437 @param key the pubsub key (optional)
438 @param value the default value (optional)
439 @param width the width of the button in pixels (optional)
440 @param style style arguments (optional)
441 @param label the button's label
443 def __init__(self, label='click for callback', **kwargs):
444 toggle_button.__init__(self, true_label=label, false_label=label, value=True, **kwargs)
446 ########################################################################
447 # Radio Buttons Chooser Form
448 ########################################################################
449 class radio_buttons(_chooser_base):
451 Create a radio button form.
452 @param parent the parent widget
453 @param sizer add this widget to sizer if provided (optional)
454 @param proportion the proportion when added to the sizer (default=0)
455 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
456 @param ps the pubsub object (optional)
457 @param key the pubsub key (optional)
458 @param value the default value (optional)
459 @param choices list of possible values
460 @param labels list of labels for each choice (default=choices)
461 @param major_dimension the number of rows/cols (default=auto)
462 @param label title label for this widget (optional)
463 @param style useful style args: wx.RA_HORIZONTAL, wx.RA_VERTICAL, wx.NO_BORDER (default=wx.RA_HORIZONTAL)
465 def __init__(self, style=wx.RA_HORIZONTAL, label='', major_dimension=0, **kwargs):
466 _chooser_base.__init__(self, **kwargs)
467 #create radio buttons
468 self._radio_buttons = wx.RadioBox(self._parent, choices=self._labels, style=style, label=label, majorDimension=major_dimension)
469 self._radio_buttons.Bind(wx.EVT_RADIOBOX, self._handle)
470 self._add_widget(self._radio_buttons)
472 def _handle(self, event): self[INT_KEY] = self._radio_buttons.GetSelection()
473 def _update(self, i): self._radio_buttons.SetSelection(i)
475 ########################################################################
476 # Notebook Chooser Form
477 # The notebook pages/tabs are for selecting between choices.
478 # A page must be added to the notebook for each choice.
479 ########################################################################
480 class notebook(_chooser_base):
481 def __init__(self, pages, notebook, **kwargs):
482 _chooser_base.__init__(self, **kwargs)
483 assert len(pages) == len(self._choices)
484 self._notebook = notebook
485 self._notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self._handle)
486 #add pages, setting the label on each tab
487 for i, page in enumerate(pages):
488 self._notebook.AddPage(page, self._labels[i])
489 self._add_widget(self._notebook)
491 def _handle(self, event): self[INT_KEY] = self._notebook.GetSelection()
492 def _update(self, i): self._notebook.SetSelection(i)
494 # ----------------------------------------------------------------
495 # Stand-alone test application
496 # ----------------------------------------------------------------
499 from gnuradio.wxgui import gui
501 class app_gui (object):
502 def __init__(self, frame, panel, vbox, top_block, options, args):
504 def callback(v): print v
509 choices=[2, 4, 8, 16],
510 labels=['two', 'four', 'eight', 'sixteen'],
512 style=wx.RA_HORIZONTAL,
513 label='test radio long string',
515 #major_dimension = 2,
521 choices=[2, 4, 8, 16],
522 labels=['two', 'four', 'eight', 'sixteen'],
524 style=wx.RA_VERTICAL,
525 label='test radio long string',
527 #major_dimension = 2,
533 choices=[2, 4, 8, 16],
534 labels=['two', 'four', 'eight', 'sixteen'],
536 style=wx.RA_VERTICAL | wx.NO_BORDER,
538 #major_dimension = 2,
544 choices=[2, 4, 8, 16],
545 labels=['two', 'four', 'eight', 'sixteen'],
547 label='button value',
556 choices=[2, 4, 8, 16],
608 style=wx.SL_VERTICAL,
627 if __name__ == "__main__":
630 # Create the GUI application
632 gui=app_gui, # User interface class
633 title="Test Forms", # Top window title
639 except RuntimeError, e: