Mercurial > repos > rliterman > csp2
comparison CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/sidebar.py @ 68:5028fdace37b
planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author | jpayne |
---|---|
date | Tue, 18 Mar 2025 16:23:26 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
67:0e9998148a16 | 68:5028fdace37b |
---|---|
1 """Line numbering implementation for IDLE as an extension. | |
2 Includes BaseSideBar which can be extended for other sidebar based extensions | |
3 """ | |
4 import functools | |
5 import itertools | |
6 | |
7 import tkinter as tk | |
8 from idlelib.config import idleConf | |
9 from idlelib.delegator import Delegator | |
10 | |
11 | |
12 def get_end_linenumber(text): | |
13 """Utility to get the last line's number in a Tk text widget.""" | |
14 return int(float(text.index('end-1c'))) | |
15 | |
16 | |
17 def get_widget_padding(widget): | |
18 """Get the total padding of a Tk widget, including its border.""" | |
19 # TODO: use also in codecontext.py | |
20 manager = widget.winfo_manager() | |
21 if manager == 'pack': | |
22 info = widget.pack_info() | |
23 elif manager == 'grid': | |
24 info = widget.grid_info() | |
25 else: | |
26 raise ValueError(f"Unsupported geometry manager: {manager}") | |
27 | |
28 # All values are passed through getint(), since some | |
29 # values may be pixel objects, which can't simply be added to ints. | |
30 padx = sum(map(widget.tk.getint, [ | |
31 info['padx'], | |
32 widget.cget('padx'), | |
33 widget.cget('border'), | |
34 ])) | |
35 pady = sum(map(widget.tk.getint, [ | |
36 info['pady'], | |
37 widget.cget('pady'), | |
38 widget.cget('border'), | |
39 ])) | |
40 return padx, pady | |
41 | |
42 | |
43 class BaseSideBar: | |
44 """ | |
45 The base class for extensions which require a sidebar. | |
46 """ | |
47 def __init__(self, editwin): | |
48 self.editwin = editwin | |
49 self.parent = editwin.text_frame | |
50 self.text = editwin.text | |
51 | |
52 _padx, pady = get_widget_padding(self.text) | |
53 self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE, | |
54 padx=2, pady=pady, | |
55 borderwidth=0, highlightthickness=0) | |
56 self.sidebar_text.config(state=tk.DISABLED) | |
57 self.text['yscrollcommand'] = self.redirect_yscroll_event | |
58 self.update_font() | |
59 self.update_colors() | |
60 | |
61 self.is_shown = False | |
62 | |
63 def update_font(self): | |
64 """Update the sidebar text font, usually after config changes.""" | |
65 font = idleConf.GetFont(self.text, 'main', 'EditorWindow') | |
66 self._update_font(font) | |
67 | |
68 def _update_font(self, font): | |
69 self.sidebar_text['font'] = font | |
70 | |
71 def update_colors(self): | |
72 """Update the sidebar text colors, usually after config changes.""" | |
73 colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'normal') | |
74 self._update_colors(foreground=colors['foreground'], | |
75 background=colors['background']) | |
76 | |
77 def _update_colors(self, foreground, background): | |
78 self.sidebar_text.config( | |
79 fg=foreground, bg=background, | |
80 selectforeground=foreground, selectbackground=background, | |
81 inactiveselectbackground=background, | |
82 ) | |
83 | |
84 def show_sidebar(self): | |
85 if not self.is_shown: | |
86 self.sidebar_text.grid(row=1, column=0, sticky=tk.NSEW) | |
87 self.is_shown = True | |
88 | |
89 def hide_sidebar(self): | |
90 if self.is_shown: | |
91 self.sidebar_text.grid_forget() | |
92 self.is_shown = False | |
93 | |
94 def redirect_yscroll_event(self, *args, **kwargs): | |
95 """Redirect vertical scrolling to the main editor text widget. | |
96 | |
97 The scroll bar is also updated. | |
98 """ | |
99 self.editwin.vbar.set(*args) | |
100 self.sidebar_text.yview_moveto(args[0]) | |
101 return 'break' | |
102 | |
103 def redirect_focusin_event(self, event): | |
104 """Redirect focus-in events to the main editor text widget.""" | |
105 self.text.focus_set() | |
106 return 'break' | |
107 | |
108 def redirect_mousebutton_event(self, event, event_name): | |
109 """Redirect mouse button events to the main editor text widget.""" | |
110 self.text.focus_set() | |
111 self.text.event_generate(event_name, x=0, y=event.y) | |
112 return 'break' | |
113 | |
114 def redirect_mousewheel_event(self, event): | |
115 """Redirect mouse wheel events to the editwin text widget.""" | |
116 self.text.event_generate('<MouseWheel>', | |
117 x=0, y=event.y, delta=event.delta) | |
118 return 'break' | |
119 | |
120 | |
121 class EndLineDelegator(Delegator): | |
122 """Generate callbacks with the current end line number after | |
123 insert or delete operations""" | |
124 def __init__(self, changed_callback): | |
125 """ | |
126 changed_callback - Callable, will be called after insert | |
127 or delete operations with the current | |
128 end line number. | |
129 """ | |
130 Delegator.__init__(self) | |
131 self.changed_callback = changed_callback | |
132 | |
133 def insert(self, index, chars, tags=None): | |
134 self.delegate.insert(index, chars, tags) | |
135 self.changed_callback(get_end_linenumber(self.delegate)) | |
136 | |
137 def delete(self, index1, index2=None): | |
138 self.delegate.delete(index1, index2) | |
139 self.changed_callback(get_end_linenumber(self.delegate)) | |
140 | |
141 | |
142 class LineNumbers(BaseSideBar): | |
143 """Line numbers support for editor windows.""" | |
144 def __init__(self, editwin): | |
145 BaseSideBar.__init__(self, editwin) | |
146 self.prev_end = 1 | |
147 self._sidebar_width_type = type(self.sidebar_text['width']) | |
148 self.sidebar_text.config(state=tk.NORMAL) | |
149 self.sidebar_text.insert('insert', '1', 'linenumber') | |
150 self.sidebar_text.config(state=tk.DISABLED) | |
151 self.sidebar_text.config(takefocus=False, exportselection=False) | |
152 self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT) | |
153 | |
154 self.bind_events() | |
155 | |
156 end = get_end_linenumber(self.text) | |
157 self.update_sidebar_text(end) | |
158 | |
159 end_line_delegator = EndLineDelegator(self.update_sidebar_text) | |
160 # Insert the delegator after the undo delegator, so that line numbers | |
161 # are properly updated after undo and redo actions. | |
162 end_line_delegator.setdelegate(self.editwin.undo.delegate) | |
163 self.editwin.undo.setdelegate(end_line_delegator) | |
164 # Reset the delegator caches of the delegators "above" the | |
165 # end line delegator we just inserted. | |
166 delegator = self.editwin.per.top | |
167 while delegator is not end_line_delegator: | |
168 delegator.resetcache() | |
169 delegator = delegator.delegate | |
170 | |
171 self.is_shown = False | |
172 | |
173 def bind_events(self): | |
174 # Ensure focus is always redirected to the main editor text widget. | |
175 self.sidebar_text.bind('<FocusIn>', self.redirect_focusin_event) | |
176 | |
177 # Redirect mouse scrolling to the main editor text widget. | |
178 # | |
179 # Note that without this, scrolling with the mouse only scrolls | |
180 # the line numbers. | |
181 self.sidebar_text.bind('<MouseWheel>', self.redirect_mousewheel_event) | |
182 | |
183 # Redirect mouse button events to the main editor text widget, | |
184 # except for the left mouse button (1). | |
185 # | |
186 # Note: X-11 sends Button-4 and Button-5 events for the scroll wheel. | |
187 def bind_mouse_event(event_name, target_event_name): | |
188 handler = functools.partial(self.redirect_mousebutton_event, | |
189 event_name=target_event_name) | |
190 self.sidebar_text.bind(event_name, handler) | |
191 | |
192 for button in [2, 3, 4, 5]: | |
193 for event_name in (f'<Button-{button}>', | |
194 f'<ButtonRelease-{button}>', | |
195 f'<B{button}-Motion>', | |
196 ): | |
197 bind_mouse_event(event_name, target_event_name=event_name) | |
198 | |
199 # Convert double- and triple-click events to normal click events, | |
200 # since event_generate() doesn't allow generating such events. | |
201 for event_name in (f'<Double-Button-{button}>', | |
202 f'<Triple-Button-{button}>', | |
203 ): | |
204 bind_mouse_event(event_name, | |
205 target_event_name=f'<Button-{button}>') | |
206 | |
207 # This is set by b1_mousedown_handler() and read by | |
208 # drag_update_selection_and_insert_mark(), to know where dragging | |
209 # began. | |
210 start_line = None | |
211 # These are set by b1_motion_handler() and read by selection_handler(). | |
212 # last_y is passed this way since the mouse Y-coordinate is not | |
213 # available on selection event objects. last_yview is passed this way | |
214 # to recognize scrolling while the mouse isn't moving. | |
215 last_y = last_yview = None | |
216 | |
217 def b1_mousedown_handler(event): | |
218 # select the entire line | |
219 lineno = int(float(self.sidebar_text.index(f"@0,{event.y}"))) | |
220 self.text.tag_remove("sel", "1.0", "end") | |
221 self.text.tag_add("sel", f"{lineno}.0", f"{lineno+1}.0") | |
222 self.text.mark_set("insert", f"{lineno+1}.0") | |
223 | |
224 # remember this line in case this is the beginning of dragging | |
225 nonlocal start_line | |
226 start_line = lineno | |
227 self.sidebar_text.bind('<Button-1>', b1_mousedown_handler) | |
228 | |
229 def b1_mouseup_handler(event): | |
230 # On mouse up, we're no longer dragging. Set the shared persistent | |
231 # variables to None to represent this. | |
232 nonlocal start_line | |
233 nonlocal last_y | |
234 nonlocal last_yview | |
235 start_line = None | |
236 last_y = None | |
237 last_yview = None | |
238 self.sidebar_text.bind('<ButtonRelease-1>', b1_mouseup_handler) | |
239 | |
240 def drag_update_selection_and_insert_mark(y_coord): | |
241 """Helper function for drag and selection event handlers.""" | |
242 lineno = int(float(self.sidebar_text.index(f"@0,{y_coord}"))) | |
243 a, b = sorted([start_line, lineno]) | |
244 self.text.tag_remove("sel", "1.0", "end") | |
245 self.text.tag_add("sel", f"{a}.0", f"{b+1}.0") | |
246 self.text.mark_set("insert", | |
247 f"{lineno if lineno == a else lineno + 1}.0") | |
248 | |
249 # Special handling of dragging with mouse button 1. In "normal" text | |
250 # widgets this selects text, but the line numbers text widget has | |
251 # selection disabled. Still, dragging triggers some selection-related | |
252 # functionality under the hood. Specifically, dragging to above or | |
253 # below the text widget triggers scrolling, in a way that bypasses the | |
254 # other scrolling synchronization mechanisms.i | |
255 def b1_drag_handler(event, *args): | |
256 nonlocal last_y | |
257 nonlocal last_yview | |
258 last_y = event.y | |
259 last_yview = self.sidebar_text.yview() | |
260 if not 0 <= last_y <= self.sidebar_text.winfo_height(): | |
261 self.text.yview_moveto(last_yview[0]) | |
262 drag_update_selection_and_insert_mark(event.y) | |
263 self.sidebar_text.bind('<B1-Motion>', b1_drag_handler) | |
264 | |
265 # With mouse-drag scrolling fixed by the above, there is still an edge- | |
266 # case we need to handle: When drag-scrolling, scrolling can continue | |
267 # while the mouse isn't moving, leading to the above fix not scrolling | |
268 # properly. | |
269 def selection_handler(event): | |
270 if last_yview is None: | |
271 # This logic is only needed while dragging. | |
272 return | |
273 yview = self.sidebar_text.yview() | |
274 if yview != last_yview: | |
275 self.text.yview_moveto(yview[0]) | |
276 drag_update_selection_and_insert_mark(last_y) | |
277 self.sidebar_text.bind('<<Selection>>', selection_handler) | |
278 | |
279 def update_colors(self): | |
280 """Update the sidebar text colors, usually after config changes.""" | |
281 colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') | |
282 self._update_colors(foreground=colors['foreground'], | |
283 background=colors['background']) | |
284 | |
285 def update_sidebar_text(self, end): | |
286 """ | |
287 Perform the following action: | |
288 Each line sidebar_text contains the linenumber for that line | |
289 Synchronize with editwin.text so that both sidebar_text and | |
290 editwin.text contain the same number of lines""" | |
291 if end == self.prev_end: | |
292 return | |
293 | |
294 width_difference = len(str(end)) - len(str(self.prev_end)) | |
295 if width_difference: | |
296 cur_width = int(float(self.sidebar_text['width'])) | |
297 new_width = cur_width + width_difference | |
298 self.sidebar_text['width'] = self._sidebar_width_type(new_width) | |
299 | |
300 self.sidebar_text.config(state=tk.NORMAL) | |
301 if end > self.prev_end: | |
302 new_text = '\n'.join(itertools.chain( | |
303 [''], | |
304 map(str, range(self.prev_end + 1, end + 1)), | |
305 )) | |
306 self.sidebar_text.insert(f'end -1c', new_text, 'linenumber') | |
307 else: | |
308 self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c') | |
309 self.sidebar_text.config(state=tk.DISABLED) | |
310 | |
311 self.prev_end = end | |
312 | |
313 | |
314 def _linenumbers_drag_scrolling(parent): # htest # | |
315 from idlelib.idle_test.test_sidebar import Dummy_editwin | |
316 | |
317 toplevel = tk.Toplevel(parent) | |
318 text_frame = tk.Frame(toplevel) | |
319 text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) | |
320 text_frame.rowconfigure(1, weight=1) | |
321 text_frame.columnconfigure(1, weight=1) | |
322 | |
323 font = idleConf.GetFont(toplevel, 'main', 'EditorWindow') | |
324 text = tk.Text(text_frame, width=80, height=24, wrap=tk.NONE, font=font) | |
325 text.grid(row=1, column=1, sticky=tk.NSEW) | |
326 | |
327 editwin = Dummy_editwin(text) | |
328 editwin.vbar = tk.Scrollbar(text_frame) | |
329 | |
330 linenumbers = LineNumbers(editwin) | |
331 linenumbers.show_sidebar() | |
332 | |
333 text.insert('1.0', '\n'.join('a'*i for i in range(1, 101))) | |
334 | |
335 | |
336 if __name__ == '__main__': | |
337 from unittest import main | |
338 main('idlelib.idle_test.test_sidebar', verbosity=2, exit=False) | |
339 | |
340 from idlelib.idle_test.htest import run | |
341 run(_linenumbers_drag_scrolling) |