393 lines · 16.4 KB · Python
RawDownload
"""An auto-completion window for IDLE, used by the AutoComplete extension"""from Tkinter import *from MultiCall import MC_SHIFTimport AutoComplete HIDE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-hide>>"HIDE_SEQUENCES = ("<FocusOut>", "<ButtonPress>")KEYPRESS_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keypress>>"# We need to bind event beyond <Key> so that the function will be called# before the default specific IDLE functionKEYPRESS_SEQUENCES = ("<Key>", "<Key-BackSpace>", "<Key-Return>",                      "<Key-Up>", "<Key-Down>", "<Key-Home>", "<Key-End>")KEYRELEASE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keyrelease>>"KEYRELEASE_SEQUENCE = "<KeyRelease>"LISTUPDATE_SEQUENCE = "<ButtonRelease>"WINCONFIG_SEQUENCE = "<Configure>"DOUBLECLICK_SEQUENCE = "<Double-ButtonRelease>" class AutoCompleteWindow:     def __init__(self, widget):        # The widget (Text) on which we place the AutoCompleteWindow        self.widget = widget        # The widgets we create        self.autocompletewindow = self.listbox = self.scrollbar = None        # The default foreground and background of a selection. Saved because        # they are changed to the regular colors of list items when the        # completion start is not a prefix of the selected completion        self.origselforeground = self.origselbackground = None        # The list of completions        self.completions = None        # A list with more completions, or None        self.morecompletions = None        # The completion mode. Either AutoComplete.COMPLETE_ATTRIBUTES or        # AutoComplete.COMPLETE_FILES        self.mode = None        # The current completion start, on the text box (a string)        self.start = None        # The index of the start of the completion        self.startindex = None        # The last typed start, used so that when the selection changes,        # the new start will be as close as possible to the last typed one.        self.lasttypedstart = None        # Do we have an indication that the user wants the completion window        # (for example, he clicked the list)        self.userwantswindow = None        # event ids        self.hideid = self.keypressid = self.listupdateid = self.winconfigid \        = self.keyreleaseid = self.doubleclickid                         = None     def _change_start(self, newstart):        i = 0        while i < len(self.start) and i < len(newstart) and \              self.start[i] == newstart[i]:            i += 1        if i < len(self.start):            self.widget.delete("%s+%dc" % (self.startindex, i),                               "%s+%dc" % (self.startindex, len(self.start)))        if i < len(newstart):            self.widget.insert("%s+%dc" % (self.startindex, i),                               newstart[i:])        self.start = newstart     def _binary_search(self, s):        """Find the first index in self.completions where completions[i] is        greater or equal to s, or the last index if there is no such        one."""        i = 0; j = len(self.completions)        while j > i:            m = (i + j) // 2            if self.completions[m] >= s:                j = m            else:                i = m + 1        return min(i, len(self.completions)-1)     def _complete_string(self, s):        """Assuming that s is the prefix of a string in self.completions,        return the longest string which is a prefix of all the strings which        s is a prefix of them. If s is not a prefix of a string, return s."""        first = self._binary_search(s)        if self.completions[first][:len(s)] != s:            # There is not even one completion which s is a prefix of.            return s        # Find the end of the range of completions where s is a prefix of.        i = first + 1        j = len(self.completions)        while j > i:            m = (i + j) // 2            if self.completions[m][:len(s)] != s:                j = m            else:                i = m + 1        last = i-1         # We should return the maximum prefix of first and last        i = len(s)        while len(self.completions[first]) > i and \              len(self.completions[last]) > i and \              self.completions[first][i] == self.completions[last][i]:            i += 1        return self.completions[first][:i]     def _selection_changed(self):        """Should be called when the selection of the Listbox has changed.        Updates the Listbox display and calls _change_start."""        cursel = int(self.listbox.curselection()[0])         self.listbox.see(cursel)         lts = self.lasttypedstart        selstart = self.completions[cursel]        if self._binary_search(lts) == cursel:            newstart = lts        else:            i = 0            while i < len(lts) and i < len(selstart) and lts[i] == selstart[i]:                i += 1            while cursel > 0 and selstart[:i] <= self.completions[cursel-1]:                i += 1            newstart = selstart[:i]        self._change_start(newstart)         if self.completions[cursel][:len(self.start)] == self.start:            # start is a prefix of the selected completion            self.listbox.configure(selectbackground=self.origselbackground,                                   selectforeground=self.origselforeground)        else:            self.listbox.configure(selectbackground=self.listbox.cget("bg"),                                   selectforeground=self.listbox.cget("fg"))            # If there are more completions, show them, and call me again.            if self.morecompletions:                self.completions = self.morecompletions                self.morecompletions = None                self.listbox.delete(0, END)                for item in self.completions:                    self.listbox.insert(END, item)                self.listbox.select_set(self._binary_search(self.start))                self._selection_changed()     def show_window(self, comp_lists, index, complete, mode, userWantsWin):        """Show the autocomplete list, bind events.        If complete is True, complete the text, and if there is exactly one        matching completion, don't open a list."""        # Handle the start we already have        self.completions, self.morecompletions = comp_lists        self.mode = mode        self.startindex = self.widget.index(index)        self.start = self.widget.get(self.startindex, "insert")        if complete:            completed = self._complete_string(self.start)            self._change_start(completed)            i = self._binary_search(completed)            if self.completions[i] == completed and \               (i == len(self.completions)-1 or                self.completions[i+1][:len(completed)] != completed):                # There is exactly one matching completion                return        self.userwantswindow = userWantsWin        self.lasttypedstart = self.start         # Put widgets in place        self.autocompletewindow = acw = Toplevel(self.widget)        # Put it in a position so that it is not seen.        acw.wm_geometry("+10000+10000")        # Make it float        acw.wm_overrideredirect(1)        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.            acw.tk.call("::tk::unsupported::MacWindowStyle", "style", acw._w,                        "help", "noActivates")        except TclError:            pass        self.scrollbar = scrollbar = Scrollbar(acw, orient=VERTICAL)        self.listbox = listbox = Listbox(acw, yscrollcommand=scrollbar.set,                                         exportselection=False, bg="white")        for item in self.completions:            listbox.insert(END, item)        self.origselforeground = listbox.cget("selectforeground")        self.origselbackground = listbox.cget("selectbackground")        scrollbar.config(command=listbox.yview)        scrollbar.pack(side=RIGHT, fill=Y)        listbox.pack(side=LEFT, fill=BOTH, expand=True)         # Initialize the listbox selection        self.listbox.select_set(self._binary_search(self.start))        self._selection_changed()         # bind events        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)        self.keypressid = self.widget.bind(KEYPRESS_VIRTUAL_EVENT_NAME,                                           self.keypress_event)        for seq in KEYPRESS_SEQUENCES:            self.widget.event_add(KEYPRESS_VIRTUAL_EVENT_NAME, seq)        self.keyreleaseid = self.widget.bind(KEYRELEASE_VIRTUAL_EVENT_NAME,                                             self.keyrelease_event)        self.widget.event_add(KEYRELEASE_VIRTUAL_EVENT_NAME,KEYRELEASE_SEQUENCE)        self.listupdateid = listbox.bind(LISTUPDATE_SEQUENCE,                                         self.listupdate_event)        self.winconfigid = acw.bind(WINCONFIG_SEQUENCE, self.winconfig_event)        self.doubleclickid = listbox.bind(DOUBLECLICK_SEQUENCE,                                          self.doubleclick_event)     def winconfig_event(self, event):        if not self.is_active():            return        # Position the completion list window        acw = self.autocompletewindow        self.widget.see(self.startindex)        x, y, cx, cy = self.widget.bbox(self.startindex)        acw.wm_geometry("+%d+%d" % (x + self.widget.winfo_rootx(),                                    y + self.widget.winfo_rooty() \                                    -acw.winfo_height()))      def hide_event(self, event):        if not self.is_active():            return        self.hide_window()     def listupdate_event(self, event):        if not self.is_active():            return        self.userwantswindow = True        self._selection_changed()     def doubleclick_event(self, event):        # Put the selected completion in the text, and close the list        cursel = int(self.listbox.curselection()[0])        self._change_start(self.completions[cursel])        self.hide_window()     def keypress_event(self, event):        if not self.is_active():            return        keysym = event.keysym        if hasattr(event, "mc_state"):            state = event.mc_state        else:            state = 0         if (len(keysym) == 1 or keysym in ("underscore", "BackSpace")            or (self.mode==AutoComplete.COMPLETE_FILES and keysym in                ("period", "minus"))) \           and not (state & ~MC_SHIFT):            # Normal editing of text            if len(keysym) == 1:                self._change_start(self.start + keysym)            elif keysym == "underscore":                self._change_start(self.start + '_')            elif keysym == "period":                self._change_start(self.start + '.')            elif keysym == "minus":                self._change_start(self.start + '-')            else:                # keysym == "BackSpace"                if len(self.start) == 0:                    self.hide_window()                    return                self._change_start(self.start[:-1])            self.lasttypedstart = self.start            self.listbox.select_clear(0, int(self.listbox.curselection()[0]))            self.listbox.select_set(self._binary_search(self.start))            self._selection_changed()            return "break"         elif keysym == "Return" and not state:            # If start is a prefix of the selection, or there was an indication            # that the user used the completion window, put the selected            # completion in the text, and close the list.            # Otherwise, close the window and let the event through.            cursel = int(self.listbox.curselection()[0])            if self.completions[cursel][:len(self.start)] == self.start or \               self.userwantswindow:                self._change_start(self.completions[cursel])                self.hide_window()                return "break"            else:                self.hide_window()                return         elif (self.mode == AutoComplete.COMPLETE_ATTRIBUTES and keysym in              ("period", "space", "parenleft", "parenright", "bracketleft",               "bracketright")) or \             (self.mode == AutoComplete.COMPLETE_FILES and keysym in              ("slash", "backslash", "quotedbl", "apostrophe")) \             and not (state & ~MC_SHIFT):            # If start is a prefix of the selection, but is not '' when            # completing file names, put the whole            # selected completion. Anyway, close the list.            cursel = int(self.listbox.curselection()[0])            if self.completions[cursel][:len(self.start)] == self.start \               and (self.mode==AutoComplete.COMPLETE_ATTRIBUTES or self.start):                self._change_start(self.completions[cursel])            self.hide_window()            return         elif keysym in ("Home", "End", "Prior", "Next", "Up", "Down") and \             not state:            # Move the selection in the listbox            self.userwantswindow = True            cursel = int(self.listbox.curselection()[0])            if keysym == "Home":                newsel = 0            elif keysym == "End":                newsel = len(self.completions)-1            elif keysym in ("Prior", "Next"):                jump = self.listbox.nearest(self.listbox.winfo_height()) - \                       self.listbox.nearest(0)                if keysym == "Prior":                    newsel = max(0, cursel-jump)                else:                    assert keysym == "Next"                    newsel = min(len(self.completions)-1, cursel+jump)            elif keysym == "Up":                newsel = max(0, cursel-1)            else:                assert keysym == "Down"                newsel = min(len(self.completions)-1, cursel+1)            self.listbox.select_clear(cursel)            self.listbox.select_set(newsel)            self._selection_changed()            return "break"         elif (keysym == "Tab" and not state):            # The user wants a completion, but it is handled by AutoComplete            # (not AutoCompleteWindow), so ignore.            self.userwantswindow = True            return         elif reduce(lambda x, y: x or y,                    [keysym.find(s) != -1 for s in ("Shift", "Control", "Alt",                                                    "Meta", "Command", "Option")                     ]):            # A modifier key, so ignore            return         else:            # Unknown event, close the window and let it through.            self.hide_window()            return     def keyrelease_event(self, event):        if not self.is_active():            return        if self.widget.index("insert") != \           self.widget.index("%s+%dc" % (self.startindex, len(self.start))):            # If we didn't catch an event which moved the insert, close window            self.hide_window()     def is_active(self):        return self.autocompletewindow is not None     def complete(self):        self._change_start(self._complete_string(self.start))        # The selection doesn't change.     def hide_window(self):        if not self.is_active():            return         # unbind events        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        for seq in KEYPRESS_SEQUENCES:            self.widget.event_delete(KEYPRESS_VIRTUAL_EVENT_NAME, seq)        self.widget.unbind(KEYPRESS_VIRTUAL_EVENT_NAME, self.keypressid)        self.keypressid = None        self.widget.event_delete(KEYRELEASE_VIRTUAL_EVENT_NAME,                                 KEYRELEASE_SEQUENCE)        self.widget.unbind(KEYRELEASE_VIRTUAL_EVENT_NAME, self.keyreleaseid)        self.keyreleaseid = None        self.listbox.unbind(LISTUPDATE_SEQUENCE, self.listupdateid)        self.listupdateid = None        self.autocompletewindow.unbind(WINCONFIG_SEQUENCE, self.winconfigid)        self.winconfigid = None         # destroy widgets        self.scrollbar.destroy()        self.scrollbar = None        self.listbox.destroy()        self.listbox = None        self.autocompletewindow.destroy()        self.autocompletewindow = None