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(int(round(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 units a suffix to add after the text
198 @param converter forms.str_converter(), int_converter(), float_converter()...
200 def __init__(self, label='', width=-1, bold=False, units='', converter=converters.str_converter(), **kwargs):
202 _form_base.__init__(self, converter=converter, **kwargs)
203 self._static_text = wx.StaticText(self._parent, size=wx.Size(width, -1))
204 if bold: make_bold(self._static_text)
205 self._add_widget(self._static_text, label)
207 def _update(self, label):
208 if self._units: label += ' ' + self._units
209 self._static_text.SetLabel(label); self._parent.Layout()
211 ########################################################################
213 ########################################################################
214 class text_box(_form_base):
217 @param parent the parent widget
218 @param sizer add this widget to sizer if provided (optional)
219 @param proportion the proportion when added to the sizer (default=0)
220 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
221 @param ps the pubsub object (optional)
222 @param key the pubsub key (optional)
223 @param value the default value (optional)
224 @param label title label for this widget (optional)
225 @param width the width of the form in px
226 @param converter forms.str_converter(), int_converter(), float_converter()...
228 def __init__(self, label='', width=-1, converter=converters.eval_converter(), **kwargs):
229 _form_base.__init__(self, converter=converter, **kwargs)
230 self._text_box = wx.TextCtrl(self._parent, size=wx.Size(width, -1), style=wx.TE_PROCESS_ENTER)
231 self._default_bg_colour = self._text_box.GetBackgroundColour()
232 self._text_box.Bind(wx.EVT_TEXT_ENTER, self._handle)
233 self._text_box.Bind(wx.EVT_TEXT, self._update_color)
234 self._add_widget(self._text_box, label)
236 def _update_color(self, *args):
237 if self._text_box.GetValue() == self[INT_KEY]:
238 self._text_box.SetBackgroundColour(self._default_bg_colour)
239 else: self._text_box.SetBackgroundColour('#EEDDDD')
241 def _handle(self, event): self[INT_KEY] = self._text_box.GetValue()
242 def _update(self, value): self._text_box.SetValue(value); self._update_color()
244 ########################################################################
248 ########################################################################
249 class slider(_slider_base):
251 A generic linear slider.
252 @param parent the parent widget
253 @param sizer add this widget to sizer if provided (optional)
254 @param proportion the proportion when added to the sizer (default=0)
255 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
256 @param ps the pubsub object (optional)
257 @param key the pubsub key (optional)
258 @param value the default value (optional)
259 @param label title label for this widget (optional)
260 @param length the length of the slider in px (optional)
261 @param style wx.SL_HORIZONTAL or wx.SL_VERTICAL (default=horizontal)
262 @param minimum the minimum value
263 @param maximum the maximum value
264 @param num_steps the number of slider steps (or specify step_size)
265 @param step_size the step between slider jumps (or specify num_steps)
266 @param cast a cast function, int, or float (default=float)
268 def __init__(self, minimum=-100, maximum=100, num_steps=100, step_size=None, cast=float, **kwargs):
269 assert step_size or num_steps
270 if step_size is not None: num_steps = (maximum - minimum)/step_size
271 converter = converters.slider_converter(minimum=minimum, maximum=maximum, num_steps=num_steps, cast=cast)
272 _slider_base.__init__(self, converter=converter, num_steps=num_steps, **kwargs)
274 class log_slider(_slider_base):
276 A generic logarithmic slider.
277 The sliders min and max values are base**min_exp and base**max_exp.
278 @param parent the parent widget
279 @param sizer add this widget to sizer if provided (optional)
280 @param proportion the proportion when added to the sizer (default=0)
281 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
282 @param ps the pubsub object (optional)
283 @param key the pubsub key (optional)
284 @param value the default value (optional)
285 @param label title label for this widget (optional)
286 @param length the length of the slider in px (optional)
287 @param style wx.SL_HORIZONTAL or wx.SL_VERTICAL (default=horizontal)
288 @param min_exp the minimum exponent
289 @param max_exp the maximum exponent
290 @param base the exponent base in base**exp
291 @param num_steps the number of slider steps (or specify step_size)
292 @param step_size the exponent step size (or specify num_steps)
294 def __init__(self, min_exp=0, max_exp=1, base=10, num_steps=100, step_size=None, **kwargs):
295 assert step_size or num_steps
296 if step_size is not None: num_steps = (max_exp - min_exp)/step_size
297 converter = converters.log_slider_converter(min_exp=min_exp, max_exp=max_exp, num_steps=num_steps, base=base)
298 _slider_base.__init__(self, converter=converter, num_steps=num_steps, **kwargs)
300 ########################################################################
302 ########################################################################
303 class gauge(_form_base):
306 The gauge displays floating point values between the minimum and maximum.
307 @param parent the parent widget
308 @param sizer add this widget to sizer if provided (optional)
309 @param proportion the proportion when added to the sizer (default=0)
310 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
311 @param ps the pubsub object (optional)
312 @param key the pubsub key (optional)
313 @param value the default value (optional)
314 @param label title label for this widget (optional)
315 @param length the length of the slider in px (optional)
316 @param style wx.GA_HORIZONTAL or wx.GA_VERTICAL (default=horizontal)
317 @param minimum the minimum value
318 @param maximum the maximum value
319 @param num_steps the number of slider steps (or specify step_size)
320 @param step_size the step between slider jumps (or specify num_steps)
322 def __init__(self, label='', length=-1, minimum=-100, maximum=100, num_steps=100, step_size=None, style=wx.GA_HORIZONTAL, **kwargs):
323 assert step_size or num_steps
324 if step_size is not None: num_steps = (maximum - minimum)/step_size
325 converter = converters.slider_converter(minimum=minimum, maximum=maximum, num_steps=num_steps, cast=float)
326 _form_base.__init__(self, converter=converter, **kwargs)
327 if style & wx.SL_HORIZONTAL: gauge_size = wx.Size(length, -1)
328 elif style & wx.SL_VERTICAL: gauge_size = wx.Size(-1, length)
329 else: raise NotImplementedError
330 self._gauge = wx.Gauge(self._parent, range=num_steps, size=gauge_size, style=style)
331 self._add_widget(self._gauge, label, flag=wx.EXPAND)
333 def _update(self, value): self._gauge.SetValue(value)
335 ########################################################################
337 ########################################################################
338 class check_box(_form_base):
340 Create a check box form.
341 @param parent the parent widget
342 @param sizer add this widget to sizer if provided (optional)
343 @param proportion the proportion when added to the sizer (default=0)
344 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
345 @param ps the pubsub object (optional)
346 @param key the pubsub key (optional)
347 @param value the default value (optional)
348 @param true the value for form when checked (default=True)
349 @param false the value for form when unchecked (default=False)
350 @param label title label for this widget (optional)
352 def __init__(self, label='', true=True, false=False, **kwargs):
353 _form_base.__init__(self, converter=converters.bool_converter(true=true, false=false), **kwargs)
354 self._check_box = wx.CheckBox(self._parent, style=wx.CHK_2STATE, label=label)
355 self._check_box.Bind(wx.EVT_CHECKBOX, self._handle)
356 self._add_widget(self._check_box)
358 def _handle(self, event): self[INT_KEY] = self._check_box.IsChecked()
359 def _update(self, checked): self._check_box.SetValue(checked)
361 ########################################################################
362 # Drop Down Chooser Form
363 ########################################################################
364 class drop_down(_chooser_base):
366 Create a drop down menu form.
367 @param parent the parent widget
368 @param sizer add this widget to sizer if provided (optional)
369 @param proportion the proportion when added to the sizer (default=0)
370 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
371 @param ps the pubsub object (optional)
372 @param key the pubsub key (optional)
373 @param value the default value (optional)
374 @param choices list of possible values
375 @param labels list of labels for each choice (default=choices)
376 @param label title label for this widget (optional)
377 @param width the form width in px (optional)
379 def __init__(self, label='', width=-1, **kwargs):
380 _chooser_base.__init__(self, **kwargs)
381 self._drop_down = wx.Choice(self._parent, choices=self._labels, size=wx.Size(width, -1))
382 self._drop_down.Bind(wx.EVT_CHOICE, self._handle)
383 self._add_widget(self._drop_down, label, widget_prop=0, label_prop=1)
385 def _handle(self, event): self[INT_KEY] = self._drop_down.GetSelection()
386 def _update(self, i): self._drop_down.SetSelection(i)
388 ########################################################################
389 # Button Chooser Form
390 # Circularly move through the choices with each click.
391 # Can be a single-click button with one choice.
392 # Can be a 2-state button with two choices.
393 ########################################################################
394 class button(_chooser_base):
396 Create a multi-state button.
397 @param parent the parent widget
398 @param sizer add this widget to sizer if provided (optional)
399 @param proportion the proportion when added to the sizer (default=0)
400 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
401 @param ps the pubsub object (optional)
402 @param key the pubsub key (optional)
403 @param value the default value (optional)
404 @param choices list of possible values
405 @param labels list of labels for each choice (default=choices)
406 @param width the width of the button in pixels (optional)
407 @param style style arguments (optional)
408 @param label title label for this widget (optional)
410 def __init__(self, label='', style=0, width=-1, **kwargs):
411 _chooser_base.__init__(self, **kwargs)
412 self._button = wx.Button(self._parent, size=wx.Size(width, -1), style=style)
413 self._button.Bind(wx.EVT_BUTTON, self._handle)
414 self._add_widget(self._button, label, widget_prop=((not style&wx.BU_EXACTFIT) and 1 or 0))
416 def _handle(self, event): self[INT_KEY] = (self[INT_KEY] + 1)%len(self._choices) #circularly increment index
417 def _update(self, i): self._button.SetLabel(self._labels[i]); self.Layout()
419 class toggle_button(button):
421 Create a dual-state button.
422 This button will alternate between True and False when clicked.
423 @param parent the parent widget
424 @param sizer add this widget to sizer if provided (optional)
425 @param proportion the proportion when added to the sizer (default=0)
426 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
427 @param ps the pubsub object (optional)
428 @param key the pubsub key (optional)
429 @param value the default value (optional)
430 @param width the width of the button in pixels (optional)
431 @param style style arguments (optional)
432 @param true_label the button's label in the true state
433 @param false_label the button's label in the false state
435 def __init__(self, true_label='On (click to stop)', false_label='Off (click to start)', **kwargs):
436 button.__init__(self, choices=[True, False], labels=[true_label, false_label], **kwargs)
438 class single_button(toggle_button):
440 Create a single state button.
441 This button will callback() when clicked.
442 For use when state holding is not important.
443 @param parent the parent widget
444 @param sizer add this widget to sizer if provided (optional)
445 @param proportion the proportion when added to the sizer (default=0)
446 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
447 @param ps the pubsub object (optional)
448 @param key the pubsub key (optional)
449 @param value the default value (optional)
450 @param width the width of the button in pixels (optional)
451 @param style style arguments (optional)
452 @param label the button's label
454 def __init__(self, label='click for callback', **kwargs):
455 toggle_button.__init__(self, true_label=label, false_label=label, value=True, **kwargs)
457 ########################################################################
458 # Radio Buttons Chooser Form
459 ########################################################################
460 class radio_buttons(_chooser_base):
462 Create a radio button form.
463 @param parent the parent widget
464 @param sizer add this widget to sizer if provided (optional)
465 @param proportion the proportion when added to the sizer (default=0)
466 @param flag the flag argument when added to the sizer (default=wx.EXPAND)
467 @param ps the pubsub object (optional)
468 @param key the pubsub key (optional)
469 @param value the default value (optional)
470 @param choices list of possible values
471 @param labels list of labels for each choice (default=choices)
472 @param major_dimension the number of rows/cols (default=auto)
473 @param label title label for this widget (optional)
474 @param style useful style args: wx.RA_HORIZONTAL, wx.RA_VERTICAL, wx.NO_BORDER (default=wx.RA_HORIZONTAL)
476 def __init__(self, style=wx.RA_HORIZONTAL, label='', major_dimension=0, **kwargs):
477 _chooser_base.__init__(self, **kwargs)
478 #create radio buttons
479 self._radio_buttons = wx.RadioBox(self._parent, choices=self._labels, style=style, label=label, majorDimension=major_dimension)
480 self._radio_buttons.Bind(wx.EVT_RADIOBOX, self._handle)
481 self._add_widget(self._radio_buttons)
483 def _handle(self, event): self[INT_KEY] = self._radio_buttons.GetSelection()
484 def _update(self, i): self._radio_buttons.SetSelection(i)
486 ########################################################################
487 # Notebook Chooser Form
488 # The notebook pages/tabs are for selecting between choices.
489 # A page must be added to the notebook for each choice.
490 ########################################################################
491 class notebook(_chooser_base):
492 def __init__(self, pages, notebook, **kwargs):
493 _chooser_base.__init__(self, **kwargs)
494 assert len(pages) == len(self._choices)
495 self._notebook = notebook
496 self._notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self._handle)
497 #add pages, setting the label on each tab
498 for i, page in enumerate(pages):
499 self._notebook.AddPage(page, self._labels[i])
500 self._add_widget(self._notebook)
502 def _handle(self, event): self[INT_KEY] = self._notebook.GetSelection()
503 def _update(self, i): self._notebook.SetSelection(i)
505 # ----------------------------------------------------------------
506 # Stand-alone test application
507 # ----------------------------------------------------------------
510 from gnuradio.wxgui import gui
512 class app_gui (object):
513 def __init__(self, frame, panel, vbox, top_block, options, args):
515 def callback(v): print v
520 choices=[2, 4, 8, 16],
521 labels=['two', 'four', 'eight', 'sixteen'],
523 style=wx.RA_HORIZONTAL,
524 label='test radio long string',
526 #major_dimension = 2,
532 choices=[2, 4, 8, 16],
533 labels=['two', 'four', 'eight', 'sixteen'],
535 style=wx.RA_VERTICAL,
536 label='test radio long string',
538 #major_dimension = 2,
544 choices=[2, 4, 8, 16],
545 labels=['two', 'four', 'eight', 'sixteen'],
547 style=wx.RA_VERTICAL | wx.NO_BORDER,
549 #major_dimension = 2,
555 choices=[2, 4, 8, 16],
556 labels=['two', 'four', 'eight', 'sixteen'],
558 label='button value',
567 choices=[2, 4, 8, 16],
619 style=wx.SL_VERTICAL,
638 if __name__ == "__main__":
641 # Create the GUI application
643 gui=app_gui, # User interface class
644 title="Test Forms", # Top window title
650 except RuntimeError, e: