(root)/
Python-3.12.0/
Lib/
idlelib/
autocomplete_w.py
       1  """
       2  An auto-completion window for IDLE, used by the autocomplete extension
       3  """
       4  import platform
       5  
       6  from tkinter import *
       7  from tkinter.ttk import Scrollbar
       8  
       9  from idlelib.autocomplete import FILES, ATTRS
      10  from idlelib.multicall import MC_SHIFT
      11  
      12  HIDE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-hide>>"
      13  HIDE_FOCUS_OUT_SEQUENCE = "<FocusOut>"
      14  HIDE_SEQUENCES = (HIDE_FOCUS_OUT_SEQUENCE, "<ButtonPress>")
      15  KEYPRESS_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keypress>>"
      16  # We need to bind event beyond <Key> so that the function will be called
      17  # before the default specific IDLE function
      18  KEYPRESS_SEQUENCES = ("<Key>", "<Key-BackSpace>", "<Key-Return>", "<Key-Tab>",
      19                        "<Key-Up>", "<Key-Down>", "<Key-Home>", "<Key-End>",
      20                        "<Key-Prior>", "<Key-Next>", "<Key-Escape>")
      21  KEYRELEASE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keyrelease>>"
      22  KEYRELEASE_SEQUENCE = "<KeyRelease>"
      23  LISTUPDATE_SEQUENCE = "<B1-ButtonRelease>"
      24  WINCONFIG_SEQUENCE = "<Configure>"
      25  DOUBLECLICK_SEQUENCE = "<B1-Double-ButtonRelease>"
      26  
      27  class ESC[4;38;5;81mAutoCompleteWindow:
      28  
      29      def __init__(self, widget, tags):
      30          # The widget (Text) on which we place the AutoCompleteWindow
      31          self.widget = widget
      32          # Tags to mark inserted text with
      33          self.tags = tags
      34          # The widgets we create
      35          self.autocompletewindow = self.listbox = self.scrollbar = None
      36          # The default foreground and background of a selection. Saved because
      37          # they are changed to the regular colors of list items when the
      38          # completion start is not a prefix of the selected completion
      39          self.origselforeground = self.origselbackground = None
      40          # The list of completions
      41          self.completions = None
      42          # A list with more completions, or None
      43          self.morecompletions = None
      44          # The completion mode, either autocomplete.ATTRS or .FILES.
      45          self.mode = None
      46          # The current completion start, on the text box (a string)
      47          self.start = None
      48          # The index of the start of the completion
      49          self.startindex = None
      50          # The last typed start, used so that when the selection changes,
      51          # the new start will be as close as possible to the last typed one.
      52          self.lasttypedstart = None
      53          # Do we have an indication that the user wants the completion window
      54          # (for example, he clicked the list)
      55          self.userwantswindow = None
      56          # event ids
      57          self.hideid = self.keypressid = self.listupdateid = \
      58              self.winconfigid = self.keyreleaseid = self.doubleclickid = None
      59          # Flag set if last keypress was a tab
      60          self.lastkey_was_tab = False
      61          # Flag set to avoid recursive <Configure> callback invocations.
      62          self.is_configuring = False
      63  
      64      def _change_start(self, newstart):
      65          min_len = min(len(self.start), len(newstart))
      66          i = 0
      67          while i < min_len and self.start[i] == newstart[i]:
      68              i += 1
      69          if i < len(self.start):
      70              self.widget.delete("%s+%dc" % (self.startindex, i),
      71                                 "%s+%dc" % (self.startindex, len(self.start)))
      72          if i < len(newstart):
      73              self.widget.insert("%s+%dc" % (self.startindex, i),
      74                                 newstart[i:],
      75                                 self.tags)
      76          self.start = newstart
      77  
      78      def _binary_search(self, s):
      79          """Find the first index in self.completions where completions[i] is
      80          greater or equal to s, or the last index if there is no such.
      81          """
      82          i = 0; j = len(self.completions)
      83          while j > i:
      84              m = (i + j) // 2
      85              if self.completions[m] >= s:
      86                  j = m
      87              else:
      88                  i = m + 1
      89          return min(i, len(self.completions)-1)
      90  
      91      def _complete_string(self, s):
      92          """Assuming that s is the prefix of a string in self.completions,
      93          return the longest string which is a prefix of all the strings which
      94          s is a prefix of them. If s is not a prefix of a string, return s.
      95          """
      96          first = self._binary_search(s)
      97          if self.completions[first][:len(s)] != s:
      98              # There is not even one completion which s is a prefix of.
      99              return s
     100          # Find the end of the range of completions where s is a prefix of.
     101          i = first + 1
     102          j = len(self.completions)
     103          while j > i:
     104              m = (i + j) // 2
     105              if self.completions[m][:len(s)] != s:
     106                  j = m
     107              else:
     108                  i = m + 1
     109          last = i-1
     110  
     111          if first == last: # only one possible completion
     112              return self.completions[first]
     113  
     114          # We should return the maximum prefix of first and last
     115          first_comp = self.completions[first]
     116          last_comp = self.completions[last]
     117          min_len = min(len(first_comp), len(last_comp))
     118          i = len(s)
     119          while i < min_len and first_comp[i] == last_comp[i]:
     120              i += 1
     121          return first_comp[:i]
     122  
     123      def _selection_changed(self):
     124          """Call when the selection of the Listbox has changed.
     125  
     126          Updates the Listbox display and calls _change_start.
     127          """
     128          cursel = int(self.listbox.curselection()[0])
     129  
     130          self.listbox.see(cursel)
     131  
     132          lts = self.lasttypedstart
     133          selstart = self.completions[cursel]
     134          if self._binary_search(lts) == cursel:
     135              newstart = lts
     136          else:
     137              min_len = min(len(lts), len(selstart))
     138              i = 0
     139              while i < min_len and lts[i] == selstart[i]:
     140                  i += 1
     141              newstart = selstart[:i]
     142          self._change_start(newstart)
     143  
     144          if self.completions[cursel][:len(self.start)] == self.start:
     145              # start is a prefix of the selected completion
     146              self.listbox.configure(selectbackground=self.origselbackground,
     147                                     selectforeground=self.origselforeground)
     148          else:
     149              self.listbox.configure(selectbackground=self.listbox.cget("bg"),
     150                                     selectforeground=self.listbox.cget("fg"))
     151              # If there are more completions, show them, and call me again.
     152              if self.morecompletions:
     153                  self.completions = self.morecompletions
     154                  self.morecompletions = None
     155                  self.listbox.delete(0, END)
     156                  for item in self.completions:
     157                      self.listbox.insert(END, item)
     158                  self.listbox.select_set(self._binary_search(self.start))
     159                  self._selection_changed()
     160  
     161      def show_window(self, comp_lists, index, complete, mode, userWantsWin):
     162          """Show the autocomplete list, bind events.
     163  
     164          If complete is True, complete the text, and if there is exactly
     165          one matching completion, don't open a list.
     166          """
     167          # Handle the start we already have
     168          self.completions, self.morecompletions = comp_lists
     169          self.mode = mode
     170          self.startindex = self.widget.index(index)
     171          self.start = self.widget.get(self.startindex, "insert")
     172          if complete:
     173              completed = self._complete_string(self.start)
     174              start = self.start
     175              self._change_start(completed)
     176              i = self._binary_search(completed)
     177              if self.completions[i] == completed and \
     178                 (i == len(self.completions)-1 or
     179                  self.completions[i+1][:len(completed)] != completed):
     180                  # There is exactly one matching completion
     181                  return completed == start
     182          self.userwantswindow = userWantsWin
     183          self.lasttypedstart = self.start
     184  
     185          self.autocompletewindow = acw = Toplevel(self.widget)
     186          acw.withdraw()
     187          acw.wm_overrideredirect(1)
     188          try:
     189              # Prevent grabbing focus on macOS.
     190              acw.tk.call("::tk::unsupported::MacWindowStyle", "style", acw._w,
     191                          "help", "noActivates")
     192          except TclError:
     193              pass
     194          self.scrollbar = scrollbar = Scrollbar(acw, orient=VERTICAL)
     195          self.listbox = listbox = Listbox(acw, yscrollcommand=scrollbar.set,
     196                                           exportselection=False)
     197          for item in self.completions:
     198              listbox.insert(END, item)
     199          self.origselforeground = listbox.cget("selectforeground")
     200          self.origselbackground = listbox.cget("selectbackground")
     201          scrollbar.config(command=listbox.yview)
     202          scrollbar.pack(side=RIGHT, fill=Y)
     203          listbox.pack(side=LEFT, fill=BOTH, expand=True)
     204          #acw.update_idletasks() # Need for tk8.6.8 on macOS: #40128.
     205          acw.lift()  # work around bug in Tk 8.5.18+ (issue #24570)
     206  
     207          # Initialize the listbox selection
     208          self.listbox.select_set(self._binary_search(self.start))
     209          self._selection_changed()
     210  
     211          # bind events
     212          self.hideaid = acw.bind(HIDE_VIRTUAL_EVENT_NAME, self.hide_event)
     213          self.hidewid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME, self.hide_event)
     214          acw.event_add(HIDE_VIRTUAL_EVENT_NAME, HIDE_FOCUS_OUT_SEQUENCE)
     215          for seq in HIDE_SEQUENCES:
     216              self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq)
     217  
     218          self.keypressid = self.widget.bind(KEYPRESS_VIRTUAL_EVENT_NAME,
     219                                             self.keypress_event)
     220          for seq in KEYPRESS_SEQUENCES:
     221              self.widget.event_add(KEYPRESS_VIRTUAL_EVENT_NAME, seq)
     222          self.keyreleaseid = self.widget.bind(KEYRELEASE_VIRTUAL_EVENT_NAME,
     223                                               self.keyrelease_event)
     224          self.widget.event_add(KEYRELEASE_VIRTUAL_EVENT_NAME,KEYRELEASE_SEQUENCE)
     225          self.listupdateid = listbox.bind(LISTUPDATE_SEQUENCE,
     226                                           self.listselect_event)
     227          self.is_configuring = False
     228          self.winconfigid = acw.bind(WINCONFIG_SEQUENCE, self.winconfig_event)
     229          self.doubleclickid = listbox.bind(DOUBLECLICK_SEQUENCE,
     230                                            self.doubleclick_event)
     231          return None
     232  
     233      def winconfig_event(self, event):
     234          if self.is_configuring:
     235              # Avoid running on recursive <Configure> callback invocations.
     236              return
     237  
     238          self.is_configuring = True
     239          if not self.is_active():
     240              return
     241  
     242          # Since the <Configure> event may occur after the completion window is gone,
     243          # catch potential TclError exceptions when accessing acw.  See: bpo-41611.
     244          try:
     245              # Position the completion list window
     246              text = self.widget
     247              text.see(self.startindex)
     248              x, y, cx, cy = text.bbox(self.startindex)
     249              acw = self.autocompletewindow
     250              if platform.system().startswith('Windows'):
     251                  # On Windows an update() call is needed for the completion
     252                  # list window to be created, so that we can fetch its width
     253                  # and height.  However, this is not needed on other platforms
     254                  # (tested on Ubuntu and macOS) but at one point began
     255                  # causing freezes on macOS.  See issues 37849 and 41611.
     256                  acw.update()
     257              acw_width, acw_height = acw.winfo_width(), acw.winfo_height()
     258              text_width, text_height = text.winfo_width(), text.winfo_height()
     259              new_x = text.winfo_rootx() + min(x, max(0, text_width - acw_width))
     260              new_y = text.winfo_rooty() + y
     261              if (text_height - (y + cy) >= acw_height # enough height below
     262                  or y < acw_height): # not enough height above
     263                  # place acw below current line
     264                  new_y += cy
     265              else:
     266                  # place acw above current line
     267                  new_y -= acw_height
     268              acw.wm_geometry("+%d+%d" % (new_x, new_y))
     269              acw.deiconify()
     270              acw.update_idletasks()
     271          except TclError:
     272              pass
     273  
     274          if platform.system().startswith('Windows'):
     275              # See issue 15786.  When on Windows platform, Tk will misbehave
     276              # to call winconfig_event multiple times, we need to prevent this,
     277              # otherwise mouse button double click will not be able to used.
     278              try:
     279                  acw.unbind(WINCONFIG_SEQUENCE, self.winconfigid)
     280              except TclError:
     281                  pass
     282              self.winconfigid = None
     283  
     284          self.is_configuring = False
     285  
     286      def _hide_event_check(self):
     287          if not self.autocompletewindow:
     288              return
     289  
     290          try:
     291              if not self.autocompletewindow.focus_get():
     292                  self.hide_window()
     293          except KeyError:
     294              # See issue 734176, when user click on menu, acw.focus_get()
     295              # will get KeyError.
     296              self.hide_window()
     297  
     298      def hide_event(self, event):
     299          # Hide autocomplete list if it exists and does not have focus or
     300          # mouse click on widget / text area.
     301          if self.is_active():
     302              if event.type == EventType.FocusOut:
     303                  # On Windows platform, it will need to delay the check for
     304                  # acw.focus_get() when click on acw, otherwise it will return
     305                  # None and close the window
     306                  self.widget.after(1, self._hide_event_check)
     307              elif event.type == EventType.ButtonPress:
     308                  # ButtonPress event only bind to self.widget
     309                  self.hide_window()
     310  
     311      def listselect_event(self, event):
     312          if self.is_active():
     313              self.userwantswindow = True
     314              cursel = int(self.listbox.curselection()[0])
     315              self._change_start(self.completions[cursel])
     316  
     317      def doubleclick_event(self, event):
     318          # Put the selected completion in the text, and close the list
     319          cursel = int(self.listbox.curselection()[0])
     320          self._change_start(self.completions[cursel])
     321          self.hide_window()
     322  
     323      def keypress_event(self, event):
     324          if not self.is_active():
     325              return None
     326          keysym = event.keysym
     327          if hasattr(event, "mc_state"):
     328              state = event.mc_state
     329          else:
     330              state = 0
     331          if keysym != "Tab":
     332              self.lastkey_was_tab = False
     333          if (len(keysym) == 1 or keysym in ("underscore", "BackSpace")
     334              or (self.mode == FILES and keysym in
     335                  ("period", "minus"))) \
     336             and not (state & ~MC_SHIFT):
     337              # Normal editing of text
     338              if len(keysym) == 1:
     339                  self._change_start(self.start + keysym)
     340              elif keysym == "underscore":
     341                  self._change_start(self.start + '_')
     342              elif keysym == "period":
     343                  self._change_start(self.start + '.')
     344              elif keysym == "minus":
     345                  self._change_start(self.start + '-')
     346              else:
     347                  # keysym == "BackSpace"
     348                  if len(self.start) == 0:
     349                      self.hide_window()
     350                      return None
     351                  self._change_start(self.start[:-1])
     352              self.lasttypedstart = self.start
     353              self.listbox.select_clear(0, int(self.listbox.curselection()[0]))
     354              self.listbox.select_set(self._binary_search(self.start))
     355              self._selection_changed()
     356              return "break"
     357  
     358          elif keysym == "Return":
     359              self.complete()
     360              self.hide_window()
     361              return 'break'
     362  
     363          elif (self.mode == ATTRS and keysym in
     364                ("period", "space", "parenleft", "parenright", "bracketleft",
     365                 "bracketright")) or \
     366               (self.mode == FILES and keysym in
     367                ("slash", "backslash", "quotedbl", "apostrophe")) \
     368               and not (state & ~MC_SHIFT):
     369              # If start is a prefix of the selection, but is not '' when
     370              # completing file names, put the whole
     371              # selected completion. Anyway, close the list.
     372              cursel = int(self.listbox.curselection()[0])
     373              if self.completions[cursel][:len(self.start)] == self.start \
     374                 and (self.mode == ATTRS or self.start):
     375                  self._change_start(self.completions[cursel])
     376              self.hide_window()
     377              return None
     378  
     379          elif keysym in ("Home", "End", "Prior", "Next", "Up", "Down") and \
     380               not state:
     381              # Move the selection in the listbox
     382              self.userwantswindow = True
     383              cursel = int(self.listbox.curselection()[0])
     384              if keysym == "Home":
     385                  newsel = 0
     386              elif keysym == "End":
     387                  newsel = len(self.completions)-1
     388              elif keysym in ("Prior", "Next"):
     389                  jump = self.listbox.nearest(self.listbox.winfo_height()) - \
     390                         self.listbox.nearest(0)
     391                  if keysym == "Prior":
     392                      newsel = max(0, cursel-jump)
     393                  else:
     394                      assert keysym == "Next"
     395                      newsel = min(len(self.completions)-1, cursel+jump)
     396              elif keysym == "Up":
     397                  newsel = max(0, cursel-1)
     398              else:
     399                  assert keysym == "Down"
     400                  newsel = min(len(self.completions)-1, cursel+1)
     401              self.listbox.select_clear(cursel)
     402              self.listbox.select_set(newsel)
     403              self._selection_changed()
     404              self._change_start(self.completions[newsel])
     405              return "break"
     406  
     407          elif (keysym == "Tab" and not state):
     408              if self.lastkey_was_tab:
     409                  # two tabs in a row; insert current selection and close acw
     410                  cursel = int(self.listbox.curselection()[0])
     411                  self._change_start(self.completions[cursel])
     412                  self.hide_window()
     413                  return "break"
     414              else:
     415                  # first tab; let AutoComplete handle the completion
     416                  self.userwantswindow = True
     417                  self.lastkey_was_tab = True
     418                  return None
     419  
     420          elif any(s in keysym for s in ("Shift", "Control", "Alt",
     421                                         "Meta", "Command", "Option")):
     422              # A modifier key, so ignore
     423              return None
     424  
     425          elif event.char and event.char >= ' ':
     426              # Regular character with a non-length-1 keycode
     427              self._change_start(self.start + event.char)
     428              self.lasttypedstart = self.start
     429              self.listbox.select_clear(0, int(self.listbox.curselection()[0]))
     430              self.listbox.select_set(self._binary_search(self.start))
     431              self._selection_changed()
     432              return "break"
     433  
     434          else:
     435              # Unknown event, close the window and let it through.
     436              self.hide_window()
     437              return None
     438  
     439      def keyrelease_event(self, event):
     440          if not self.is_active():
     441              return
     442          if self.widget.index("insert") != \
     443             self.widget.index("%s+%dc" % (self.startindex, len(self.start))):
     444              # If we didn't catch an event which moved the insert, close window
     445              self.hide_window()
     446  
     447      def is_active(self):
     448          return self.autocompletewindow is not None
     449  
     450      def complete(self):
     451          self._change_start(self._complete_string(self.start))
     452          # The selection doesn't change.
     453  
     454      def hide_window(self):
     455          if not self.is_active():
     456              return
     457  
     458          # unbind events
     459          self.autocompletewindow.event_delete(HIDE_VIRTUAL_EVENT_NAME,
     460                                               HIDE_FOCUS_OUT_SEQUENCE)
     461          for seq in HIDE_SEQUENCES:
     462              self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq)
     463  
     464          self.autocompletewindow.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideaid)
     465          self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hidewid)
     466          self.hideaid = None
     467          self.hidewid = None
     468          for seq in KEYPRESS_SEQUENCES:
     469              self.widget.event_delete(KEYPRESS_VIRTUAL_EVENT_NAME, seq)
     470          self.widget.unbind(KEYPRESS_VIRTUAL_EVENT_NAME, self.keypressid)
     471          self.keypressid = None
     472          self.widget.event_delete(KEYRELEASE_VIRTUAL_EVENT_NAME,
     473                                   KEYRELEASE_SEQUENCE)
     474          self.widget.unbind(KEYRELEASE_VIRTUAL_EVENT_NAME, self.keyreleaseid)
     475          self.keyreleaseid = None
     476          self.listbox.unbind(LISTUPDATE_SEQUENCE, self.listupdateid)
     477          self.listupdateid = None
     478          if self.winconfigid:
     479              self.autocompletewindow.unbind(WINCONFIG_SEQUENCE, self.winconfigid)
     480              self.winconfigid = None
     481  
     482          # Re-focusOn frame.text (See issue #15786)
     483          self.widget.focus_set()
     484  
     485          # destroy widgets
     486          self.scrollbar.destroy()
     487          self.scrollbar = None
     488          self.listbox.destroy()
     489          self.listbox = None
     490          self.autocompletewindow.destroy()
     491          self.autocompletewindow = None
     492  
     493  
     494  if __name__ == '__main__':
     495      from unittest import main
     496      main('idlelib.idle_test.test_autocomplete_w', verbosity=2, exit=False)
     497  
     498  # TODO: autocomplete/w htest here