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.

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