You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

203 lines
7.8 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. from typing import Optional
  2. import wx # type: ignore
  3. from gooey.gui.components.widgets.bases import BaseWidget
  4. from gooey.gui.lang.i18n import _
  5. from gooey.gui.util import wx_util
  6. from gooey.gui.components.widgets import CheckBox
  7. from gooey.util.functional import getin, merge
  8. from gooey.python_bindings import types as t
  9. class RadioGroup(BaseWidget):
  10. """
  11. """
  12. def __init__(self, parent, widgetInfo, *args, **kwargs):
  13. super(RadioGroup, self).__init__(parent, *args, **kwargs)
  14. self._parent = parent
  15. self.info = widgetInfo
  16. self._id = widgetInfo['id']
  17. self._options = widgetInfo['options']
  18. self.widgetInfo = widgetInfo
  19. self.error = wx.StaticText(self, label='')
  20. self.radioButtons = self.createRadioButtons()
  21. self.selected = None
  22. self.widgets = self.createWidgets()
  23. self.arrange()
  24. for button in self.radioButtons:
  25. button.Bind(wx.EVT_LEFT_DOWN, self.handleButtonClick)
  26. initialSelection = getin(self.info, ['options', 'initial_selection'], None)
  27. if initialSelection is not None:
  28. self.selected = self.radioButtons[initialSelection]
  29. self.selected.SetValue(True)
  30. self.handleImplicitCheck()
  31. self.applyStyleRules()
  32. def getValue(self):
  33. for button, widget in zip(self.radioButtons, self.widgets):
  34. if button.GetValue(): # is Checked
  35. return merge(widget.getValue(), {'id': self._id})
  36. else:
  37. # just return the first widget's value even though it's
  38. # not active so that the expected interface is satisfied
  39. return self.widgets[0].getValue()
  40. def syncUiState(self, state: t.RadioGroup):
  41. if state['selected'] is not None:
  42. self.radioButtons[state['selected']].SetValue(True)
  43. for option, widget in zip(state['options'], self.widgets):
  44. widget.syncUiState(option)
  45. # Fit required here to force WX to actually
  46. # show newly Enabled/Shown things for some reason.
  47. self.Fit()
  48. def getUiState(self):
  49. return t.RadioGroup(
  50. id=self._id,
  51. type=self.widgetInfo['type'],
  52. error=self.error.GetLabel(),
  53. enabled=self.Enabled,
  54. visible=self.Shown,
  55. selected=self.getSelectedIndex(),
  56. options=[x.getUiState() for x in self.widgets]
  57. )
  58. def getSelectedIndex(self) -> Optional[int]:
  59. for index, btn in enumerate(self.radioButtons):
  60. if btn.GetValue():
  61. return index
  62. return None
  63. def setErrorString(self, message):
  64. for button, widget in zip(self.radioButtons, self.widgets):
  65. if button.GetValue(): # is Checked
  66. widget.setErrorString(message)
  67. self.Layout()
  68. def showErrorString(self, b):
  69. for button, widget in zip(self.radioButtons, self.widgets):
  70. if button.GetValue(): # is Checked
  71. widget.showErrorString(b)
  72. def arrange(self, *args, **kwargs):
  73. title = getin(self.widgetInfo, ['options', 'title'], _('choose_one'))
  74. if getin(self.widgetInfo, ['options', 'show_border'], False):
  75. boxDetails = wx.StaticBox(self, -1, title)
  76. boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL)
  77. else:
  78. title = wx_util.h1(self, title)
  79. title.SetForegroundColour(self._options['label_color'])
  80. boxSizer = wx.BoxSizer(wx.VERTICAL)
  81. boxSizer.AddSpacer(10)
  82. boxSizer.Add(title, 0)
  83. for btn, widget in zip(self.radioButtons, self.widgets):
  84. sizer = wx.BoxSizer(wx.HORIZONTAL)
  85. sizer.Add(btn,0, wx.RIGHT, 4)
  86. sizer.Add(widget, 1, wx.EXPAND)
  87. boxSizer.Add(sizer, 0, wx.ALL | wx.EXPAND, 5)
  88. self.SetSizer(boxSizer)
  89. def handleButtonClick(self, event):
  90. currentSelection = self.selected
  91. nextSelection = event.EventObject
  92. if not self.isSameRadioButton(currentSelection, nextSelection):
  93. self.selected = nextSelection
  94. self.selected.SetValue(True)
  95. else:
  96. # user clicked on an already enabled radio button.
  97. # if it is not in the required section, allow it to be deselected
  98. if not self.widgetInfo['required']:
  99. self.selected.SetValue(False)
  100. self.selected = None
  101. self.applyStyleRules()
  102. self.handleImplicitCheck()
  103. def isSameRadioButton(self, radioButton1, radioButton2):
  104. return (getattr(radioButton1, 'Id', 'r1-not-found') ==
  105. getattr(radioButton2, 'Id', 'r2-not-found'))
  106. def applyStyleRules(self):
  107. """
  108. Conditionally disabled/enables form fields based on the current
  109. section in the radio group
  110. """
  111. # for reasons I have been completely unable to figure out
  112. # or understand, IFF you've interacted with one of the radio Buttons's
  113. # child components, then the act of disabling that component will
  114. # reset the state of the radioButtons thus causing it to forget
  115. # what should be selected. So, that is why we're collected the initial
  116. # state of all the buttons and resetting each button's state as we go.
  117. # it's wonky as hell
  118. states = [x.GetValue() for x in self.radioButtons]
  119. for widget in self.widgets:
  120. widget.Enable()
  121. for button, selected, widget in zip(self.radioButtons, states, self.widgets):
  122. if isinstance(widget, CheckBox):
  123. widget.hideInput()
  124. if not selected: # not checked
  125. widget.Disable()
  126. else:
  127. # More "I don't understand" style code
  128. # Under some conditions, Enable() doesn't cascade
  129. # as listed in the docs. We have to manually drill
  130. # into the children to enable everything.
  131. widget = widget
  132. while widget:
  133. widget.Enable()
  134. widget = getattr(widget, 'widget', None)
  135. button.SetValue(selected)
  136. def handleImplicitCheck(self):
  137. """
  138. Checkboxes are hidden when inside of a RadioGroup as a selection of
  139. the Radio button is an implicit selection of the Checkbox. As such, we have
  140. to manually "check" any checkbox as needed.
  141. """
  142. for button, widget in zip(self.radioButtons, self.widgets):
  143. if isinstance(widget, CheckBox):
  144. if button.GetValue(): # checked
  145. widget.setValue(True)
  146. else:
  147. widget.setValue(False)
  148. def createRadioButtons(self):
  149. # button groups in wx are statefully determined via a style flag
  150. # on the first button (what???). All button instances are part of the
  151. # same group until a new button is created with the style flag RG_GROUP
  152. # https://wxpython.org/Phoenix/docs/html/wx.RadioButton.html
  153. # (What???)
  154. firstButton = wx.RadioButton(self, style=wx.RB_GROUP)
  155. firstButton.SetValue(False)
  156. buttons = [firstButton]
  157. for _ in getin(self.widgetInfo, ['data','widgets'], [])[1:]:
  158. buttons.append(wx.RadioButton(self))
  159. return buttons
  160. def createWidgets(self):
  161. """
  162. Instantiate the Gooey Widgets that are used within the RadioGroup
  163. """
  164. from gooey.gui.components import widgets
  165. widgets = [getattr(widgets, item['type'])(self, item)
  166. for item in getin(self.widgetInfo, ['data', 'widgets'], [])]
  167. # widgets should be disabled unless
  168. # explicitly selected
  169. for widget in widgets:
  170. widget.Disable()
  171. return widgets