diff --git a/README.md b/README.md
index ff03d9e..e5af77a 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,7 @@ Table of Contents
- [Full/Advanced](#advanced)
- [Basic](#basic)
- [No Config](#no-config)
+- [Menus](#menus)
- [Input Validation](#input-validation)
- [Using Dynamic Values](#using-dynamic-values)
- [Showing Progress](#showing-progress)
@@ -302,6 +303,7 @@ Just about everything in Gooey's overall look and feel can be customized by pass
| terminal_font_weight | Weight of the font (NORMAL|BOLD) |
| terminal_font_size | Point size of the font displayed in the terminal |
| error_color | HEX value of the text displayed when a validation error occurs |
+| menus | Show custom menu groups and items (see: [Menus](#menus) |
@@ -439,6 +441,158 @@ No Config pretty much does what you'd expect: it doesn't show a configuration sc
+
+--------------------------------------
+
+
+### Menus
+
+
+
+
+>Added 1.0.2
+
+You can add a Menu Bar to the top of Gooey with customized menu groups and items.
+
+Menus are specified on the main `@Gooey` decorator as a list of maps.
+
+```
+@Gooey(menu=[{}, {}, ...])
+```
+
+Each map is made up of two key/value pairs
+
+1. `name` - the name for this menu group
+2. `items` - the individual menu items within this group
+
+You can have as many menu groups as you want. They're passed as a list to the `menu` argument on the `@Gooey` decorator.
+
+```
+@Gooey(menu=[{'name': 'File', 'items: []},
+ {'name': 'Tools', 'items': []},
+ {'name': 'Help', 'items': []}])
+```
+
+Individual menu items in a group are also just maps of key / value pairs. Their exact key set varies based on their `type`, but two keys will always be present:
+
+* `type` - this controls the behavior that will be attached to the menu item as well as the keys it needs specified
+* `menuTitle` - the name for this MenuItem
+
+
+Currently, three types of menu options are supported:
+
+ * AboutDialog
+ * MessageDialog
+ * Link
+
+
+
+
+**About Dialog** is your run-of-the-mill About Dialog. It displays program information such as name, version, and license info in a standard native AboutBox.
+
+Schema
+
+ * `name` - (_optional_)
+ * `description` - (_optional_)
+ * `version` - (_optional_)
+ * `copyright` - (_optional_)
+ * `license` - (_optional_)
+ * `website` - (_optional_)
+ * `developer` - (_optional_)
+
+Example:
+
+```
+{
+ 'type': 'AboutDialog',
+ 'menuTitle': 'About',
+ 'name': 'Gooey Layout Demo',
+ 'description': 'An example of Gooey\'s layout flexibility',
+ 'version': '1.2.1',
+ 'copyright': '2018',
+ 'website': 'https://github.com/chriskiehl/Gooey',
+ 'developer': 'http://chriskiehl.com/',
+ 'license': 'MIT'
+}
+```
+
+
+
+**MessageDialog** is a generic informational dialog box. You can display anything from small alerts, to long-form informational text to the user.
+
+Schema:
+
+ * `message` - (_required_) the text to display in the body of the modal
+ * `caption` - (_optional_) the caption in the title bar of the modal
+
+Example:
+
+```python
+{
+ 'type': 'MessageDialog',
+ 'menuTitle': 'Information',
+ 'message': 'Hey, here is some cool info for ya!',
+ 'caption': 'Stuff you should know'
+}
+```
+
+**Link** is for sending the user to an external website. This will spawn their default browser at the URL you specify.
+
+Schema:
+
+ * `url` - (_required_) - the fully qualified URL to visit
+
+Example:
+
+```python
+{
+ 'type': 'Link',
+ 'menuTitle': 'Visit Out Site',
+ 'url': 'http://www.example.com'
+}
+```
+
+**A full example:**
+
+Two menu groups ("File" and "Help") with four menu items between them.
+
+```python
+@Gooey(
+ program_name='Advanced Layout Groups',
+ menu=[{
+ 'name': 'File',
+ 'items': [{
+ 'type': 'AboutDialog',
+ 'menuTitle': 'About',
+ 'name': 'Gooey Layout Demo',
+ 'description': 'An example of Gooey\'s layout flexibility',
+ 'version': '1.2.1',
+ 'copyright': '2018',
+ 'website': 'https://github.com/chriskiehl/Gooey',
+ 'developer': 'http://chriskiehl.com/',
+ 'license': 'MIT'
+ }, {
+ 'type': 'MessageDialog',
+ 'menuTitle': 'Information',
+ 'caption': 'My Message',
+ 'message': 'I am demoing an informational dialog!'
+ }, {
+ 'type': 'Link',
+ 'menuTitle': 'Visit Our Site',
+ 'url': 'https://github.com/chriskiehl/Gooey'
+ }]
+ },{
+ 'name': 'Help',
+ 'items': [{
+ 'type': 'Link',
+ 'menuTitle': 'Documentation',
+ 'url': 'https://www.readthedocs.com/foo'
+ }]
+ }]
+)
+```
+
+
---------------------------------------
diff --git a/gooey/gui/components/menubar.py b/gooey/gui/components/menubar.py
new file mode 100644
index 0000000..c021546
--- /dev/null
+++ b/gooey/gui/components/menubar.py
@@ -0,0 +1,81 @@
+import webbrowser
+from functools import partial
+
+import wx
+
+from gui import three_to_four
+
+
+class MenuBar(wx.MenuBar):
+ """
+ Wx.MenuBar handles converting the users list of Menu Groups into
+ concrete wx.Menu instances.
+ """
+
+ def __init__(self, buildSpec, *args, **kwargs):
+ super(MenuBar,self).__init__(*args, **kwargs)
+ self.buildSpec = buildSpec
+ self.makeMenuItems(buildSpec.get('menu', []))
+
+
+ def makeMenuItems(self, menuGroups):
+ """
+ Assign the menu groups list to wx.Menu instances
+ and bind the appropriate handlers.
+ """
+ for menuGroup in menuGroups:
+ menu = wx.Menu()
+ for item in menuGroup.get('items'):
+ option = menu.Append(wx.NewId(), item.get('menuTitle', ''))
+ self.Bind(wx.EVT_MENU, self.handleMenuAction(item), option)
+ self.Append(menu, '&' + menuGroup.get('name'))
+
+
+ def handleMenuAction(self, item):
+ """
+ Dispatch based on the value of the type field.
+ """
+ handlers = {
+ 'Link': self.openBrowser,
+ 'AboutDialog': self.spawnAboutDialog,
+ 'MessageDialog': self.spawnMessageDialog
+ }
+ f = handlers[item['type']]
+ return partial(f, item)
+
+
+ def openBrowser(self, item, *args, **kwargs):
+ """
+ Open the supplied URL in the user's default browser.
+ """
+ webbrowser.open(item.get('url'))
+
+
+ def spawnMessageDialog(self, item, *args, **kwargs):
+ """
+ Show a simple message dialog with the user's message and caption.
+ """
+ wx.MessageDialog(self, item.get('message', ''),
+ caption=item.get('caption', '')).ShowModal()
+
+
+ def spawnAboutDialog(self, item, *args, **kwargs):
+ """
+ Fill the wx.AboutBox with any relevant info the user provided
+ and launch the dialog
+ """
+ aboutOptions = {
+ 'name': 'SetName',
+ 'version': 'SetVersion',
+ 'description': 'SetDescription',
+ 'copyright': 'SetCopyright',
+ 'website': 'SetWebSite',
+ 'developer': 'AddDeveloper',
+ 'license': 'SetLicense'
+ }
+ about = three_to_four.AboutDialog()
+ for field, method in aboutOptions.items():
+ if field in item:
+ getattr(about, method)(item[field])
+
+ three_to_four.AboutBox(about)
\ No newline at end of file
diff --git a/gooey/gui/containers/application.py b/gooey/gui/containers/application.py
index ced3944..32d84e7 100644
--- a/gooey/gui/containers/application.py
+++ b/gooey/gui/containers/application.py
@@ -36,8 +36,8 @@ class GooeyApplication(wx.Frame):
self.buildSpec = buildSpec
self.applyConfiguration()
- self.menuBar = MenuBar(buildSpec)
- self.SetMenuBar(self.menuBar)
+ self.menu = MenuBar(buildSpec)
+ self.SetMenuBar(self.menu)
self.header = FrameHeader(self, buildSpec)
self.configs = self.buildConfigPanels(self)
self.navbar = self.buildNavigation()
diff --git a/gooey/gui/three_to_four.py b/gooey/gui/three_to_four.py
index 43969b9..69c4ad3 100644
--- a/gooey/gui/three_to_four.py
+++ b/gooey/gui/three_to_four.py
@@ -49,3 +49,15 @@ def bitmapFromBufferRGBA(im, rgba):
else:
return wx.BitmapFromBufferRGBA(im.size[0], im.size[1], rgba)
+def AboutDialog():
+ if isLatestVersion:
+ return wx.adv.AboutDialogInfo()
+ else:
+ return wx.AboutDialogInfo()
+
+
+def AboutBox(aboutDialog):
+ return (wx.adv.AboutBox(aboutDialog)
+ if isLatestVersion
+ else wx.AboutBox(aboutDialog))
+
diff --git a/gooey/python_bindings/config_generator.py b/gooey/python_bindings/config_generator.py
index d3aa65a..d7f4b43 100644
--- a/gooey/python_bindings/config_generator.py
+++ b/gooey/python_bindings/config_generator.py
@@ -49,6 +49,7 @@ def create_from_parser(parser, source_path, **kwargs):
'return_to_config': kwargs.get('return_to_config', False),
'show_restart_button': kwargs.get('show_restart_button', True),
'requires_shell': kwargs.get('requires_shell', True),
+ 'menu': kwargs.get('menu', []),
# Legacy/Backward compatibility interop
'use_legacy_titles': kwargs.get('use_legacy_titles', True),