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 + + +![image](https://user-images.githubusercontent.com/1408720/47250909-74782a00-d3df-11e8-88ac-182d06c4435a.png) + +>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),