import tkinter as tk from tkinter import ttk from tkinter import messagebox from functools import partial class GenericMainMenu(tk.Menu): """The Application's main menu""" def __init__(self, parent, settings, callbacks, **kwargs): """Constructor for MainMenu arguments: parent - The parent widget settings - a dict containing Tkinter variables callbacks - a dict containing Python callables """ super().__init__(parent, **kwargs) self.settings = settings self.callbacks = callbacks self._build_menu() self._bind_accelerators() def _build_menu(self): # The file menu file_menu = tk.Menu(self, tearoff=False) file_menu.add_command( label="Select file…", command=self.callbacks['file->select'], accelerator='Ctrl+O' ) file_menu.add_separator() file_menu.add_command( label="Quit", command=self.callbacks['file->quit'], accelerator='Ctrl+Q' ) self.add_cascade(label='File', menu=file_menu) # The options menu options_menu = tk.Menu(self, tearoff=False) options_menu.add_checkbutton( label='Autofill Date', variable=self.settings['autofill date'] ) options_menu.add_checkbutton( label='Autofill Sheet data', variable=self.settings['autofill sheet data'] ) # font size submenu font_size_menu = tk.Menu(self, tearoff=False) for size in range(6, 17, 1): font_size_menu.add_radiobutton( label=size, value=size, variable=self.settings['font size'] ) options_menu.add_cascade(label='Font size', menu=font_size_menu) # themes menu style = ttk.Style() themes_menu = tk.Menu(self, tearoff=False) for theme in style.theme_names(): themes_menu.add_radiobutton( label=theme, value=theme, variable=self.settings['theme'] ) options_menu.add_cascade(label='Theme', menu=themes_menu) self.settings['theme'].trace('w', self.on_theme_change) self.add_cascade(label='Options', menu=options_menu) # switch from recordlist to recordform go_menu = tk.Menu(self, tearoff=False) go_menu.add_command( label="Record List", command=self.callbacks['show_recordlist'], accelerator='Ctrl+L' ) go_menu.add_command( label="New Record", command=self.callbacks['new_record'], accelerator='Ctrl+N' ) self.add_cascade(label='Go', menu=go_menu) # The help menu help_menu = tk.Menu(self, tearoff=False) help_menu.add_command(label='About…', command=self.show_about) self.add_cascade(label='Help', menu=help_menu) def get_keybinds(self): return { '<Control-o>': self.callbacks['file->select'], '<Control-q>': self.callbacks['file->quit'], '<Control-n>': self.callbacks['new_record'], '<Control-l>': self.callbacks['show_recordlist'] } @staticmethod def _argstrip(function, *args): return function() def _bind_accelerators(self): keybinds = self.get_keybinds() for key, command in keybinds.items(): self.bind_all( key, partial(self._argstrip, command) ) def on_theme_change(self, *args): """Popup a message about theme changes""" message = "Change requires restart" detail = ( "Theme changes do not take effect" " until application restart") messagebox.showwarning( title='Warning', message=message, detail=detail) def show_about(self): """Show the about dialog""" about_message = 'ABQ Data Entry' about_detail = ( 'by Alan D Moore\n' 'For assistance please contact the author.' ) messagebox.showinfo( title='About', message=about_message, detail=about_detail ) class WindowsMainMenu(GenericMainMenu): """ Changes: - Windows uses file->exit instead of file->quit, and no accelerator is used. - Windows can handle commands on the menubar, so put 'Record List' / 'New Record' on the bar - Put 'options' under 'Tools' with separator """ def _build_menu(self): file_menu = tk.Menu(self, tearoff=False) file_menu.add_command( label="Select file…", command=self.callbacks['file->select'], accelerator='Ctrl+O' ) file_menu.add_separator() file_menu.add_command( label="Exit", command=self.callbacks['file->quit'] ) self.add_cascade(label='File', menu=file_menu) self.add_command( label="Record List", command=self.callbacks['show_recordlist'], accelerator='Ctrl+L' ) self.add_command( label="New Record", command=self.callbacks['new_record'], accelerator='Ctrl+N' ) # The Tools menu tools_menu = tk.Menu(self, tearoff=False) # The options menu options_menu = tk.Menu(tools_menu, tearoff=False) options_menu.add_checkbutton( label='Autofill Date', variable=self.settings['autofill date'] ) options_menu.add_checkbutton( label='Autofill Sheet data', variable=self.settings['autofill sheet data'] ) # font size submenu font_size_menu = tk.Menu(self, tearoff=False) for size in range(6, 17, 1): font_size_menu.add_radiobutton( label=size, value=size, variable=self.settings['font size'] ) options_menu.add_cascade(label='Font size', menu=font_size_menu) # themes menu style = ttk.Style() themes_menu = tk.Menu(self, tearoff=False) for theme in style.theme_names(): themes_menu.add_radiobutton( label=theme, value=theme, variable=self.settings['theme'] ) options_menu.add_cascade(label='Theme', menu=themes_menu) self.settings['theme'].trace('w', self.on_theme_change) tools_menu.add_separator() tools_menu.add_cascade(label='Options', menu=options_menu) self.add_cascade(label='Tools', menu=tools_menu) # The help menu help_menu = tk.Menu(self, tearoff=False) self.add_cascade(label='Help', menu=help_menu) def get_keybinds(self): return { '<Control-o>': self.callbacks['file->select'], '<Control-n>': self.callbacks['new_record'], '<Control-l>': self.callbacks['show_recordlist'] } class LinuxMainMenu(GenericMainMenu): """Differences for Linux: - Edit menu for autofill options - View menu for font & theme options """ def _build_menu(self): file_menu = tk.Menu(self, tearoff=False) file_menu.add_command( label="Select file…", command=self.callbacks['file->select'], accelerator='Ctrl+O' ) file_menu.add_separator() file_menu.add_command( label="Quit", command=self.callbacks['file->quit'], accelerator='Ctrl+Q' ) self.add_cascade(label='File', menu=file_menu) # The edit menu edit_menu = tk.Menu(self, tearoff=False) edit_menu.add_checkbutton( label='Autofill Date', variable=self.settings['autofill date'] ) edit_menu.add_checkbutton( label='Autofill Sheet data', variable=self.settings['autofill sheet data'] ) self.add_cascade(label='Edit', menu=edit_menu) # The View menu view_menu = tk.Menu(self, tearoff=False) # font size submenu font_size_menu = tk.Menu(view_menu, tearoff=False) for size in range(6, 17, 1): font_size_menu.add_radiobutton( label=size, value=size, variable=self.settings['font size'] ) view_menu.add_cascade(label='Font size', menu=font_size_menu) # themes menu style = ttk.Style() themes_menu = tk.Menu(view_menu, tearoff=False) for theme in style.theme_names(): themes_menu.add_radiobutton( label=theme, value=theme, variable=self.settings['theme'] ) view_menu.add_cascade(label='Theme', menu=themes_menu) self.settings['theme'].trace('w', self.on_theme_change) self.add_cascade(label='View', menu=view_menu) # switch from recordlist to recordform go_menu = tk.Menu(self, tearoff=False) go_menu.add_command( label="Record List", command=self.callbacks['show_recordlist'], accelerator='Ctrl+L' ) go_menu.add_command( label="New Record", command=self.callbacks['new_record'], accelerator='Ctrl+N' ) self.add_cascade(label='Go', menu=go_menu) # The help menu help_menu = tk.Menu(self, tearoff=False) help_menu.add_command(label='About…', command=self.show_about) self.add_cascade(label='Help', menu=help_menu) class MacOsMainMenu(GenericMainMenu): """ Differences for MacOS: - Create App Menu - Move about to app menu, remove 'help' - Remove redundant quit command - Change accelerators to Command-[] - Add View menu for font & theme options - Add Edit menu for autofill options - Add Window menu for navigation commands """ def _build_menu(self): app_menu = tk.Menu(self, tearoff=False, name='apple') app_menu.add_command( label='About ABQ Data Entry', command=self.show_about ) self.add_cascade(label='ABQ Data Entry', menu=app_menu) file_menu = tk.Menu(self, tearoff=False) file_menu.add_command( label="Select file…", command=self.callbacks['file->select'], accelerator="Cmd-O" ) self.add_cascade(label='File', menu=file_menu) edit_menu = tk.Menu(self, tearoff=False) edit_menu.add_checkbutton( label='Autofill Date', variable=self.settings['autofill date'] ) edit_menu.add_checkbutton( label='Autofill Sheet data', variable=self.settings['autofill sheet data'] ) self.add_cascade(label='Edit', menu=edit_menu) # View menu view_menu = tk.Menu(self, tearoff=False) # font size submenu font_size_menu = tk.Menu(view_menu, tearoff=False) for size in range(6, 17, 1): font_size_menu.add_radiobutton( label=size, value=size, variable=self.settings['font size'] ) view_menu.add_cascade(label='Font size', menu=font_size_menu) # themes menu style = ttk.Style() themes_menu = tk.Menu(view_menu, tearoff=False) for theme in style.theme_names(): themes_menu.add_radiobutton( label=theme, value=theme, variable=self.settings['theme'] ) view_menu.add_cascade(label='Theme', menu=themes_menu) self.settings['theme'].trace('w', self.on_theme_change) self.add_cascade(label='View', menu=view_menu) # Window Menu window_menu = tk.Menu(self, name='window', tearoff=False) window_menu.add_command( label="Record List", command=self.callbacks['show_recordlist'], accelerator="Cmd-L" ) window_menu.add_command( label="New Record", command=self.callbacks['new_record'], accelerator="Cmd-N" ) self.add_cascade(label='Window', menu=window_menu) def get_keybinds(self): return { '<Command-o>': self.callbacks['file->select'], '<Command-n>': self.callbacks['new_record'], '<Command-l>': self.callbacks['show_recordlist'] } def get_main_menu_for_os(os_name): menus = { 'Linux': LinuxMainMenu, 'Darwin': MacOsMainMenu, 'freebsd7': LinuxMainMenu, 'Windows': WindowsMainMenu } return menus.get(os_name, GenericMainMenu)