diff options
Diffstat (limited to 'gr-wxgui/src/python/forms/forms.py')
-rw-r--r-- | gr-wxgui/src/python/forms/forms.py | 641 |
1 files changed, 641 insertions, 0 deletions
diff --git a/gr-wxgui/src/python/forms/forms.py b/gr-wxgui/src/python/forms/forms.py new file mode 100644 index 000000000..10f6a4823 --- /dev/null +++ b/gr-wxgui/src/python/forms/forms.py @@ -0,0 +1,641 @@ +# +# Copyright 2009 Free Software Foundation, Inc. +# +# This file is part of GNU Radio +# +# GNU Radio 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 3, or (at your option) +# any later version. +# +# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. +# + +""" +The forms module contains general purpose wx-gui forms for gnuradio apps. + +The forms follow a layered model: + * internal layer + * deals with the wxgui objects directly + * implemented in event handler and update methods + * translation layer + * translates the between the external and internal layers + * handles parsing errors between layers + * external layer + * provided external access to the user + * set_value, get_value, and optional callback + * set and get through optional pubsub and key + +Known problems: + * An empty label in the radio box still consumes space. + * The static text cannot resize the parent at runtime. +""" + +EXT_KEY = 'external' +INT_KEY = 'internal' + +import wx +import sys +from gnuradio.gr.pubsub import pubsub +import converters + +EVT_DATA = wx.PyEventBinder(wx.NewEventType()) +class DataEvent(wx.PyEvent): + def __init__(self, data): + wx.PyEvent.__init__(self, wx.NewId(), EVT_DATA.typeId) + self.data = data + +def make_bold(widget): + font = widget.GetFont() + font.SetWeight(wx.FONTWEIGHT_BOLD) + widget.SetFont(font) + +######################################################################## +# Base Class Form +######################################################################## +class _form_base(pubsub, wx.BoxSizer): + def __init__(self, parent=None, sizer=None, proportion=0, flag=wx.EXPAND, ps=None, key='', value=None, callback=None, converter=converters.identity_converter()): + pubsub.__init__(self) + wx.BoxSizer.__init__(self, wx.HORIZONTAL) + self._parent = parent + self._key = key + self._converter = converter + self._callback = callback + self._widgets = list() + #add to the sizer if provided + if sizer: sizer.Add(self, proportion, flag) + #proxy the pubsub and key into this form + if ps is not None: + assert key + self.proxy(EXT_KEY, ps, key) + #no pubsub passed, must set initial value + else: self.set_value(value) + + def __str__(self): + return "Form: %s -> %s"%(self.__class__, self._key) + + def _add_widget(self, widget, label='', flag=0, label_prop=0, widget_prop=1): + """ + Add the main widget to this object sizer. + If label is passed, add a label as well. + Register the widget and the label in the widgets list (for enable/disable). + Bind the update handler to the widget for data events. + This ensures that the gui thread handles updating widgets. + Setup the pusub triggers for external and internal. + @param widget the main widget + @param label the optional label + @param flag additional flags for widget + @param label_prop the proportion for the label + @param widget_prop the proportion for the widget + """ + #setup data event + widget.Bind(EVT_DATA, lambda x: self._update(x.data)) + update = lambda x: wx.PostEvent(widget, DataEvent(x)) + #register widget + self._widgets.append(widget) + #create optional label + if not label: self.Add(widget, widget_prop, wx.ALIGN_CENTER_VERTICAL | flag) + else: + label_text = wx.StaticText(self._parent, label='%s: '%label) + self._widgets.append(label_text) + self.Add(label_text, label_prop, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT) + self.Add(widget, widget_prop, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | flag) + #initialize without triggering pubsubs + self._translate_external_to_internal(self[EXT_KEY]) + update(self[INT_KEY]) + #subscribe all the functions + self.subscribe(INT_KEY, update) + self.subscribe(INT_KEY, self._translate_internal_to_external) + self.subscribe(EXT_KEY, self._translate_external_to_internal) + if self._callback: self.subscribe(EXT_KEY, self._callback) + + def _translate_external_to_internal(self, external): + try: + internal = self._converter.external_to_internal(external) + #prevent infinite loop between internal and external pubsub keys by only setting if changed + if self[INT_KEY] != internal: self[INT_KEY] = internal + except Exception, e: + self._err_msg(external, e) + self[INT_KEY] = self[INT_KEY] #reset to last good setting + + def _translate_internal_to_external(self, internal): + try: + external = self._converter.internal_to_external(internal) + #prevent infinite loop between internal and external pubsub keys by only setting if changed + if self[EXT_KEY] != external: self[EXT_KEY] = external + except Exception, e: + self._err_msg(internal, e) + self[EXT_KEY] = self[EXT_KEY] #reset to last good setting + + def _err_msg(self, value, e): + print >> sys.stderr, self, 'Error translating value: "%s"\n\t%s\n\t%s'%(value, e, self._converter.help()) + + #override in subclasses to handle the wxgui object + def _update(self, value): raise NotImplementedError + def _handle(self, event): raise NotImplementedError + + #provide a set/get interface for this form + def get_value(self): return self[EXT_KEY] + def set_value(self, value): self[EXT_KEY] = value + + def Disable(self, disable=True): self.Enable(not disable) + def Enable(self, enable=True): + if enable: + for widget in self._widgets: widget.Enable() + else: + for widget in self._widgets: widget.Disable() + +######################################################################## +# Base Class Chooser Form +######################################################################## +class _chooser_base(_form_base): + def __init__(self, choices=[], labels=None, **kwargs): + _form_base.__init__(self, converter=converters.chooser_converter(choices), **kwargs) + self._choices = choices + self._labels = map(str, labels or choices) + +######################################################################## +# Base Class Slider Form +######################################################################## +class _slider_base(_form_base): + def __init__(self, label='', length=-1, converter=None, num_steps=100, style=wx.SL_HORIZONTAL, **kwargs): + _form_base.__init__(self, converter=converter, **kwargs) + if style & wx.SL_HORIZONTAL: slider_size = wx.Size(length, -1) + elif style & wx.SL_VERTICAL: slider_size = wx.Size(-1, length) + else: raise NotImplementedError + self._slider = wx.Slider(self._parent, minValue=0, maxValue=num_steps, size=slider_size, style=style) + self._slider.Bind(wx.EVT_SCROLL, self._handle) + self._add_widget(self._slider, label, flag=wx.EXPAND) + + def _handle(self, event): self[INT_KEY] = self._slider.GetValue() + def _update(self, value): self._slider.SetValue(value) + +######################################################################## +# Static Text Form +######################################################################## +class static_text(_form_base): + """ + A text box form. + @param parent the parent widget + @param sizer add this widget to sizer if provided (optional) + @param proportion the proportion when added to the sizer (default=0) + @param flag the flag argument when added to the sizer (default=wx.EXPAND) + @param ps the pubsub object (optional) + @param key the pubsub key (optional) + @param value the default value (optional) + @param label title label for this widget (optional) + @param width the width of the form in px + @param bold true to bold-ify the text (default=False) + @param converter forms.str_converter(), int_converter(), float_converter()... + """ + def __init__(self, label='', width=-1, bold=False, converter=converters.str_converter(), **kwargs): + _form_base.__init__(self, converter=converter, **kwargs) + self._static_text = wx.StaticText(self._parent, size=wx.Size(width, -1)) + if bold: make_bold(self._static_text) + self._add_widget(self._static_text, label) + + def _update(self, label): self._static_text.SetLabel(label); self._parent.Layout() + +######################################################################## +# Text Box Form +######################################################################## +class text_box(_form_base): + """ + A text box form. + @param parent the parent widget + @param sizer add this widget to sizer if provided (optional) + @param proportion the proportion when added to the sizer (default=0) + @param flag the flag argument when added to the sizer (default=wx.EXPAND) + @param ps the pubsub object (optional) + @param key the pubsub key (optional) + @param value the default value (optional) + @param label title label for this widget (optional) + @param width the width of the form in px + @param converter forms.str_converter(), int_converter(), float_converter()... + """ + def __init__(self, label='', width=-1, converter=converters.eval_converter(), **kwargs): + _form_base.__init__(self, converter=converter, **kwargs) + self._text_box = wx.TextCtrl(self._parent, size=wx.Size(width, -1), style=wx.TE_PROCESS_ENTER) + self._text_box.Bind(wx.EVT_TEXT_ENTER, self._handle) + self._add_widget(self._text_box, label) + + def _handle(self, event): self[INT_KEY] = self._text_box.GetValue() + def _update(self, value): self._text_box.SetValue(value) + +######################################################################## +# Slider Form +# Linear Slider +# Logarithmic Slider +######################################################################## +class slider(_slider_base): + """ + A generic linear slider. + @param parent the parent widget + @param sizer add this widget to sizer if provided (optional) + @param proportion the proportion when added to the sizer (default=0) + @param flag the flag argument when added to the sizer (default=wx.EXPAND) + @param ps the pubsub object (optional) + @param key the pubsub key (optional) + @param value the default value (optional) + @param label title label for this widget (optional) + @param length the length of the slider in px (optional) + @param style wx.SL_HORIZONTAL or wx.SL_VERTICAL (default=horizontal) + @param minimum the minimum value + @param maximum the maximum value + @param num_steps the number of slider steps (or specify step_size) + @param step_size the step between slider jumps (or specify num_steps) + @param cast a cast function, int, or float (default=float) + """ + def __init__(self, minimum=-100, maximum=100, num_steps=100, step_size=None, cast=float, **kwargs): + assert step_size or num_steps + if step_size is not None: num_steps = (maximum - minimum)/step_size + converter = converters.slider_converter(minimum=minimum, maximum=maximum, num_steps=num_steps, cast=cast) + _slider_base.__init__(self, converter=converter, num_steps=num_steps, **kwargs) + +class log_slider(_slider_base): + """ + A generic logarithmic slider. + The sliders min and max values are base**min_exp and base**max_exp. + @param parent the parent widget + @param sizer add this widget to sizer if provided (optional) + @param proportion the proportion when added to the sizer (default=0) + @param flag the flag argument when added to the sizer (default=wx.EXPAND) + @param ps the pubsub object (optional) + @param key the pubsub key (optional) + @param value the default value (optional) + @param label title label for this widget (optional) + @param length the length of the slider in px (optional) + @param style wx.SL_HORIZONTAL or wx.SL_VERTICAL (default=horizontal) + @param min_exp the minimum exponent + @param max_exp the maximum exponent + @param base the exponent base in base**exp + @param num_steps the number of slider steps (or specify step_size) + @param step_size the exponent step size (or specify num_steps) + """ + def __init__(self, min_exp=0, max_exp=1, base=10, num_steps=100, step_size=None, **kwargs): + assert step_size or num_steps + if step_size is not None: num_steps = (max_exp - min_exp)/step_size + converter = converters.log_slider_converter(min_exp=min_exp, max_exp=max_exp, num_steps=num_steps, base=base) + _slider_base.__init__(self, converter=converter, num_steps=num_steps, **kwargs) + +######################################################################## +# Gauge Form +######################################################################## +class gauge(_form_base): + """ + A gauge bar. + The gauge displays floating point values between the minimum and maximum. + @param parent the parent widget + @param sizer add this widget to sizer if provided (optional) + @param proportion the proportion when added to the sizer (default=0) + @param flag the flag argument when added to the sizer (default=wx.EXPAND) + @param ps the pubsub object (optional) + @param key the pubsub key (optional) + @param value the default value (optional) + @param label title label for this widget (optional) + @param length the length of the slider in px (optional) + @param style wx.GA_HORIZONTAL or wx.GA_VERTICAL (default=horizontal) + @param minimum the minimum value + @param maximum the maximum value + @param num_steps the number of slider steps (or specify step_size) + @param step_size the step between slider jumps (or specify num_steps) + """ + def __init__(self, label='', length=-1, minimum=-100, maximum=100, num_steps=100, step_size=None, style=wx.GA_HORIZONTAL, **kwargs): + assert step_size or num_steps + if step_size is not None: num_steps = (maximum - minimum)/step_size + converter = converters.slider_converter(minimum=minimum, maximum=maximum, num_steps=num_steps, cast=float) + _form_base.__init__(self, converter=converter, **kwargs) + if style & wx.SL_HORIZONTAL: gauge_size = wx.Size(length, -1) + elif style & wx.SL_VERTICAL: gauge_size = wx.Size(-1, length) + else: raise NotImplementedError + self._gauge = wx.Gauge(self._parent, range=num_steps, size=gauge_size, style=style) + self._add_widget(self._gauge, label, flag=wx.EXPAND) + + def _update(self, value): self._gauge.SetValue(value) + +######################################################################## +# Check Box Form +######################################################################## +class check_box(_form_base): + """ + Create a check box form. + @param parent the parent widget + @param sizer add this widget to sizer if provided (optional) + @param proportion the proportion when added to the sizer (default=0) + @param flag the flag argument when added to the sizer (default=wx.EXPAND) + @param ps the pubsub object (optional) + @param key the pubsub key (optional) + @param value the default value (optional) + @param true the value for form when checked (default=True) + @param false the value for form when unchecked (default=False) + @param label title label for this widget (optional) + """ + def __init__(self, label='', true=True, false=False, **kwargs): + _form_base.__init__(self, converter=converters.bool_converter(true=true, false=false), **kwargs) + self._check_box = wx.CheckBox(self._parent, style=wx.CHK_2STATE, label=label) + self._check_box.Bind(wx.EVT_CHECKBOX, self._handle) + self._add_widget(self._check_box) + + def _handle(self, event): self[INT_KEY] = self._check_box.IsChecked() + def _update(self, checked): self._check_box.SetValue(checked) + +######################################################################## +# Drop Down Chooser Form +######################################################################## +class drop_down(_chooser_base): + """ + Create a drop down menu form. + @param parent the parent widget + @param sizer add this widget to sizer if provided (optional) + @param proportion the proportion when added to the sizer (default=0) + @param flag the flag argument when added to the sizer (default=wx.EXPAND) + @param ps the pubsub object (optional) + @param key the pubsub key (optional) + @param value the default value (optional) + @param choices list of possible values + @param labels list of labels for each choice (default=choices) + @param label title label for this widget (optional) + @param width the form width in px (optional) + """ + def __init__(self, label='', width=-1, **kwargs): + _chooser_base.__init__(self, **kwargs) + self._drop_down = wx.Choice(self._parent, choices=self._labels, size=wx.Size(width, -1)) + self._drop_down.Bind(wx.EVT_CHOICE, self._handle) + self._add_widget(self._drop_down, label, widget_prop=0, label_prop=1) + + def _handle(self, event): self[INT_KEY] = self._drop_down.GetSelection() + def _update(self, i): self._drop_down.SetSelection(i) + +######################################################################## +# Button Chooser Form +# Circularly move through the choices with each click. +# Can be a single-click button with one choice. +# Can be a 2-state button with two choices. +######################################################################## +class button(_chooser_base): + """ + Create a multi-state button. + @param parent the parent widget + @param sizer add this widget to sizer if provided (optional) + @param proportion the proportion when added to the sizer (default=0) + @param flag the flag argument when added to the sizer (default=wx.EXPAND) + @param ps the pubsub object (optional) + @param key the pubsub key (optional) + @param value the default value (optional) + @param choices list of possible values + @param labels list of labels for each choice (default=choices) + @param width the width of the button in pixels (optional) + @param style style arguments (optional) + @param label title label for this widget (optional) + """ + def __init__(self, label='', style=0, width=-1, **kwargs): + _chooser_base.__init__(self, **kwargs) + self._button = wx.Button(self._parent, size=wx.Size(width, -1), style=style) + self._button.Bind(wx.EVT_BUTTON, self._handle) + self._add_widget(self._button, label, widget_prop=((not style&wx.BU_EXACTFIT) and 1 or 0)) + + def _handle(self, event): self[INT_KEY] = (self[INT_KEY] + 1)%len(self._choices) #circularly increment index + def _update(self, i): self._button.SetLabel(self._labels[i]); self.Layout() + +class toggle_button(button): + """ + Create a dual-state button. + This button will alternate between True and False when clicked. + @param parent the parent widget + @param sizer add this widget to sizer if provided (optional) + @param proportion the proportion when added to the sizer (default=0) + @param flag the flag argument when added to the sizer (default=wx.EXPAND) + @param ps the pubsub object (optional) + @param key the pubsub key (optional) + @param value the default value (optional) + @param width the width of the button in pixels (optional) + @param style style arguments (optional) + @param true_label the button's label in the true state + @param false_label the button's label in the false state + """ + def __init__(self, true_label='On (click to stop)', false_label='Off (click to start)', **kwargs): + button.__init__(self, choices=[True, False], labels=[true_label, false_label], **kwargs) + +class single_button(toggle_button): + """ + Create a single state button. + This button will callback() when clicked. + For use when state holding is not important. + @param parent the parent widget + @param sizer add this widget to sizer if provided (optional) + @param proportion the proportion when added to the sizer (default=0) + @param flag the flag argument when added to the sizer (default=wx.EXPAND) + @param ps the pubsub object (optional) + @param key the pubsub key (optional) + @param value the default value (optional) + @param width the width of the button in pixels (optional) + @param style style arguments (optional) + @param label the button's label + """ + def __init__(self, label='click for callback', **kwargs): + toggle_button.__init__(self, true_label=label, false_label=label, value=True, **kwargs) + +######################################################################## +# Radio Buttons Chooser Form +######################################################################## +class radio_buttons(_chooser_base): + """ + Create a radio button form. + @param parent the parent widget + @param sizer add this widget to sizer if provided (optional) + @param proportion the proportion when added to the sizer (default=0) + @param flag the flag argument when added to the sizer (default=wx.EXPAND) + @param ps the pubsub object (optional) + @param key the pubsub key (optional) + @param value the default value (optional) + @param choices list of possible values + @param labels list of labels for each choice (default=choices) + @param major_dimension the number of rows/cols (default=auto) + @param label title label for this widget (optional) + @param style useful style args: wx.RA_HORIZONTAL, wx.RA_VERTICAL, wx.NO_BORDER (default=wx.RA_HORIZONTAL) + """ + def __init__(self, style=wx.RA_HORIZONTAL, label='', major_dimension=0, **kwargs): + _chooser_base.__init__(self, **kwargs) + #create radio buttons + self._radio_buttons = wx.RadioBox(self._parent, choices=self._labels, style=style, label=label, majorDimension=major_dimension) + self._radio_buttons.Bind(wx.EVT_RADIOBOX, self._handle) + self._add_widget(self._radio_buttons) + + def _handle(self, event): self[INT_KEY] = self._radio_buttons.GetSelection() + def _update(self, i): self._radio_buttons.SetSelection(i) + +######################################################################## +# Notebook Chooser Form +# The notebook pages/tabs are for selecting between choices. +# A page must be added to the notebook for each choice. +######################################################################## +class notebook(_chooser_base): + def __init__(self, pages, notebook, **kwargs): + _chooser_base.__init__(self, **kwargs) + assert len(pages) == len(self._choices) + self._notebook = notebook + self._notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self._handle) + #add pages, setting the label on each tab + for i, page in enumerate(pages): + self._notebook.AddPage(page, self._labels[i]) + self._add_widget(self._notebook) + + def _handle(self, event): self[INT_KEY] = self._notebook.GetSelection() + def _update(self, i): self._notebook.SetSelection(i) + +# ---------------------------------------------------------------- +# Stand-alone test application +# ---------------------------------------------------------------- + +import wx +from gnuradio.wxgui import gui + +class app_gui (object): + def __init__(self, frame, panel, vbox, top_block, options, args): + + def callback(v): print v + + radio_buttons( + sizer=vbox, + parent=panel, + choices=[2, 4, 8, 16], + labels=['two', 'four', 'eight', 'sixteen'], + value=4, + style=wx.RA_HORIZONTAL, + label='test radio long string', + callback=callback, + #major_dimension = 2, + ) + + radio_buttons( + sizer=vbox, + parent=panel, + choices=[2, 4, 8, 16], + labels=['two', 'four', 'eight', 'sixteen'], + value=4, + style=wx.RA_VERTICAL, + label='test radio long string', + callback=callback, + #major_dimension = 2, + ) + + radio_buttons( + sizer=vbox, + parent=panel, + choices=[2, 4, 8, 16], + labels=['two', 'four', 'eight', 'sixteen'], + value=4, + style=wx.RA_VERTICAL | wx.NO_BORDER, + callback=callback, + #major_dimension = 2, + ) + + button( + sizer=vbox, + parent=panel, + choices=[2, 4, 8, 16], + labels=['two', 'four', 'eight', 'sixteen'], + value=2, + label='button value', + callback=callback, + #width=100, + ) + + + drop_down( + sizer=vbox, + parent=panel, + choices=[2, 4, 8, 16], + value=2, + label='Choose One', + callback=callback, + ) + check_box( + sizer=vbox, + parent=panel, + value=False, + label='check me', + callback=callback, + ) + text_box( + sizer=vbox, + parent=panel, + value=3, + label='text box', + callback=callback, + width=200, + ) + + static_text( + sizer=vbox, + parent=panel, + value='bob', + label='static text', + width=-1, + bold=True, + ) + + slider( + sizer=vbox, + parent=panel, + value=12, + label='slider', + callback=callback, + ) + + log_slider( + sizer=vbox, + parent=panel, + value=12, + label='slider', + callback=callback, + ) + + slider( + sizer=vbox, + parent=panel, + value=12, + label='slider', + callback=callback, + style=wx.SL_VERTICAL, + length=30, + ) + + toggle_button( + sizer=vbox, + parent=panel, + value=True, + label='toggle it', + callback=callback, + ) + + single_button( + sizer=vbox, + parent=panel, + label='sig test', + callback=callback, + ) + +if __name__ == "__main__": + try: + + # Create the GUI application + app = gui.app( + gui=app_gui, # User interface class + title="Test Forms", # Top window title + ) + + # And run it + app.MainLoop() + + except RuntimeError, e: + print e + sys.exit(1) |