calltip_w.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. """A call-tip window class for Tkinter/IDLE.
  2. After tooltip.py, which uses ideas gleaned from PySol.
  3. Used by calltip.py.
  4. """
  5. from tkinter import Label, LEFT, SOLID, TclError
  6. from idlelib.tooltip import TooltipBase
  7. HIDE_EVENT = "<<calltipwindow-hide>>"
  8. HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>")
  9. CHECKHIDE_EVENT = "<<calltipwindow-checkhide>>"
  10. CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>")
  11. CHECKHIDE_TIME = 100 # milliseconds
  12. MARK_RIGHT = "calltipwindowregion_right"
  13. class CalltipWindow(TooltipBase):
  14. """A call-tip widget for tkinter text widgets."""
  15. def __init__(self, text_widget):
  16. """Create a call-tip; shown by showtip().
  17. text_widget: a Text widget with code for which call-tips are desired
  18. """
  19. # Note: The Text widget will be accessible as self.anchor_widget
  20. super().__init__(text_widget)
  21. self.label = self.text = None
  22. self.parenline = self.parencol = self.lastline = None
  23. self.hideid = self.checkhideid = None
  24. self.checkhide_after_id = None
  25. def get_position(self):
  26. """Choose the position of the call-tip."""
  27. curline = int(self.anchor_widget.index("insert").split('.')[0])
  28. if curline == self.parenline:
  29. anchor_index = (self.parenline, self.parencol)
  30. else:
  31. anchor_index = (curline, 0)
  32. box = self.anchor_widget.bbox("%d.%d" % anchor_index)
  33. if not box:
  34. box = list(self.anchor_widget.bbox("insert"))
  35. # align to left of window
  36. box[0] = 0
  37. box[2] = 0
  38. return box[0] + 2, box[1] + box[3]
  39. def position_window(self):
  40. "Reposition the window if needed."
  41. curline = int(self.anchor_widget.index("insert").split('.')[0])
  42. if curline == self.lastline:
  43. return
  44. self.lastline = curline
  45. self.anchor_widget.see("insert")
  46. super().position_window()
  47. def showtip(self, text, parenleft, parenright):
  48. """Show the call-tip, bind events which will close it and reposition it.
  49. text: the text to display in the call-tip
  50. parenleft: index of the opening parenthesis in the text widget
  51. parenright: index of the closing parenthesis in the text widget,
  52. or the end of the line if there is no closing parenthesis
  53. """
  54. # Only called in calltip.Calltip, where lines are truncated
  55. self.text = text
  56. if self.tipwindow or not self.text:
  57. return
  58. self.anchor_widget.mark_set(MARK_RIGHT, parenright)
  59. self.parenline, self.parencol = map(
  60. int, self.anchor_widget.index(parenleft).split("."))
  61. super().showtip()
  62. self._bind_events()
  63. def showcontents(self):
  64. """Create the call-tip widget."""
  65. self.label = Label(self.tipwindow, text=self.text, justify=LEFT,
  66. background="#ffffd0", foreground="black",
  67. relief=SOLID, borderwidth=1,
  68. font=self.anchor_widget['font'])
  69. self.label.pack()
  70. def checkhide_event(self, event=None):
  71. """Handle CHECK_HIDE_EVENT: call hidetip or reschedule."""
  72. if not self.tipwindow:
  73. # If the event was triggered by the same event that unbound
  74. # this function, the function will be called nevertheless,
  75. # so do nothing in this case.
  76. return None
  77. # Hide the call-tip if the insertion cursor moves outside of the
  78. # parenthesis.
  79. curline, curcol = map(int, self.anchor_widget.index("insert").split('.'))
  80. if curline < self.parenline or \
  81. (curline == self.parenline and curcol <= self.parencol) or \
  82. self.anchor_widget.compare("insert", ">", MARK_RIGHT):
  83. self.hidetip()
  84. return "break"
  85. # Not hiding the call-tip.
  86. self.position_window()
  87. # Re-schedule this function to be called again in a short while.
  88. if self.checkhide_after_id is not None:
  89. self.anchor_widget.after_cancel(self.checkhide_after_id)
  90. self.checkhide_after_id = \
  91. self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event)
  92. return None
  93. def hide_event(self, event):
  94. """Handle HIDE_EVENT by calling hidetip."""
  95. if not self.tipwindow:
  96. # See the explanation in checkhide_event.
  97. return None
  98. self.hidetip()
  99. return "break"
  100. def hidetip(self):
  101. """Hide the call-tip."""
  102. if not self.tipwindow:
  103. return
  104. try:
  105. self.label.destroy()
  106. except TclError:
  107. pass
  108. self.label = None
  109. self.parenline = self.parencol = self.lastline = None
  110. try:
  111. self.anchor_widget.mark_unset(MARK_RIGHT)
  112. except TclError:
  113. pass
  114. try:
  115. self._unbind_events()
  116. except (TclError, ValueError):
  117. # ValueError may be raised by MultiCall
  118. pass
  119. super().hidetip()
  120. def _bind_events(self):
  121. """Bind event handlers."""
  122. self.checkhideid = self.anchor_widget.bind(CHECKHIDE_EVENT,
  123. self.checkhide_event)
  124. for seq in CHECKHIDE_SEQUENCES:
  125. self.anchor_widget.event_add(CHECKHIDE_EVENT, seq)
  126. self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event)
  127. self.hideid = self.anchor_widget.bind(HIDE_EVENT,
  128. self.hide_event)
  129. for seq in HIDE_SEQUENCES:
  130. self.anchor_widget.event_add(HIDE_EVENT, seq)
  131. def _unbind_events(self):
  132. """Unbind event handlers."""
  133. for seq in CHECKHIDE_SEQUENCES:
  134. self.anchor_widget.event_delete(CHECKHIDE_EVENT, seq)
  135. self.anchor_widget.unbind(CHECKHIDE_EVENT, self.checkhideid)
  136. self.checkhideid = None
  137. for seq in HIDE_SEQUENCES:
  138. self.anchor_widget.event_delete(HIDE_EVENT, seq)
  139. self.anchor_widget.unbind(HIDE_EVENT, self.hideid)
  140. self.hideid = None
  141. def _calltip_window(parent): # htest #
  142. from tkinter import Toplevel, Text, LEFT, BOTH
  143. top = Toplevel(parent)
  144. top.title("Test call-tips")
  145. x, y = map(int, parent.geometry().split('+')[1:])
  146. top.geometry("250x100+%d+%d" % (x + 175, y + 150))
  147. text = Text(top)
  148. text.pack(side=LEFT, fill=BOTH, expand=1)
  149. text.insert("insert", "string.split")
  150. top.update()
  151. calltip = CalltipWindow(text)
  152. def calltip_show(event):
  153. calltip.showtip("(s='Hello world')", "insert", "end")
  154. def calltip_hide(event):
  155. calltip.hidetip()
  156. text.event_add("<<calltip-show>>", "(")
  157. text.event_add("<<calltip-hide>>", ")")
  158. text.bind("<<calltip-show>>", calltip_show)
  159. text.bind("<<calltip-hide>>", calltip_hide)
  160. text.focus_set()
  161. if __name__ == '__main__':
  162. from unittest import main
  163. main('idlelib.idle_test.test_calltip_w', verbosity=2, exit=False)
  164. from idlelib.idle_test.htest import run
  165. run(_calltip_window)