Skip to content

Commit

Permalink
Merge pull request #6 from alexmoglia/contextmenu_better_open_close
Browse files Browse the repository at this point in the history
calc_menu_position fixes; delay and check to open submenus
  • Loading branch information
alexmoglia authored Mar 21, 2024
2 parents ca06e0a + 64c7146 commit ded63f1
Showing 1 changed file with 82 additions and 44 deletions.
126 changes: 82 additions & 44 deletions src/SlickCTk/slick_context_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"""

import tkinter as tk
from typing import Callable, Any
from typing import Callable
from customtkinter import CTkFrame, CTk
from SlickCTk.slick_buttons import ContextMenuButton, SubmenuButton
from SlickCTk.utilities.dpi_scaler import DPIScaler
Expand All @@ -40,7 +40,7 @@
MENU_CORNER_RADIUS_ROUNDED,
)

PRINT_DEBUG: bool = False
PRINT_DEBUG: bool = True


class SlickContextMenu(CTkFrame):
Expand All @@ -59,8 +59,8 @@ def __init__(
**kwargs,
)

self.root: Any = parent
self.dpi_scaler = DPIScaler()
self.root: CTk = parent
self.dpi_scaler: DPIScaler = DPIScaler()
self.len_menu_choices: int = len(menu_choices)

self.menu_subframe = _ContextMenuSubframe(self, self.root, menu_choices)
Expand Down Expand Up @@ -146,79 +146,94 @@ def __print_debug() -> None:
print("==================================")
print(f"Window height: {window_height}")
print(f"Window width: {window_width}")
print(f"Context-menu height: {menu_height}")
print(f"Context-menu width: {menu_width}")
print(f"Normalized x, y: {normalized_x, normalized_y}")
print(f"Normalized x end: {normalized_x_end}")
print(f"Normalized y end: {normalized_y_end}")
print(f"Normalized x spill: {normalized_x_spill}")
print(f"Normalized y spill: {normalized_y_spill}")
print(f"Menu height: {menu_height}")
print(f"Menu width: {menu_width}")
print(f"Screen x, y: {screen_x, screen_y}")
print(f"Scren x, y end: {screen_x_end, screen_y_end}")
print(f"DPI Normalized x, y: {normalized_x, normalized_y}")
print(f"Final x, y: {x, y}")

# INITIAL VALUES
submenu_shift: int = 4

scale_factor: float = self.dpi_scaler.get_scale_factor()

window_height: int = self.root.winfo_height()
window_width: int = self.root.winfo_width()

menu_height: int = self.winfo_height()
menu_width: int = self.winfo_width()
menu_height: int = self.winfo_reqheight()
menu_width: int = self.winfo_reqwidth()

root_x: int = self.root.winfo_rootx()
root_y: int = self.root.winfo_rooty()

normalized_x: float = (x_in - root_x) / scale_factor
normalized_y: float = (y_in - root_y) / scale_factor
# SCREEN ADJUST (Adjust x,y for window size and placement)
screen_x: float = x_in - root_x
screen_y: float = y_in - root_y

normalized_x_end: float = (normalized_x * scale_factor) + menu_width
normalized_y_end: float = (normalized_y * scale_factor) + menu_height
screen_x_end: float = screen_x + menu_width
screen_y_end: float = screen_y + menu_height

normalized_x_spill: float = normalized_x_end - window_width
normalized_y_spill: float = normalized_y_end - window_height
# DPI ADJUST (adjust x,y for monitor dpi/scaling)
normalized_x: float = screen_x / scale_factor
normalized_y: float = screen_y / scale_factor

x: float = normalized_x
y: float = normalized_y

if normalized_y_end > window_height:
# Adjust the place(x,y) coords if the menu will spill over the edge of the
# root window. Adjusting by normalized_spill will open the menu as close to
# the event position, and the edge of the menu will be up against the edge of
# the window. Adjusting by menu_height will invert the menu so that it opens
# upwards from the point of origin (or left instead of right, in the case of
# x). The order of menu items are not changed in either case.
# WINDOW-EDGE ADJUST (Adjust x,y if menu would spill off screen)
if screen_y_end > window_height:
# Adjusting by screen_spill will open the menu as close as possible to the
# original y position, so that the bottom edge of the menu will be touching
# the edge of the app window. Adjusting by menu_height will invert the menu
# so that it opens upwards instead of downwards from the original y position.
# The order of menu items is not changed in either case.

screen_y_spill: float = screen_y_end - window_height
y = y - (screen_y_spill / scale_factor)
# y = y - (menu_height / scale_factor) # ALTERNATE METHOD

if screen_x_end > window_width:
# The original x position is adjusted by the menu's width to effectively flip
# the menu on the y axis, so that it opens to the left of the x position
# instead of right. If the menu being opened is a submenu, it's menu_depth
# will have the int value 2, which we use to move the submenu's x position by
# twice the menu's width (so as to prevent it from opening on top of the main
# menu). We also add or subtract a small amount from x so as to let the
# submenu slightly overlap with the main menu.

# screen_x_spill: float = screen_x_end - window_width # NOT USED

y = y - (normalized_y_spill / scale_factor)
# y = y - (menu_height / scale_factor)

if normalized_x_end > window_width:
if menu_depth == 1:
x = x - (menu_width * menu_depth / scale_factor)
# x = x - (normalized_x_spill / scale_factor)
# x = x - (normalized_x_spill / scale_factor) # ALTERNATE METHOD

elif menu_depth > 1:
x = x - (menu_width * menu_depth / scale_factor) + submenu_shift
else:
if menu_depth > 1:
x = x - submenu_shift

if PRINT_DEBUG:
__print_debug()
__print_debug()

return x, y

def open_menu(self, x: float, y: float) -> None:
"""Open the SlickContextMenu at the passed coords, and give focus to menu"""
self.after(200, lambda: self.place(x=x, y=y))
self.place(x=x, y=y)
self.focus_set()

def configure_window(self) -> None:
self.dpi_scaler.get_dpi_current_monitor()

def close_menu(self) -> None:
"""Close the SlickContextMenu"""
self.close_all_submenus()
self.place_forget()

def close_all_submenus(self) -> None:
for submenu in self.descendant_submenus:
submenu.close_menu()
self.place_forget()

def check_should_menu_close(self) -> None:
"""Check if menu should close. Mouse must be hovering over menu or
Expand Down Expand Up @@ -269,8 +284,8 @@ def __init__(
) -> None:
super().__init__(parent, fg_color="transparent", corner_radius=0)

self.parent: SlickContextMenu = parent
self.root: CTk = root
self.parent: SlickContextMenu = parent
self.submenu_is_open = False
self.process_menu_choices(menu_choices)

Expand Down Expand Up @@ -302,18 +317,42 @@ def add_submenu(self, button: SubmenuButton, menu_subitems: dict) -> None:
"""Create submenu and add hover bindings"""

submenu = SlickContextMenu(self.root, menu_subitems)
submenu.bind("<Leave>", lambda e: self.delay_submenu_close(submenu, button))
submenu.bind(
"<Leave>", lambda e: self.delay_check_submenu_should_close(submenu, button)
)

button.bind("<Enter>", lambda e: self.submenu_button_hover_in(submenu, button))
button.bind("<Leave>", lambda e: self.delay_submenu_close(submenu, button))
button.bind(
"<Leave>", lambda e: self.delay_check_submenu_should_close(submenu, button)
)

def submenu_button_hover_in(
self, submenu: SlickContextMenu, button: SubmenuButton
) -> None:
"""Hover controls for submenu-buttons"""
if not submenu.winfo_ismapped():
x, y = self.calc_submenu_position(submenu, button)
submenu.open_menu(x, y)
"""Open submenu when submenu-button hovered if not already open."""
self.delay_check_submenu_should_open(submenu, button)
# self.parent

def delay_check_submenu_should_open(
self, submenu: SlickContextMenu, button: SubmenuButton
):
self.after(250, lambda: self.check_submenu_should_open(submenu, button))

def check_submenu_should_open(
self, submenu: SlickContextMenu, button: SubmenuButton
):
if (
self.parent.winfo_ismapped()
and not submenu.winfo_ismapped()
and self.is_submenu_button_hovered(button)
):
self.open_submenu(submenu, button)

def open_submenu(self, submenu: SlickContextMenu, button: SubmenuButton) -> None:
"""Calculate x,y for submenu and open it."""
x, y = self.calc_submenu_position(submenu, button)
submenu.open_menu(x, y)
self.delay_check_submenu_should_close(submenu, button)

def calc_submenu_position(
self, submenu: SlickContextMenu, button: SubmenuButton
Expand Down Expand Up @@ -350,7 +389,7 @@ def __print_debug() -> None:
shift_submenu_x, shift_submenu_y, menu_depth=2
)

def delay_submenu_close(
def delay_check_submenu_should_close(
self, submenu: SlickContextMenu, button: SubmenuButton
) -> None:
self.after(500, lambda: self.check_submenu_should_close(submenu, button))
Expand Down Expand Up @@ -379,7 +418,6 @@ def is_submenu_hovered(self, submenu: SlickContextMenu) -> bool:
return ".!slickcontextmenu2" in ".!slickcontextmenu2.!_contextmenusubframe
.!contextmenubutton"
"""

hovered_widget: str = self.get_widget_at_mouse().winfo_parent()
return submenu.winfo_name() in hovered_widget

Expand Down

0 comments on commit ded63f1

Please # to comment.