17db96d56Sopenharmony_ci"""Line numbering implementation for IDLE as an extension. 27db96d56Sopenharmony_ciIncludes BaseSideBar which can be extended for other sidebar based extensions 37db96d56Sopenharmony_ci""" 47db96d56Sopenharmony_ciimport contextlib 57db96d56Sopenharmony_ciimport functools 67db96d56Sopenharmony_ciimport itertools 77db96d56Sopenharmony_ci 87db96d56Sopenharmony_ciimport tkinter as tk 97db96d56Sopenharmony_cifrom tkinter.font import Font 107db96d56Sopenharmony_cifrom idlelib.config import idleConf 117db96d56Sopenharmony_cifrom idlelib.delegator import Delegator 127db96d56Sopenharmony_cifrom idlelib import macosx 137db96d56Sopenharmony_ci 147db96d56Sopenharmony_ci 157db96d56Sopenharmony_cidef get_lineno(text, index): 167db96d56Sopenharmony_ci """Return the line number of an index in a Tk text widget.""" 177db96d56Sopenharmony_ci text_index = text.index(index) 187db96d56Sopenharmony_ci return int(float(text_index)) if text_index else None 197db96d56Sopenharmony_ci 207db96d56Sopenharmony_ci 217db96d56Sopenharmony_cidef get_end_linenumber(text): 227db96d56Sopenharmony_ci """Return the number of the last line in a Tk text widget.""" 237db96d56Sopenharmony_ci return get_lineno(text, 'end-1c') 247db96d56Sopenharmony_ci 257db96d56Sopenharmony_ci 267db96d56Sopenharmony_cidef get_displaylines(text, index): 277db96d56Sopenharmony_ci """Display height, in lines, of a logical line in a Tk text widget.""" 287db96d56Sopenharmony_ci res = text.count(f"{index} linestart", 297db96d56Sopenharmony_ci f"{index} lineend", 307db96d56Sopenharmony_ci "displaylines") 317db96d56Sopenharmony_ci return res[0] if res else 0 327db96d56Sopenharmony_ci 337db96d56Sopenharmony_cidef get_widget_padding(widget): 347db96d56Sopenharmony_ci """Get the total padding of a Tk widget, including its border.""" 357db96d56Sopenharmony_ci # TODO: use also in codecontext.py 367db96d56Sopenharmony_ci manager = widget.winfo_manager() 377db96d56Sopenharmony_ci if manager == 'pack': 387db96d56Sopenharmony_ci info = widget.pack_info() 397db96d56Sopenharmony_ci elif manager == 'grid': 407db96d56Sopenharmony_ci info = widget.grid_info() 417db96d56Sopenharmony_ci else: 427db96d56Sopenharmony_ci raise ValueError(f"Unsupported geometry manager: {manager}") 437db96d56Sopenharmony_ci 447db96d56Sopenharmony_ci # All values are passed through getint(), since some 457db96d56Sopenharmony_ci # values may be pixel objects, which can't simply be added to ints. 467db96d56Sopenharmony_ci padx = sum(map(widget.tk.getint, [ 477db96d56Sopenharmony_ci info['padx'], 487db96d56Sopenharmony_ci widget.cget('padx'), 497db96d56Sopenharmony_ci widget.cget('border'), 507db96d56Sopenharmony_ci ])) 517db96d56Sopenharmony_ci pady = sum(map(widget.tk.getint, [ 527db96d56Sopenharmony_ci info['pady'], 537db96d56Sopenharmony_ci widget.cget('pady'), 547db96d56Sopenharmony_ci widget.cget('border'), 557db96d56Sopenharmony_ci ])) 567db96d56Sopenharmony_ci return padx, pady 577db96d56Sopenharmony_ci 587db96d56Sopenharmony_ci 597db96d56Sopenharmony_ci@contextlib.contextmanager 607db96d56Sopenharmony_cidef temp_enable_text_widget(text): 617db96d56Sopenharmony_ci text.configure(state=tk.NORMAL) 627db96d56Sopenharmony_ci try: 637db96d56Sopenharmony_ci yield 647db96d56Sopenharmony_ci finally: 657db96d56Sopenharmony_ci text.configure(state=tk.DISABLED) 667db96d56Sopenharmony_ci 677db96d56Sopenharmony_ci 687db96d56Sopenharmony_ciclass BaseSideBar: 697db96d56Sopenharmony_ci """A base class for sidebars using Text.""" 707db96d56Sopenharmony_ci def __init__(self, editwin): 717db96d56Sopenharmony_ci self.editwin = editwin 727db96d56Sopenharmony_ci self.parent = editwin.text_frame 737db96d56Sopenharmony_ci self.text = editwin.text 747db96d56Sopenharmony_ci 757db96d56Sopenharmony_ci self.is_shown = False 767db96d56Sopenharmony_ci 777db96d56Sopenharmony_ci self.main_widget = self.init_widgets() 787db96d56Sopenharmony_ci 797db96d56Sopenharmony_ci self.bind_events() 807db96d56Sopenharmony_ci 817db96d56Sopenharmony_ci self.update_font() 827db96d56Sopenharmony_ci self.update_colors() 837db96d56Sopenharmony_ci 847db96d56Sopenharmony_ci def init_widgets(self): 857db96d56Sopenharmony_ci """Initialize the sidebar's widgets, returning the main widget.""" 867db96d56Sopenharmony_ci raise NotImplementedError 877db96d56Sopenharmony_ci 887db96d56Sopenharmony_ci def update_font(self): 897db96d56Sopenharmony_ci """Update the sidebar text font, usually after config changes.""" 907db96d56Sopenharmony_ci raise NotImplementedError 917db96d56Sopenharmony_ci 927db96d56Sopenharmony_ci def update_colors(self): 937db96d56Sopenharmony_ci """Update the sidebar text colors, usually after config changes.""" 947db96d56Sopenharmony_ci raise NotImplementedError 957db96d56Sopenharmony_ci 967db96d56Sopenharmony_ci def grid(self): 977db96d56Sopenharmony_ci """Layout the widget, always using grid layout.""" 987db96d56Sopenharmony_ci raise NotImplementedError 997db96d56Sopenharmony_ci 1007db96d56Sopenharmony_ci def show_sidebar(self): 1017db96d56Sopenharmony_ci if not self.is_shown: 1027db96d56Sopenharmony_ci self.grid() 1037db96d56Sopenharmony_ci self.is_shown = True 1047db96d56Sopenharmony_ci 1057db96d56Sopenharmony_ci def hide_sidebar(self): 1067db96d56Sopenharmony_ci if self.is_shown: 1077db96d56Sopenharmony_ci self.main_widget.grid_forget() 1087db96d56Sopenharmony_ci self.is_shown = False 1097db96d56Sopenharmony_ci 1107db96d56Sopenharmony_ci def yscroll_event(self, *args, **kwargs): 1117db96d56Sopenharmony_ci """Hook for vertical scrolling for sub-classes to override.""" 1127db96d56Sopenharmony_ci raise NotImplementedError 1137db96d56Sopenharmony_ci 1147db96d56Sopenharmony_ci def redirect_yscroll_event(self, *args, **kwargs): 1157db96d56Sopenharmony_ci """Redirect vertical scrolling to the main editor text widget. 1167db96d56Sopenharmony_ci 1177db96d56Sopenharmony_ci The scroll bar is also updated. 1187db96d56Sopenharmony_ci """ 1197db96d56Sopenharmony_ci self.editwin.vbar.set(*args) 1207db96d56Sopenharmony_ci return self.yscroll_event(*args, **kwargs) 1217db96d56Sopenharmony_ci 1227db96d56Sopenharmony_ci def redirect_focusin_event(self, event): 1237db96d56Sopenharmony_ci """Redirect focus-in events to the main editor text widget.""" 1247db96d56Sopenharmony_ci self.text.focus_set() 1257db96d56Sopenharmony_ci return 'break' 1267db96d56Sopenharmony_ci 1277db96d56Sopenharmony_ci def redirect_mousebutton_event(self, event, event_name): 1287db96d56Sopenharmony_ci """Redirect mouse button events to the main editor text widget.""" 1297db96d56Sopenharmony_ci self.text.focus_set() 1307db96d56Sopenharmony_ci self.text.event_generate(event_name, x=0, y=event.y) 1317db96d56Sopenharmony_ci return 'break' 1327db96d56Sopenharmony_ci 1337db96d56Sopenharmony_ci def redirect_mousewheel_event(self, event): 1347db96d56Sopenharmony_ci """Redirect mouse wheel events to the editwin text widget.""" 1357db96d56Sopenharmony_ci self.text.event_generate('<MouseWheel>', 1367db96d56Sopenharmony_ci x=0, y=event.y, delta=event.delta) 1377db96d56Sopenharmony_ci return 'break' 1387db96d56Sopenharmony_ci 1397db96d56Sopenharmony_ci def bind_events(self): 1407db96d56Sopenharmony_ci self.text['yscrollcommand'] = self.redirect_yscroll_event 1417db96d56Sopenharmony_ci 1427db96d56Sopenharmony_ci # Ensure focus is always redirected to the main editor text widget. 1437db96d56Sopenharmony_ci self.main_widget.bind('<FocusIn>', self.redirect_focusin_event) 1447db96d56Sopenharmony_ci 1457db96d56Sopenharmony_ci # Redirect mouse scrolling to the main editor text widget. 1467db96d56Sopenharmony_ci # 1477db96d56Sopenharmony_ci # Note that without this, scrolling with the mouse only scrolls 1487db96d56Sopenharmony_ci # the line numbers. 1497db96d56Sopenharmony_ci self.main_widget.bind('<MouseWheel>', self.redirect_mousewheel_event) 1507db96d56Sopenharmony_ci 1517db96d56Sopenharmony_ci # Redirect mouse button events to the main editor text widget, 1527db96d56Sopenharmony_ci # except for the left mouse button (1). 1537db96d56Sopenharmony_ci # 1547db96d56Sopenharmony_ci # Note: X-11 sends Button-4 and Button-5 events for the scroll wheel. 1557db96d56Sopenharmony_ci def bind_mouse_event(event_name, target_event_name): 1567db96d56Sopenharmony_ci handler = functools.partial(self.redirect_mousebutton_event, 1577db96d56Sopenharmony_ci event_name=target_event_name) 1587db96d56Sopenharmony_ci self.main_widget.bind(event_name, handler) 1597db96d56Sopenharmony_ci 1607db96d56Sopenharmony_ci for button in [2, 3, 4, 5]: 1617db96d56Sopenharmony_ci for event_name in (f'<Button-{button}>', 1627db96d56Sopenharmony_ci f'<ButtonRelease-{button}>', 1637db96d56Sopenharmony_ci f'<B{button}-Motion>', 1647db96d56Sopenharmony_ci ): 1657db96d56Sopenharmony_ci bind_mouse_event(event_name, target_event_name=event_name) 1667db96d56Sopenharmony_ci 1677db96d56Sopenharmony_ci # Convert double- and triple-click events to normal click events, 1687db96d56Sopenharmony_ci # since event_generate() doesn't allow generating such events. 1697db96d56Sopenharmony_ci for event_name in (f'<Double-Button-{button}>', 1707db96d56Sopenharmony_ci f'<Triple-Button-{button}>', 1717db96d56Sopenharmony_ci ): 1727db96d56Sopenharmony_ci bind_mouse_event(event_name, 1737db96d56Sopenharmony_ci target_event_name=f'<Button-{button}>') 1747db96d56Sopenharmony_ci 1757db96d56Sopenharmony_ci # start_line is set upon <Button-1> to allow selecting a range of rows 1767db96d56Sopenharmony_ci # by dragging. It is cleared upon <ButtonRelease-1>. 1777db96d56Sopenharmony_ci start_line = None 1787db96d56Sopenharmony_ci 1797db96d56Sopenharmony_ci # last_y is initially set upon <B1-Leave> and is continuously updated 1807db96d56Sopenharmony_ci # upon <B1-Motion>, until <B1-Enter> or the mouse button is released. 1817db96d56Sopenharmony_ci # It is used in text_auto_scroll(), which is called repeatedly and 1827db96d56Sopenharmony_ci # does have a mouse event available. 1837db96d56Sopenharmony_ci last_y = None 1847db96d56Sopenharmony_ci 1857db96d56Sopenharmony_ci # auto_scrolling_after_id is set whenever text_auto_scroll is 1867db96d56Sopenharmony_ci # scheduled via .after(). It is used to stop the auto-scrolling 1877db96d56Sopenharmony_ci # upon <B1-Enter>, as well as to avoid scheduling the function several 1887db96d56Sopenharmony_ci # times in parallel. 1897db96d56Sopenharmony_ci auto_scrolling_after_id = None 1907db96d56Sopenharmony_ci 1917db96d56Sopenharmony_ci def drag_update_selection_and_insert_mark(y_coord): 1927db96d56Sopenharmony_ci """Helper function for drag and selection event handlers.""" 1937db96d56Sopenharmony_ci lineno = get_lineno(self.text, f"@0,{y_coord}") 1947db96d56Sopenharmony_ci a, b = sorted([start_line, lineno]) 1957db96d56Sopenharmony_ci self.text.tag_remove("sel", "1.0", "end") 1967db96d56Sopenharmony_ci self.text.tag_add("sel", f"{a}.0", f"{b+1}.0") 1977db96d56Sopenharmony_ci self.text.mark_set("insert", 1987db96d56Sopenharmony_ci f"{lineno if lineno == a else lineno + 1}.0") 1997db96d56Sopenharmony_ci 2007db96d56Sopenharmony_ci def b1_mousedown_handler(event): 2017db96d56Sopenharmony_ci nonlocal start_line 2027db96d56Sopenharmony_ci nonlocal last_y 2037db96d56Sopenharmony_ci start_line = int(float(self.text.index(f"@0,{event.y}"))) 2047db96d56Sopenharmony_ci last_y = event.y 2057db96d56Sopenharmony_ci 2067db96d56Sopenharmony_ci drag_update_selection_and_insert_mark(event.y) 2077db96d56Sopenharmony_ci self.main_widget.bind('<Button-1>', b1_mousedown_handler) 2087db96d56Sopenharmony_ci 2097db96d56Sopenharmony_ci def b1_mouseup_handler(event): 2107db96d56Sopenharmony_ci # On mouse up, we're no longer dragging. Set the shared persistent 2117db96d56Sopenharmony_ci # variables to None to represent this. 2127db96d56Sopenharmony_ci nonlocal start_line 2137db96d56Sopenharmony_ci nonlocal last_y 2147db96d56Sopenharmony_ci start_line = None 2157db96d56Sopenharmony_ci last_y = None 2167db96d56Sopenharmony_ci self.text.event_generate('<ButtonRelease-1>', x=0, y=event.y) 2177db96d56Sopenharmony_ci self.main_widget.bind('<ButtonRelease-1>', b1_mouseup_handler) 2187db96d56Sopenharmony_ci 2197db96d56Sopenharmony_ci def b1_drag_handler(event): 2207db96d56Sopenharmony_ci nonlocal last_y 2217db96d56Sopenharmony_ci if last_y is None: # i.e. if not currently dragging 2227db96d56Sopenharmony_ci return 2237db96d56Sopenharmony_ci last_y = event.y 2247db96d56Sopenharmony_ci drag_update_selection_and_insert_mark(event.y) 2257db96d56Sopenharmony_ci self.main_widget.bind('<B1-Motion>', b1_drag_handler) 2267db96d56Sopenharmony_ci 2277db96d56Sopenharmony_ci def text_auto_scroll(): 2287db96d56Sopenharmony_ci """Mimic Text auto-scrolling when dragging outside of it.""" 2297db96d56Sopenharmony_ci # See: https://github.com/tcltk/tk/blob/064ff9941b4b80b85916a8afe86a6c21fd388b54/library/text.tcl#L670 2307db96d56Sopenharmony_ci nonlocal auto_scrolling_after_id 2317db96d56Sopenharmony_ci y = last_y 2327db96d56Sopenharmony_ci if y is None: 2337db96d56Sopenharmony_ci self.main_widget.after_cancel(auto_scrolling_after_id) 2347db96d56Sopenharmony_ci auto_scrolling_after_id = None 2357db96d56Sopenharmony_ci return 2367db96d56Sopenharmony_ci elif y < 0: 2377db96d56Sopenharmony_ci self.text.yview_scroll(-1 + y, 'pixels') 2387db96d56Sopenharmony_ci drag_update_selection_and_insert_mark(y) 2397db96d56Sopenharmony_ci elif y > self.main_widget.winfo_height(): 2407db96d56Sopenharmony_ci self.text.yview_scroll(1 + y - self.main_widget.winfo_height(), 2417db96d56Sopenharmony_ci 'pixels') 2427db96d56Sopenharmony_ci drag_update_selection_and_insert_mark(y) 2437db96d56Sopenharmony_ci auto_scrolling_after_id = \ 2447db96d56Sopenharmony_ci self.main_widget.after(50, text_auto_scroll) 2457db96d56Sopenharmony_ci 2467db96d56Sopenharmony_ci def b1_leave_handler(event): 2477db96d56Sopenharmony_ci # Schedule the initial call to text_auto_scroll(), if not already 2487db96d56Sopenharmony_ci # scheduled. 2497db96d56Sopenharmony_ci nonlocal auto_scrolling_after_id 2507db96d56Sopenharmony_ci if auto_scrolling_after_id is None: 2517db96d56Sopenharmony_ci nonlocal last_y 2527db96d56Sopenharmony_ci last_y = event.y 2537db96d56Sopenharmony_ci auto_scrolling_after_id = \ 2547db96d56Sopenharmony_ci self.main_widget.after(0, text_auto_scroll) 2557db96d56Sopenharmony_ci self.main_widget.bind('<B1-Leave>', b1_leave_handler) 2567db96d56Sopenharmony_ci 2577db96d56Sopenharmony_ci def b1_enter_handler(event): 2587db96d56Sopenharmony_ci # Cancel the scheduling of text_auto_scroll(), if it exists. 2597db96d56Sopenharmony_ci nonlocal auto_scrolling_after_id 2607db96d56Sopenharmony_ci if auto_scrolling_after_id is not None: 2617db96d56Sopenharmony_ci self.main_widget.after_cancel(auto_scrolling_after_id) 2627db96d56Sopenharmony_ci auto_scrolling_after_id = None 2637db96d56Sopenharmony_ci self.main_widget.bind('<B1-Enter>', b1_enter_handler) 2647db96d56Sopenharmony_ci 2657db96d56Sopenharmony_ci 2667db96d56Sopenharmony_ciclass EndLineDelegator(Delegator): 2677db96d56Sopenharmony_ci """Generate callbacks with the current end line number. 2687db96d56Sopenharmony_ci 2697db96d56Sopenharmony_ci The provided callback is called after every insert and delete. 2707db96d56Sopenharmony_ci """ 2717db96d56Sopenharmony_ci def __init__(self, changed_callback): 2727db96d56Sopenharmony_ci Delegator.__init__(self) 2737db96d56Sopenharmony_ci self.changed_callback = changed_callback 2747db96d56Sopenharmony_ci 2757db96d56Sopenharmony_ci def insert(self, index, chars, tags=None): 2767db96d56Sopenharmony_ci self.delegate.insert(index, chars, tags) 2777db96d56Sopenharmony_ci self.changed_callback(get_end_linenumber(self.delegate)) 2787db96d56Sopenharmony_ci 2797db96d56Sopenharmony_ci def delete(self, index1, index2=None): 2807db96d56Sopenharmony_ci self.delegate.delete(index1, index2) 2817db96d56Sopenharmony_ci self.changed_callback(get_end_linenumber(self.delegate)) 2827db96d56Sopenharmony_ci 2837db96d56Sopenharmony_ci 2847db96d56Sopenharmony_ciclass LineNumbers(BaseSideBar): 2857db96d56Sopenharmony_ci """Line numbers support for editor windows.""" 2867db96d56Sopenharmony_ci def __init__(self, editwin): 2877db96d56Sopenharmony_ci super().__init__(editwin) 2887db96d56Sopenharmony_ci 2897db96d56Sopenharmony_ci end_line_delegator = EndLineDelegator(self.update_sidebar_text) 2907db96d56Sopenharmony_ci # Insert the delegator after the undo delegator, so that line numbers 2917db96d56Sopenharmony_ci # are properly updated after undo and redo actions. 2927db96d56Sopenharmony_ci self.editwin.per.insertfilterafter(end_line_delegator, 2937db96d56Sopenharmony_ci after=self.editwin.undo) 2947db96d56Sopenharmony_ci 2957db96d56Sopenharmony_ci def init_widgets(self): 2967db96d56Sopenharmony_ci _padx, pady = get_widget_padding(self.text) 2977db96d56Sopenharmony_ci self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE, 2987db96d56Sopenharmony_ci padx=2, pady=pady, 2997db96d56Sopenharmony_ci borderwidth=0, highlightthickness=0) 3007db96d56Sopenharmony_ci self.sidebar_text.config(state=tk.DISABLED) 3017db96d56Sopenharmony_ci 3027db96d56Sopenharmony_ci self.prev_end = 1 3037db96d56Sopenharmony_ci self._sidebar_width_type = type(self.sidebar_text['width']) 3047db96d56Sopenharmony_ci with temp_enable_text_widget(self.sidebar_text): 3057db96d56Sopenharmony_ci self.sidebar_text.insert('insert', '1', 'linenumber') 3067db96d56Sopenharmony_ci self.sidebar_text.config(takefocus=False, exportselection=False) 3077db96d56Sopenharmony_ci self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT) 3087db96d56Sopenharmony_ci 3097db96d56Sopenharmony_ci end = get_end_linenumber(self.text) 3107db96d56Sopenharmony_ci self.update_sidebar_text(end) 3117db96d56Sopenharmony_ci 3127db96d56Sopenharmony_ci return self.sidebar_text 3137db96d56Sopenharmony_ci 3147db96d56Sopenharmony_ci def grid(self): 3157db96d56Sopenharmony_ci self.sidebar_text.grid(row=1, column=0, sticky=tk.NSEW) 3167db96d56Sopenharmony_ci 3177db96d56Sopenharmony_ci def update_font(self): 3187db96d56Sopenharmony_ci font = idleConf.GetFont(self.text, 'main', 'EditorWindow') 3197db96d56Sopenharmony_ci self.sidebar_text['font'] = font 3207db96d56Sopenharmony_ci 3217db96d56Sopenharmony_ci def update_colors(self): 3227db96d56Sopenharmony_ci """Update the sidebar text colors, usually after config changes.""" 3237db96d56Sopenharmony_ci colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') 3247db96d56Sopenharmony_ci foreground = colors['foreground'] 3257db96d56Sopenharmony_ci background = colors['background'] 3267db96d56Sopenharmony_ci self.sidebar_text.config( 3277db96d56Sopenharmony_ci fg=foreground, bg=background, 3287db96d56Sopenharmony_ci selectforeground=foreground, selectbackground=background, 3297db96d56Sopenharmony_ci inactiveselectbackground=background, 3307db96d56Sopenharmony_ci ) 3317db96d56Sopenharmony_ci 3327db96d56Sopenharmony_ci def update_sidebar_text(self, end): 3337db96d56Sopenharmony_ci """ 3347db96d56Sopenharmony_ci Perform the following action: 3357db96d56Sopenharmony_ci Each line sidebar_text contains the linenumber for that line 3367db96d56Sopenharmony_ci Synchronize with editwin.text so that both sidebar_text and 3377db96d56Sopenharmony_ci editwin.text contain the same number of lines""" 3387db96d56Sopenharmony_ci if end == self.prev_end: 3397db96d56Sopenharmony_ci return 3407db96d56Sopenharmony_ci 3417db96d56Sopenharmony_ci width_difference = len(str(end)) - len(str(self.prev_end)) 3427db96d56Sopenharmony_ci if width_difference: 3437db96d56Sopenharmony_ci cur_width = int(float(self.sidebar_text['width'])) 3447db96d56Sopenharmony_ci new_width = cur_width + width_difference 3457db96d56Sopenharmony_ci self.sidebar_text['width'] = self._sidebar_width_type(new_width) 3467db96d56Sopenharmony_ci 3477db96d56Sopenharmony_ci with temp_enable_text_widget(self.sidebar_text): 3487db96d56Sopenharmony_ci if end > self.prev_end: 3497db96d56Sopenharmony_ci new_text = '\n'.join(itertools.chain( 3507db96d56Sopenharmony_ci [''], 3517db96d56Sopenharmony_ci map(str, range(self.prev_end + 1, end + 1)), 3527db96d56Sopenharmony_ci )) 3537db96d56Sopenharmony_ci self.sidebar_text.insert(f'end -1c', new_text, 'linenumber') 3547db96d56Sopenharmony_ci else: 3557db96d56Sopenharmony_ci self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c') 3567db96d56Sopenharmony_ci 3577db96d56Sopenharmony_ci self.prev_end = end 3587db96d56Sopenharmony_ci 3597db96d56Sopenharmony_ci def yscroll_event(self, *args, **kwargs): 3607db96d56Sopenharmony_ci self.sidebar_text.yview_moveto(args[0]) 3617db96d56Sopenharmony_ci return 'break' 3627db96d56Sopenharmony_ci 3637db96d56Sopenharmony_ci 3647db96d56Sopenharmony_ciclass WrappedLineHeightChangeDelegator(Delegator): 3657db96d56Sopenharmony_ci def __init__(self, callback): 3667db96d56Sopenharmony_ci """ 3677db96d56Sopenharmony_ci callback - Callable, will be called when an insert, delete or replace 3687db96d56Sopenharmony_ci action on the text widget may require updating the shell 3697db96d56Sopenharmony_ci sidebar. 3707db96d56Sopenharmony_ci """ 3717db96d56Sopenharmony_ci Delegator.__init__(self) 3727db96d56Sopenharmony_ci self.callback = callback 3737db96d56Sopenharmony_ci 3747db96d56Sopenharmony_ci def insert(self, index, chars, tags=None): 3757db96d56Sopenharmony_ci is_single_line = '\n' not in chars 3767db96d56Sopenharmony_ci if is_single_line: 3777db96d56Sopenharmony_ci before_displaylines = get_displaylines(self, index) 3787db96d56Sopenharmony_ci 3797db96d56Sopenharmony_ci self.delegate.insert(index, chars, tags) 3807db96d56Sopenharmony_ci 3817db96d56Sopenharmony_ci if is_single_line: 3827db96d56Sopenharmony_ci after_displaylines = get_displaylines(self, index) 3837db96d56Sopenharmony_ci if after_displaylines == before_displaylines: 3847db96d56Sopenharmony_ci return # no need to update the sidebar 3857db96d56Sopenharmony_ci 3867db96d56Sopenharmony_ci self.callback() 3877db96d56Sopenharmony_ci 3887db96d56Sopenharmony_ci def delete(self, index1, index2=None): 3897db96d56Sopenharmony_ci if index2 is None: 3907db96d56Sopenharmony_ci index2 = index1 + "+1c" 3917db96d56Sopenharmony_ci is_single_line = get_lineno(self, index1) == get_lineno(self, index2) 3927db96d56Sopenharmony_ci if is_single_line: 3937db96d56Sopenharmony_ci before_displaylines = get_displaylines(self, index1) 3947db96d56Sopenharmony_ci 3957db96d56Sopenharmony_ci self.delegate.delete(index1, index2) 3967db96d56Sopenharmony_ci 3977db96d56Sopenharmony_ci if is_single_line: 3987db96d56Sopenharmony_ci after_displaylines = get_displaylines(self, index1) 3997db96d56Sopenharmony_ci if after_displaylines == before_displaylines: 4007db96d56Sopenharmony_ci return # no need to update the sidebar 4017db96d56Sopenharmony_ci 4027db96d56Sopenharmony_ci self.callback() 4037db96d56Sopenharmony_ci 4047db96d56Sopenharmony_ci 4057db96d56Sopenharmony_ciclass ShellSidebar(BaseSideBar): 4067db96d56Sopenharmony_ci """Sidebar for the PyShell window, for prompts etc.""" 4077db96d56Sopenharmony_ci def __init__(self, editwin): 4087db96d56Sopenharmony_ci self.canvas = None 4097db96d56Sopenharmony_ci self.line_prompts = {} 4107db96d56Sopenharmony_ci 4117db96d56Sopenharmony_ci super().__init__(editwin) 4127db96d56Sopenharmony_ci 4137db96d56Sopenharmony_ci change_delegator = \ 4147db96d56Sopenharmony_ci WrappedLineHeightChangeDelegator(self.change_callback) 4157db96d56Sopenharmony_ci # Insert the TextChangeDelegator after the last delegator, so that 4167db96d56Sopenharmony_ci # the sidebar reflects final changes to the text widget contents. 4177db96d56Sopenharmony_ci d = self.editwin.per.top 4187db96d56Sopenharmony_ci if d.delegate is not self.text: 4197db96d56Sopenharmony_ci while d.delegate is not self.editwin.per.bottom: 4207db96d56Sopenharmony_ci d = d.delegate 4217db96d56Sopenharmony_ci self.editwin.per.insertfilterafter(change_delegator, after=d) 4227db96d56Sopenharmony_ci 4237db96d56Sopenharmony_ci self.is_shown = True 4247db96d56Sopenharmony_ci 4257db96d56Sopenharmony_ci def init_widgets(self): 4267db96d56Sopenharmony_ci self.canvas = tk.Canvas(self.parent, width=30, 4277db96d56Sopenharmony_ci borderwidth=0, highlightthickness=0, 4287db96d56Sopenharmony_ci takefocus=False) 4297db96d56Sopenharmony_ci self.update_sidebar() 4307db96d56Sopenharmony_ci self.grid() 4317db96d56Sopenharmony_ci return self.canvas 4327db96d56Sopenharmony_ci 4337db96d56Sopenharmony_ci def bind_events(self): 4347db96d56Sopenharmony_ci super().bind_events() 4357db96d56Sopenharmony_ci 4367db96d56Sopenharmony_ci self.main_widget.bind( 4377db96d56Sopenharmony_ci # AquaTk defines <2> as the right button, not <3>. 4387db96d56Sopenharmony_ci "<Button-2>" if macosx.isAquaTk() else "<Button-3>", 4397db96d56Sopenharmony_ci self.context_menu_event, 4407db96d56Sopenharmony_ci ) 4417db96d56Sopenharmony_ci 4427db96d56Sopenharmony_ci def context_menu_event(self, event): 4437db96d56Sopenharmony_ci rmenu = tk.Menu(self.main_widget, tearoff=0) 4447db96d56Sopenharmony_ci has_selection = bool(self.text.tag_nextrange('sel', '1.0')) 4457db96d56Sopenharmony_ci def mkcmd(eventname): 4467db96d56Sopenharmony_ci return lambda: self.text.event_generate(eventname) 4477db96d56Sopenharmony_ci rmenu.add_command(label='Copy', 4487db96d56Sopenharmony_ci command=mkcmd('<<copy>>'), 4497db96d56Sopenharmony_ci state='normal' if has_selection else 'disabled') 4507db96d56Sopenharmony_ci rmenu.add_command(label='Copy with prompts', 4517db96d56Sopenharmony_ci command=mkcmd('<<copy-with-prompts>>'), 4527db96d56Sopenharmony_ci state='normal' if has_selection else 'disabled') 4537db96d56Sopenharmony_ci rmenu.tk_popup(event.x_root, event.y_root) 4547db96d56Sopenharmony_ci return "break" 4557db96d56Sopenharmony_ci 4567db96d56Sopenharmony_ci def grid(self): 4577db96d56Sopenharmony_ci self.canvas.grid(row=1, column=0, sticky=tk.NSEW, padx=2, pady=0) 4587db96d56Sopenharmony_ci 4597db96d56Sopenharmony_ci def change_callback(self): 4607db96d56Sopenharmony_ci if self.is_shown: 4617db96d56Sopenharmony_ci self.update_sidebar() 4627db96d56Sopenharmony_ci 4637db96d56Sopenharmony_ci def update_sidebar(self): 4647db96d56Sopenharmony_ci text = self.text 4657db96d56Sopenharmony_ci text_tagnames = text.tag_names 4667db96d56Sopenharmony_ci canvas = self.canvas 4677db96d56Sopenharmony_ci line_prompts = self.line_prompts = {} 4687db96d56Sopenharmony_ci 4697db96d56Sopenharmony_ci canvas.delete(tk.ALL) 4707db96d56Sopenharmony_ci 4717db96d56Sopenharmony_ci index = text.index("@0,0") 4727db96d56Sopenharmony_ci if index.split('.', 1)[1] != '0': 4737db96d56Sopenharmony_ci index = text.index(f'{index}+1line linestart') 4747db96d56Sopenharmony_ci while (lineinfo := text.dlineinfo(index)) is not None: 4757db96d56Sopenharmony_ci y = lineinfo[1] 4767db96d56Sopenharmony_ci prev_newline_tagnames = text_tagnames(f"{index} linestart -1c") 4777db96d56Sopenharmony_ci prompt = ( 4787db96d56Sopenharmony_ci '>>>' if "console" in prev_newline_tagnames else 4797db96d56Sopenharmony_ci '...' if "stdin" in prev_newline_tagnames else 4807db96d56Sopenharmony_ci None 4817db96d56Sopenharmony_ci ) 4827db96d56Sopenharmony_ci if prompt: 4837db96d56Sopenharmony_ci canvas.create_text(2, y, anchor=tk.NW, text=prompt, 4847db96d56Sopenharmony_ci font=self.font, fill=self.colors[0]) 4857db96d56Sopenharmony_ci lineno = get_lineno(text, index) 4867db96d56Sopenharmony_ci line_prompts[lineno] = prompt 4877db96d56Sopenharmony_ci index = text.index(f'{index}+1line') 4887db96d56Sopenharmony_ci 4897db96d56Sopenharmony_ci def yscroll_event(self, *args, **kwargs): 4907db96d56Sopenharmony_ci """Redirect vertical scrolling to the main editor text widget. 4917db96d56Sopenharmony_ci 4927db96d56Sopenharmony_ci The scroll bar is also updated. 4937db96d56Sopenharmony_ci """ 4947db96d56Sopenharmony_ci self.change_callback() 4957db96d56Sopenharmony_ci return 'break' 4967db96d56Sopenharmony_ci 4977db96d56Sopenharmony_ci def update_font(self): 4987db96d56Sopenharmony_ci """Update the sidebar text font, usually after config changes.""" 4997db96d56Sopenharmony_ci font = idleConf.GetFont(self.text, 'main', 'EditorWindow') 5007db96d56Sopenharmony_ci tk_font = Font(self.text, font=font) 5017db96d56Sopenharmony_ci char_width = max(tk_font.measure(char) for char in ['>', '.']) 5027db96d56Sopenharmony_ci self.canvas.configure(width=char_width * 3 + 4) 5037db96d56Sopenharmony_ci self.font = font 5047db96d56Sopenharmony_ci self.change_callback() 5057db96d56Sopenharmony_ci 5067db96d56Sopenharmony_ci def update_colors(self): 5077db96d56Sopenharmony_ci """Update the sidebar text colors, usually after config changes.""" 5087db96d56Sopenharmony_ci linenumbers_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') 5097db96d56Sopenharmony_ci prompt_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'console') 5107db96d56Sopenharmony_ci foreground = prompt_colors['foreground'] 5117db96d56Sopenharmony_ci background = linenumbers_colors['background'] 5127db96d56Sopenharmony_ci self.colors = (foreground, background) 5137db96d56Sopenharmony_ci self.canvas.configure(background=background) 5147db96d56Sopenharmony_ci self.change_callback() 5157db96d56Sopenharmony_ci 5167db96d56Sopenharmony_ci 5177db96d56Sopenharmony_cidef _linenumbers_drag_scrolling(parent): # htest # 5187db96d56Sopenharmony_ci from idlelib.idle_test.test_sidebar import Dummy_editwin 5197db96d56Sopenharmony_ci 5207db96d56Sopenharmony_ci toplevel = tk.Toplevel(parent) 5217db96d56Sopenharmony_ci text_frame = tk.Frame(toplevel) 5227db96d56Sopenharmony_ci text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 5237db96d56Sopenharmony_ci text_frame.rowconfigure(1, weight=1) 5247db96d56Sopenharmony_ci text_frame.columnconfigure(1, weight=1) 5257db96d56Sopenharmony_ci 5267db96d56Sopenharmony_ci font = idleConf.GetFont(toplevel, 'main', 'EditorWindow') 5277db96d56Sopenharmony_ci text = tk.Text(text_frame, width=80, height=24, wrap=tk.NONE, font=font) 5287db96d56Sopenharmony_ci text.grid(row=1, column=1, sticky=tk.NSEW) 5297db96d56Sopenharmony_ci 5307db96d56Sopenharmony_ci editwin = Dummy_editwin(text) 5317db96d56Sopenharmony_ci editwin.vbar = tk.Scrollbar(text_frame) 5327db96d56Sopenharmony_ci 5337db96d56Sopenharmony_ci linenumbers = LineNumbers(editwin) 5347db96d56Sopenharmony_ci linenumbers.show_sidebar() 5357db96d56Sopenharmony_ci 5367db96d56Sopenharmony_ci text.insert('1.0', '\n'.join('a'*i for i in range(1, 101))) 5377db96d56Sopenharmony_ci 5387db96d56Sopenharmony_ci 5397db96d56Sopenharmony_ciif __name__ == '__main__': 5407db96d56Sopenharmony_ci from unittest import main 5417db96d56Sopenharmony_ci main('idlelib.idle_test.test_sidebar', verbosity=2, exit=False) 5427db96d56Sopenharmony_ci 5437db96d56Sopenharmony_ci from idlelib.idle_test.htest import run 5447db96d56Sopenharmony_ci run(_linenumbers_drag_scrolling) 545