CallTipWindow.py 5.79 KB
Newer Older
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
1
"""A CallTip window class for Tkinter/IDLE.
David Scherer's avatar
David Scherer committed
2

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
3 4 5 6
After ToolTip.py, which uses ideas gleaned from PySol
Used by the CallTips IDLE extension.

"""
7
from tkinter import *
David Scherer's avatar
David Scherer committed
8

9
HIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-hide>>"
10 11 12 13 14 15 16
HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>")
CHECKHIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-checkhide>>"
CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>")
CHECKHIDE_TIME = 100 # miliseconds

MARK_RIGHT = "calltipwindowregion_right"

David Scherer's avatar
David Scherer committed
17 18 19 20
class CallTip:

    def __init__(self, widget):
        self.widget = widget
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
        self.tipwindow = self.label = None
        self.parenline = self.parencol = None
        self.lastline = None
        self.hideid = self.checkhideid = None

    def position_window(self):
        """Check if needs to reposition the window, and if so - do it."""
        curline = int(self.widget.index("insert").split('.')[0])
        if curline == self.lastline:
            return
        self.lastline = curline
        self.widget.see("insert")
        if curline == self.parenline:
            box = self.widget.bbox("%d.%d" % (self.parenline,
                                              self.parencol))
        else:
            box = self.widget.bbox("%d.0" % curline)
        if not box:
            box = list(self.widget.bbox("insert"))
            # align to left of window
            box[0] = 0
            box[2] = 0
        x = box[0] + self.widget.winfo_rootx() + 2
        y = box[1] + box[3] + self.widget.winfo_rooty()
        self.tipwindow.wm_geometry("+%d+%d" % (x, y))
David Scherer's avatar
David Scherer committed
46

47 48 49
    def showtip(self, text, parenleft, parenright):
        """Show the calltip, bind events which will close it and reposition it.
        """
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
50
        # truncate overly long calltip
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
51
        if len(text) >= 79:
52 53 54 55 56
            textlines = text.splitlines()
            for i, line in enumerate(textlines):
                if len(line) > 79:
                    textlines[i] = line[:75] + ' ...'
            text = '\n'.join(textlines)
David Scherer's avatar
David Scherer committed
57 58 59
        self.text = text
        if self.tipwindow or not self.text:
            return
60 61 62 63 64

        self.widget.mark_set(MARK_RIGHT, parenright)
        self.parenline, self.parencol = map(
            int, self.widget.index(parenleft).split("."))

David Scherer's avatar
David Scherer committed
65
        self.tipwindow = tw = Toplevel(self.widget)
66
        self.position_window()
67
        # remove border on calltip window
David Scherer's avatar
David Scherer committed
68
        tw.wm_overrideredirect(1)
69 70 71 72
        try:
            # This command is only needed and available on Tk >= 8.4.0 for OSX
            # Without it, call tips intrude on the typing process by grabbing
            # the focus.
73
            tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w,
74 75 76
                       "help", "noActivates")
        except TclError:
            pass
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
        self.label = Label(tw, text=self.text, justify=LEFT,
                           background="#ffffe0", relief=SOLID, borderwidth=1,
                           font = self.widget['font'])
        self.label.pack()

        self.checkhideid = self.widget.bind(CHECKHIDE_VIRTUAL_EVENT_NAME,
                                            self.checkhide_event)
        for seq in CHECKHIDE_SEQUENCES:
            self.widget.event_add(CHECKHIDE_VIRTUAL_EVENT_NAME, seq)
        self.widget.after(CHECKHIDE_TIME, self.checkhide_event)
        self.hideid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME,
                                       self.hide_event)
        for seq in HIDE_SEQUENCES:
            self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq)

    def checkhide_event(self, event=None):
        if not self.tipwindow:
            # If the event was triggered by the same event that unbinded
            # this function, the function will be called nevertheless,
            # so do nothing in this case.
            return
        curline, curcol = map(int, self.widget.index("insert").split('.'))
        if curline < self.parenline or \
           (curline == self.parenline and curcol <= self.parencol) or \
           self.widget.compare("insert", ">", MARK_RIGHT):
            self.hidetip()
        else:
            self.position_window()
            self.widget.after(CHECKHIDE_TIME, self.checkhide_event)

    def hide_event(self, event):
        if not self.tipwindow:
            # See the explanation in checkhide_event.
            return
        self.hidetip()
112

David Scherer's avatar
David Scherer committed
113
    def hidetip(self):
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
        if not self.tipwindow:
            return

        for seq in CHECKHIDE_SEQUENCES:
            self.widget.event_delete(CHECKHIDE_VIRTUAL_EVENT_NAME, seq)
        self.widget.unbind(CHECKHIDE_VIRTUAL_EVENT_NAME, self.checkhideid)
        self.checkhideid = None
        for seq in HIDE_SEQUENCES:
            self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq)
        self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideid)
        self.hideid = None

        self.label.destroy()
        self.label = None
        self.tipwindow.destroy()
David Scherer's avatar
David Scherer committed
129
        self.tipwindow = None
130 131 132 133 134 135 136

        self.widget.mark_unset(MARK_RIGHT)
        self.parenline = self.parencol = self.lastline = None

    def is_active(self):
        return bool(self.tipwindow)

David Scherer's avatar
David Scherer committed
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155


###############################
#
# Test Code
#
class container: # Conceptually an editor_window
    def __init__(self):
        root = Tk()
        text = self.text = Text(root)
        text.pack(side=LEFT, fill=BOTH, expand=1)
        text.insert("insert", "string.split")
        root.update()
        self.calltip = CallTip(text)

        text.event_add("<<calltip-show>>", "(")
        text.event_add("<<calltip-hide>>", ")")
        text.bind("<<calltip-show>>", self.calltip_show)
        text.bind("<<calltip-hide>>", self.calltip_hide)
156

David Scherer's avatar
David Scherer committed
157
        text.focus_set()
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
158
        root.mainloop()
David Scherer's avatar
David Scherer committed
159 160 161 162 163 164 165 166 167 168 169 170 171

    def calltip_show(self, event):
        self.calltip.showtip("Hello world")

    def calltip_hide(self, event):
        self.calltip.hidetip()

def main():
    # Test code
    c=container()

if __name__=='__main__':
    main()