annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/sidebar.py @ 69:33d812a61356

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