Coverage for tooltip.py: 32%

92 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 13:22 -0700

1"""Tools for displaying tool-tips. 

2 

3This includes: 

4 * an abstract base-class for different kinds of tooltips 

5 * a simple text-only Tooltip class 

6""" 

7from tkinter import * 

8 

9 

10class TooltipBase: 

11 """abstract base class for tooltips""" 

12 

13 def __init__(self, anchor_widget): 

14 """Create a tooltip. 

15 

16 anchor_widget: the widget next to which the tooltip will be shown 

17 

18 Note that a widget will only be shown when showtip() is called. 

19 """ 

20 self.anchor_widget = anchor_widget 1b

21 self.tipwindow = None 1b

22 

23 def __del__(self): 

24 self.hidetip() 

25 

26 def showtip(self): 

27 """display the tooltip""" 

28 if self.tipwindow: 

29 return 

30 self.tipwindow = tw = Toplevel(self.anchor_widget) 

31 # show no border on the top level window 

32 tw.wm_overrideredirect(1) 

33 try: 

34 # This command is only needed and available on Tk >= 8.4.0 for OSX. 

35 # Without it, call tips intrude on the typing process by grabbing 

36 # the focus. 

37 tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w, 

38 "help", "noActivates") 

39 except TclError: 

40 pass 

41 

42 self.position_window() 

43 self.showcontents() 

44 self.tipwindow.update_idletasks() # Needed on MacOS -- see #34275. 

45 self.tipwindow.lift() # work around bug in Tk 8.5.18+ (issue #24570) 

46 

47 def position_window(self): 

48 """(re)-set the tooltip's screen position""" 

49 x, y = self.get_position() 

50 root_x = self.anchor_widget.winfo_rootx() + x 

51 root_y = self.anchor_widget.winfo_rooty() + y 

52 self.tipwindow.wm_geometry("+%d+%d" % (root_x, root_y)) 

53 

54 def get_position(self): 

55 """choose a screen position for the tooltip""" 

56 # The tip window must be completely outside the anchor widget; 

57 # otherwise when the mouse enters the tip window we get 

58 # a leave event and it disappears, and then we get an enter 

59 # event and it reappears, and so on forever :-( 

60 # 

61 # Note: This is a simplistic implementation; sub-classes will likely 

62 # want to override this. 

63 return 20, self.anchor_widget.winfo_height() + 1 

64 

65 def showcontents(self): 

66 """content display hook for sub-classes""" 

67 # See ToolTip for an example 

68 raise NotImplementedError 

69 

70 def hidetip(self): 

71 """hide the tooltip""" 

72 # Note: This is called by __del__, so careful when overriding/extending 

73 tw = self.tipwindow 

74 self.tipwindow = None 

75 if tw: 

76 try: 

77 tw.destroy() 

78 except TclError: # pragma: no cover 

79 pass 

80 

81 

82class OnHoverTooltipBase(TooltipBase): 

83 """abstract base class for tooltips, with delayed on-hover display""" 

84 

85 def __init__(self, anchor_widget, hover_delay=1000): 

86 """Create a tooltip with a mouse hover delay. 

87 

88 anchor_widget: the widget next to which the tooltip will be shown 

89 hover_delay: time to delay before showing the tooltip, in milliseconds 

90 

91 Note that a widget will only be shown when showtip() is called, 

92 e.g. after hovering over the anchor widget with the mouse for enough 

93 time. 

94 """ 

95 super(OnHoverTooltipBase, self).__init__(anchor_widget) 1b

96 self.hover_delay = hover_delay 1b

97 

98 self._after_id = None 1b

99 self._id1 = self.anchor_widget.bind("<Enter>", self._show_event) 1b

100 self._id2 = self.anchor_widget.bind("<Leave>", self._hide_event) 1b

101 self._id3 = self.anchor_widget.bind("<Button>", self._hide_event) 1b

102 

103 def __del__(self): 

104 try: 

105 self.anchor_widget.unbind("<Enter>", self._id1) 

106 self.anchor_widget.unbind("<Leave>", self._id2) # pragma: no cover 

107 self.anchor_widget.unbind("<Button>", self._id3) # pragma: no cover 

108 except TclError: 

109 pass 

110 super(OnHoverTooltipBase, self).__del__() 

111 

112 def _show_event(self, event=None): 

113 """event handler to display the tooltip""" 

114 if self.hover_delay: 

115 self.schedule() 

116 else: 

117 self.showtip() 

118 

119 def _hide_event(self, event=None): 

120 """event handler to hide the tooltip""" 

121 self.hidetip() 

122 

123 def schedule(self): 

124 """schedule the future display of the tooltip""" 

125 self.unschedule() 

126 self._after_id = self.anchor_widget.after(self.hover_delay, 

127 self.showtip) 

128 

129 def unschedule(self): 

130 """cancel the future display of the tooltip""" 

131 after_id = self._after_id 

132 self._after_id = None 

133 if after_id: 

134 self.anchor_widget.after_cancel(after_id) 

135 

136 def hidetip(self): 

137 """hide the tooltip""" 

138 try: 

139 self.unschedule() 

140 except TclError: # pragma: no cover 

141 pass 

142 super(OnHoverTooltipBase, self).hidetip() 

143 

144 

145class Hovertip(OnHoverTooltipBase): 

146 "A tooltip that pops up when a mouse hovers over an anchor widget." 

147 def __init__(self, anchor_widget, text, hover_delay=1000): 

148 """Create a text tooltip with a mouse hover delay. 

149 

150 anchor_widget: the widget next to which the tooltip will be shown 

151 hover_delay: time to delay before showing the tooltip, in milliseconds 

152 

153 Note that a widget will only be shown when showtip() is called, 

154 e.g. after hovering over the anchor widget with the mouse for enough 

155 time. 

156 """ 

157 super(Hovertip, self).__init__(anchor_widget, hover_delay=hover_delay) 1b

158 self.text = text 1b

159 

160 def showcontents(self): 

161 label = Label(self.tipwindow, text=self.text, justify=LEFT, 

162 background="#ffffe0", relief=SOLID, borderwidth=1) 

163 label.pack() 

164 

165 

166def _tooltip(parent): # htest # 

167 top = Toplevel(parent) 

168 top.title("Test tooltip") 

169 x, y = map(int, parent.geometry().split('+')[1:]) 

170 top.geometry("+%d+%d" % (x, y + 150)) 

171 label = Label(top, text="Place your mouse over buttons") 

172 label.pack() 

173 button1 = Button(top, text="Button 1 -- 1/2 second hover delay") 

174 button1.pack() 

175 Hovertip(button1, "This is tooltip text for button1.", hover_delay=500) 

176 button2 = Button(top, text="Button 2 -- no hover delay") 

177 button2.pack() 

178 Hovertip(button2, "This is tooltip\ntext for button2.", hover_delay=None) 

179 

180 

181if __name__ == '__main__': 181 ↛ 182line 181 didn't jump to line 182, because the condition on line 181 was never true

182 from unittest import main 

183 main('idlelib.idle_test.test_tooltip', verbosity=2, exit=False) 

184 

185 from idlelib.idle_test.htest import run 

186 run(_tooltip)