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.

276 lines
11 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. from typing import Mapping, List
  2. import wx # type: ignore
  3. from wx.lib.scrolledpanel import ScrolledPanel # type: ignore
  4. from gooey.gui.components.mouse import notifyMouseEvent
  5. from gooey.gui.components.util.wrapped_static_text import AutoWrappedStaticText
  6. from gooey.gui.lang.i18n import _
  7. from gooey.gui.util import wx_util
  8. from gooey.python_bindings.types import FormField
  9. from gooey.util.functional import getin, flatmap, indexunique
  10. class ConfigPage(ScrolledPanel):
  11. self_managed = True
  12. def __init__(self, parent, rawWidgets, buildSpec, *args, **kwargs):
  13. super(ConfigPage, self).__init__(parent, *args, **kwargs)
  14. self.SetupScrolling(scroll_x=False, scrollToTop=False)
  15. self.rawWidgets = rawWidgets
  16. self.buildSpec = buildSpec
  17. self.reifiedWidgets = []
  18. self.layoutComponent()
  19. self.Layout()
  20. self.widgetsMap = indexunique(lambda x: x._id, self.reifiedWidgets)
  21. self.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
  22. ## TODO: need to rethink what uniquely identifies an argument.
  23. ## Out-of-band IDs, while simple, make talking to the client program difficult
  24. ## unless they're agreed upon before hand. Commands, as used here, have the problem
  25. ## of (a) not being nearly granular enough (for instance, `-v` could represent totally different
  26. ## things given context/parser position), and (b) cannot identify positional args.
  27. def getName(self, group):
  28. """
  29. retrieve the group name from the group object while accounting for
  30. legacy fixed-name manual translation requirements.
  31. """
  32. name = group['name']
  33. return (_(name)
  34. if name in {'optional_args_msg', 'required_args_msg'}
  35. else name)
  36. def firstCommandIfPresent(self, widget):
  37. commands = widget._meta['commands']
  38. return commands[0] if commands else ''
  39. def getPositionalArgs(self):
  40. return [widget.getValue()['cmd'] for widget in self.reifiedWidgets
  41. if widget.info['cli_type'] == 'positional']
  42. def getOptionalArgs(self):
  43. return [widget.getValue()['cmd'] for widget in self.reifiedWidgets
  44. if widget.info['cli_type'] != 'positional']
  45. def getPositionalValues(self):
  46. return [widget.getValue() for widget in self.reifiedWidgets
  47. if widget.info['cli_type'] == 'positional']
  48. def getOptionalValues(self):
  49. return [widget.getValue() for widget in self.reifiedWidgets
  50. if widget.info['cli_type'] != 'positional']
  51. def getFormState(self) -> List[FormField]:
  52. return [widget.getUiState()
  53. for widget in self.reifiedWidgets]
  54. def syncFormState(self, formState: List[FormField]):
  55. for item in formState:
  56. self.widgetsMap[item['id']].syncUiState(item)
  57. def isValid(self):
  58. return not any(self.getErrors())
  59. def getErrors(self):
  60. states = [widget.getValue() for widget in self.reifiedWidgets]
  61. return {state['meta']['dest']: state['error'] for state in states
  62. if state['error']}
  63. def seedUI(self, seeds):
  64. radioWidgets = self.indexInternalRadioGroupWidgets()
  65. for id, values in seeds.items():
  66. if id in self.widgetsMap:
  67. self.widgetsMap[id].setOptions(values)
  68. if id in radioWidgets:
  69. radioWidgets[id].setOptions(values)
  70. def setErrors(self, errorMap: Mapping[str, str]):
  71. self.resetErrors()
  72. radioWidgets = self.indexInternalRadioGroupWidgets()
  73. widgetsByDest = {v._meta['dest']: v for k,v in self.widgetsMap.items()
  74. if v.info['type'] != 'RadioGroup'}
  75. # if there are any errors, then all error blocks should
  76. # be displayed so that the UI elements remain inline with
  77. # each other.
  78. if errorMap:
  79. for widget in self.widgetsMap.values():
  80. widget.showErrorString(True)
  81. for id, message in errorMap.items():
  82. if id in widgetsByDest:
  83. widgetsByDest[id].setErrorString(message)
  84. widgetsByDest[id].showErrorString(True)
  85. if id in radioWidgets:
  86. radioWidgets[id].setErrorString(message)
  87. radioWidgets[id].showErrorString(True)
  88. def indexInternalRadioGroupWidgets(self):
  89. groups = filter(lambda x: x.info['type'] == 'RadioGroup', self.reifiedWidgets)
  90. widgets = flatmap(lambda group: group.widgets, groups)
  91. return indexunique(lambda x: x._meta['dest'], widgets)
  92. def displayErrors(self):
  93. states = [widget.getValue() for widget in self.reifiedWidgets]
  94. errors = [state for state in states if state['error']]
  95. for error in errors:
  96. widget = self.widgetsMap[error['id']]
  97. widget.setErrorString(error['error'])
  98. widget.showErrorString(True)
  99. while widget.GetParent():
  100. widget.Layout()
  101. widget = widget.GetParent()
  102. def resetErrors(self):
  103. for widget in self.reifiedWidgets:
  104. widget.setErrorString('')
  105. widget.showErrorString(False)
  106. def hideErrors(self):
  107. for widget in self.reifiedWidgets:
  108. widget.hideErrorString()
  109. def layoutComponent(self):
  110. sizer = wx.BoxSizer(wx.VERTICAL)
  111. for item in self.rawWidgets['contents']:
  112. self.makeGroup(self, sizer, item, 0, wx.EXPAND | wx.ALL, 10)
  113. self.SetSizer(sizer)
  114. def makeGroup(self, parent, thissizer, group, *args):
  115. '''
  116. Messily builds the (potentially) nested and grouped layout
  117. Note! Mutates `self.reifiedWidgets` in place with the widgets as they're
  118. instantiated! I cannot figure out how to split out the creation of the
  119. widgets from their styling without WxPython violently exploding
  120. TODO: sort out the WX quirks and clean this up.
  121. '''
  122. # determine the type of border , if any, the main sizer will use
  123. if getin(group, ['options', 'show_border'], False):
  124. boxDetails = wx.StaticBox(parent, -1, self.getName(group) or '')
  125. boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL)
  126. else:
  127. boxSizer = wx.BoxSizer(wx.VERTICAL)
  128. boxSizer.AddSpacer(10)
  129. if group['name']:
  130. groupName = wx_util.h1(parent, self.getName(group) or '')
  131. groupName.SetForegroundColour(getin(group, ['options', 'label_color']))
  132. groupName.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
  133. boxSizer.Add(groupName, 0, wx.TOP | wx.BOTTOM | wx.LEFT, 8)
  134. group_description = getin(group, ['description'])
  135. if group_description:
  136. description = AutoWrappedStaticText(parent, label=group_description, target=boxSizer)
  137. description.SetForegroundColour(getin(group, ['options', 'description_color']))
  138. description.SetMinSize((0, -1))
  139. description.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
  140. boxSizer.Add(description, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
  141. # apply an underline when a grouping border is not specified
  142. # unless the user specifically requests not to show it
  143. if not getin(group, ['options', 'show_border'], False) and group['name'] \
  144. and getin(group, ['options', 'show_underline'], True):
  145. boxSizer.Add(wx_util.horizontal_rule(parent), 0, wx.EXPAND | wx.LEFT, 10)
  146. ui_groups = self.chunkWidgets(group)
  147. for uigroup in ui_groups:
  148. sizer = wx.BoxSizer(wx.HORIZONTAL)
  149. for item in uigroup:
  150. widget = self.reifyWidget(parent, item)
  151. if not getin(item, ['options', 'visible'], True):
  152. widget.Hide()
  153. # !Mutate the reifiedWidgets instance variable in place
  154. self.reifiedWidgets.append(widget)
  155. sizer.Add(widget, 1, wx.ALL | wx.EXPAND, 5)
  156. boxSizer.Add(sizer, 0, wx.ALL | wx.EXPAND, 5)
  157. # apply the same layout rules recursively for subgroups
  158. hs = wx.BoxSizer(wx.HORIZONTAL)
  159. for e, subgroup in enumerate(group['groups']):
  160. self.makeGroup(parent, hs, subgroup, 1, wx.EXPAND)
  161. if len(group['groups']) != e:
  162. hs.AddSpacer(5)
  163. # self.makeGroup(parent, hs, subgroup, 1, wx.ALL | wx.EXPAND, 5)
  164. itemsPerColumn = getin(group, ['options', 'columns'], 2)
  165. if e % itemsPerColumn or (e + 1) == len(group['groups']):
  166. boxSizer.Add(hs, *args)
  167. hs = wx.BoxSizer(wx.HORIZONTAL)
  168. group_top_margin = getin(group, ['options', 'margin_top'], 1)
  169. marginSizer = wx.BoxSizer(wx.VERTICAL)
  170. marginSizer.Add(boxSizer, 1, wx.EXPAND | wx.TOP, group_top_margin)
  171. thissizer.Add(marginSizer, *args)
  172. def chunkWidgets(self, group):
  173. ''' chunk the widgets up into groups based on their sizing hints '''
  174. ui_groups = []
  175. subgroup = []
  176. for index, item in enumerate(group['items']):
  177. if getin(item, ['options', 'full_width'], False):
  178. ui_groups.append(subgroup)
  179. ui_groups.append([item])
  180. subgroup = []
  181. else:
  182. subgroup.append(item)
  183. if len(subgroup) == getin(group, ['options', 'columns'], 2) \
  184. or item == group['items'][-1]:
  185. ui_groups.append(subgroup)
  186. subgroup = []
  187. return ui_groups
  188. def reifyWidget(self, parent, item):
  189. ''' Convert a JSON description of a widget into a WxObject '''
  190. from gooey.gui.components import widgets
  191. widgetClass = getattr(widgets, item['type'])
  192. return widgetClass(parent, item)
  193. class TabbedConfigPage(ConfigPage):
  194. """
  195. Splits top-level groups across tabs
  196. """
  197. def layoutComponent(self):
  198. # self.rawWidgets['contents'] = self.rawWidgets['contents'][1:2]
  199. self.notebook = wx.Notebook(self, style=wx.BK_DEFAULT)
  200. panels = [wx.Panel(self.notebook) for _ in self.rawWidgets['contents']]
  201. sizers = [wx.BoxSizer(wx.VERTICAL) for _ in panels]
  202. for group, panel, sizer in zip(self.rawWidgets['contents'], panels, sizers):
  203. self.makeGroup(panel, sizer, group, 0, wx.EXPAND)
  204. panel.SetSizer(sizer)
  205. panel.Layout()
  206. self.notebook.AddPage(panel, self.getName(group))
  207. self.notebook.Layout()
  208. _sizer = wx.BoxSizer(wx.VERTICAL)
  209. _sizer.Add(self.notebook, 1, wx.EXPAND)
  210. self.SetSizer(_sizer)
  211. self.Layout()
  212. def snapToErrorTab(self):
  213. pass