python-2.5.2/win32/Lib/idlelib/AutoCompleteWindow.py
changeset 0 ae805ac0140d
equal deleted inserted replaced
-1:000000000000 0:ae805ac0140d
       
     1 """
       
     2 An auto-completion window for IDLE, used by the AutoComplete extension
       
     3 """
       
     4 from Tkinter import *
       
     5 from MultiCall import MC_SHIFT
       
     6 import AutoComplete
       
     7 
       
     8 HIDE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-hide>>"
       
     9 HIDE_SEQUENCES = ("<FocusOut>", "<ButtonPress>")
       
    10 KEYPRESS_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keypress>>"
       
    11 # We need to bind event beyond <Key> so that the function will be called
       
    12 # before the default specific IDLE function
       
    13 KEYPRESS_SEQUENCES = ("<Key>", "<Key-BackSpace>", "<Key-Return>", "<Key-Tab>",
       
    14                       "<Key-Up>", "<Key-Down>", "<Key-Home>", "<Key-End>",
       
    15                       "<Key-Prior>", "<Key-Next>")
       
    16 KEYRELEASE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keyrelease>>"
       
    17 KEYRELEASE_SEQUENCE = "<KeyRelease>"
       
    18 LISTUPDATE_SEQUENCE = "<B1-ButtonRelease>"
       
    19 WINCONFIG_SEQUENCE = "<Configure>"
       
    20 DOUBLECLICK_SEQUENCE = "<B1-Double-ButtonRelease>"
       
    21 
       
    22 class AutoCompleteWindow:
       
    23 
       
    24     def __init__(self, widget):
       
    25         # The widget (Text) on which we place the AutoCompleteWindow
       
    26         self.widget = widget
       
    27         # The widgets we create
       
    28         self.autocompletewindow = self.listbox = self.scrollbar = None
       
    29         # The default foreground and background of a selection. Saved because
       
    30         # they are changed to the regular colors of list items when the
       
    31         # completion start is not a prefix of the selected completion
       
    32         self.origselforeground = self.origselbackground = None
       
    33         # The list of completions
       
    34         self.completions = None
       
    35         # A list with more completions, or None
       
    36         self.morecompletions = None
       
    37         # The completion mode. Either AutoComplete.COMPLETE_ATTRIBUTES or
       
    38         # AutoComplete.COMPLETE_FILES
       
    39         self.mode = None
       
    40         # The current completion start, on the text box (a string)
       
    41         self.start = None
       
    42         # The index of the start of the completion
       
    43         self.startindex = None
       
    44         # The last typed start, used so that when the selection changes,
       
    45         # the new start will be as close as possible to the last typed one.
       
    46         self.lasttypedstart = None
       
    47         # Do we have an indication that the user wants the completion window
       
    48         # (for example, he clicked the list)
       
    49         self.userwantswindow = None
       
    50         # event ids
       
    51         self.hideid = self.keypressid = self.listupdateid = self.winconfigid \
       
    52         = self.keyreleaseid = self.doubleclickid                         = None
       
    53         # Flag set if last keypress was a tab
       
    54         self.lastkey_was_tab = False
       
    55 
       
    56     def _change_start(self, newstart):
       
    57         i = 0
       
    58         while i < len(self.start) and i < len(newstart) and \
       
    59               self.start[i] == newstart[i]:
       
    60             i += 1
       
    61         if i < len(self.start):
       
    62             self.widget.delete("%s+%dc" % (self.startindex, i),
       
    63                                "%s+%dc" % (self.startindex, len(self.start)))
       
    64         if i < len(newstart):
       
    65             self.widget.insert("%s+%dc" % (self.startindex, i),
       
    66                                newstart[i:])
       
    67         self.start = newstart
       
    68 
       
    69     def _binary_search(self, s):
       
    70         """Find the first index in self.completions where completions[i] is
       
    71         greater or equal to s, or the last index if there is no such
       
    72         one."""
       
    73         i = 0; j = len(self.completions)
       
    74         while j > i:
       
    75             m = (i + j) // 2
       
    76             if self.completions[m] >= s:
       
    77                 j = m
       
    78             else:
       
    79                 i = m + 1
       
    80         return min(i, len(self.completions)-1)
       
    81 
       
    82     def _complete_string(self, s):
       
    83         """Assuming that s is the prefix of a string in self.completions,
       
    84         return the longest string which is a prefix of all the strings which
       
    85         s is a prefix of them. If s is not a prefix of a string, return s."""
       
    86         first = self._binary_search(s)
       
    87         if self.completions[first][:len(s)] != s:
       
    88             # There is not even one completion which s is a prefix of.
       
    89             return s
       
    90         # Find the end of the range of completions where s is a prefix of.
       
    91         i = first + 1
       
    92         j = len(self.completions)
       
    93         while j > i:
       
    94             m = (i + j) // 2
       
    95             if self.completions[m][:len(s)] != s:
       
    96                 j = m
       
    97             else:
       
    98                 i = m + 1
       
    99         last = i-1
       
   100 
       
   101         # We should return the maximum prefix of first and last
       
   102         i = len(s)
       
   103         while len(self.completions[first]) > i and \
       
   104               len(self.completions[last]) > i and \
       
   105               self.completions[first][i] == self.completions[last][i]:
       
   106             i += 1
       
   107         return self.completions[first][:i]
       
   108 
       
   109     def _selection_changed(self):
       
   110         """Should be called when the selection of the Listbox has changed.
       
   111         Updates the Listbox display and calls _change_start."""
       
   112         cursel = int(self.listbox.curselection()[0])
       
   113 
       
   114         self.listbox.see(cursel)
       
   115 
       
   116         lts = self.lasttypedstart
       
   117         selstart = self.completions[cursel]
       
   118         if self._binary_search(lts) == cursel:
       
   119             newstart = lts
       
   120         else:
       
   121             i = 0
       
   122             while i < len(lts) and i < len(selstart) and lts[i] == selstart[i]:
       
   123                 i += 1
       
   124             newstart = selstart[:i]
       
   125         self._change_start(newstart)
       
   126 
       
   127         if self.completions[cursel][:len(self.start)] == self.start:
       
   128             # start is a prefix of the selected completion
       
   129             self.listbox.configure(selectbackground=self.origselbackground,
       
   130                                    selectforeground=self.origselforeground)
       
   131         else:
       
   132             self.listbox.configure(selectbackground=self.listbox.cget("bg"),
       
   133                                    selectforeground=self.listbox.cget("fg"))
       
   134             # If there are more completions, show them, and call me again.
       
   135             if self.morecompletions:
       
   136                 self.completions = self.morecompletions
       
   137                 self.morecompletions = None
       
   138                 self.listbox.delete(0, END)
       
   139                 for item in self.completions:
       
   140                     self.listbox.insert(END, item)
       
   141                 self.listbox.select_set(self._binary_search(self.start))
       
   142                 self._selection_changed()
       
   143 
       
   144     def show_window(self, comp_lists, index, complete, mode, userWantsWin):
       
   145         """Show the autocomplete list, bind events.
       
   146         If complete is True, complete the text, and if there is exactly one
       
   147         matching completion, don't open a list."""
       
   148         # Handle the start we already have
       
   149         self.completions, self.morecompletions = comp_lists
       
   150         self.mode = mode
       
   151         self.startindex = self.widget.index(index)
       
   152         self.start = self.widget.get(self.startindex, "insert")
       
   153         if complete:
       
   154             completed = self._complete_string(self.start)
       
   155             self._change_start(completed)
       
   156             i = self._binary_search(completed)
       
   157             if self.completions[i] == completed and \
       
   158                (i == len(self.completions)-1 or
       
   159                 self.completions[i+1][:len(completed)] != completed):
       
   160                 # There is exactly one matching completion
       
   161                 return
       
   162         self.userwantswindow = userWantsWin
       
   163         self.lasttypedstart = self.start
       
   164 
       
   165         # Put widgets in place
       
   166         self.autocompletewindow = acw = Toplevel(self.widget)
       
   167         # Put it in a position so that it is not seen.
       
   168         acw.wm_geometry("+10000+10000")
       
   169         # Make it float
       
   170         acw.wm_overrideredirect(1)
       
   171         try:
       
   172             # This command is only needed and available on Tk >= 8.4.0 for OSX
       
   173             # Without it, call tips intrude on the typing process by grabbing
       
   174             # the focus.
       
   175             acw.tk.call("::tk::unsupported::MacWindowStyle", "style", acw._w,
       
   176                         "help", "noActivates")
       
   177         except TclError:
       
   178             pass
       
   179         self.scrollbar = scrollbar = Scrollbar(acw, orient=VERTICAL)
       
   180         self.listbox = listbox = Listbox(acw, yscrollcommand=scrollbar.set,
       
   181                                          exportselection=False, bg="white")
       
   182         for item in self.completions:
       
   183             listbox.insert(END, item)
       
   184         self.origselforeground = listbox.cget("selectforeground")
       
   185         self.origselbackground = listbox.cget("selectbackground")
       
   186         scrollbar.config(command=listbox.yview)
       
   187         scrollbar.pack(side=RIGHT, fill=Y)
       
   188         listbox.pack(side=LEFT, fill=BOTH, expand=True)
       
   189 
       
   190         # Initialize the listbox selection
       
   191         self.listbox.select_set(self._binary_search(self.start))
       
   192         self._selection_changed()
       
   193 
       
   194         # bind events
       
   195         self.hideid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME,
       
   196                                        self.hide_event)
       
   197         for seq in HIDE_SEQUENCES:
       
   198             self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq)
       
   199         self.keypressid = self.widget.bind(KEYPRESS_VIRTUAL_EVENT_NAME,
       
   200                                            self.keypress_event)
       
   201         for seq in KEYPRESS_SEQUENCES:
       
   202             self.widget.event_add(KEYPRESS_VIRTUAL_EVENT_NAME, seq)
       
   203         self.keyreleaseid = self.widget.bind(KEYRELEASE_VIRTUAL_EVENT_NAME,
       
   204                                              self.keyrelease_event)
       
   205         self.widget.event_add(KEYRELEASE_VIRTUAL_EVENT_NAME,KEYRELEASE_SEQUENCE)
       
   206         self.listupdateid = listbox.bind(LISTUPDATE_SEQUENCE,
       
   207                                          self.listselect_event)
       
   208         self.winconfigid = acw.bind(WINCONFIG_SEQUENCE, self.winconfig_event)
       
   209         self.doubleclickid = listbox.bind(DOUBLECLICK_SEQUENCE,
       
   210                                           self.doubleclick_event)
       
   211 
       
   212     def winconfig_event(self, event):
       
   213         if not self.is_active():
       
   214             return
       
   215         # Position the completion list window
       
   216         acw = self.autocompletewindow
       
   217         self.widget.see(self.startindex)
       
   218         x, y, cx, cy = self.widget.bbox(self.startindex)
       
   219         acw.wm_geometry("+%d+%d" % (x + self.widget.winfo_rootx(),
       
   220                                     y + self.widget.winfo_rooty() \
       
   221                                     -acw.winfo_height()))
       
   222 
       
   223 
       
   224     def hide_event(self, event):
       
   225         if not self.is_active():
       
   226             return
       
   227         self.hide_window()
       
   228 
       
   229     def listselect_event(self, event):
       
   230         if not self.is_active():
       
   231             return
       
   232         self.userwantswindow = True
       
   233         cursel = int(self.listbox.curselection()[0])
       
   234         self._change_start(self.completions[cursel])
       
   235 
       
   236     def doubleclick_event(self, event):
       
   237         # Put the selected completion in the text, and close the list
       
   238         cursel = int(self.listbox.curselection()[0])
       
   239         self._change_start(self.completions[cursel])
       
   240         self.hide_window()
       
   241 
       
   242     def keypress_event(self, event):
       
   243         if not self.is_active():
       
   244             return
       
   245         keysym = event.keysym
       
   246         if hasattr(event, "mc_state"):
       
   247             state = event.mc_state
       
   248         else:
       
   249             state = 0
       
   250         if keysym != "Tab":
       
   251             self.lastkey_was_tab = False
       
   252         if (len(keysym) == 1 or keysym in ("underscore", "BackSpace")
       
   253             or (self.mode==AutoComplete.COMPLETE_FILES and keysym in
       
   254                 ("period", "minus"))) \
       
   255            and not (state & ~MC_SHIFT):
       
   256             # Normal editing of text
       
   257             if len(keysym) == 1:
       
   258                 self._change_start(self.start + keysym)
       
   259             elif keysym == "underscore":
       
   260                 self._change_start(self.start + '_')
       
   261             elif keysym == "period":
       
   262                 self._change_start(self.start + '.')
       
   263             elif keysym == "minus":
       
   264                 self._change_start(self.start + '-')
       
   265             else:
       
   266                 # keysym == "BackSpace"
       
   267                 if len(self.start) == 0:
       
   268                     self.hide_window()
       
   269                     return
       
   270                 self._change_start(self.start[:-1])
       
   271             self.lasttypedstart = self.start
       
   272             self.listbox.select_clear(0, int(self.listbox.curselection()[0]))
       
   273             self.listbox.select_set(self._binary_search(self.start))
       
   274             self._selection_changed()
       
   275             return "break"
       
   276 
       
   277         elif keysym == "Return":
       
   278             self.hide_window()
       
   279             return
       
   280 
       
   281         elif (self.mode == AutoComplete.COMPLETE_ATTRIBUTES and keysym in
       
   282               ("period", "space", "parenleft", "parenright", "bracketleft",
       
   283                "bracketright")) or \
       
   284              (self.mode == AutoComplete.COMPLETE_FILES and keysym in
       
   285               ("slash", "backslash", "quotedbl", "apostrophe")) \
       
   286              and not (state & ~MC_SHIFT):
       
   287             # If start is a prefix of the selection, but is not '' when
       
   288             # completing file names, put the whole
       
   289             # selected completion. Anyway, close the list.
       
   290             cursel = int(self.listbox.curselection()[0])
       
   291             if self.completions[cursel][:len(self.start)] == self.start \
       
   292                and (self.mode==AutoComplete.COMPLETE_ATTRIBUTES or self.start):
       
   293                 self._change_start(self.completions[cursel])
       
   294             self.hide_window()
       
   295             return
       
   296 
       
   297         elif keysym in ("Home", "End", "Prior", "Next", "Up", "Down") and \
       
   298              not state:
       
   299             # Move the selection in the listbox
       
   300             self.userwantswindow = True
       
   301             cursel = int(self.listbox.curselection()[0])
       
   302             if keysym == "Home":
       
   303                 newsel = 0
       
   304             elif keysym == "End":
       
   305                 newsel = len(self.completions)-1
       
   306             elif keysym in ("Prior", "Next"):
       
   307                 jump = self.listbox.nearest(self.listbox.winfo_height()) - \
       
   308                        self.listbox.nearest(0)
       
   309                 if keysym == "Prior":
       
   310                     newsel = max(0, cursel-jump)
       
   311                 else:
       
   312                     assert keysym == "Next"
       
   313                     newsel = min(len(self.completions)-1, cursel+jump)
       
   314             elif keysym == "Up":
       
   315                 newsel = max(0, cursel-1)
       
   316             else:
       
   317                 assert keysym == "Down"
       
   318                 newsel = min(len(self.completions)-1, cursel+1)
       
   319             self.listbox.select_clear(cursel)
       
   320             self.listbox.select_set(newsel)
       
   321             self._selection_changed()
       
   322             self._change_start(self.completions[newsel])
       
   323             return "break"
       
   324 
       
   325         elif (keysym == "Tab" and not state):
       
   326             if self.lastkey_was_tab:
       
   327                 # two tabs in a row; insert current selection and close acw
       
   328                 cursel = int(self.listbox.curselection()[0])
       
   329                 self._change_start(self.completions[cursel])
       
   330                 self.hide_window()
       
   331                 return "break"
       
   332             else:
       
   333                 # first tab; let AutoComplete handle the completion
       
   334                 self.userwantswindow = True
       
   335                 self.lastkey_was_tab = True
       
   336                 return
       
   337 
       
   338         elif reduce(lambda x, y: x or y,
       
   339                     [keysym.find(s) != -1 for s in ("Shift", "Control", "Alt",
       
   340                                                     "Meta", "Command", "Option")
       
   341                      ]):
       
   342             # A modifier key, so ignore
       
   343             return
       
   344 
       
   345         else:
       
   346             # Unknown event, close the window and let it through.
       
   347             self.hide_window()
       
   348             return
       
   349 
       
   350     def keyrelease_event(self, event):
       
   351         if not self.is_active():
       
   352             return
       
   353         if self.widget.index("insert") != \
       
   354            self.widget.index("%s+%dc" % (self.startindex, len(self.start))):
       
   355             # If we didn't catch an event which moved the insert, close window
       
   356             self.hide_window()
       
   357 
       
   358     def is_active(self):
       
   359         return self.autocompletewindow is not None
       
   360 
       
   361     def complete(self):
       
   362         self._change_start(self._complete_string(self.start))
       
   363         # The selection doesn't change.
       
   364 
       
   365     def hide_window(self):
       
   366         if not self.is_active():
       
   367             return
       
   368 
       
   369         # unbind events
       
   370         for seq in HIDE_SEQUENCES:
       
   371             self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq)
       
   372         self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideid)
       
   373         self.hideid = None
       
   374         for seq in KEYPRESS_SEQUENCES:
       
   375             self.widget.event_delete(KEYPRESS_VIRTUAL_EVENT_NAME, seq)
       
   376         self.widget.unbind(KEYPRESS_VIRTUAL_EVENT_NAME, self.keypressid)
       
   377         self.keypressid = None
       
   378         self.widget.event_delete(KEYRELEASE_VIRTUAL_EVENT_NAME,
       
   379                                  KEYRELEASE_SEQUENCE)
       
   380         self.widget.unbind(KEYRELEASE_VIRTUAL_EVENT_NAME, self.keyreleaseid)
       
   381         self.keyreleaseid = None
       
   382         self.listbox.unbind(LISTUPDATE_SEQUENCE, self.listupdateid)
       
   383         self.listupdateid = None
       
   384         self.autocompletewindow.unbind(WINCONFIG_SEQUENCE, self.winconfigid)
       
   385         self.winconfigid = None
       
   386 
       
   387         # destroy widgets
       
   388         self.scrollbar.destroy()
       
   389         self.scrollbar = None
       
   390         self.listbox.destroy()
       
   391         self.listbox = None
       
   392         self.autocompletewindow.destroy()
       
   393         self.autocompletewindow = None