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.

209 lines
8.2 KiB

  1. import wx
  2. from wx.lib.scrolledpanel import ScrolledPanel
  3. from gooey.gui.components.util.wrapped_static_text import AutoWrappedStaticText
  4. from gooey.gui.util import wx_util
  5. from gooey.util.functional import getin, flatmap, compact, indexunique
  6. class ConfigPage(ScrolledPanel):
  7. def __init__(self, parent, rawWidgets, buildSpec, *args, **kwargs):
  8. super(ConfigPage, self).__init__(parent, *args, **kwargs)
  9. self.SetupScrolling(scroll_x=False, scrollToTop=False)
  10. self.rawWidgets = rawWidgets
  11. self.buildSpec = buildSpec
  12. self.reifiedWidgets = []
  13. self.layoutComponent()
  14. self.Layout()
  15. self.widgetsMap = indexunique(lambda x: x._id, self.reifiedWidgets)
  16. ## TODO: need to rethink what uniquely identifies an argument.
  17. ## Out-of-band IDs, while simple, make talking to the client program difficult
  18. ## unless they're agreed upon before hand. Commands, as used here, have the problem
  19. ## of (a) not being nearly granular enough (for instance, `-v` could represent totally different
  20. ## things given context/parser position), and (b) cannot identify positional args.
  21. def firstCommandIfPresent(self, widget):
  22. commands = widget._meta['commands']
  23. return commands[0] if commands else ''
  24. def getPositionalArgs(self):
  25. return [widget.getValue()['cmd'] for widget in self.reifiedWidgets
  26. if widget.info['cli_type'] == 'positional']
  27. def getOptionalArgs(self):
  28. return [widget.getValue()['cmd'] for widget in self.reifiedWidgets
  29. if widget.info['cli_type'] != 'positional']
  30. def isValid(self):
  31. states = [widget.getValue() for widget in self.reifiedWidgets]
  32. return not any(compact([state['error'] for state in states]))
  33. def seedUI(self, seeds):
  34. radioWidgets = self.indexInternalRadioGroupWidgets()
  35. for id, values in seeds.items():
  36. if id in self.widgetsMap:
  37. self.widgetsMap[id].setOptions(values)
  38. if id in radioWidgets:
  39. radioWidgets[id].setOptions(values)
  40. def indexInternalRadioGroupWidgets(self):
  41. groups = filter(lambda x: x.info['type'] == 'RadioGroup', self.reifiedWidgets)
  42. widgets = flatmap(lambda group: group.widgets, groups)
  43. return indexunique(lambda x: x._id, widgets)
  44. def displayErrors(self):
  45. states = [widget.getValue() for widget in self.reifiedWidgets]
  46. errors = [state for state in states if state['error']]
  47. for error in errors:
  48. widget = self.widgetsMap[error['id']]
  49. widget.setErrorString(error['error'])
  50. widget.showErrorString(True)
  51. while widget.GetParent():
  52. widget.Layout()
  53. widget = widget.GetParent()
  54. def resetErrors(self):
  55. for widget in self.reifiedWidgets:
  56. widget.setErrorString('')
  57. widget.showErrorString(False)
  58. def hideErrors(self):
  59. for widget in self.reifiedWidgets:
  60. widget.hideErrorString()
  61. def layoutComponent(self):
  62. sizer = wx.BoxSizer(wx.VERTICAL)
  63. for item in self.rawWidgets['contents']:
  64. self.makeGroup(self, sizer, item, 0, wx.EXPAND | wx.ALL, 10)
  65. self.SetSizer(sizer)
  66. def makeGroup(self, parent, thissizer, group, *args):
  67. '''
  68. Messily builds the (potentially) nested and grouped layout
  69. Note! Mutates `self.reifiedWidgets` in place with the widgets as they're
  70. instantiated! I cannot figure out how to split out the creation of the
  71. widgets from their styling without WxPython violently exploding
  72. TODO: sort out the WX quirks and clean this up.
  73. '''
  74. # determine the type of border , if any, the main sizer will use
  75. if getin(group, ['options', 'show_border'], False):
  76. boxDetails = wx.StaticBox(parent, -1, group['name'] or '')
  77. boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL)
  78. else:
  79. boxSizer = wx.BoxSizer(wx.VERTICAL)
  80. boxSizer.AddSpacer(10)
  81. if group['name']:
  82. groupName = wx_util.h1(parent, group['name'] or '')
  83. groupName.SetForegroundColour(getin(group, ['options', 'label_color']))
  84. boxSizer.Add(groupName, 0, wx.TOP | wx.BOTTOM | wx.LEFT, 8)
  85. group_description = getin(group, ['description'])
  86. if group_description:
  87. description = AutoWrappedStaticText(parent, label=group_description, target=boxSizer)
  88. description.SetForegroundColour(getin(group, ['options', 'description_color']))
  89. description.SetMinSize((0, -1))
  90. boxSizer.Add(description, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
  91. # apply an underline when a grouping border is not specified
  92. # unless the user specifically requests not to show it
  93. if not getin(group, ['options', 'show_border'], False) and group['name'] \
  94. and getin(group, ['options', 'show_underline'], True):
  95. boxSizer.Add(wx_util.horizontal_rule(parent), 0, wx.EXPAND | wx.LEFT, 10)
  96. ui_groups = self.chunkWidgets(group)
  97. for uigroup in ui_groups:
  98. sizer = wx.BoxSizer(wx.HORIZONTAL)
  99. for item in uigroup:
  100. widget = self.reifyWidget(parent, item)
  101. # !Mutate the reifiedWidgets instance variable in place
  102. self.reifiedWidgets.append(widget)
  103. sizer.Add(widget, 1, wx.ALL | wx.EXPAND, 5)
  104. boxSizer.Add(sizer, 0, wx.ALL | wx.EXPAND, 5)
  105. # apply the same layout rules recursively for subgroups
  106. hs = wx.BoxSizer(wx.HORIZONTAL)
  107. for e, subgroup in enumerate(group['groups']):
  108. self.makeGroup(parent, hs, subgroup, 1, wx.EXPAND)
  109. if len(group['groups']) != e:
  110. hs.AddSpacer(5)
  111. # self.makeGroup(parent, hs, subgroup, 1, wx.ALL | wx.EXPAND, 5)
  112. if e % getin(group, ['options', 'columns'], 2) \
  113. or e == len(group['groups']):
  114. boxSizer.Add(hs, *args)
  115. hs = wx.BoxSizer(wx.HORIZONTAL)
  116. group_top_margin = getin(group, ['options', 'margin_top'], 1)
  117. marginSizer = wx.BoxSizer(wx.VERTICAL)
  118. marginSizer.Add(boxSizer, 1, wx.EXPAND | wx.TOP, group_top_margin)
  119. thissizer.Add(marginSizer, *args)
  120. def chunkWidgets(self, group):
  121. ''' chunk the widgets up into groups based on their sizing hints '''
  122. ui_groups = []
  123. subgroup = []
  124. for index, item in enumerate(group['items']):
  125. if getin(item, ['options', 'full_width'], False):
  126. ui_groups.append(subgroup)
  127. ui_groups.append([item])
  128. subgroup = []
  129. else:
  130. subgroup.append(item)
  131. if len(subgroup) == getin(group, ['options', 'columns'], 2) \
  132. or item == group['items'][-1]:
  133. ui_groups.append(subgroup)
  134. subgroup = []
  135. return ui_groups
  136. def reifyWidget(self, parent, item):
  137. ''' Convert a JSON description of a widget into a WxObject '''
  138. from gooey.gui.components import widgets
  139. widgetClass = getattr(widgets, item['type'])
  140. return widgetClass(parent, item)
  141. class TabbedConfigPage(ConfigPage):
  142. """
  143. Splits top-level groups across tabs
  144. """
  145. def layoutComponent(self):
  146. # self.rawWidgets['contents'] = self.rawWidgets['contents'][1:2]
  147. self.notebook = wx.Notebook(self, style=wx.BK_DEFAULT)
  148. panels = [wx.Panel(self.notebook) for _ in self.rawWidgets['contents']]
  149. sizers = [wx.BoxSizer(wx.VERTICAL) for _ in panels]
  150. for group, panel, sizer in zip(self.rawWidgets['contents'], panels, sizers):
  151. self.makeGroup(panel, sizer, group, 0, wx.EXPAND)
  152. panel.SetSizer(sizer)
  153. panel.Layout()
  154. self.notebook.AddPage(panel, group['name'])
  155. self.notebook.Layout()
  156. _sizer = wx.BoxSizer(wx.VERTICAL)
  157. _sizer.Add(self.notebook, 1, wx.EXPAND)
  158. self.SetSizer(_sizer)
  159. self.Layout()