diff --git a/gooey/gui/components/__init__.py b/gooey/gui/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gooey/gui/components/config.py b/gooey/gui/components/config.py new file mode 100644 index 0000000..2421a8d --- /dev/null +++ b/gooey/gui/components/config.py @@ -0,0 +1,191 @@ +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() + + + + diff --git a/gooey/gui/components/console.py b/gooey/gui/components/console.py new file mode 100644 index 0000000..faae774 --- /dev/null +++ b/gooey/gui/components/console.py @@ -0,0 +1,82 @@ +import wx + +from gooey.gui.lang import i18n + + +class Console(wx.Panel): + ''' + Textbox console/terminal displayed during the client program's execution. + ''' + + def __init__(self, parent, buildSpec, **kwargs): + wx.Panel.__init__(self, parent, **kwargs) + self.buildSpec = buildSpec + + self.text = wx.StaticText(self, label=i18n._("status")) + self.textbox = wx.TextCtrl( + self, -1, "", style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH + ) + + self.defaultFont = self.textbox.GetFont() + + self.textbox.SetFont(wx.Font( + self.buildSpec['terminal_font_size'] or self.defaultFont.GetPointSize(), + self.getFontStyle(), + wx.NORMAL, + self.buildSpec['terminal_font_weight'] or wx.NORMAL, + False, + self.getFontFace(), + )) + self.textbox.SetForegroundColour(self.buildSpec['terminal_font_color']) + + self.layoutComponent() + self.Layout() + + + def getFontStyle(self): + """ + Force wx.Modern style to support legacy + monospace_display param when present + """ + return (wx.MODERN + if self.buildSpec['monospace_display'] + else wx.DEFAULT) + + + def getFontFace(self): + """Choose the best font face available given the user options""" + userFace = self.buildSpec['terminal_font_family'] or self.defaultFont.GetFaceName() + return ('' + if self.buildSpec['monospace_display'] + else userFace) + + + def logOutput(self, *args, **kwargs): + """Event Handler for console updates coming from the client's program""" + self.appendText(kwargs.get('msg')) + + + def appendText(self, txt): + """ + Append the text to the main TextCtrl. + + Note! Must be called from a Wx specific thread handler to avoid + multi-threaded explosions (e.g. wx.CallAfter) + """ + self.textbox.AppendText(txt) + + + def getText(self): + return self.textbox.GetValue() + + def layoutComponent(self): + self.SetBackgroundColour(self.buildSpec.get('terminal_bg_color', '#F0F0F0')) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.AddSpacer(10) + sizer.Add(self.text, 0, wx.LEFT, 20) + sizer.AddSpacer(10) + sizer.Add(self.textbox, 1, wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, 20) + sizer.AddSpacer(20) + self.SetSizer(sizer) + + diff --git a/gooey/gui/components/footer.py b/gooey/gui/components/footer.py new file mode 100644 index 0000000..6058b51 --- /dev/null +++ b/gooey/gui/components/footer.py @@ -0,0 +1,134 @@ +import sys +import wx + +from gooey.gui import events +from gooey.gui.lang import i18n +from gooey.gui.pubsub import pub + + +class Footer(wx.Panel): + ''' + Footer section used on the configuration + screen of the application + ''' + + def __init__(self, parent, buildSpec, **kwargs): + wx.Panel.__init__(self, parent, **kwargs) + self.buildSpec = buildSpec + + self.SetMinSize((30, 53)) + # components + self.cancel_button = None + self.start_button = None + self.progress_bar = None + self.close_button = None + self.stop_button = None + self.restart_button = None + self.edit_button = None + self.buttons = [] + + self.layouts = {} + + self._init_components() + self._do_layout() + + for button in self.buttons: + self.Bind(wx.EVT_BUTTON, self.dispatch_click, button) + + def updateProgressBar(self, *args, **kwargs): + ''' + value, disable_animation=False + :param args: + :param kwargs: + :return: + ''' + value = kwargs.get('progress') + pb = self.progress_bar + if value is None: + return + if value < 0: + pb.Pulse() + else: + value = min(int(value), pb.GetRange()) + if pb.GetValue() != value: + # Windows 7 progress bar animation hack + # http://stackoverflow.com/questions/5332616/disabling-net-progressbar-animation-when-changing-value + if self.buildSpec['disable_progress_bar_animation'] \ + and sys.platform.startswith("win"): + if pb.GetRange() == value: + pb.SetValue(value) + pb.SetValue(value - 1) + else: + pb.SetValue(value + 1) + pb.SetValue(value) + + + def showButtons(self, *buttonsToShow): + for button in self.buttons: + button.Show(False) + for button in buttonsToShow: + getattr(self, button).Show(True) + self.Layout() + + + def _init_components(self): + self.cancel_button = self.button(i18n._('cancel'), wx.ID_CANCEL, event_id=events.WINDOW_CANCEL) + self.stop_button = self.button(i18n._('stop'), wx.ID_OK, event_id=events.WINDOW_STOP) + self.start_button = self.button(i18n._('start'), wx.ID_OK, event_id=int(events.WINDOW_START)) + self.close_button = self.button(i18n._("close"), wx.ID_OK, event_id=int(events.WINDOW_CLOSE)) + self.restart_button = self.button(i18n._('restart'), wx.ID_OK, event_id=int(events.WINDOW_RESTART)) + self.edit_button = self.button(i18n._('edit'), wx.ID_OK, event_id=int(events.WINDOW_EDIT)) + + self.progress_bar = wx.Gauge(self, range=100) + + self.buttons = [self.cancel_button, self.start_button, + self.stop_button, self.close_button, + self.restart_button, self.edit_button] + + if self.buildSpec['disable_stop_button']: + self.stop_button.Enable(False) + + + def _do_layout(self): + self.stop_button.Hide() + self.restart_button.Hide() + + v_sizer = wx.BoxSizer(wx.VERTICAL) + h_sizer = wx.BoxSizer(wx.HORIZONTAL) + + h_sizer.Add(self.progress_bar, 1, + wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 20) + self.progress_bar.Hide() + + h_sizer.AddStretchSpacer(1) + h_sizer.Add(self.cancel_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 20) + h_sizer.Add(self.start_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 20) + h_sizer.Add(self.stop_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 20) + + v_sizer.AddStretchSpacer(1) + v_sizer.Add(h_sizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.EXPAND) + + h_sizer.Add(self.edit_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 10) + h_sizer.Add(self.restart_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 10) + h_sizer.Add(self.close_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 20) + self.edit_button.Hide() + self.restart_button.Hide() + self.close_button.Hide() + + v_sizer.AddStretchSpacer(1) + self.SetSizer(v_sizer) + + def button(self, label=None, style=None, event_id=-1): + return wx.Button( + parent=self, + id=event_id, + size=(90, 24), + label=i18n._(label), + style=style) + + def dispatch_click(self, event): + pub.send_message(event.GetId()) + + def hide_all_buttons(self): + for button in self.buttons: + button.Hide() diff --git a/gooey/gui/components/header.py b/gooey/gui/components/header.py new file mode 100644 index 0000000..19343d8 --- /dev/null +++ b/gooey/gui/components/header.py @@ -0,0 +1,101 @@ +''' +Created on Dec 23, 2013 + +@author: Chris +''' + +import wx + +from gooey.gui import imageutil, image_repository +from gooey.gui.util import wx_util +from gooey.gui.three_to_four import bitmapFromImage +from gooey.util.functional import getin + +PAD_SIZE = 10 + + +class FrameHeader(wx.Panel): + def __init__(self, parent, buildSpec, **kwargs): + wx.Panel.__init__(self, parent, **kwargs) + self.SetDoubleBuffered(True) + + self.buildSpec = buildSpec + + self._header = None + self._subheader = None + self.settings_img = None + self.running_img = None + self.check_mark = None + self.error_symbol = None + + self.images = [] + + self.layoutComponent() + + + def setTitle(self, title): + self._header.SetLabel(title) + + def setSubtitle(self, subtitle): + self._subheader.SetLabel(subtitle) + + def setImage(self, image): + for img in self.images: + img.Show(False) + getattr(self, image).Show(True) + self.Layout() + + + def layoutComponent(self): + + self.SetBackgroundColour(self.buildSpec['header_bg_color']) + self.SetSize((30, self.buildSpec['header_height'])) + self.SetMinSize((120, self.buildSpec['header_height'])) + + self._header = wx_util.h1(self, label=self.buildSpec['program_name']) + self._subheader = wx.StaticText(self, label=self.buildSpec['program_description']) + + images = self.buildSpec['images'] + targetHeight = self.buildSpec['header_height'] - 10 + self.settings_img = self._load_image(images['configIcon'], targetHeight) + self.running_img = self._load_image(images['runningIcon'], targetHeight) + self.check_mark = self._load_image(images['successIcon'], targetHeight) + self.error_symbol = self._load_image(images['errorIcon'], targetHeight) + + self.images = [ + self.settings_img, + self.running_img, + self.check_mark, + self.error_symbol + ] + + vsizer = wx.BoxSizer(wx.VERTICAL) + sizer = wx.BoxSizer(wx.HORIZONTAL) + headings_sizer = self.build_heading_sizer() + sizer.Add(headings_sizer, 1, + wx.ALIGN_LEFT | wx.ALIGN_CENTER_HORIZONTAL | wx.EXPAND | wx.LEFT, + PAD_SIZE) + sizer.Add(self.settings_img, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.RIGHT, PAD_SIZE) + sizer.Add(self.running_img, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.RIGHT, PAD_SIZE) + sizer.Add(self.check_mark, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.RIGHT, PAD_SIZE) + sizer.Add(self.error_symbol, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.RIGHT, PAD_SIZE) + self.running_img.Hide() + self.check_mark.Hide() + self.error_symbol.Hide() + vsizer.Add(sizer, 1, wx.EXPAND) + self.SetSizer(vsizer) + + + def _load_image(self, imgPath, targetHeight): + rawImage = imageutil.loadImage(imgPath) + sizedImage = imageutil.resizeImage(rawImage, targetHeight) + return imageutil.wrapBitmap(sizedImage, self) + + + def build_heading_sizer(self): + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.AddStretchSpacer(1) + sizer.Add(self._header, 0) + sizer.Add(self._subheader, 0) + sizer.AddStretchSpacer(1) + return sizer diff --git a/gooey/gui/components/layouts/__init__.py b/gooey/gui/components/layouts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gooey/gui/components/layouts/layouts.py b/gooey/gui/components/layouts/layouts.py new file mode 100644 index 0000000..c64faf8 --- /dev/null +++ b/gooey/gui/components/layouts/layouts.py @@ -0,0 +1,17 @@ +import wx + + +def standard_layout(title, subtitle, widget): + container = wx.BoxSizer(wx.VERTICAL) + + container.Add(title) + container.AddSpacer(2) + + if subtitle: + container.Add(subtitle, 1, wx.EXPAND) + container.AddSpacer(2) + else: + container.AddStretchSpacer(1) + + container.Add(widget, 0, wx.EXPAND) + return container diff --git a/gooey/gui/components/modals.py b/gooey/gui/components/modals.py new file mode 100644 index 0000000..19d91fd --- /dev/null +++ b/gooey/gui/components/modals.py @@ -0,0 +1,46 @@ +""" +All of the dialogs used throughout Gooey +""" +from collections import namedtuple + +import wx + +from gooey.gui.lang.i18n import _ + + +# These don't seem to be specified anywhere in WX for some reason +DialogConstants = namedtuple('DialogConstants', 'YES NO')(5103, 5104) + + +def showDialog(title, content, style): + dlg = wx.MessageDialog(None, content, title, style) + result = dlg.ShowModal() + dlg.Destroy() + return result + + +def missingArgsDialog(): + showDialog(_('error_title'), _('error_required_fields'), wx.ICON_ERROR) + + +def validationFailure(): + showDialog(_('error_title'), _('validation_failed'), wx.ICON_WARNING) + + +def showSuccess(): + showDialog(_('execution_finished'), _('success_message'), wx.ICON_INFORMATION) + + +def showFailure(): + showDialog(_('execution_finished'), _('uh_oh'), wx.ICON_ERROR) + + +def confirmExit(): + result = showDialog(_('sure_you_want_to_exit'), _('close_program'), wx.YES_NO | wx.ICON_INFORMATION) + return result == DialogConstants.YES + + +def confirmForceStop(): + result = showDialog(_('stop_task'), _('sure_you_want_to_stop'), wx.YES_NO | wx.ICON_WARNING) + return result == DialogConstants.YES + diff --git a/gooey/gui/components/sidebar.py b/gooey/gui/components/sidebar.py new file mode 100644 index 0000000..f268a5b --- /dev/null +++ b/gooey/gui/components/sidebar.py @@ -0,0 +1,86 @@ +import wx + +from gooey.gui.util import wx_util + + +class Sidebar(wx.Panel): + """ + Sidebar handles the show/hide logic so that it mirrors the functionality + of the wx.Notebook class (which wants to control everything) + """ + def __init__(self, parent, buildSpec, configPanels, *args, **kwargs): + super(Sidebar, self).__init__(parent, *args, **kwargs) + self._parent = parent + self.buildSpec = buildSpec + self.configPanels = configPanels + self.activeSelection = 0 + self.options = list(self.buildSpec['widgets'].keys()) + self.leftPanel = wx.Panel(self) + self.label = wx_util.h1(self.leftPanel, self.buildSpec.get('sidebar_title')) + self.listbox = wx.ListBox(self.leftPanel, -1, choices=self.options) + self.Bind(wx.EVT_LISTBOX, self.swapConfigPanels, self.listbox) + self.layoutComponent() + self.listbox.SetSelection(0) + + + def getSelectedGroup(self): + """Return the currently active 'group' i.e. the root SubParser """ + return self.options[self.activeSelection] + + + def getActiveConfig(self): + """Return the currently visible config screen""" + return self.configPanels[self.activeSelection] + + + def swapConfigPanels(self, event): + """Hide/show configuration panels based on the currently selected + option in the sidebar """ + for id, panel in enumerate(self.configPanels): + panel.Hide() + self.activeSelection = event.Selection + self.configPanels[event.Selection].Show() + self._parent.Layout() + + + def layoutComponent(self): + left = self.layoutLeftSide() + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(left, 0, wx.EXPAND) + + if not self.buildSpec['tabbed_groups']: + # only add it for non-tabbed layouts as it looks + # weird against the tabbed ones + hsizer.Add(wx_util.vertical_rule(self), 0, wx.EXPAND) + + for body in self.configPanels: + body.Reparent(self) + hsizer.Add(body, 1, wx.EXPAND) + body.Hide() + self.configPanels[0].Show() + self.SetSizer(hsizer) + + if not self.buildSpec['show_sidebar']: + left.Show(False) + + self.Layout() + + + def layoutLeftSide(self): + self.leftPanel.SetBackgroundColour(self.buildSpec['sidebar_bg_color']) + self.leftPanel.SetSize((180, 0)) + self.leftPanel.SetMinSize((180, 0)) + + container = wx.BoxSizer(wx.VERTICAL) + container.AddSpacer(15) + container.Add(self.label, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 10) + container.AddSpacer(5) + + container.Add(self.listbox, 1, wx.LEFT | wx.RIGHT | wx.EXPAND, 10) + container.AddSpacer(20) + self.leftPanel.SetSizer(container) + return self.leftPanel + + + diff --git a/gooey/gui/components/tabbar.py b/gooey/gui/components/tabbar.py new file mode 100644 index 0000000..e23c76c --- /dev/null +++ b/gooey/gui/components/tabbar.py @@ -0,0 +1,37 @@ +import wx + +from gooey.gui import events +from gooey.gui.pubsub import pub +from gooey.gui.util import wx_util + + +class Tabbar(wx.Panel): + def __init__(self, parent, buildSpec, configPanels, *args, **kwargs): + super(Tabbar, self).__init__(parent, *args, **kwargs) + self._parent = parent + self.notebook = wx.Notebook(self, style=wx.BK_DEFAULT) + self.buildSpec = buildSpec + self.configPanels = configPanels + self.options = list(self.buildSpec['widgets'].keys()) + self.layoutComponent() + + + def layoutComponent(self): + for group, panel in zip(self.options, self.configPanels): + panel.Reparent( self.notebook) + self.notebook.AddPage(panel, group) + self.notebook.Layout() + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self.notebook, 1, wx.EXPAND) + self.SetSizer(sizer) + self.Layout() + + def getSelectedGroup(self): + return self.options[self.notebook.Selection] + + def getActiveConfig(self): + return self.configPanels[self.notebook.Selection] + + def show(self, b): + self.Show(b) diff --git a/gooey/gui/components/widgets/__init__.py b/gooey/gui/components/widgets/__init__.py new file mode 100644 index 0000000..ac3184d --- /dev/null +++ b/gooey/gui/components/widgets/__init__.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from .textfield import TextField +from .textarea import Textarea +from .password import PasswordField +from .command import CommandField +from .dropdown import Dropdown +from .listbox import Listbox +from .checkbox import CheckBox +from .counter import Counter +from .radio_group import RadioGroup +from .choosers import * diff --git a/gooey/gui/components/widgets/bases.py b/gooey/gui/components/widgets/bases.py new file mode 100644 index 0000000..1b31728 --- /dev/null +++ b/gooey/gui/components/widgets/bases.py @@ -0,0 +1,151 @@ +import wx +# from rx.subjects import Subject + +from gooey.gui import formatters, events +from gooey.gui.pubsub import pub +from gooey.gui.util import wx_util +from gooey.util.functional import getin, ifPresent +from gooey.gui.validators import runValidator + + +class BaseWidget(wx.Panel): + widget_class = None + + def arrange(self, label, text): + raise NotImplementedError + + def getWidget(self, parent, **options): + return self.widget_class(parent, **options) + + def connectSignal(self): + raise NotImplementedError + + def getSublayout(self, *args, **kwargs): + raise NotImplementedError + + def setValue(self, value): + raise NotImplementedError + + def receiveChange(self, *args, **kwargs): + raise NotImplementedError + + def dispatchChange(self, value, **kwargs): + raise NotImplementedError + + def formatOutput(self, metatdata, value): + raise NotImplementedError + + +class TextContainer(BaseWidget): + widget_class = None + + def __init__(self, parent, widgetInfo, *args, **kwargs): + super(TextContainer, self).__init__(parent, *args, **kwargs) + + self.info = widgetInfo + self._id = widgetInfo['id'] + self._meta = widgetInfo['data'] + self._options = widgetInfo['options'] + self.label = wx.StaticText(self, label=widgetInfo['data']['display_name']) + self.help_text = wx.StaticText(self, label=widgetInfo['data']['help'] or '') + self.error = wx.StaticText(self, label='') + self.error.Hide() + self.widget = self.getWidget(self) + self.layout = self.arrange(*args, **kwargs) + self.SetSizer(self.layout) + self.Bind(wx.EVT_SIZE, self.onSize) + if self._meta['default']: + self.setValue(self._meta['default']) + + + def arrange(self, *args, **kwargs): + wx_util.make_bold(self.label) + wx_util.dark_grey(self.help_text) + wx_util.withColor(self.error, self._options['error_color']) + + self.help_text.SetMinSize((0,-1)) + + layout = wx.BoxSizer(wx.VERTICAL) + layout.Add(self.label) + layout.AddSpacer(2) + if self.help_text: + layout.Add(self.help_text, 1, wx.EXPAND) + layout.AddSpacer(2) + else: + layout.AddStretchSpacer(1) + layout.Add(self.getSublayout(), 0, wx.EXPAND) + layout.Add(self.error) + self.error.Hide() + return layout + + def getWidget(self, *args, **options): + return self.widget_class(*args, **options) + + def getWidgetValue(self): + raise NotImplementedError + + def getSublayout(self, *args, **kwargs): + layout = wx.BoxSizer(wx.HORIZONTAL) + layout.Add(self.widget, 1, wx.EXPAND) + return layout + + def onSize(self, event): + self.error.Wrap(self.GetSize().width) + event.Skip() + + def getValue(self): + userValidator = getin(self._options, ['validator', 'test'], 'True') + message = getin(self._options, ['validator', 'message'], '') + testFunc = eval('lambda user_input: bool(%s)' % userValidator) + satisfies = testFunc if self._meta['required'] else ifPresent(testFunc) + value = self.getWidgetValue() + + return { + 'id': self._id, + 'cmd': self.formatOutput(self._meta, value), + 'rawValue': value, + 'test': runValidator(satisfies, value), + 'error': None if runValidator(satisfies, value) else message, + 'clitype': 'positional' + if self._meta['required'] and not self._meta['commands'] + else 'optional' + } + + def setValue(self, value): + self.widget.SetValue(value) + + def setErrorString(self, message): + self.error.SetLabel(message) + self.error.Wrap(self.Size.width) + self.Layout() + + def showErrorString(self, b): + self.error.Wrap(self.Size.width) + self.error.Show(b) + + def setOptions(self, values): + return None + + def receiveChange(self, metatdata, value): + raise NotImplementedError + + def dispatchChange(self, value, **kwargs): + raise NotImplementedError + + def formatOutput(self, metadata, value): + raise NotImplementedError + + + + +class BaseChooser(TextContainer): + """ Base Class for the Chooser widget types """ + + def setValue(self, value): + self.widget.setValue(value) + + def getWidgetValue(self): + return self.widget.getValue() + + def formatOutput(self, metatdata, value): + return formatters.general(metatdata, value) diff --git a/gooey/gui/components/widgets/beep_boop.py b/gooey/gui/components/widgets/beep_boop.py new file mode 100644 index 0000000..cd6a8ae --- /dev/null +++ b/gooey/gui/components/widgets/beep_boop.py @@ -0,0 +1,83 @@ +import wx + +import wx.lib.inspection +from gooey.gui.components.widgets.textfield import TextField +from gooey.gui.components.widgets.textarea import Textarea +from gooey.gui.components.widgets.password import PasswordField +from gooey.gui.components.widgets.choosers import FileChooser, FileSaver, DirChooser, DateChooser +from gooey.gui.components.widgets.dropdown import Dropdown +from gooey.gui.components.widgets.listbox import Listbox + + +class CCC(wx.Frame): + def __init__(self, *args, **kwargs): + super(CCC, self).__init__(*args, **kwargs) + x = {'data':{'choices':['one', 'tw'], 'display_name': 'foo', 'help': 'bar', 'commands': ['-t']}, 'id': 1, 'options': {}} + + a = TextField(self, x) + c = Textarea(self, x) + b = PasswordField(self, x) + d = DirChooser(self, x) + e = FileChooser(self,x) + f = FileSaver(self, x) + g = DateChooser(self, x) + h = Dropdown(self, x) + i = Listbox(self, x) + + s = wx.BoxSizer(wx.VERTICAL) + s.Add(a, 0, wx.EXPAND) + s.Add(b, 0, wx.EXPAND) + s.Add(c, 0, wx.EXPAND) + s.Add(d, 0, wx.EXPAND) + s.Add(e, 0, wx.EXPAND) + s.Add(f, 0, wx.EXPAND) + s.Add(g, 0, wx.EXPAND) + s.Add(h, 0, wx.EXPAND) + s.Add(i, 0, wx.EXPAND) + + self.SetSizer(s) + + + + + +app = wx.App() + +frame = CCC(None, -1, 'simple.py') +frame.Show() + +app.MainLoop() + + +# import wx +# +# class MainWindow(wx.Frame): +# def __init__(self, *args, **kwargs): +# wx.Frame.__init__(self, *args, **kwargs) +# +# self.panel = wx.Panel(self) +# +# self.label = wx.StaticText(self.panel, label="Label") +# self.text = wx.TextCtrl(self.panel) +# self.button = wx.Button(self.panel, label="Test") +# +# self.button1 = wx.Button(self.panel, label="ABOVE") +# self.button2 = wx.Button(self.panel, label="BELLOW") +# +# self.horizontal = wx.BoxSizer() +# self.horizontal.Add(self.label, flag=wx.CENTER) +# self.horizontal.Add(self.text, proportion=1, flag=wx.CENTER) +# self.horizontal.Add(self.button, flag=wx.CENTER) +# +# self.vertical = wx.BoxSizer(wx.VERTICAL) +# self.vertical.Add(self.button1, flag=wx.EXPAND) +# self.vertical.Add(self.horizontal, proportion=1, flag=wx.EXPAND) +# self.vertical.Add(self.button2, flag=wx.EXPAND) +# +# self.panel.SetSizerAndFit(self.vertical) +# self.Show() +# +# +# app = wx.App(False) +# win = MainWindow(None) +# app.MainLoop() diff --git a/gooey/gui/components/widgets/checkbox.py b/gooey/gui/components/widgets/checkbox.py new file mode 100644 index 0000000..08eefef --- /dev/null +++ b/gooey/gui/components/widgets/checkbox.py @@ -0,0 +1,50 @@ +import wx + +from gooey.gui import formatters, events +from gooey.gui.components.widgets.bases import TextContainer +from gooey.gui.pubsub import pub +from gooey.gui.util import wx_util +from gooey.util.functional import getin + + +class CheckBox(TextContainer): + + widget_class = wx.CheckBox + + def arrange(self, *args, **kwargs): + wx_util.make_bold(self.label) + wx_util.dark_grey(self.help_text) + wx_util.withColor(self.error, self._options['error_color']) + self.error.Hide() + + self.help_text.SetMinSize((0,-1)) + + layout = wx.BoxSizer(wx.VERTICAL) + layout.Add(self.label) + layout.AddSpacer(2) + layout.AddStretchSpacer(1) + if self.help_text: + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(self.widget, 0) + hsizer.Add(self.help_text, 1) + layout.Add(hsizer, 1, wx.EXPAND) + layout.AddSpacer(2) + else: + layout.Add(self.widget, 0, wx.EXPAND) + layout.AddStretchSpacer(1) + return layout + + + def setValue(self, value): + self.widget.SetValue(value) + + def getWidgetValue(self): + return self.widget.GetValue() + + + def formatOutput(self, metatdata, value): + return formatters.checkbox(metatdata, value) + + + def hideInput(self): + self.widget.Hide() diff --git a/gooey/gui/components/widgets/choosers.py b/gooey/gui/components/widgets/choosers.py new file mode 100644 index 0000000..a3f865b --- /dev/null +++ b/gooey/gui/components/widgets/choosers.py @@ -0,0 +1,35 @@ +from gooey.gui.components.widgets import core +from gooey.gui.components.widgets.bases import TextContainer, BaseChooser + + +__ALL__ = [ + 'FileChooser', + 'FileSaver', + 'DirChooser', + 'DateChooser' +] + +class FileChooser(BaseChooser): + # todo: allow wildcard from argparse + widget_class = core.FileChooser + + +class MultiFileChooser(BaseChooser): + # todo: allow wildcard from argparse + widget_class = core.MultiFileChooser + + +class FileSaver(BaseChooser): + # todo: allow wildcard + widget_class = core.FileSaver + + +class DirChooser(BaseChooser): + # todo: allow wildcard + widget_class = core.DirChooser + + +class DateChooser(BaseChooser): + # todo: allow wildcard + widget_class = core.DateChooser + diff --git a/gooey/gui/components/widgets/command.py b/gooey/gui/components/widgets/command.py new file mode 100644 index 0000000..34eb14d --- /dev/null +++ b/gooey/gui/components/widgets/command.py @@ -0,0 +1,8 @@ +from gooey.gui.components.widgets.textfield import TextField + + + +__ALL__ = ('CommandField',) + +class CommandField(TextField): + pass diff --git a/gooey/gui/components/widgets/core/__init__.py b/gooey/gui/components/widgets/core/__init__.py new file mode 100644 index 0000000..d8c3758 --- /dev/null +++ b/gooey/gui/components/widgets/core/__init__.py @@ -0,0 +1,2 @@ +from . chooser import Chooser, FileChooser, FileSaver, DirChooser, DateChooser, MultiFileChooser +from . text_input import PasswordInput, MultilineTextInput, TextInput diff --git a/gooey/gui/components/widgets/core/chooser.py b/gooey/gui/components/widgets/core/chooser.py new file mode 100644 index 0000000..6657d1d --- /dev/null +++ b/gooey/gui/components/widgets/core/chooser.py @@ -0,0 +1,102 @@ +import wx + +from gooey.gui.components.widgets.core.text_input import TextInput +from gooey.gui.components.widgets.dialogs.calender_dialog import CalendarDlg +from gooey.util.functional import merge + + +class Chooser(wx.Panel): + """ + Base 'Chooser' type. + + Launches a Dialog box that allows the user to pick files, directories, + dates, etc.. and places the result into a TextInput in the UI + """ + + def __init__(self, parent, *args, **kwargs): + super(Chooser, self).__init__(parent) + buttonLabel = kwargs.pop('label', 'Browse') + self.widget = TextInput(self, *args, **kwargs) + self.button = wx.Button(self, label=buttonLabel) + self.button.Bind(wx.EVT_BUTTON, self.spawnDialog) + self.layout() + + + def layout(self): + layout = wx.BoxSizer(wx.HORIZONTAL) + layout.Add(self.widget, 1, wx.EXPAND | wx.TOP, 2) + layout.Add(self.button, 0, wx.LEFT, 10) + + v = wx.BoxSizer(wx.VERTICAL) + v.Add(layout, 1, wx.EXPAND, wx.TOP, 1) + self.SetSizer(v) + + + def spawnDialog(self, event): + fd = self.getDialog() + if fd.ShowModal() == wx.ID_CANCEL: + return + self.processResult(self.getResult(fd)) + + + def getDialog(self): + return wx.FileDialog(self, 'Open File') + + def getResult(self, dialog): + return dialog.GetPath() + + + def processResult(self, result): + self.setValue(result) + + + def setValue(self, value): + self.widget.setValue(value) + + def getValue(self): + return self.widget.getValue() + + + +class FileChooser(Chooser): + """ Retrieve an existing file from the system """ + def getDialog(self): + return wx.FileDialog(self, style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) + + +class MultiFileChooser(Chooser): + """ Retrieve an multiple files from the system """ + def getDialog(self): + return wx.FileDialog(self, style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) + + +class FileSaver(Chooser): + """ Specify the path to save a new file """ + def getDialog(self): + return wx.FileDialog( + self, + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, + defaultFile="Enter Filename" + ) + + +class DirChooser(Chooser): + """ Retrieve a path to the supplied directory """ + def getDialog(self): + return wx.DirDialog(self) + + +class DateChooser(Chooser): + """ Launches a date picker which returns and ISO Date """ + def __init__(self, *args, **kwargs): + defaults = {'label': 'Choose Date'} + super(DateChooser, self).__init__(*args, **merge(kwargs, defaults)) + + + def getDialog(self): + return CalendarDlg(self) + + + + + diff --git a/gooey/gui/components/widgets/core/text_input.py b/gooey/gui/components/widgets/core/text_input.py new file mode 100644 index 0000000..5f63b6c --- /dev/null +++ b/gooey/gui/components/widgets/core/text_input.py @@ -0,0 +1,43 @@ +import wx + +from gooey.gui.util.filedrop import FileDrop +from gooey.util.functional import merge + + +class TextInput(wx.Panel): + def __init__(self, parent, *args, **kwargs): + super(TextInput, self).__init__(parent) + self.widget = wx.TextCtrl(self, *args, **kwargs) + dt = FileDrop(self.widget) + self.widget.SetDropTarget(dt) + self.widget.SetMinSize((0, -1)) + self.widget.SetDoubleBuffered(True) + self.widget.AppendText('') + self.layout() + + + def layout(self): + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self.widget, 0, wx.EXPAND) + self.SetSizer(sizer) + + + def setValue(self, value): + self.widget.Clear() + self.widget.AppendText(str(value)) + self.widget.SetInsertionPoint(0) + + + def getValue(self): + return self.widget.GetValue() + + + +def PasswordInput(_, parent, *args, **kwargs): + style = {'style': wx.TE_PASSWORD} + return TextInput(parent, *args, **merge(kwargs, style)) + + +def MultilineTextInput(_, parent, *args, **kwargs): + style = {'style': wx.TE_MULTILINE} + return TextInput(parent, *args, **merge(kwargs, style)) diff --git a/gooey/gui/components/widgets/counter.py b/gooey/gui/components/widgets/counter.py new file mode 100644 index 0000000..137f7e3 --- /dev/null +++ b/gooey/gui/components/widgets/counter.py @@ -0,0 +1,16 @@ +from gooey.gui.components.widgets.dropdown import Dropdown + +from gooey.gui import formatters + + +class Counter(Dropdown): + + def setValue(self, value): + self.widget.SetSelection(value) + + def getWidgetValue(self): + return self.widget.GetValue() + + + def formatOutput(self, metadata, value): + return formatters.counter(metadata, value) diff --git a/gooey/gui/components/widgets/dialogs/__init__.py b/gooey/gui/components/widgets/dialogs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gooey/gui/components/widgets/dialogs/calender_dialog.py b/gooey/gui/components/widgets/dialogs/calender_dialog.py new file mode 100644 index 0000000..4b98161 --- /dev/null +++ b/gooey/gui/components/widgets/dialogs/calender_dialog.py @@ -0,0 +1,50 @@ +__author__ = 'Chris' + +import wx + +from gooey.gui.util import wx_util + +from gooey.gui.three_to_four import Classes, Constants + + +class CalendarDlg(wx.Dialog): + def __init__(self, parent): + wx.Dialog.__init__(self, parent) + + self.SetBackgroundColour('#ffffff') + + self.ok_button = wx.Button(self, wx.ID_OK, label='Ok') + self.datepicker = Classes.DatePickerCtrl(self, style=Constants.WX_DP_DROPDOWN) + + vertical_container = wx.BoxSizer(wx.VERTICAL) + vertical_container.AddSpacer(10) + vertical_container.Add(wx_util.h1(self, label='Select a Date'), 0, wx.LEFT | wx.RIGHT, 15) + vertical_container.AddSpacer(10) + vertical_container.Add(self.datepicker, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 15) + + vertical_container.AddSpacer(10) + button_sizer = wx.BoxSizer(wx.HORIZONTAL) + button_sizer.AddStretchSpacer(1) + button_sizer.Add(self.ok_button, 0) + + vertical_container.Add(button_sizer, 0, wx.LEFT | wx.RIGHT, 15) + vertical_container.AddSpacer(20) + self.SetSizerAndFit(vertical_container) + + self.Bind(wx.EVT_BUTTON, self.OnOkButton, self.ok_button) + + def OnOkButton(self, event): + self.EndModal(wx.ID_OK) + event.Skip() + + def OnCancellButton(self, event): + try: + return None + except: + self.Close() + + def GetPath(self): + return self.datepicker.GetValue().FormatISODate() + + + diff --git a/gooey/gui/components/widgets/dropdown.py b/gooey/gui/components/widgets/dropdown.py new file mode 100644 index 0000000..facefa0 --- /dev/null +++ b/gooey/gui/components/widgets/dropdown.py @@ -0,0 +1,35 @@ +from gooey.gui.components.widgets.bases import TextContainer +import wx + +from gooey.gui import formatters + + +class Dropdown(TextContainer): + + def getWidget(self, parent, *args, **options): + default = 'Select Option' + return wx.ComboBox( + parent=parent, + id=-1, + value=default, + choices=[default] + self._meta['choices'], + style=wx.CB_DROPDOWN) + + def setOptions(self, options): + prevSelection = self.widget.GetSelection() + self.widget.Clear() + for option in ['Select Option'] + options: + self.widget.Append(option) + self.widget.SetSelection(0) + + + def setValue(self, value): + ## +1 to offset the default placeholder value + index = self._meta['choices'].index(value) + 1 + self.widget.SetSelection(index) + + def getWidgetValue(self): + return self.widget.GetValue() + + def formatOutput(self, metadata, value): + return formatters.dropdown(metadata, value) diff --git a/gooey/gui/components/widgets/listbox.py b/gooey/gui/components/widgets/listbox.py new file mode 100644 index 0000000..88c7d0b --- /dev/null +++ b/gooey/gui/components/widgets/listbox.py @@ -0,0 +1,30 @@ +from gooey.gui.components.widgets.bases import TextContainer +import wx + +from gooey.gui import formatters + + +class Listbox(TextContainer): + + def getWidget(self, parent, *args, **options): + default = 'Select Option' + return wx.ListBox( + parent=parent, + choices=self._meta['choices'], + size=(-1,60), + style=wx.LB_MULTIPLE + ) + + def setOptions(self, options): + self.widget.SetChoices() + + def setValue(self, values): + for string in values: + self.widget.SetStringSelection(string) + + def getWidgetValue(self): + return [self.widget.GetString(index) + for index in self.widget.GetSelections()] + + def formatOutput(self, metadata, value): + return formatters.listbox(metadata, value) diff --git a/gooey/gui/components/widgets/password.py b/gooey/gui/components/widgets/password.py new file mode 100644 index 0000000..e132f23 --- /dev/null +++ b/gooey/gui/components/widgets/password.py @@ -0,0 +1,12 @@ +from gooey.gui.components.widgets.core.text_input import PasswordInput +from gooey.gui.components.widgets.textfield import TextField + + +__ALL__ = ('PasswordField',) + +class PasswordField(TextField): + widget_class = PasswordInput + + def __init__(self, *args, **kwargs): + super(PasswordField, self).__init__(*args, **kwargs) + diff --git a/gooey/gui/components/widgets/radio_group.py b/gooey/gui/components/widgets/radio_group.py new file mode 100644 index 0000000..0d489ce --- /dev/null +++ b/gooey/gui/components/widgets/radio_group.py @@ -0,0 +1,132 @@ +import wx +from gooey.gui.components.widgets.bases import BaseWidget +from gooey.gui.util import wx_util +from gooey.gui.components.widgets import CheckBox +from gooey.util.functional import getin, findfirst, merge + + +class RadioGroup(BaseWidget): + + def __init__(self, parent, widgetInfo, *args, **kwargs): + super(RadioGroup, self).__init__(parent, *args, **kwargs) + self._parent = parent + self.info = widgetInfo + self._id = widgetInfo['id'] + self.widgetInfo = widgetInfo + self.error = wx.StaticText(self, label='') + self.radioButtons = self.createRadioButtons() + self.selected = None + self.widgets = self.createWidgets() + self.arrange() + self.applyStyleRules() + + for button in self.radioButtons: + button.Bind(wx.EVT_LEFT_DOWN, self.handleButtonClick) + + initialSelection = getin(self.info, ['options', 'initial_selection'], None) + if initialSelection is not None: + self.selected = self.radioButtons[initialSelection] + self.selected.SetValue(True) + self.handleImplicitCheck() + + + def getValue(self): + for button, widget in zip(self.radioButtons, self.widgets): + if button.GetValue(): # is Checked + return merge(widget.getValue(), {'id': self._id}) + else: + # just return the first widget's value even though it's + # not active so that the expected interface is satisfied + return self.widgets[0].getValue() + + def setErrorString(self, message): + for button, widget in zip(self.radioButtons, self.widgets): + if button.GetValue(): # is Checked + widget.setErrorString(message) + self.Layout() + + def showErrorString(self, b): + for button, widget in zip(self.radioButtons, self.widgets): + if button.GetValue(): # is Checked + widget.showErrorString(b) + + + def arrange(self, *args, **kwargs): + title = getin(self.widgetInfo, ['options', 'title'], 'Choose One') + if getin(self.widgetInfo, ['options', 'show_border'], False): + boxDetails = wx.StaticBox(self, -1, title) + boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL) + else: + boxSizer = wx.BoxSizer(wx.VERTICAL) + boxSizer.AddSpacer(10) + boxSizer.Add(wx_util.h1(self, title), 0) + + for btn, widget in zip(self.radioButtons, self.widgets): + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(btn,0, wx.RIGHT, 4) + sizer.Add(widget, 1, wx.EXPAND) + boxSizer.Add(sizer, 1, wx.ALL | wx.EXPAND, 5) + self.SetSizer(boxSizer) + + + def handleButtonClick(self, event): + if not self.widgetInfo['required']: + # if it's not a required group, allow deselection of the + # current option if the user clicks on a selected radio button + if event.EventObject.Id == getattr(self.selected, 'Id', None)\ + and event.EventObject.GetValue(): + event.EventObject.SetValue(False) + else: + self.selected = event.EventObject + self.selected.SetValue(True) + self.applyStyleRules() + self.handleImplicitCheck() + + def applyStyleRules(self): + """ + Conditionally disabled/enables form fields based on the current + section in the radio group + """ + for button, widget in zip(self.radioButtons, self.widgets): + if isinstance(widget, CheckBox): + widget.hideInput() + if not button.GetValue(): # not checked + widget.widget.Disable() + else: + widget.widget.Enable() + + def handleImplicitCheck(self): + """ + Checkboxes are hidden when inside of a RadioGroup as a selection of + the Radio button is an implicit selection of the Checkbox. As such, we have + to manually "check" any checkbox as needed. + """ + for button, widget in zip(self.radioButtons, self.widgets): + if isinstance(widget, CheckBox): + if button.GetValue(): # checked + widget.setValue(True) + else: + widget.setValue(False) + + + def createRadioButtons(self): + # button groups in wx are statefully determined via a style flag + # on the first button (what???). All button instances are part of the + # same group until a new button is created with the style flag RG_GROUP + # https://wxpython.org/Phoenix/docs/html/wx.RadioButton.html + # (What???) + firstButton = wx.RadioButton(self, style=wx.RB_GROUP) + firstButton.SetValue(False) + buttons = [firstButton] + + for _ in getin(self.widgetInfo, ['data','widgets'], [])[1:]: + buttons.append(wx.RadioButton(self)) + return buttons + + def createWidgets(self): + """ + Instantiate the Gooey Widgets that are used within the RadioGroup + """ + from gooey.gui.components import widgets + return [getattr(widgets, item['type'])(self, item) + for item in getin(self.widgetInfo, ['data', 'widgets'], [])] diff --git a/gooey/gui/components/widgets/textarea.py b/gooey/gui/components/widgets/textarea.py new file mode 100644 index 0000000..0281598 --- /dev/null +++ b/gooey/gui/components/widgets/textarea.py @@ -0,0 +1,6 @@ +from gooey.gui.components.widgets.core.text_input import MultilineTextInput +from gooey.gui.components.widgets.textfield import TextField + + +class Textarea(TextField): + widget_class = MultilineTextInput diff --git a/gooey/gui/components/widgets/textfield.py b/gooey/gui/components/widgets/textfield.py new file mode 100644 index 0000000..b286bfb --- /dev/null +++ b/gooey/gui/components/widgets/textfield.py @@ -0,0 +1,20 @@ +import wx +from gooey.gui.components.widgets.bases import TextContainer +from gooey.gui import formatters, events +from gooey.gui.components.widgets.core.text_input import TextInput +from gooey.gui.pubsub import pub +from gooey.util.functional import getin + + +class TextField(TextContainer): + widget_class = TextInput + + def getWidgetValue(self): + return self.widget.getValue() + + def setValue(self, value): + self.widget.setValue(str(value)) + + def formatOutput(self, metatdata, value): + return formatters.general(metatdata, value) +