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.

191 lines
7.3 KiB

import wx
from wx.lib.scrolledpanel import ScrolledPanel
from gooey.gui.util import wx_util
from gooey.util.functional import getin, flatmap, merge, compact, indexunique
from gooey.gui.components.widgets.radio_group import RadioGroup
class ConfigPage(ScrolledPanel):
def __init__(self, parent, rawWidgets, *args, **kwargs):
super(ConfigPage, self).__init__(parent, *args, **kwargs)
self.SetupScrolling(scroll_x=False, scrollToTop=False)
self.rawWidgets = rawWidgets
self.reifiedWidgets = []
self.layoutComponent()
self.widgetsMap = indexunique(lambda x: x._id, self.reifiedWidgets)
## TODO: need to rethink what uniquely identifies an argument.
## Out-of-band IDs, while simple, make talking to the client program difficult
## unless they're agreed upon before hand. Commands, as used here, have the problem
## of (a) not being nearly granular enough (for instance, `-v` could represent totally different
## things given context/parser position), and (b) cannot identify positional args.
def firstCommandIfPresent(self, widget):
commands = widget._meta['commands']
return commands[0] if commands else ''
def getPositionalArgs(self):
return [widget.getValue()['cmd'] for widget in self.reifiedWidgets
if widget.info['cli_type'] == 'positional']
def getOptionalArgs(self):
return [widget.getValue()['cmd'] for widget in self.reifiedWidgets
if widget.info['cli_type'] != 'positional']
def isValid(self):
states = [widget.getValue() for widget in self.reifiedWidgets]
return not any(compact([state['error'] for state in states]))
def seedUI(self, seeds):
radioWidgets = self.indexInternalRadioGroupWidgets()
for id, values in seeds.items():
if id in self.widgetsMap:
self.widgetsMap[id].setOptions(values)
if id in radioWidgets:
radioWidgets[id].setOptions(values)
def indexInternalRadioGroupWidgets(self):
groups = filter(lambda x: x.info['type'] == 'RadioGroup', self.reifiedWidgets)
widgets = flatmap(lambda group: group.widgets, groups)
return indexunique(lambda x: x._id, widgets)
def displayErrors(self):
states = [widget.getValue() for widget in self.reifiedWidgets]
errors = [state for state in states if state['error']]
for error in errors:
widget = self.widgetsMap[error['id']]
widget.setErrorString(error['error'])
widget.showErrorString(True)
while widget.GetParent():
widget.Layout()
widget = widget.GetParent()
def resetErrors(self):
for widget in self.reifiedWidgets:
widget.setErrorString('')
widget.showErrorString(False)
def hideErrors(self):
for widget in self.reifiedWidgets:
widget.hideErrorString()
def layoutComponent(self):
sizer = wx.BoxSizer(wx.VERTICAL)
for item in self.rawWidgets['contents']:
self.makeGroup(self, sizer, item, 0, wx.EXPAND)
self.SetSizer(sizer)
def makeGroup(self, parent, thissizer, group, *args):
'''
Messily builds the (potentially) nested and grouped layout
Note! Mutates `self.reifiedWidgets` in place with the widgets as they're
instantiated! I cannot figure out how to split out the creation of the
widgets from their styling without WxPython violently exploding
TODO: sort out the WX quirks and clean this up.
'''
# determine the type of border , if any, the main sizer will use
if getin(group, ['options', 'show_border'], False):
boxDetails = wx.StaticBox(parent, -1, group['name'] or '')
boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL)
else:
boxSizer = wx.BoxSizer(wx.VERTICAL)
boxSizer.AddSpacer(10)
if group['name']:
boxSizer.Add(wx_util.h1(parent, group['name'] or ''), 0, wx.TOP | wx.BOTTOM | wx.LEFT, 8)
group_description = getin(group, ['description'])
if group_description:
description = wx.StaticText(parent, label=group_description)
boxSizer.Add(description, 0, wx.EXPAND | wx.LEFT, 10)
# apply an underline when a grouping border is not specified
if not getin(group, ['options', 'show_border'], False) and group['name']:
boxSizer.Add(wx_util.horizontal_rule(parent), 0, wx.EXPAND | wx.LEFT, 10)
ui_groups = self.chunkWidgets(group)
for uigroup in ui_groups:
sizer = wx.BoxSizer(wx.HORIZONTAL)
for item in uigroup:
widget = self.reifyWidget(parent, item)
# !Mutate the reifiedWidgets instance variable in place
self.reifiedWidgets.append(widget)
sizer.Add(widget, 1, wx.ALL, 5)
boxSizer.Add(sizer, 0, wx.ALL | wx.EXPAND, 5)
# apply the same layout rules recursively for subgroups
hs = wx.BoxSizer(wx.HORIZONTAL)
for e, subgroup in enumerate(group['groups']):
self.makeGroup(parent, hs, subgroup, 1, wx.ALL | wx.EXPAND, 5)
if e % getin(group, ['options', 'columns'], 2) \
or e == len(group['groups']):
boxSizer.Add(hs, *args)
hs = wx.BoxSizer(wx.HORIZONTAL)
thissizer.Add(boxSizer, *args)
def chunkWidgets(self, group):
''' chunk the widgets up into groups based on their sizing hints '''
ui_groups = []
subgroup = []
for index, item in enumerate(group['items']):
if getin(item, ['options', 'full_width'], False):
ui_groups.append(subgroup)
ui_groups.append([item])
subgroup = []
else:
subgroup.append(item)
if len(subgroup) == getin(group, ['options', 'columns'], 2) \
or item == group['items'][-1]:
ui_groups.append(subgroup)
subgroup = []
return ui_groups
def reifyWidget(self, parent, item):
''' Convert a JSON description of a widget into a WxObject '''
from gooey.gui.components import widgets
widgetClass = getattr(widgets, item['type'])
return widgetClass(parent, item)
class TabbedConfigPage(ConfigPage):
"""
Splits top-level groups across tabs
"""
def layoutComponent(self):
# self.rawWidgets['contents'] = self.rawWidgets['contents'][1:2]
self.notebook = wx.Notebook(self, style=wx.BK_DEFAULT)
panels = [wx.Panel(self.notebook) for _ in self.rawWidgets['contents']]
sizers = [wx.BoxSizer(wx.VERTICAL) for _ in panels]
for group, panel, sizer in zip(self.rawWidgets['contents'], panels, sizers):
self.makeGroup(panel, sizer, group, 0, wx.EXPAND)
panel.SetSizer(sizer)
panel.Layout()
self.notebook.AddPage(panel, group['name'])
self.notebook.Layout()
_sizer = wx.BoxSizer(wx.VERTICAL)
_sizer.Add(self.notebook, 1, wx.EXPAND)
self.SetSizer(_sizer)
self.Layout()