1313import tkinter as tk
1414from platform import system as platform_system
1515from tkinter import messagebox , ttk
16- from typing import Optional
16+ from typing import Optional , cast
1717
1818from 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
4147def 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+
5591class 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