CustomTkinter icon indicating copy to clipboard operation
CustomTkinter copied to clipboard

Request: Messageboxes and Menu

Open BenjaminTGa opened this issue 3 years ago • 5 comments

Hi, I've got a suggestion for a new feature to CustomTkinter: adding messageboxes! There is no such feature available in CustomTkinter, so I tried using Tkinter messageboxes. They don't fit with the styling of CustomTkinter, so I was wondering if you could make that an available feature. Also, adding a menu option (like the Tkinter Menu option), would be nice. Thanks, Ben

BenjaminTGa avatar Nov 26 '22 18:11 BenjaminTGa

Note: On Windows, regular tk menus work fine:

image

until you use Dark mode

image

And if you want to get fancy, I'm happy to contribute the following code for allowing you to define menus in yaml...

menu_defn_yaml = """
- File:
  - !item  [ Open New Workspace,        on_new_workspace                    ]
  - !sep
  - !item  [ Reload Config,             on_reload_config                    ]
  - !sep
  - !item  [ Exit,                      on_closing                          ]

- View:
  - !check [ Show Debug Logging,        show_debug_logging                  ]
  - !sep
  - !item  [ Open Logs Folder,          on_open_logs_folder                 ]
  - !sep
  - !radio [ Theme,                     var_theme,
            [ "System", "Light", "Dark" ]
    ]

- Help:
  - !item  [ About,                     on_about,                           ]
  - !item  [ Support,                   on_support,                         ]
  - !item  [ Report a Bug,              on_bug_report,                      ]
"""


class MenuItem:
    """ Base type for something we can 'add' to the menu system. """

    name: str    # How the item appears in the menu
    func: str      # Name of the app method to call.
    shortcut: typing.Optional[str]  # not implemented

    def __init__(self, name, func, shortcut) -> None:
        self.name = name
        self.func = func
        self.shortcut = shortcut

    def _missing_command(self, app):
        print(f"** {app.__class__.__name__} missing command: {self.func}")

    def command(self, app):
        return getattr(app, self.func, lambda: self._missing_command(app) )

    def add(self, menubar, app):
        menubar.add_command(label=self.name, command=self.command(app))


class MenuToggle(MenuItem):
    shortcut: typing.Optional[str]
    check: bool
    var_name: str

    def __init__(self, name, func, shortcut) -> None:
        var_name = func.replace("on_change_", "")
        super().__init__(name, f"on_change_{var_name}", shortcut=shortcut)
        self.var_name = var_name

    def add(self, menubar, app):
        var = tk.BooleanVar()
        setattr(app, self.var_name, var)
        menubar.add_checkbutton(label=self.name, variable=var, command=self.command(app))

class MenuRadio(MenuItem):
    def __init__(self, name, var_name, items) -> None:
        super().__init__(name, f"on_change_{var_name}", shortcut=None)
        self.var_name = var_name
        self.items = items

    def add(self, menubar, app):
        submenu = tk.Menu(menubar, tearoff=0)
        var = tk.StringVar(value=self.items[0])
        setattr(app, self.var_name, var)
        for item in self.items:
            submenu.add_radiobutton(label=item, variable=var, value=item, command=self.command(app))
        menubar.add_cascade(label=self.name, menu=submenu)

class MenuSeparator:
    def add(self, menubar, _):
        menubar.add_separator()

def yaml_menu_sep(loader, node):
    """ Helper: !sep: Inject a separator into the menu. """
    return MenuSeparator()

def yaml_menu_item(loader, node):
    """ Helper: !item: Inject a new item into the menu. """
    seq = loader.construct_sequence(node)
    shortcut = seq[2] if len(seq) > 2 and seq[2] else None
    return MenuItem(name=seq[0], func=seq[1], shortcut=shortcut)

def yaml_menu_check(loader, node):
    """ Helper: !check: Inject a toggle (checkbox) item into the menu. """
    seq = loader.construct_sequence(node)
    shortcut = seq[2] if len(seq) > 2 and seq[2] else None
    return MenuToggle(name=seq[0], func=seq[1], shortcut=shortcut)

def yaml_menu_radio(loader, node):
    """ Helper: !radio: Inject a radio menu. """
    return MenuRadio(*loader.construct_sequence(node))

yaml.add_constructor("!sep", yaml_menu_sep)
yaml.add_constructor("!item", yaml_menu_item)
yaml.add_constructor("!check", yaml_menu_check)
yaml.add_constructor("!radio", yaml_menu_radio)

# Code in the menubar type that adds the constructed data:
# takes a 'menu' which can be a menu bar or a nested
# menu item.

class MenuHolder(tk.Menu):
    # ... __init__ etc.

    def add_menu_items(self, menu, menu_level):
        if isinstance(menu_level, dict):
            for menu_name, menu_items in menu_level.items():
                submenu= tk.Menu(menubar, tearoff=False)
                self.add_menu_items(submenu, menu_items)
                menu.add_cascade(label=menu_name, menu=submenu, underline=0)
        elif isinstance(menu_level, list):
            for item in menu_level:
                self.add_menu_items(menu, item)
        else:
            menu_level.add(menu, self)

    def populate(self, yaml_data):
        menu_defn = yaml.load(yaml_data, Loader=yaml.Loader)

        self.menubar = tk.Menu()
        self.config(menu=self.menubar)

        for menu_level in MENUS:
            self.add_menu_items(self.menubar, menu_level)

class App:
    def __init__(self):
        ...
        self.menubar = MenuHolder(...)
        m.poupulate(menu_defn_yaml)

image

kfsone avatar Dec 01 '22 00:12 kfsone

Nicely done!

Thanks for contributing the code. Any ideas about the messageboxes? Thanks!

BenjaminTGa avatar Dec 01 '22 17:12 BenjaminTGa

@BenjaminTGa

Any ideas about the messageboxes?

Here is a messagebox for customtkinter: https://github.com/Akascape/CTkMessagebox

Akascape avatar May 21 '23 09:05 Akascape

@Akascape Love it! Can it show modal messageboxes? Like, the ones that show above the main window and don't let the user interact with it until they've closed the dialog?

BenjaminTGa avatar May 21 '23 14:05 BenjaminTGa

@Akascape I try everything to make the CTkMessageBox modal or dialog.. thinking topmost=True or topmost=False would work. But sadly neither work

csduke19 avatar Jul 30 '24 22:07 csduke19