diff --git a/README.md b/README.md index 02e52a6..ff03d9e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,24 @@ Turn (almost) any Python 2 or 3 Console Program into a GUI application with one # Gooey now supports Python 3!! +# 1.0.2 Release TODO: + +Finish of the readonly param for all components. Maybe this 'll be passing +totally arbitrary style around. + +Document all the new layout stuff + +* group - show_underline +* group - marginTop +* gooey_options.label_color +* gooey_options.help_color +* gooey_options.error_color +* gooey_options.show_label +* gooey_options.show_help +* gooey_options.block_checkbox.checkbox_label [string] +* gooey_options.textarea.readonly + + Table of Contents ----------------- @@ -199,6 +217,7 @@ However, by dropping in `GooeyParser` and supplying a `widget` name, you can dis | DateChooser                                             |

| | PasswordField |

| | Listbox | ![image](https://user-images.githubusercontent.com/1408720/31590191-fadd06f2-b1c0-11e7-9a49-7cbf0c6d33d1.png) | +| BlockCheckbox | ![image](https://user-images.githubusercontent.com/1408720/46922288-9296f200-cfbb-11e8-8b0d-ddde08064247.png)
The default InlineCheck box can look less than ideal if a large help text block is present. `BlockCheckbox` moves the text block to the normal position and provides a short-form `block_label` for display next to the control. Use `gooey_options.checkbox_label` to control the label text | @@ -611,7 +630,6 @@ Screenshots - Wanna help? ----------- diff --git a/gooey/gui/application.py b/gooey/gui/application.py index eea9b97..35d4b41 100644 --- a/gooey/gui/application.py +++ b/gooey/gui/application.py @@ -22,7 +22,7 @@ def build_app(build_spec): i18n.load(build_spec['language_dir'], build_spec['language'], build_spec['encoding']) imagesPaths = image_repository.loadImages(build_spec['image_dir']) gapp = GooeyApplication(merge(build_spec, imagesPaths)) - # wx.lib.inspection.InspectionTool().Show() + wx.lib.inspection.InspectionTool().Show() gapp.Show() return app diff --git a/gooey/gui/components/config.py b/gooey/gui/components/config.py index 62c4b46..ee74c80 100644 --- a/gooey/gui/components/config.py +++ b/gooey/gui/components/config.py @@ -121,7 +121,7 @@ class ConfigPage(ScrolledPanel): widget = self.reifyWidget(parent, item) # !Mutate the reifiedWidgets instance variable in place self.reifiedWidgets.append(widget) - sizer.Add(widget, 1, wx.ALL, 5) + 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 diff --git a/gooey/gui/components/util/__init__.py b/gooey/gui/components/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gooey/gui/components/util/wrapped_static_text.py b/gooey/gui/components/util/wrapped_static_text.py new file mode 100644 index 0000000..4f8770a --- /dev/null +++ b/gooey/gui/components/util/wrapped_static_text.py @@ -0,0 +1,83 @@ +import wx +from wx.lib.wordwrap import wordwrap + + + +class AutoWrappedStaticText(wx.StaticText): + """ + Copy/pasta of wx.lib.agw.infobar.AutoWrapStaticText with two modifications: + + 1. Extends wx.StaticText rather than GenStaticText + 2. Does not set the fore/background colors to sys defaults + + The behavior of GenStaticText's background color is pretty buggy cross- + platform. It doesn't reliably match its parent components background + colors[0] (for instance when rendered inside of a Notebook) which leads to + ugly 'boxing' around the text components. + + [0] more specifically, they'll match 1:1 on paper, but still ultimately + render differently. + """ + + def __init__(self, parent, *args, **kwargs): + super(AutoWrappedStaticText, self).__init__(parent, *args, **kwargs) + self.label = kwargs.get('label') + self.Bind(wx.EVT_SIZE, self.OnSize) + + + def OnSize(self, event): + """ + Handles the ``wx.EVT_SIZE`` event for :class:`AutoWrapStaticText`. + + :param `event`: a :class:`wx.SizeEvent` event to be processed. + """ + + event.Skip() + self.Wrap(event.GetSize().width) + + def Wrap(self, width): + """ + This functions wraps the controls label so that each of its lines becomes at + most `width` pixels wide if possible (the lines are broken at words boundaries + so it might not be the case if words are too long). + + If `width` is negative, no wrapping is done. + + :param integer `width`: the maximum available width for the text, in pixels. + + :note: Note that this `width` is not necessarily the total width of the control, + since a few pixels for the border (depending on the controls border style) may be added. + """ + + if width < 0: + return + + self.Freeze() + + dc = wx.ClientDC(self) + dc.SetFont(self.GetFont()) + text = wordwrap(self.label, width, dc) + self.SetLabel(text, wrapped=True) + + self.Thaw() + + def SetLabel(self, label, wrapped=False): + """ + Sets the :class:`AutoWrapStaticText` label. + + All "&" characters in the label are special and indicate that the following character is + a mnemonic for this control and can be used to activate it from the keyboard (typically + by using ``Alt`` key in combination with it). To insert a literal ampersand character, you + need to double it, i.e. use "&&". If this behaviour is undesirable, use :meth:`~Control.SetLabelText` instead. + + :param string `label`: the new :class:`AutoWrapStaticText` text label; + :param bool `wrapped`: ``True`` if this method was called by the developer using :meth:`~AutoWrapStaticText.SetLabel`, + ``False`` if it comes from the :meth:`~AutoWrapStaticText.OnSize` event handler. + + :note: Reimplemented from :class:`wx.Control`. + """ + + if not wrapped: + self.label = label + + wx.StaticText.SetLabel(self, label) \ No newline at end of file diff --git a/gooey/gui/components/widgets/__init__.py b/gooey/gui/components/widgets/__init__.py index ac3184d..5789312 100644 --- a/gooey/gui/components/widgets/__init__.py +++ b/gooey/gui/components/widgets/__init__.py @@ -7,6 +7,7 @@ from .command import CommandField from .dropdown import Dropdown from .listbox import Listbox from .checkbox import CheckBox +from .checkbox import BlockCheckbox 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 index e70122c..dd0e5b0 100644 --- a/gooey/gui/components/widgets/bases.py +++ b/gooey/gui/components/widgets/bases.py @@ -1,11 +1,12 @@ +from functools import reduce + 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 +from gooey.gui.components.util.wrapped_static_text import AutoWrappedStaticText class BaseWidget(wx.Panel): @@ -47,11 +48,12 @@ class TextContainer(BaseWidget): 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.help_text = AutoWrappedStaticText(self, label=widgetInfo['data']['help'] or '') + self.error = AutoWrappedStaticText(self, label='') self.error.Hide() self.widget = self.getWidget(self) self.layout = self.arrange(*args, **kwargs) + self.setColors() self.SetSizer(self.layout) self.Bind(wx.EVT_SIZE, self.onSize) if self._meta['default']: @@ -69,7 +71,7 @@ class TextContainer(BaseWidget): layout = wx.BoxSizer(wx.VERTICAL) if self._options.get('show_label', True): - layout.Add(self.label) + layout.Add(self.label, 0, wx.EXPAND) else: layout.AddStretchSpacer(1) @@ -80,10 +82,24 @@ class TextContainer(BaseWidget): else: layout.AddStretchSpacer(1) layout.Add(self.getSublayout(), 0, wx.EXPAND) - layout.Add(self.error) + layout.Add(self.error, 1, wx.EXPAND) + self.error.Hide() return layout + + def setColors(self): + wx_util.make_bold(self.label) + wx_util.withColor(self.label, self._options['label_color']) + wx_util.withColor(self.help_text, self._options['help_color']) + wx_util.withColor(self.error, self._options['error_color']) + if self._options.get('label_bg_color'): + self.label.SetBackgroundColour(self._options.get('label_bg_color')) + if self._options.get('help_bg_color'): + self.help_text.SetBackgroundColour(self._options.get('help_bg_color')) + if self._options.get('error_bg_color'): + self.error.SetBackgroundColour(self._options.get('error_bg_color')) + def getWidget(self, *args, **options): return self.widget_class(*args, **options) @@ -96,9 +112,13 @@ class TextContainer(BaseWidget): return layout def onSize(self, event): - self.error.Wrap(self.GetSize().width) + # print(self.GetSize()) + # self.error.Wrap(self.GetSize().width) + # self.help_text.Wrap(500) + # self.Layout() event.Skip() + def getValue(self): userValidator = getin(self._options, ['validator', 'test'], 'True') message = getin(self._options, ['validator', 'message'], '') diff --git a/gooey/gui/components/widgets/checkbox.py b/gooey/gui/components/widgets/checkbox.py index 15fec6f..5b3da66 100644 --- a/gooey/gui/components/widgets/checkbox.py +++ b/gooey/gui/components/widgets/checkbox.py @@ -1,10 +1,9 @@ import wx -from gooey.gui import formatters, events +from gooey.gui import formatters from gooey.gui.components.widgets.bases import TextContainer -from gooey.gui.pubsub import pub +from gooey.gui.lang.i18n import _ from gooey.gui.util import wx_util -from gooey.util.functional import getin class CheckBox(TextContainer): @@ -49,3 +48,57 @@ class CheckBox(TextContainer): def hideInput(self): self.widget.Hide() + + + + + +class BlockCheckbox(CheckBox): + """ + A block style layout which places the help text in the normal + location rather than inline next to the checkbox. A replacement label + called `block_label` is shown next to the checkbox control. + + +-----------------+ + |label | + |help_text | + |[ ] block_label | + +-----------------+ + This option tends to look better when there is a large amount of + help text. + """ + + + def arrange(self, *args, **kwargs): + wx_util.make_bold(self.label) + wx_util.withColor(self.label, self._options['label_color']) + wx_util.withColor(self.help_text, self._options['help_color']) + wx_util.withColor(self.error, self._options['error_color']) + self.error.Hide() + + self.help_text.SetMinSize((0,-1)) + + layout = wx.BoxSizer(wx.VERTICAL) + + if self._options.get('show_label', True): + layout.Add(self.label, 0, wx.EXPAND) + else: + layout.AddStretchSpacer(1) + + layout.AddSpacer(2) + if self.help_text and self._options.get('show_help', True): + layout.Add(self.help_text, 1, wx.EXPAND) + layout.AddSpacer(2) + else: + layout.AddStretchSpacer(1) + + layout.AddSpacer(2) + + block_label = self._options.get('checkbox_label', _('checkbox_label')) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(self.widget, 0) + hsizer.Add(wx.StaticText(self, label=block_label), 1) + layout.Add(hsizer, 1, wx.EXPAND) + layout.AddSpacer(2) + + return layout \ No newline at end of file diff --git a/gooey/gui/components/widgets/textarea.py b/gooey/gui/components/widgets/textarea.py index 608c6e4..fe1cd40 100644 --- a/gooey/gui/components/widgets/textarea.py +++ b/gooey/gui/components/widgets/textarea.py @@ -1,4 +1,6 @@ import wx +from functools import reduce + from gooey.gui.components.widgets.core.text_input import MultilineTextInput from gooey.gui.components.widgets.textfield import TextField from gooey.gui.components.widgets.bases import TextContainer @@ -12,9 +14,16 @@ class Textarea(TextContainer): return wx.TextCtrl( parent=parent, size=(-1, widgetHeight), - style=wx.TE_MULTILINE | wx.TE_READONLY + style=self.getModifiers() ) + def getModifiers(self): + readonly = (wx.TE_READONLY + if self._options.get('readonly', False) + # using TE_MUTLI as a safe OR-able no-op value + else wx.TE_MULTILINE) + return reduce(lambda acc, val: acc | val, [wx.TE_MULTILINE, readonly]) + def getWidgetValue(self): return self.widget.GetValue() diff --git a/gooey/gui/containers/application.py b/gooey/gui/containers/application.py index 6cc21c3..ced3944 100644 --- a/gooey/gui/containers/application.py +++ b/gooey/gui/containers/application.py @@ -3,27 +3,26 @@ Primary orchestration and control point for Gooey. """ import sys -from itertools import chain import wx +from gooey.gui import cli from gooey.gui import events -from gooey.gui.components.header import FrameHeader -from gooey.gui.components.footer import Footer -from gooey.gui.util import wx_util +from gooey.gui import seeder +from gooey.gui.components import modals from gooey.gui.components.config import ConfigPage, TabbedConfigPage +from gooey.gui.components.console import Console +from gooey.gui.components.footer import Footer +from gooey.gui.components.header import FrameHeader +from gooey.gui.components.menubar import MenuBar from gooey.gui.components.sidebar import Sidebar from gooey.gui.components.tabbar import Tabbar -from gooey.util.functional import getin, assoc, flatmap, compact -from gooey.python_bindings import constants -from gooey.gui.pubsub import pub -from gooey.gui import cli -from gooey.gui.components.console import Console from gooey.gui.lang.i18n import _ from gooey.gui.processor import ProcessController +from gooey.gui.pubsub import pub +from gooey.gui.util import wx_util from gooey.gui.util.wx_util import transactUI -from gooey.gui.components import modals -from gooey.gui import seeder +from gooey.python_bindings import constants class GooeyApplication(wx.Frame): @@ -36,7 +35,9 @@ class GooeyApplication(wx.Frame): self._state = {} self.buildSpec = buildSpec - self.SetTitle(self.buildSpec['program_name']) + self.applyConfiguration() + self.menuBar = MenuBar(buildSpec) + self.SetMenuBar(self.menuBar) self.header = FrameHeader(self, buildSpec) self.configs = self.buildConfigPanels(self) self.navbar = self.buildNavigation() @@ -70,6 +71,10 @@ class GooeyApplication(wx.Frame): self.onStart() + def applyConfiguration(self): + self.SetTitle(self.buildSpec['program_name']) + self.SetBackgroundColour(self.buildSpec.get('body_bg_color')) + def onStart(self, *args, **kwarg): """ Verify user input and kick off the client's program if valid diff --git a/gooey/languages/english.json b/gooey/languages/english.json index c49ba4f..5133725 100644 --- a/gooey/languages/english.json +++ b/gooey/languages/english.json @@ -1,6 +1,7 @@ { "browse": "Browse", "cancel": "Cancel", + "checkbox_label": "Enable", "choose_date": "Choose Date", "choose_file": "Choose File", "choose_folder": "Choose Folder", diff --git a/gooey/python_bindings/argparse_to_json.py b/gooey/python_bindings/argparse_to_json.py index c668f9b..a2793ee 100644 --- a/gooey/python_bindings/argparse_to_json.py +++ b/gooey/python_bindings/argparse_to_json.py @@ -1,8 +1,6 @@ """ Converts argparse parser actions into json "Build Specs" """ -import functools -import pprint import argparse import os import sys @@ -19,6 +17,7 @@ from uuid import uuid4 from gooey.util.functional import merge, getin + VALID_WIDGETS = ( 'FileChooser', 'MultiFileChooser', @@ -30,6 +29,7 @@ VALID_WIDGETS = ( 'Counter', 'RadioGroup', 'CheckBox', + 'BlockCheckbox', 'MultiDirChooser', 'Textarea', 'PasswordField', @@ -45,16 +45,17 @@ class UnsupportedConfiguration(Exception): pass - group_defaults = { 'columns': 2, 'padding': 10, 'show_border': False } +# TODO: merge the default foreground and bg colors from the +# baseline build_spec item_default = { 'error_color': '#ea7878', - 'label_color': '#ff1111', + 'label_color': '#000000', 'help_color': '#363636', 'validator': { 'type': 'local', @@ -93,6 +94,7 @@ def process(parser, widget_dict, options): return categorize2(strip_empty(corrected_action_groups), widget_dict, options) + def strip_empty(groups): return [group for group in groups if group['items']]