mirror of https://github.com/chriskiehl/Gooey.git
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
276 lines
11 KiB
from typing import Mapping, List
|
|
|
|
import wx # type: ignore
|
|
from wx.lib.scrolledpanel import ScrolledPanel # type: ignore
|
|
|
|
from gooey.gui.components.mouse import notifyMouseEvent
|
|
from gooey.gui.components.util.wrapped_static_text import AutoWrappedStaticText
|
|
from gooey.gui.lang.i18n import _
|
|
from gooey.gui.util import wx_util
|
|
from gooey.python_bindings.types import FormField
|
|
from gooey.util.functional import getin, flatmap, indexunique
|
|
|
|
|
|
class ConfigPage(ScrolledPanel):
|
|
self_managed = True
|
|
|
|
def __init__(self, parent, rawWidgets, buildSpec, *args, **kwargs):
|
|
super(ConfigPage, self).__init__(parent, *args, **kwargs)
|
|
|
|
self.SetupScrolling(scroll_x=False, scrollToTop=False)
|
|
self.rawWidgets = rawWidgets
|
|
self.buildSpec = buildSpec
|
|
self.reifiedWidgets = []
|
|
self.layoutComponent()
|
|
self.Layout()
|
|
self.widgetsMap = indexunique(lambda x: x._id, self.reifiedWidgets)
|
|
self.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
|
|
## 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 getName(self, group):
|
|
"""
|
|
retrieve the group name from the group object while accounting for
|
|
legacy fixed-name manual translation requirements.
|
|
"""
|
|
name = group['name']
|
|
return (_(name)
|
|
if name in {'optional_args_msg', 'required_args_msg'}
|
|
else name)
|
|
|
|
|
|
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 getPositionalValues(self):
|
|
return [widget.getValue() for widget in self.reifiedWidgets
|
|
if widget.info['cli_type'] == 'positional']
|
|
|
|
|
|
def getOptionalValues(self):
|
|
return [widget.getValue() for widget in self.reifiedWidgets
|
|
if widget.info['cli_type'] != 'positional']
|
|
|
|
|
|
def getFormState(self) -> List[FormField]:
|
|
return [widget.getUiState()
|
|
for widget in self.reifiedWidgets]
|
|
|
|
|
|
def syncFormState(self, formState: List[FormField]):
|
|
for item in formState:
|
|
self.widgetsMap[item['id']].syncUiState(item)
|
|
|
|
def isValid(self):
|
|
return not any(self.getErrors())
|
|
|
|
def getErrors(self):
|
|
states = [widget.getValue() for widget in self.reifiedWidgets]
|
|
return {state['meta']['dest']: state['error'] for state in states
|
|
if state['error']}
|
|
|
|
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 setErrors(self, errorMap: Mapping[str, str]):
|
|
self.resetErrors()
|
|
radioWidgets = self.indexInternalRadioGroupWidgets()
|
|
widgetsByDest = {v._meta['dest']: v for k,v in self.widgetsMap.items()
|
|
if v.info['type'] != 'RadioGroup'}
|
|
|
|
# if there are any errors, then all error blocks should
|
|
# be displayed so that the UI elements remain inline with
|
|
# each other.
|
|
if errorMap:
|
|
for widget in self.widgetsMap.values():
|
|
widget.showErrorString(True)
|
|
|
|
for id, message in errorMap.items():
|
|
if id in widgetsByDest:
|
|
widgetsByDest[id].setErrorString(message)
|
|
widgetsByDest[id].showErrorString(True)
|
|
if id in radioWidgets:
|
|
radioWidgets[id].setErrorString(message)
|
|
radioWidgets[id].showErrorString(True)
|
|
|
|
|
|
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._meta['dest'], 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 | wx.ALL, 10)
|
|
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, self.getName(group) or '')
|
|
boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL)
|
|
else:
|
|
boxSizer = wx.BoxSizer(wx.VERTICAL)
|
|
boxSizer.AddSpacer(10)
|
|
if group['name']:
|
|
groupName = wx_util.h1(parent, self.getName(group) or '')
|
|
groupName.SetForegroundColour(getin(group, ['options', 'label_color']))
|
|
groupName.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
|
|
boxSizer.Add(groupName, 0, wx.TOP | wx.BOTTOM | wx.LEFT, 8)
|
|
|
|
group_description = getin(group, ['description'])
|
|
if group_description:
|
|
description = AutoWrappedStaticText(parent, label=group_description, target=boxSizer)
|
|
description.SetForegroundColour(getin(group, ['options', 'description_color']))
|
|
description.SetMinSize((0, -1))
|
|
description.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
|
|
boxSizer.Add(description, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
|
|
|
|
# apply an underline when a grouping border is not specified
|
|
# unless the user specifically requests not to show it
|
|
if not getin(group, ['options', 'show_border'], False) and group['name'] \
|
|
and getin(group, ['options', 'show_underline'], True):
|
|
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)
|
|
if not getin(item, ['options', 'visible'], True):
|
|
widget.Hide()
|
|
# !Mutate the reifiedWidgets instance variable in place
|
|
self.reifiedWidgets.append(widget)
|
|
sizer.Add(widget, 1, wx.ALL | wx.EXPAND, 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.EXPAND)
|
|
if len(group['groups']) != e:
|
|
hs.AddSpacer(5)
|
|
|
|
# self.makeGroup(parent, hs, subgroup, 1, wx.ALL | wx.EXPAND, 5)
|
|
itemsPerColumn = getin(group, ['options', 'columns'], 2)
|
|
if e % itemsPerColumn or (e + 1) == len(group['groups']):
|
|
boxSizer.Add(hs, *args)
|
|
hs = wx.BoxSizer(wx.HORIZONTAL)
|
|
|
|
|
|
group_top_margin = getin(group, ['options', 'margin_top'], 1)
|
|
|
|
marginSizer = wx.BoxSizer(wx.VERTICAL)
|
|
marginSizer.Add(boxSizer, 1, wx.EXPAND | wx.TOP, group_top_margin)
|
|
|
|
thissizer.Add(marginSizer, *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, self.getName(group))
|
|
self.notebook.Layout()
|
|
|
|
|
|
_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
_sizer.Add(self.notebook, 1, wx.EXPAND)
|
|
self.SetSizer(_sizer)
|
|
self.Layout()
|
|
|
|
|
|
def snapToErrorTab(self):
|
|
pass
|
|
|