CallTipWindow.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. """A CallTip window class for Tkinter/IDLE.
  2. After ToolTip.py, which uses ideas gleaned from PySol
  3. Used by the CallTips IDLE extension.
  4. """
  5. from Tkinter import Toplevel, Label, LEFT, SOLID, TclError
  6. HIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-hide>>"
  7. HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>")
  8. CHECKHIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-checkhide>>"
  9. CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>")
  10. CHECKHIDE_TIME = 100 # miliseconds
  11. MARK_RIGHT = "calltipwindowregion_right"
  12. class CallTip:
  13. def __init__(self, widget):
  14. self.widget = widget
  15. self.tipwindow = self.label = None
  16. self.parenline = self.parencol = None
  17. self.lastline = None
  18. self.hideid = self.checkhideid = None
  19. self.checkhide_after_id = None
  20. def position_window(self):
  21. """Check if needs to reposition the window, and if so - do it."""
  22. curline = int(self.widget.index("insert").split('.')[0])
  23. if curline == self.lastline:
  24. return
  25. self.lastline = curline
  26. self.widget.see("insert")
  27. if curline == self.parenline:
  28. box = self.widget.bbox("%d.%d" % (self.parenline,
  29. self.parencol))
  30. else:
  31. box = self.widget.bbox("%d.0" % curline)
  32. if not box:
  33. box = list(self.widget.bbox("insert"))
  34. # align to left of window
  35. box[0] = 0
  36. box[2] = 0
  37. x = box[0] + self.widget.winfo_rootx() + 2
  38. y = box[1] + box[3] + self.widget.winfo_rooty()
  39. self.tipwindow.wm_geometry("+%d+%d" % (x, y))
  40. def showtip(self, text, parenleft, parenright):
  41. """Show the calltip, bind events which will close it and reposition it.
  42. """
  43. # Only called in CallTips, where lines are truncated
  44. self.text = text
  45. if self.tipwindow or not self.text:
  46. return
  47. self.widget.mark_set(MARK_RIGHT, parenright)
  48. self.parenline, self.parencol = map(
  49. int, self.widget.index(parenleft).split("."))
  50. self.tipwindow = tw = Toplevel(self.widget)
  51. self.position_window()
  52. # remove border on calltip window
  53. tw.wm_overrideredirect(1)
  54. try:
  55. # This command is only needed and available on Tk >= 8.4.0 for OSX
  56. # Without it, call tips intrude on the typing process by grabbing
  57. # the focus.
  58. tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w,
  59. "help", "noActivates")
  60. except TclError:
  61. pass
  62. self.label = Label(tw, text=self.text, justify=LEFT,
  63. background="#ffffe0", relief=SOLID, borderwidth=1,
  64. font = self.widget['font'])
  65. self.label.pack()
  66. tw.lift() # work around bug in Tk 8.5.18+ (issue #24570)
  67. self.checkhideid = self.widget.bind(CHECKHIDE_VIRTUAL_EVENT_NAME,
  68. self.checkhide_event)
  69. for seq in CHECKHIDE_SEQUENCES:
  70. self.widget.event_add(CHECKHIDE_VIRTUAL_EVENT_NAME, seq)
  71. self.widget.after(CHECKHIDE_TIME, self.checkhide_event)
  72. self.hideid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME,
  73. self.hide_event)
  74. for seq in HIDE_SEQUENCES:
  75. self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq)
  76. def checkhide_event(self, event=None):
  77. if not self.tipwindow:
  78. # If the event was triggered by the same event that unbinded
  79. # this function, the function will be called nevertheless,
  80. # so do nothing in this case.
  81. return
  82. curline, curcol = map(int, self.widget.index("insert").split('.'))
  83. if curline < self.parenline or \
  84. (curline == self.parenline and curcol <= self.parencol) or \
  85. self.widget.compare("insert", ">", MARK_RIGHT):
  86. self.hidetip()
  87. else:
  88. self.position_window()
  89. if self.checkhide_after_id is not None:
  90. self.widget.after_cancel(self.checkhide_after_id)
  91. self.checkhide_after_id = \
  92. self.widget.after(CHECKHIDE_TIME, self.checkhide_event)
  93. def hide_event(self, event):
  94. if not self.tipwindow:
  95. # See the explanation in checkhide_event.
  96. return
  97. self.hidetip()
  98. def hidetip(self):
  99. if not self.tipwindow:
  100. return
  101. for seq in CHECKHIDE_SEQUENCES:
  102. self.widget.event_delete(CHECKHIDE_VIRTUAL_EVENT_NAME, seq)
  103. self.widget.unbind(CHECKHIDE_VIRTUAL_EVENT_NAME, self.checkhideid)
  104. self.checkhideid = None
  105. for seq in HIDE_SEQUENCES:
  106. self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq)
  107. self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideid)
  108. self.hideid = None
  109. self.label.destroy()
  110. self.label = None
  111. self.tipwindow.destroy()
  112. self.tipwindow = None
  113. self.widget.mark_unset(MARK_RIGHT)
  114. self.parenline = self.parencol = self.lastline = None
  115. def is_active(self):
  116. return bool(self.tipwindow)
  117. def _calltip_window(parent): # htest #
  118. from Tkinter import Toplevel, Text, LEFT, BOTH
  119. top = Toplevel(parent)
  120. top.title("Test calltips")
  121. top.geometry("200x100+%d+%d" % (parent.winfo_rootx() + 200,
  122. parent.winfo_rooty() + 150))
  123. text = Text(top)
  124. text.pack(side=LEFT, fill=BOTH, expand=1)
  125. text.insert("insert", "string.split")
  126. top.update()
  127. calltip = CallTip(text)
  128. def calltip_show(event):
  129. calltip.showtip("(s=Hello world)", "insert", "end")
  130. def calltip_hide(event):
  131. calltip.hidetip()
  132. text.event_add("<<calltip-show>>", "(")
  133. text.event_add("<<calltip-hide>>", ")")
  134. text.bind("<<calltip-show>>", calltip_show)
  135. text.bind("<<calltip-hide>>", calltip_hide)
  136. text.focus_set()
  137. if __name__=='__main__':
  138. from idlelib.idle_test.htest import run
  139. run(_calltip_window)