diff --git a/gooey/gui/components/widgets/dropdown.py b/gooey/gui/components/widgets/dropdown.py index f89d6c7..5511aaa 100644 --- a/gooey/gui/components/widgets/dropdown.py +++ b/gooey/gui/components/widgets/dropdown.py @@ -1,3 +1,5 @@ +from contextlib import contextmanager + from gooey.gui.components.widgets.bases import TextContainer import wx @@ -18,12 +20,9 @@ class Dropdown(TextContainer): style=wx.CB_DROPDOWN) def setOptions(self, options): - prevSelection = self.widget.GetSelection() - self.widget.Clear() - for option in [_('select_option')] + options: - self.widget.Append(option) - self.widget.SetSelection(0) - + with self.retainSelection(): + self.widget.Clear() + self.widget.SetItems([_('select_option')] + options) def setValue(self, value): ## +1 to offset the default placeholder value @@ -40,3 +39,20 @@ class Dropdown(TextContainer): def formatOutput(self, metadata, value): return formatters.dropdown(metadata, value) + + @contextmanager + def retainSelection(self): + """" + Retains the selected dropdown option (when possible) + across mutations due to dynamic updates. + """ + prevSelection = self.widget.GetSelection() + prevValue = self.widget.GetValue() + try: + yield + finally: + current_at_index = self.widget.GetString(prevSelection) + if prevValue == current_at_index: + self.widget.SetSelection(prevSelection) + else: + self.widget.SetSelection(0) diff --git a/gooey/tests/test_dropdown.py b/gooey/tests/test_dropdown.py new file mode 100644 index 0000000..97d5f43 --- /dev/null +++ b/gooey/tests/test_dropdown.py @@ -0,0 +1,52 @@ +import unittest +from argparse import ArgumentParser +from unittest.mock import patch + +from tests.harness import instrumentGooey + + +class TestGooeyDropdown(unittest.TestCase): + + def make_parser(self, **kwargs): + parser = ArgumentParser(description='description') + parser.add_argument('--dropdown', **kwargs) + return parser + + + @patch("gui.containers.application.seeder.fetchDynamicProperties") + def test_dropdown_behavior(self, mock): + """ + Testing that: + - default values are used as the initial selection (when present) + - Initial selection defaults to placeholder when no defaults supplied + - selection is preserved (when possible) across dynamic updates + """ + testcases = [ + # tuples of [choices, default, initalSelection, dynamicUpdate, expectedFinalSelection] + [['1', '2'], None, 'Select Option', ['1', '2','3'], 'Select Option'], + [['1', '2'], '2', '2', ['1', '2','3'], '2'], + [['1', '2'], '1', '1', ['1', '2','3'], '1'], + # dynamic updates removed our selected value; defaults back to placeholder + [['1', '2'], '2', '2', ['1', '3'], 'Select Option'], + ] + + for choices, default, initalSelection, dynamicUpdate, expectedFinalSelection in testcases: + parser = self.make_parser(choices=choices, default=default) + with instrumentGooey(parser) as (app, gooeyApp): + dropdown = gooeyApp.configs[0].reifiedWidgets[0] + # ensure that default values (when supplied) are selected in the UI + self.assertEqual(dropdown.widget.GetValue(), initalSelection) + # fire a dynamic update with the mock values + mock.return_value = {'--dropdown': dynamicUpdate} + gooeyApp.fetchExternalUpdates() + # the values in the UI now reflect those returned from the update + # note: we're appending the ['select option'] bit here as it gets automatically added + # in the UI. + expectedValues = ['Select Option'] + dynamicUpdate + self.assertEqual(dropdown.widget.GetItems(), expectedValues) + # and our selection is what we expect + self.assertEqual(dropdown.widget.GetValue(), expectedFinalSelection) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file