2 An auto-completion window for IDLE, used by the AutoComplete extension
5 from MultiCall
import MC_SHIFT
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>",
14 "<Key-Up>", "<Key-Down>", "<Key-Home>", "<Key-End>")
15 KEYRELEASE_VIRTUAL_EVENT_NAME
= "<<autocompletewindow-keyrelease>>"
16 KEYRELEASE_SEQUENCE
= "<KeyRelease>"
17 LISTUPDATE_SEQUENCE
= "<ButtonRelease>"
18 WINCONFIG_SEQUENCE
= "<Configure>"
19 DOUBLECLICK_SEQUENCE
= "<Double-ButtonRelease>"
21 class AutoCompleteWindow
:
23 def __init__(self
, widget
):
24 # The widget (Text) on which we place the AutoCompleteWindow
26 # The widgets we create
27 self
.autocompletewindow
= self
.listbox
= self
.scrollbar
= None
28 # The default foreground and background of a selection. Saved because
29 # they are changed to the regular colors of list items when the
30 # completion start is not a prefix of the selected completion
31 self
.origselforeground
= self
.origselbackground
= None
32 # The list of completions
33 self
.completions
= None
34 # A list with more completions, or None
35 self
.morecompletions
= None
36 # The completion mode. Either AutoComplete.COMPLETE_ATTRIBUTES or
37 # AutoComplete.COMPLETE_FILES
39 # The current completion start, on the text box (a string)
41 # The index of the start of the completion
42 self
.startindex
= None
43 # The last typed start, used so that when the selection changes,
44 # the new start will be as close as possible to the last typed one.
45 self
.lasttypedstart
= None
46 # Do we have an indication that the user wants the completion window
47 # (for example, he clicked the list)
48 self
.userwantswindow
= None
50 self
.hideid
= self
.keypressid
= self
.listupdateid
= self
.winconfigid \
51 = self
.keyreleaseid
= self
.doubleclickid
= None
53 def _change_start(self
, newstart
):
55 while i
< len(self
.start
) and i
< len(newstart
) and \
56 self
.start
[i
] == newstart
[i
]:
58 if i
< len(self
.start
):
59 self
.widget
.delete("%s+%dc" % (self
.startindex
, i
),
60 "%s+%dc" % (self
.startindex
, len(self
.start
)))
62 self
.widget
.insert("%s+%dc" % (self
.startindex
, i
),
66 def _binary_search(self
, s
):
67 """Find the first index in self.completions where completions[i] is
68 greater or equal to s, or the last index if there is no such
70 i
= 0; j
= len(self
.completions
)
73 if self
.completions
[m
] >= s
:
77 return min(i
, len(self
.completions
)-1)
79 def _complete_string(self
, s
):
80 """Assuming that s is the prefix of a string in self.completions,
81 return the longest string which is a prefix of all the strings which
82 s is a prefix of them. If s is not a prefix of a string, return s."""
83 first
= self
._binary
_search
(s
)
84 if self
.completions
[first
][:len(s
)] != s
:
85 # There is not even one completion which s is a prefix of.
87 # Find the end of the range of completions where s is a prefix of.
89 j
= len(self
.completions
)
92 if self
.completions
[m
][:len(s
)] != s
:
98 # We should return the maximum prefix of first and last
100 while len(self
.completions
[first
]) > i
and \
101 len(self
.completions
[last
]) > i
and \
102 self
.completions
[first
][i
] == self
.completions
[last
][i
]:
104 return self
.completions
[first
][:i
]
106 def _selection_changed(self
):
107 """Should be called when the selection of the Listbox has changed.
108 Updates the Listbox display and calls _change_start."""
109 cursel
= int(self
.listbox
.curselection()[0])
111 self
.listbox
.see(cursel
)
113 lts
= self
.lasttypedstart
114 selstart
= self
.completions
[cursel
]
115 if self
._binary
_search
(lts
) == cursel
:
119 while i
< len(lts
) and i
< len(selstart
) and lts
[i
] == selstart
[i
]:
121 while cursel
> 0 and selstart
[:i
] <= self
.completions
[cursel
-1]:
123 newstart
= selstart
[:i
]
124 self
._change
_start
(newstart
)
126 if self
.completions
[cursel
][:len(self
.start
)] == self
.start
:
127 # start is a prefix of the selected completion
128 self
.listbox
.configure(selectbackground
=self
.origselbackground
,
129 selectforeground
=self
.origselforeground
)
131 self
.listbox
.configure(selectbackground
=self
.listbox
.cget("bg"),
132 selectforeground
=self
.listbox
.cget("fg"))
133 # If there are more completions, show them, and call me again.
134 if self
.morecompletions
:
135 self
.completions
= self
.morecompletions
136 self
.morecompletions
= None
137 self
.listbox
.delete(0, END
)
138 for item
in self
.completions
:
139 self
.listbox
.insert(END
, item
)
140 self
.listbox
.select_set(self
._binary
_search
(self
.start
))
141 self
._selection
_changed
()
143 def show_window(self
, comp_lists
, index
, complete
, mode
, userWantsWin
):
144 """Show the autocomplete list, bind events.
145 If complete is True, complete the text, and if there is exactly one
146 matching completion, don't open a list."""
147 # Handle the start we already have
148 self
.completions
, self
.morecompletions
= comp_lists
150 self
.startindex
= self
.widget
.index(index
)
151 self
.start
= self
.widget
.get(self
.startindex
, "insert")
153 completed
= self
._complete
_string
(self
.start
)
154 self
._change
_start
(completed
)
155 i
= self
._binary
_search
(completed
)
156 if self
.completions
[i
] == completed
and \
157 (i
== len(self
.completions
)-1 or
158 self
.completions
[i
+1][:len(completed
)] != completed
):
159 # There is exactly one matching completion
161 self
.userwantswindow
= userWantsWin
162 self
.lasttypedstart
= self
.start
164 # Put widgets in place
165 self
.autocompletewindow
= acw
= Toplevel(self
.widget
)
166 # Put it in a position so that it is not seen.
167 acw
.wm_geometry("+10000+10000")
169 acw
.wm_overrideredirect(1)
171 # This command is only needed and available on Tk >= 8.4.0 for OSX
172 # Without it, call tips intrude on the typing process by grabbing
174 acw
.tk
.call("::tk::unsupported::MacWindowStyle", "style", acw
._w
,
175 "help", "noActivates")
178 self
.scrollbar
= scrollbar
= Scrollbar(acw
, orient
=VERTICAL
)
179 self
.listbox
= listbox
= Listbox(acw
, yscrollcommand
=scrollbar
.set,
180 exportselection
=False, bg
="white")
181 for item
in self
.completions
:
182 listbox
.insert(END
, item
)
183 self
.origselforeground
= listbox
.cget("selectforeground")
184 self
.origselbackground
= listbox
.cget("selectbackground")
185 scrollbar
.config(command
=listbox
.yview
)
186 scrollbar
.pack(side
=RIGHT
, fill
=Y
)
187 listbox
.pack(side
=LEFT
, fill
=BOTH
, expand
=True)
189 # Initialize the listbox selection
190 self
.listbox
.select_set(self
._binary
_search
(self
.start
))
191 self
._selection
_changed
()
194 self
.hideid
= self
.widget
.bind(HIDE_VIRTUAL_EVENT_NAME
,
196 for seq
in HIDE_SEQUENCES
:
197 self
.widget
.event_add(HIDE_VIRTUAL_EVENT_NAME
, seq
)
198 self
.keypressid
= self
.widget
.bind(KEYPRESS_VIRTUAL_EVENT_NAME
,
200 for seq
in KEYPRESS_SEQUENCES
:
201 self
.widget
.event_add(KEYPRESS_VIRTUAL_EVENT_NAME
, seq
)
202 self
.keyreleaseid
= self
.widget
.bind(KEYRELEASE_VIRTUAL_EVENT_NAME
,
203 self
.keyrelease_event
)
204 self
.widget
.event_add(KEYRELEASE_VIRTUAL_EVENT_NAME
,KEYRELEASE_SEQUENCE
)
205 self
.listupdateid
= listbox
.bind(LISTUPDATE_SEQUENCE
,
206 self
.listupdate_event
)
207 self
.winconfigid
= acw
.bind(WINCONFIG_SEQUENCE
, self
.winconfig_event
)
208 self
.doubleclickid
= listbox
.bind(DOUBLECLICK_SEQUENCE
,
209 self
.doubleclick_event
)
211 def winconfig_event(self
, event
):
212 if not self
.is_active():
214 # Position the completion list window
215 acw
= self
.autocompletewindow
216 self
.widget
.see(self
.startindex
)
217 x
, y
, cx
, cy
= self
.widget
.bbox(self
.startindex
)
218 acw
.wm_geometry("+%d+%d" % (x
+ self
.widget
.winfo_rootx(),
219 y
+ self
.widget
.winfo_rooty() \
220 -acw
.winfo_height()))
223 def hide_event(self
, event
):
224 if not self
.is_active():
228 def listupdate_event(self
, event
):
229 if not self
.is_active():
231 self
.userwantswindow
= True
232 self
._selection
_changed
()
234 def doubleclick_event(self
, event
):
235 # Put the selected completion in the text, and close the list
236 cursel
= int(self
.listbox
.curselection()[0])
237 self
._change
_start
(self
.completions
[cursel
])
240 def keypress_event(self
, event
):
241 if not self
.is_active():
243 keysym
= event
.keysym
244 if hasattr(event
, "mc_state"):
245 state
= event
.mc_state
249 if (len(keysym
) == 1 or keysym
in ("underscore", "BackSpace")
250 or (self
.mode
==AutoComplete
.COMPLETE_FILES
and keysym
in
251 ("period", "minus"))) \
252 and not (state
& ~MC_SHIFT
):
253 # Normal editing of text
255 self
._change
_start
(self
.start
+ keysym
)
256 elif keysym
== "underscore":
257 self
._change
_start
(self
.start
+ '_')
258 elif keysym
== "period":
259 self
._change
_start
(self
.start
+ '.')
260 elif keysym
== "minus":
261 self
._change
_start
(self
.start
+ '-')
263 # keysym == "BackSpace"
264 if len(self
.start
) == 0:
267 self
._change
_start
(self
.start
[:-1])
268 self
.lasttypedstart
= self
.start
269 self
.listbox
.select_clear(0, int(self
.listbox
.curselection()[0]))
270 self
.listbox
.select_set(self
._binary
_search
(self
.start
))
271 self
._selection
_changed
()
274 elif keysym
== "Return" and not state
:
275 # If start is a prefix of the selection, or there was an indication
276 # that the user used the completion window, put the selected
277 # completion in the text, and close the list.
278 # Otherwise, close the window and let the event through.
279 cursel
= int(self
.listbox
.curselection()[0])
280 if self
.completions
[cursel
][:len(self
.start
)] == self
.start
or \
281 self
.userwantswindow
:
282 self
._change
_start
(self
.completions
[cursel
])
289 elif (self
.mode
== AutoComplete
.COMPLETE_ATTRIBUTES
and keysym
in
290 ("period", "space", "parenleft", "parenright", "bracketleft",
291 "bracketright")) or \
292 (self
.mode
== AutoComplete
.COMPLETE_FILES
and keysym
in
293 ("slash", "backslash", "quotedbl", "apostrophe")) \
294 and not (state
& ~MC_SHIFT
):
295 # If start is a prefix of the selection, but is not '' when
296 # completing file names, put the whole
297 # selected completion. Anyway, close the list.
298 cursel
= int(self
.listbox
.curselection()[0])
299 if self
.completions
[cursel
][:len(self
.start
)] == self
.start \
300 and (self
.mode
==AutoComplete
.COMPLETE_ATTRIBUTES
or self
.start
):
301 self
._change
_start
(self
.completions
[cursel
])
305 elif keysym
in ("Home", "End", "Prior", "Next", "Up", "Down") and \
307 # Move the selection in the listbox
308 self
.userwantswindow
= True
309 cursel
= int(self
.listbox
.curselection()[0])
312 elif keysym
== "End":
313 newsel
= len(self
.completions
)-1
314 elif keysym
in ("Prior", "Next"):
315 jump
= self
.listbox
.nearest(self
.listbox
.winfo_height()) - \
316 self
.listbox
.nearest(0)
317 if keysym
== "Prior":
318 newsel
= max(0, cursel
-jump
)
320 assert keysym
== "Next"
321 newsel
= min(len(self
.completions
)-1, cursel
+jump
)
323 newsel
= max(0, cursel
-1)
325 assert keysym
== "Down"
326 newsel
= min(len(self
.completions
)-1, cursel
+1)
327 self
.listbox
.select_clear(cursel
)
328 self
.listbox
.select_set(newsel
)
329 self
._selection
_changed
()
332 elif (keysym
== "Tab" and not state
):
333 # The user wants a completion, but it is handled by AutoComplete
334 # (not AutoCompleteWindow), so ignore.
335 self
.userwantswindow
= True
338 elif reduce(lambda x
, y
: x
or y
,
339 [keysym
.find(s
) != -1 for s
in ("Shift", "Control", "Alt",
340 "Meta", "Command", "Option")
342 # A modifier key, so ignore
346 # Unknown event, close the window and let it through.
350 def keyrelease_event(self
, event
):
351 if not self
.is_active():
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
359 return self
.autocompletewindow
is not None
362 self
._change
_start
(self
._complete
_string
(self
.start
))
363 # The selection doesn't change.
365 def hide_window(self
):
366 if not self
.is_active():
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
)
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
,
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
388 self
.scrollbar
.destroy()
389 self
.scrollbar
= None
390 self
.listbox
.destroy()
392 self
.autocompletewindow
.destroy()
393 self
.autocompletewindow
= None