mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	 b17544551f
			
		
	
	
		b17544551f
		
	
	
	
	
		
			
			A idlelib/AutoCompleteWindow.py A idlelib/AutoComplete.py A idlelib/HyperParser.py M idlelib/PyShell.py M idlelib/ParenMatch.py M idlelib/configDialog.py M idlelib/EditorWindow.py M idlelib/PyParse.py M idlelib/CallTips.py M idlelib/CallTipWindow.py M idlelib/run.py M idlelib/config-extensions.def A idlelib/MultiCall.py
		
			
				
	
	
		
			393 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			393 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """
 | |
| An auto-completion window for IDLE, used by the AutoComplete extension
 | |
| """
 | |
| from Tkinter import *
 | |
| from MultiCall import MC_SHIFT
 | |
| import 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 function
 | |
| KEYPRESS_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
 |