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