Skip to content

Commit 14304b9

Browse files
committed
fix(tooltips): adjust the position to so that the entire tooltip fits inside the parent window
Converted the tests to BDD
1 parent 6db4d17 commit 14304b9

2 files changed

Lines changed: 924 additions & 634 deletions

File tree

ardupilot_methodic_configurator/frontend_tkinter_show.py

Lines changed: 119 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,35 @@
1313
import tkinter as tk
1414
from platform import system as platform_system
1515
from tkinter import messagebox, ttk
16-
from typing import Optional
16+
from typing import Optional, cast
1717

1818
from ardupilot_methodic_configurator import _
1919

2020

21-
def show_error_message(title: str, message: str) -> None:
22-
root = tk.Tk(className="ArduPilotMethodicConfigurator")
23-
# Set the theme to 'alt'
24-
style = ttk.Style()
25-
style.theme_use("alt")
26-
root.withdraw() # Hide the main window
27-
messagebox.showerror(title, message)
28-
root.destroy()
29-
30-
31-
def show_warning_message(title: str, message: str) -> None:
32-
root = tk.Tk(className="ArduPilotMethodicConfigurator")
33-
# Set the theme to 'alt'
34-
style = ttk.Style()
35-
style.theme_use("alt")
36-
root.withdraw() # Hide the main window
37-
messagebox.showwarning(title, message)
38-
root.destroy()
21+
def show_error_message(title: str, message: str, root: Optional[tk.Tk] = None) -> None:
22+
if root is None:
23+
root = tk.Tk(className="ArduPilotMethodicConfigurator")
24+
# Set the theme to 'alt'
25+
style = ttk.Style()
26+
style.theme_use("alt")
27+
root.withdraw() # Hide the main window
28+
messagebox.showerror(title, message)
29+
root.destroy()
30+
else:
31+
messagebox.showerror(title, message)
32+
33+
34+
def show_warning_message(title: str, message: str, root: Optional[tk.Tk] = None) -> None:
35+
if root is None:
36+
root = tk.Tk(className="ArduPilotMethodicConfigurator")
37+
# Set the theme to 'alt'
38+
style = ttk.Style()
39+
style.theme_use("alt")
40+
root.withdraw() # Hide the main window
41+
messagebox.showwarning(title, message)
42+
root.destroy()
43+
else:
44+
messagebox.showwarning(title, message)
3945

4046

4147
def show_no_param_files_error(_dirname: str) -> None:
@@ -52,18 +58,57 @@ def show_no_connection_error(_error_string: str) -> None:
5258
show_error_message(_("No Connection to the Flight Controller"), error_message.format(**locals()))
5359

5460

61+
def calculate_tooltip_position( # noqa: PLR0913
62+
widget_x: int,
63+
widget_y: int,
64+
widget_width: int,
65+
widget_height: int,
66+
tooltip_width: int,
67+
tooltip_height: int,
68+
parent_x: int,
69+
parent_y: int,
70+
parent_width: int,
71+
parent_height: int,
72+
position_below: bool,
73+
) -> tuple[int, int]:
74+
"""Calculate the position of the tooltip to ensure it fits inside the parent window."""
75+
x = widget_x + min(widget_width // 2, 100)
76+
y = widget_y + (widget_height if position_below else -10)
77+
78+
# Adjust x position to keep tooltip inside parent window
79+
if x + tooltip_width > parent_x + parent_width:
80+
x = parent_x + parent_width - tooltip_width
81+
x = max(x, parent_x)
82+
83+
# Adjust y position to keep tooltip inside parent window
84+
if y + tooltip_height > parent_y + parent_height:
85+
y = parent_y + parent_height - tooltip_height
86+
y = max(y, parent_y)
87+
88+
return x, y
89+
90+
5591
class Tooltip:
5692
"""
5793
A tooltip class for displaying tooltips on widgets.
5894
5995
Creates a tooltip that appears when the mouse hovers over a widget and disappears when the mouse leaves the widget.
6096
"""
6197

62-
def __init__(self, widget: tk.Widget, text: str, position_below: bool = True, tag_name: str = "") -> None:
98+
def __init__(
99+
self,
100+
widget: tk.Widget,
101+
text: str,
102+
position_below: bool = True,
103+
tag_name: str = "",
104+
toplevel_class: Optional[type] = None,
105+
) -> None:
63106
self.widget: tk.Widget = widget
64107
self.text: str = text
65108
self.tooltip: Optional[tk.Toplevel] = None
66109
self.position_below: bool = position_below
110+
self.toplevel_class = toplevel_class or tk.Toplevel
111+
self.hide_timer: Optional[str] = None
67112

68113
# Bind the <Enter> and <Leave> events to show and hide the tooltip
69114
if platform_system() == "Darwin":
@@ -82,26 +127,36 @@ def __init__(self, widget: tk.Widget, text: str, position_below: bool = True, ta
82127
self.widget.bind("<Enter>", self.show, "+")
83128
self.widget.bind("<Leave>", self.hide, "+")
84129
# On non-macOS, create the tooltip immediately and show/hide it on events
85-
self.tooltip = tk.Toplevel(widget)
130+
self.tooltip = cast("tk.Toplevel", self.toplevel_class(widget))
86131
self.tooltip.wm_overrideredirect(boolean=True)
87132
tooltip_label = ttk.Label(
88133
self.tooltip, text=text, background="#ffffe0", relief="solid", borderwidth=1, justify=tk.LEFT
89134
)
90135
tooltip_label.pack()
91136
self.tooltip.withdraw() # Initially hide the tooltip
137+
# Bind to tooltip to prevent hiding when mouse is over it
138+
self.tooltip.bind("<Enter>", self._cancel_hide)
139+
self.tooltip.bind("<Leave>", self.hide)
92140

93141
def show(self, event: Optional[tk.Event] = None) -> None: # noqa: ARG002 # pylint: disable=unused-argument
94142
"""On non-macOS, tooltip already exists, show it on events."""
143+
self._cancel_hide()
95144
if self.tooltip:
96145
self.position_tooltip()
97146
self.tooltip.deiconify()
98147

148+
def _cancel_hide(self, event: Optional[tk.Event] = None) -> None: # noqa: ARG002 # pylint: disable=unused-argument
149+
"""Cancel the hide timer."""
150+
if self.hide_timer:
151+
self.widget.after_cancel(self.hide_timer)
152+
self.hide_timer = None
153+
99154
def create_show(self, event: Optional[tk.Event] = None) -> None: # noqa: ARG002 # pylint: disable=unused-argument
100155
"""On macOS, only create the tooltip when the mouse enters the widget."""
101156
if self.tooltip:
102157
return # Avoid redundant tooltip creation
103158

104-
self.tooltip = tk.Toplevel(self.widget)
159+
self.tooltip = cast("tk.Toplevel", self.toplevel_class(self.widget))
105160

106161
try:
107162
self.tooltip.tk.call(
@@ -121,21 +176,57 @@ def create_show(self, event: Optional[tk.Event] = None) -> None: # noqa: ARG002
121176
)
122177
tooltip_label.pack()
123178
self.position_tooltip()
179+
# Bind to tooltip to prevent hiding when mouse is over it
180+
self.tooltip.bind("<Enter>", self._cancel_hide)
181+
self.tooltip.bind("<Leave>", self.hide)
124182

125183
def position_tooltip(self) -> None:
126-
"""Calculate the position of the tooltip based on the widget's position."""
127-
x = self.widget.winfo_rootx() + min(self.widget.winfo_width() // 2, 100)
128-
y = self.widget.winfo_rooty() + (self.widget.winfo_height() if self.position_below else -10)
129-
if self.tooltip:
130-
self.tooltip.geometry(f"+{x}+{y}")
184+
"""Calculate the position of the tooltip based on the widget's position, ensuring it fits inside the parent window."""
185+
if not self.tooltip:
186+
return
187+
188+
# Ensure tooltip geometry is calculated
189+
self.tooltip.update_idletasks()
190+
tooltip_width = self.tooltip.winfo_reqwidth()
191+
tooltip_height = self.tooltip.winfo_reqheight()
192+
193+
# Get parent window dimensions
194+
parent = self.widget.winfo_toplevel()
195+
parent_x = parent.winfo_rootx()
196+
parent_y = parent.winfo_rooty()
197+
parent_width = parent.winfo_width()
198+
parent_height = parent.winfo_height()
199+
200+
x, y = calculate_tooltip_position(
201+
self.widget.winfo_rootx(),
202+
self.widget.winfo_rooty(),
203+
self.widget.winfo_width(),
204+
self.widget.winfo_height(),
205+
tooltip_width,
206+
tooltip_height,
207+
parent_x,
208+
parent_y,
209+
parent_width,
210+
parent_height,
211+
self.position_below,
212+
)
213+
214+
self.tooltip.geometry(f"+{x}+{y}")
131215

132216
def hide(self, event: Optional[tk.Event] = None) -> None: # noqa: ARG002 # pylint: disable=unused-argument
133-
"""On non-macOS, tooltip already exists, hide it on events."""
217+
"""Hide the tooltip after a delay on non-macOS."""
218+
self._cancel_hide()
219+
self.hide_timer = self.widget.after(10, self._do_hide)
220+
221+
def _do_hide(self) -> None:
222+
"""Actually hide or destroy the tooltip depending on platform."""
134223
if self.tooltip:
135224
self.tooltip.withdraw()
225+
self.hide_timer = None
136226

137227
def destroy_hide(self, event: Optional[tk.Event] = None) -> None: # noqa: ARG002 # pylint: disable=unused-argument
138228
"""On macOS, fully destroy the tooltip when the mouse leaves the widget."""
229+
self._cancel_hide()
139230
if self.tooltip:
140231
self.tooltip.destroy()
141232
self.tooltip = None

0 commit comments

Comments
 (0)