mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	bpo-37903: IDLE: add shell sidebar mouse interactions (GH-25708)
Left click and drag to select lines. With selection, right click for context menu with copy and copy-with-prompts. Also add copy-with-prompts to the text-box context menu. Co-authored-by: Terry Jan Reedy <tjreedy@udel.edu>
This commit is contained in:
		
							parent
							
								
									90d523910a
								
							
						
					
					
						commit
						b43cc31a27
					
				
					 10 changed files with 384 additions and 230 deletions
				
			
		|  | @ -994,6 +994,32 @@ hmac | ||||||
| The hmac module now uses OpenSSL's HMAC implementation internally. | The hmac module now uses OpenSSL's HMAC implementation internally. | ||||||
| (Contributed by Christian Heimes in :issue:`40645`.) | (Contributed by Christian Heimes in :issue:`40645`.) | ||||||
| 
 | 
 | ||||||
|  | IDLE and idlelib | ||||||
|  | ---------------- | ||||||
|  | 
 | ||||||
|  | Make IDLE invoke :func:`sys.excepthook` (when started without '-n'). | ||||||
|  | User hooks were previously ignored.  (Patch by Ken Hilton in | ||||||
|  | :issue:`43008`.) | ||||||
|  | 
 | ||||||
|  | This change was backported to a 3.9 maintenance release. | ||||||
|  | 
 | ||||||
|  | Add a Shell sidebar.  Move the primary prompt ('>>>') to the sidebar. | ||||||
|  | Add secondary prompts ('...') to the sidebar.  Left click and optional | ||||||
|  | drag selects one or more lines of text, as with the editor | ||||||
|  | line number sidebar.  Right click after selecting text lines displays | ||||||
|  | a context menu with 'copy with prompts'.  This zips together prompts | ||||||
|  | from the sidebar with lines from the selected text.  This option also | ||||||
|  | appears on the context menu for the text.  (Contributed by Tal Einat | ||||||
|  | in :issue:`37903`.) | ||||||
|  | 
 | ||||||
|  | Use spaces instead of tabs to indent interactive code.  This makes | ||||||
|  | interactive code entries 'look right'.  Making this feasible was a | ||||||
|  | major motivation for adding the shell sidebar.  Contributed by | ||||||
|  | Terry Jan Reedy in :issue:`37892`.) | ||||||
|  | 
 | ||||||
|  | We expect to backport these shell changes to a future 3.9 maintenance | ||||||
|  | release. | ||||||
|  | 
 | ||||||
| importlib.metadata | importlib.metadata | ||||||
| ------------------ | ------------------ | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,7 +4,15 @@ Released on 2021-10-04? | ||||||
| ========================= | ========================= | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| bpo-37892: Change Shell input indents from tabs to spaces. | bpo-37903: Add mouse actions to the shell sidebar.  Left click and | ||||||
|  | optional drag selects one or more lines of text, as with the | ||||||
|  | editor line number sidebar.  Right click after selecting text lines | ||||||
|  | displays a context menu with 'copy with prompts'.  This zips together | ||||||
|  | prompts from the sidebar with lines from the selected text.  This option | ||||||
|  | also appears on the context menu for the text. | ||||||
|  | 
 | ||||||
|  | bpo-37892: Change Shell input indents from tabs to spaces.  Shell input | ||||||
|  | now 'looks right'.  Making this feasible motivated the shell sidebar. | ||||||
| 
 | 
 | ||||||
| bpo-37903: Move the Shell input prompt to a side bar. | bpo-37903: Move the Shell input prompt to a side bar. | ||||||
| 
 | 
 | ||||||
|  | @ -19,7 +27,8 @@ bpo-23544: Disable Debug=>Stack Viewer when user code is running or | ||||||
| Debugger is active, to prevent hang or crash.  Patch by Zackery Spytz. | Debugger is active, to prevent hang or crash.  Patch by Zackery Spytz. | ||||||
| 
 | 
 | ||||||
| bpo-43008: Make IDLE invoke :func:`sys.excepthook` in normal, | bpo-43008: Make IDLE invoke :func:`sys.excepthook` in normal, | ||||||
| 2-process mode.  Patch by Ken Hilton. | 2-process mode.  User hooks were previously ignored. | ||||||
|  | Patch by Ken Hilton. | ||||||
| 
 | 
 | ||||||
| bpo-33065: Fix problem debugging user classes with __repr__ method. | bpo-33065: Fix problem debugging user classes with __repr__ method. | ||||||
| 
 | 
 | ||||||
|  | @ -32,7 +41,7 @@ installers built on macOS 11. | ||||||
| 
 | 
 | ||||||
| bpo-42426: Fix reporting offset of the RE error in searchengine. | bpo-42426: Fix reporting offset of the RE error in searchengine. | ||||||
| 
 | 
 | ||||||
| bpo-42416: Get docstrings for IDLE calltips more often | bpo-42416: Display docstrings in IDLE calltips in more cases, | ||||||
| by using inspect.getdoc. | by using inspect.getdoc. | ||||||
| 
 | 
 | ||||||
| bpo-33987: Mostly finish using ttk widgets, mainly for editor, | bpo-33987: Mostly finish using ttk widgets, mainly for editor, | ||||||
|  |  | ||||||
|  | @ -31,10 +31,11 @@ | ||||||
| 
 | 
 | ||||||
| class AutoComplete: | class AutoComplete: | ||||||
| 
 | 
 | ||||||
|     def __init__(self, editwin=None): |     def __init__(self, editwin=None, tags=None): | ||||||
|         self.editwin = editwin |         self.editwin = editwin | ||||||
|         if editwin is not None:   # not in subprocess or no-gui test |         if editwin is not None:   # not in subprocess or no-gui test | ||||||
|             self.text = editwin.text |             self.text = editwin.text | ||||||
|  |         self.tags = tags | ||||||
|         self.autocompletewindow = None |         self.autocompletewindow = None | ||||||
|         # id of delayed call, and the index of the text insert when |         # id of delayed call, and the index of the text insert when | ||||||
|         # the delayed call was issued. If _delayed_completion_id is |         # the delayed call was issued. If _delayed_completion_id is | ||||||
|  | @ -48,7 +49,7 @@ def reload(cls): | ||||||
|             "extensions", "AutoComplete", "popupwait", type="int", default=0) |             "extensions", "AutoComplete", "popupwait", type="int", default=0) | ||||||
| 
 | 
 | ||||||
|     def _make_autocomplete_window(self):  # Makes mocking easier. |     def _make_autocomplete_window(self):  # Makes mocking easier. | ||||||
|         return autocomplete_w.AutoCompleteWindow(self.text) |         return autocomplete_w.AutoCompleteWindow(self.text, tags=self.tags) | ||||||
| 
 | 
 | ||||||
|     def _remove_autocomplete_window(self, event=None): |     def _remove_autocomplete_window(self, event=None): | ||||||
|         if self.autocompletewindow: |         if self.autocompletewindow: | ||||||
|  |  | ||||||
|  | @ -26,9 +26,11 @@ | ||||||
| 
 | 
 | ||||||
| class AutoCompleteWindow: | class AutoCompleteWindow: | ||||||
| 
 | 
 | ||||||
|     def __init__(self, widget): |     def __init__(self, widget, tags): | ||||||
|         # The widget (Text) on which we place the AutoCompleteWindow |         # The widget (Text) on which we place the AutoCompleteWindow | ||||||
|         self.widget = widget |         self.widget = widget | ||||||
|  |         # Tags to mark inserted text with | ||||||
|  |         self.tags = tags | ||||||
|         # The widgets we create |         # The widgets we create | ||||||
|         self.autocompletewindow = self.listbox = self.scrollbar = None |         self.autocompletewindow = self.listbox = self.scrollbar = None | ||||||
|         # The default foreground and background of a selection. Saved because |         # The default foreground and background of a selection. Saved because | ||||||
|  | @ -69,7 +71,8 @@ def _change_start(self, newstart): | ||||||
|                                "%s+%dc" % (self.startindex, len(self.start))) |                                "%s+%dc" % (self.startindex, len(self.start))) | ||||||
|         if i < len(newstart): |         if i < len(newstart): | ||||||
|             self.widget.insert("%s+%dc" % (self.startindex, i), |             self.widget.insert("%s+%dc" % (self.startindex, i), | ||||||
|                                newstart[i:]) |                                newstart[i:], | ||||||
|  |                                self.tags) | ||||||
|         self.start = newstart |         self.start = newstart | ||||||
| 
 | 
 | ||||||
|     def _binary_search(self, s): |     def _binary_search(self, s): | ||||||
|  |  | ||||||
|  | @ -311,7 +311,7 @@ def __init__(self, flist=None, filename=None, key=None, root=None): | ||||||
| 
 | 
 | ||||||
|         # Former extension bindings depends on frame.text being packed |         # Former extension bindings depends on frame.text being packed | ||||||
|         # (called from self.ResetColorizer()). |         # (called from self.ResetColorizer()). | ||||||
|         autocomplete = self.AutoComplete(self) |         autocomplete = self.AutoComplete(self, self.user_input_insert_tags) | ||||||
|         text.bind("<<autocomplete>>", autocomplete.autocomplete_event) |         text.bind("<<autocomplete>>", autocomplete.autocomplete_event) | ||||||
|         text.bind("<<try-open-completions>>", |         text.bind("<<try-open-completions>>", | ||||||
|                   autocomplete.try_open_completions_event) |                   autocomplete.try_open_completions_event) | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ def setUpClass(cls): | ||||||
|         cls.root = Tk() |         cls.root = Tk() | ||||||
|         cls.root.withdraw() |         cls.root.withdraw() | ||||||
|         cls.text = Text(cls.root) |         cls.text = Text(cls.root) | ||||||
|         cls.acw = acw.AutoCompleteWindow(cls.text) |         cls.acw = acw.AutoCompleteWindow(cls.text, tags=None) | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def tearDownClass(cls): |     def tearDownClass(cls): | ||||||
|  |  | ||||||
|  | @ -270,7 +270,6 @@ def test_click_selection(self): | ||||||
| 
 | 
 | ||||||
|         self.assertEqual(self.get_selection(), ('2.0', '3.0')) |         self.assertEqual(self.get_selection(), ('2.0', '3.0')) | ||||||
| 
 | 
 | ||||||
|     @unittest.skip('test disabled') |  | ||||||
|     def simulate_drag(self, start_line, end_line): |     def simulate_drag(self, start_line, end_line): | ||||||
|         start_x, start_y = self.get_line_screen_position(start_line) |         start_x, start_y = self.get_line_screen_position(start_line) | ||||||
|         end_x, end_y = self.get_line_screen_position(end_line) |         end_x, end_y = self.get_line_screen_position(end_line) | ||||||
|  | @ -704,6 +703,66 @@ def test_mousewheel(self): | ||||||
|         yield |         yield | ||||||
|         self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) |         self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) | ||||||
| 
 | 
 | ||||||
|  |     @run_in_tk_mainloop | ||||||
|  |     def test_copy(self): | ||||||
|  |         sidebar = self.shell.shell_sidebar | ||||||
|  |         text = self.shell.text | ||||||
|  | 
 | ||||||
|  |         first_line = get_end_linenumber(text) | ||||||
|  | 
 | ||||||
|  |         self.do_input(dedent('''\ | ||||||
|  |             if True: | ||||||
|  |             print(1) | ||||||
|  | 
 | ||||||
|  |             ''')) | ||||||
|  |         yield | ||||||
|  | 
 | ||||||
|  |         text.tag_add('sel', f'{first_line}.0', 'end-1c') | ||||||
|  |         selected_text = text.get('sel.first', 'sel.last') | ||||||
|  |         self.assertTrue(selected_text.startswith('if True:\n')) | ||||||
|  |         self.assertIn('\n1\n', selected_text) | ||||||
|  | 
 | ||||||
|  |         text.event_generate('<<copy>>') | ||||||
|  |         self.addCleanup(text.clipboard_clear) | ||||||
|  | 
 | ||||||
|  |         copied_text = text.clipboard_get() | ||||||
|  |         self.assertEqual(copied_text, selected_text) | ||||||
|  | 
 | ||||||
|  |     @run_in_tk_mainloop | ||||||
|  |     def test_copy_with_prompts(self): | ||||||
|  |         sidebar = self.shell.shell_sidebar | ||||||
|  |         text = self.shell.text | ||||||
|  | 
 | ||||||
|  |         first_line = get_end_linenumber(text) | ||||||
|  |         self.do_input(dedent('''\ | ||||||
|  |             if True: | ||||||
|  |             print(1) | ||||||
|  | 
 | ||||||
|  |             ''')) | ||||||
|  |         yield | ||||||
|  | 
 | ||||||
|  |         text.tag_add('sel', f'{first_line}.3', 'end-1c') | ||||||
|  |         selected_text = text.get('sel.first', 'sel.last') | ||||||
|  |         self.assertTrue(selected_text.startswith('True:\n')) | ||||||
|  | 
 | ||||||
|  |         selected_lines_text = text.get('sel.first linestart', 'sel.last') | ||||||
|  |         selected_lines = selected_lines_text.split('\n') | ||||||
|  |         # Expect a block of input, a single output line, and a new prompt | ||||||
|  |         expected_prompts = \ | ||||||
|  |             ['>>>'] + ['...'] * (len(selected_lines) - 3) + [None, '>>>'] | ||||||
|  |         selected_text_with_prompts = '\n'.join( | ||||||
|  |             line if prompt is None else prompt + ' ' + line | ||||||
|  |             for prompt, line in zip(expected_prompts, | ||||||
|  |                                     selected_lines, | ||||||
|  |                                     strict=True) | ||||||
|  |         ) + '\n' | ||||||
|  | 
 | ||||||
|  |         text.event_generate('<<copy-with-prompts>>') | ||||||
|  |         self.addCleanup(text.clipboard_clear) | ||||||
|  | 
 | ||||||
|  |         copied_text = text.clipboard_get() | ||||||
|  |         self.assertEqual(copied_text, selected_text_with_prompts) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     unittest.main(verbosity=2) |     unittest.main(verbosity=2) | ||||||
|  |  | ||||||
|  | @ -33,6 +33,7 @@ | ||||||
|     raise SystemExit(1) |     raise SystemExit(1) | ||||||
| 
 | 
 | ||||||
| from code import InteractiveInterpreter | from code import InteractiveInterpreter | ||||||
|  | import itertools | ||||||
| import linecache | import linecache | ||||||
| import os | import os | ||||||
| import os.path | import os.path | ||||||
|  | @ -865,6 +866,13 @@ class PyShell(OutputWindow): | ||||||
|     rmenu_specs = OutputWindow.rmenu_specs + [ |     rmenu_specs = OutputWindow.rmenu_specs + [ | ||||||
|         ("Squeeze", "<<squeeze-current-text>>"), |         ("Squeeze", "<<squeeze-current-text>>"), | ||||||
|     ] |     ] | ||||||
|  |     _idx = 1 + len(list(itertools.takewhile( | ||||||
|  |         lambda rmenu_item: rmenu_item[0] != "Copy", rmenu_specs) | ||||||
|  |     )) | ||||||
|  |     rmenu_specs.insert(_idx, ("Copy with prompts", | ||||||
|  |                               "<<copy-with-prompts>>", | ||||||
|  |                               "rmenu_check_copy")) | ||||||
|  |     del _idx | ||||||
| 
 | 
 | ||||||
|     allow_line_numbers = False |     allow_line_numbers = False | ||||||
|     user_input_insert_tags = "stdin" |     user_input_insert_tags = "stdin" | ||||||
|  | @ -906,6 +914,7 @@ def __init__(self, flist=None): | ||||||
|         text.bind("<<open-stack-viewer>>", self.open_stack_viewer) |         text.bind("<<open-stack-viewer>>", self.open_stack_viewer) | ||||||
|         text.bind("<<toggle-debugger>>", self.toggle_debugger) |         text.bind("<<toggle-debugger>>", self.toggle_debugger) | ||||||
|         text.bind("<<toggle-jit-stack-viewer>>", self.toggle_jit_stack_viewer) |         text.bind("<<toggle-jit-stack-viewer>>", self.toggle_jit_stack_viewer) | ||||||
|  |         text.bind("<<copy-with-prompts>>", self.copy_with_prompts_callback) | ||||||
|         if use_subprocess: |         if use_subprocess: | ||||||
|             text.bind("<<view-restart>>", self.view_restart_mark) |             text.bind("<<view-restart>>", self.view_restart_mark) | ||||||
|             text.bind("<<restart-shell>>", self.restart_shell) |             text.bind("<<restart-shell>>", self.restart_shell) | ||||||
|  | @ -979,6 +988,42 @@ def replace_event(self, event): | ||||||
|     def get_standard_extension_names(self): |     def get_standard_extension_names(self): | ||||||
|         return idleConf.GetExtensions(shell_only=True) |         return idleConf.GetExtensions(shell_only=True) | ||||||
| 
 | 
 | ||||||
|  |     def copy_with_prompts_callback(self, event=None): | ||||||
|  |         """Copy selected lines to the clipboard, with prompts. | ||||||
|  | 
 | ||||||
|  |         This makes the copied text useful for doc-tests and interactive | ||||||
|  |         shell code examples. | ||||||
|  | 
 | ||||||
|  |         This always copies entire lines, even if only part of the first | ||||||
|  |         and/or last lines is selected. | ||||||
|  |         """ | ||||||
|  |         text = self.text | ||||||
|  | 
 | ||||||
|  |         selection_indexes = ( | ||||||
|  |             self.text.index("sel.first linestart"), | ||||||
|  |             self.text.index("sel.last +1line linestart"), | ||||||
|  |         ) | ||||||
|  |         if selection_indexes[0] is None: | ||||||
|  |             # There is no selection, so do nothing. | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         selected_text = self.text.get(*selection_indexes) | ||||||
|  |         selection_lineno_range = range( | ||||||
|  |             int(float(selection_indexes[0])), | ||||||
|  |             int(float(selection_indexes[1])) | ||||||
|  |         ) | ||||||
|  |         prompts = [ | ||||||
|  |             self.shell_sidebar.line_prompts.get(lineno) | ||||||
|  |             for lineno in selection_lineno_range | ||||||
|  |         ] | ||||||
|  |         selected_text_with_prompts = "\n".join( | ||||||
|  |             line if prompt is None else f"{prompt} {line}" | ||||||
|  |             for prompt, line in zip(prompts, selected_text.splitlines()) | ||||||
|  |         ) + "\n" | ||||||
|  | 
 | ||||||
|  |         text.clipboard_clear() | ||||||
|  |         text.clipboard_append(selected_text_with_prompts) | ||||||
|  | 
 | ||||||
|     reading = False |     reading = False | ||||||
|     executing = False |     executing = False | ||||||
|     canceled = False |     canceled = False | ||||||
|  |  | ||||||
|  | @ -9,11 +9,13 @@ | ||||||
| from tkinter.font import Font | from tkinter.font import Font | ||||||
| from idlelib.config import idleConf | from idlelib.config import idleConf | ||||||
| from idlelib.delegator import Delegator | from idlelib.delegator import Delegator | ||||||
|  | from idlelib import macosx | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_lineno(text, index): | def get_lineno(text, index): | ||||||
|     """Return the line number of an index in a Tk text widget.""" |     """Return the line number of an index in a Tk text widget.""" | ||||||
|     return int(float(text.index(index))) |     text_index = text.index(index) | ||||||
|  |     return int(float(text_index)) if text_index else None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_end_linenumber(text): | def get_end_linenumber(text): | ||||||
|  | @ -70,56 +72,52 @@ def __init__(self, editwin): | ||||||
|         self.parent = editwin.text_frame |         self.parent = editwin.text_frame | ||||||
|         self.text = editwin.text |         self.text = editwin.text | ||||||
| 
 | 
 | ||||||
|         _padx, pady = get_widget_padding(self.text) |         self.is_shown = False | ||||||
|         self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE, | 
 | ||||||
|                                     padx=2, pady=pady, |         self.main_widget = self.init_widgets() | ||||||
|                                     borderwidth=0, highlightthickness=0) | 
 | ||||||
|         self.sidebar_text.config(state=tk.DISABLED) |         self.bind_events() | ||||||
|         self.text['yscrollcommand'] = self.redirect_yscroll_event | 
 | ||||||
|         self.update_font() |         self.update_font() | ||||||
|         self.update_colors() |         self.update_colors() | ||||||
| 
 | 
 | ||||||
|         self.is_shown = False |     def init_widgets(self): | ||||||
|  |         """Initialize the sidebar's widgets, returning the main widget.""" | ||||||
|  |         raise NotImplementedError | ||||||
| 
 | 
 | ||||||
|     def update_font(self): |     def update_font(self): | ||||||
|         """Update the sidebar text font, usually after config changes.""" |         """Update the sidebar text font, usually after config changes.""" | ||||||
|         font = idleConf.GetFont(self.text, 'main', 'EditorWindow') |         raise NotImplementedError | ||||||
|         self._update_font(font) |  | ||||||
| 
 |  | ||||||
|     def _update_font(self, font): |  | ||||||
|         self.sidebar_text['font'] = font |  | ||||||
| 
 | 
 | ||||||
|     def update_colors(self): |     def update_colors(self): | ||||||
|         """Update the sidebar text colors, usually after config changes.""" |         """Update the sidebar text colors, usually after config changes.""" | ||||||
|         colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'normal') |         raise NotImplementedError | ||||||
|         self._update_colors(foreground=colors['foreground'], |  | ||||||
|                             background=colors['background']) |  | ||||||
| 
 | 
 | ||||||
|     def _update_colors(self, foreground, background): |     def grid(self): | ||||||
|         self.sidebar_text.config( |         """Layout the widget, always using grid layout.""" | ||||||
|             fg=foreground, bg=background, |         raise NotImplementedError | ||||||
|             selectforeground=foreground, selectbackground=background, |  | ||||||
|             inactiveselectbackground=background, |  | ||||||
|         ) |  | ||||||
| 
 | 
 | ||||||
|     def show_sidebar(self): |     def show_sidebar(self): | ||||||
|         if not self.is_shown: |         if not self.is_shown: | ||||||
|             self.sidebar_text.grid(row=1, column=0, sticky=tk.NSEW) |             self.grid() | ||||||
|             self.is_shown = True |             self.is_shown = True | ||||||
| 
 | 
 | ||||||
|     def hide_sidebar(self): |     def hide_sidebar(self): | ||||||
|         if self.is_shown: |         if self.is_shown: | ||||||
|             self.sidebar_text.grid_forget() |             self.main_widget.grid_forget() | ||||||
|             self.is_shown = False |             self.is_shown = False | ||||||
| 
 | 
 | ||||||
|  |     def yscroll_event(self, *args, **kwargs): | ||||||
|  |         """Hook for vertical scrolling for sub-classes to override.""" | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|     def redirect_yscroll_event(self, *args, **kwargs): |     def redirect_yscroll_event(self, *args, **kwargs): | ||||||
|         """Redirect vertical scrolling to the main editor text widget. |         """Redirect vertical scrolling to the main editor text widget. | ||||||
| 
 | 
 | ||||||
|         The scroll bar is also updated. |         The scroll bar is also updated. | ||||||
|         """ |         """ | ||||||
|         self.editwin.vbar.set(*args) |         self.editwin.vbar.set(*args) | ||||||
|         self.sidebar_text.yview_moveto(args[0]) |         return self.yscroll_event(*args, **kwargs) | ||||||
|         return 'break' |  | ||||||
| 
 | 
 | ||||||
|     def redirect_focusin_event(self, event): |     def redirect_focusin_event(self, event): | ||||||
|         """Redirect focus-in events to the main editor text widget.""" |         """Redirect focus-in events to the main editor text widget.""" | ||||||
|  | @ -138,6 +136,132 @@ def redirect_mousewheel_event(self, event): | ||||||
|                                  x=0, y=event.y, delta=event.delta) |                                  x=0, y=event.y, delta=event.delta) | ||||||
|         return 'break' |         return 'break' | ||||||
| 
 | 
 | ||||||
|  |     def bind_events(self): | ||||||
|  |         self.text['yscrollcommand'] = self.redirect_yscroll_event | ||||||
|  | 
 | ||||||
|  |         # Ensure focus is always redirected to the main editor text widget. | ||||||
|  |         self.main_widget.bind('<FocusIn>', self.redirect_focusin_event) | ||||||
|  | 
 | ||||||
|  |         # Redirect mouse scrolling to the main editor text widget. | ||||||
|  |         # | ||||||
|  |         # Note that without this, scrolling with the mouse only scrolls | ||||||
|  |         # the line numbers. | ||||||
|  |         self.main_widget.bind('<MouseWheel>', self.redirect_mousewheel_event) | ||||||
|  | 
 | ||||||
|  |         # Redirect mouse button events to the main editor text widget, | ||||||
|  |         # except for the left mouse button (1). | ||||||
|  |         # | ||||||
|  |         # Note: X-11 sends Button-4 and Button-5 events for the scroll wheel. | ||||||
|  |         def bind_mouse_event(event_name, target_event_name): | ||||||
|  |             handler = functools.partial(self.redirect_mousebutton_event, | ||||||
|  |                                         event_name=target_event_name) | ||||||
|  |             self.main_widget.bind(event_name, handler) | ||||||
|  | 
 | ||||||
|  |         for button in [2, 3, 4, 5]: | ||||||
|  |             for event_name in (f'<Button-{button}>', | ||||||
|  |                                f'<ButtonRelease-{button}>', | ||||||
|  |                                f'<B{button}-Motion>', | ||||||
|  |                                ): | ||||||
|  |                 bind_mouse_event(event_name, target_event_name=event_name) | ||||||
|  | 
 | ||||||
|  |             # Convert double- and triple-click events to normal click events, | ||||||
|  |             # since event_generate() doesn't allow generating such events. | ||||||
|  |             for event_name in (f'<Double-Button-{button}>', | ||||||
|  |                                f'<Triple-Button-{button}>', | ||||||
|  |                                ): | ||||||
|  |                 bind_mouse_event(event_name, | ||||||
|  |                                  target_event_name=f'<Button-{button}>') | ||||||
|  | 
 | ||||||
|  |         # start_line is set upon <Button-1> to allow selecting a range of rows | ||||||
|  |         # by dragging.  It is cleared upon <ButtonRelease-1>. | ||||||
|  |         start_line = None | ||||||
|  | 
 | ||||||
|  |         # last_y is initially set upon <B1-Leave> and is continuously updated | ||||||
|  |         # upon <B1-Motion>, until <B1-Enter> or the mouse button is released. | ||||||
|  |         # It is used in text_auto_scroll(), which is called repeatedly and | ||||||
|  |         # does have a mouse event available. | ||||||
|  |         last_y = None | ||||||
|  | 
 | ||||||
|  |         # auto_scrolling_after_id is set whenever text_auto_scroll is | ||||||
|  |         # scheduled via .after().  It is used to stop the auto-scrolling | ||||||
|  |         # upon <B1-Enter>, as well as to avoid scheduling the function several | ||||||
|  |         # times in parallel. | ||||||
|  |         auto_scrolling_after_id = None | ||||||
|  | 
 | ||||||
|  |         def drag_update_selection_and_insert_mark(y_coord): | ||||||
|  |             """Helper function for drag and selection event handlers.""" | ||||||
|  |             lineno = get_lineno(self.text, f"@0,{y_coord}") | ||||||
|  |             a, b = sorted([start_line, lineno]) | ||||||
|  |             self.text.tag_remove("sel", "1.0", "end") | ||||||
|  |             self.text.tag_add("sel", f"{a}.0", f"{b+1}.0") | ||||||
|  |             self.text.mark_set("insert", | ||||||
|  |                                f"{lineno if lineno == a else lineno + 1}.0") | ||||||
|  | 
 | ||||||
|  |         def b1_mousedown_handler(event): | ||||||
|  |             nonlocal start_line | ||||||
|  |             nonlocal last_y | ||||||
|  |             start_line = int(float(self.text.index(f"@0,{event.y}"))) | ||||||
|  |             last_y = event.y | ||||||
|  | 
 | ||||||
|  |             drag_update_selection_and_insert_mark(event.y) | ||||||
|  |         self.main_widget.bind('<Button-1>', b1_mousedown_handler) | ||||||
|  | 
 | ||||||
|  |         def b1_mouseup_handler(event): | ||||||
|  |             # On mouse up, we're no longer dragging.  Set the shared persistent | ||||||
|  |             # variables to None to represent this. | ||||||
|  |             nonlocal start_line | ||||||
|  |             nonlocal last_y | ||||||
|  |             start_line = None | ||||||
|  |             last_y = None | ||||||
|  |             self.text.event_generate('<ButtonRelease-1>', x=0, y=event.y) | ||||||
|  |         self.main_widget.bind('<ButtonRelease-1>', b1_mouseup_handler) | ||||||
|  | 
 | ||||||
|  |         def b1_drag_handler(event): | ||||||
|  |             nonlocal last_y | ||||||
|  |             if last_y is None:  # i.e. if not currently dragging | ||||||
|  |                 return | ||||||
|  |             last_y = event.y | ||||||
|  |             drag_update_selection_and_insert_mark(event.y) | ||||||
|  |         self.main_widget.bind('<B1-Motion>', b1_drag_handler) | ||||||
|  | 
 | ||||||
|  |         def text_auto_scroll(): | ||||||
|  |             """Mimic Text auto-scrolling when dragging outside of it.""" | ||||||
|  |             # See: https://github.com/tcltk/tk/blob/064ff9941b4b80b85916a8afe86a6c21fd388b54/library/text.tcl#L670 | ||||||
|  |             nonlocal auto_scrolling_after_id | ||||||
|  |             y = last_y | ||||||
|  |             if y is None: | ||||||
|  |                 self.main_widget.after_cancel(auto_scrolling_after_id) | ||||||
|  |                 auto_scrolling_after_id = None | ||||||
|  |                 return | ||||||
|  |             elif y < 0: | ||||||
|  |                 self.text.yview_scroll(-1 + y, 'pixels') | ||||||
|  |                 drag_update_selection_and_insert_mark(y) | ||||||
|  |             elif y > self.main_widget.winfo_height(): | ||||||
|  |                 self.text.yview_scroll(1 + y - self.main_widget.winfo_height(), | ||||||
|  |                                        'pixels') | ||||||
|  |                 drag_update_selection_and_insert_mark(y) | ||||||
|  |             auto_scrolling_after_id = \ | ||||||
|  |                 self.main_widget.after(50, text_auto_scroll) | ||||||
|  | 
 | ||||||
|  |         def b1_leave_handler(event): | ||||||
|  |             # Schedule the initial call to text_auto_scroll(), if not already | ||||||
|  |             # scheduled. | ||||||
|  |             nonlocal auto_scrolling_after_id | ||||||
|  |             if auto_scrolling_after_id is None: | ||||||
|  |                 nonlocal last_y | ||||||
|  |                 last_y = event.y | ||||||
|  |                 auto_scrolling_after_id = \ | ||||||
|  |                     self.main_widget.after(0, text_auto_scroll) | ||||||
|  |         self.main_widget.bind('<B1-Leave>', b1_leave_handler) | ||||||
|  | 
 | ||||||
|  |         def b1_enter_handler(event): | ||||||
|  |             # Cancel the scheduling of text_auto_scroll(), if it exists. | ||||||
|  |             nonlocal auto_scrolling_after_id | ||||||
|  |             if auto_scrolling_after_id is not None: | ||||||
|  |                 self.main_widget.after_cancel(auto_scrolling_after_id) | ||||||
|  |                 auto_scrolling_after_id = None | ||||||
|  |         self.main_widget.bind('<B1-Enter>', b1_enter_handler) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class EndLineDelegator(Delegator): | class EndLineDelegator(Delegator): | ||||||
|     """Generate callbacks with the current end line number. |     """Generate callbacks with the current end line number. | ||||||
|  | @ -160,137 +284,50 @@ def delete(self, index1, index2=None): | ||||||
| class LineNumbers(BaseSideBar): | class LineNumbers(BaseSideBar): | ||||||
|     """Line numbers support for editor windows.""" |     """Line numbers support for editor windows.""" | ||||||
|     def __init__(self, editwin): |     def __init__(self, editwin): | ||||||
|         BaseSideBar.__init__(self, editwin) |         super().__init__(editwin) | ||||||
|         self.prev_end = 1 |  | ||||||
|         self._sidebar_width_type = type(self.sidebar_text['width']) |  | ||||||
|         self.sidebar_text.config(state=tk.NORMAL) |  | ||||||
|         self.sidebar_text.insert('insert', '1', 'linenumber') |  | ||||||
|         self.sidebar_text.config(state=tk.DISABLED) |  | ||||||
|         self.sidebar_text.config(takefocus=False, exportselection=False) |  | ||||||
|         self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT) |  | ||||||
| 
 |  | ||||||
|         self.bind_events() |  | ||||||
| 
 |  | ||||||
|         end = get_end_linenumber(self.text) |  | ||||||
|         self.update_sidebar_text(end) |  | ||||||
| 
 | 
 | ||||||
|         end_line_delegator = EndLineDelegator(self.update_sidebar_text) |         end_line_delegator = EndLineDelegator(self.update_sidebar_text) | ||||||
|         # Insert the delegator after the undo delegator, so that line numbers |         # Insert the delegator after the undo delegator, so that line numbers | ||||||
|         # are properly updated after undo and redo actions. |         # are properly updated after undo and redo actions. | ||||||
|         self.editwin.per.insertfilterafter(filter=end_line_delegator, |         self.editwin.per.insertfilterafter(end_line_delegator, | ||||||
|                                            after=self.editwin.undo) |                                            after=self.editwin.undo) | ||||||
| 
 | 
 | ||||||
|     def bind_events(self): |     def init_widgets(self): | ||||||
|         # Ensure focus is always redirected to the main editor text widget. |         _padx, pady = get_widget_padding(self.text) | ||||||
|         self.sidebar_text.bind('<FocusIn>', self.redirect_focusin_event) |         self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE, | ||||||
|  |                                     padx=2, pady=pady, | ||||||
|  |                                     borderwidth=0, highlightthickness=0) | ||||||
|  |         self.sidebar_text.config(state=tk.DISABLED) | ||||||
| 
 | 
 | ||||||
|         # Redirect mouse scrolling to the main editor text widget. |         self.prev_end = 1 | ||||||
|         # |         self._sidebar_width_type = type(self.sidebar_text['width']) | ||||||
|         # Note that without this, scrolling with the mouse only scrolls |         with temp_enable_text_widget(self.sidebar_text): | ||||||
|         # the line numbers. |             self.sidebar_text.insert('insert', '1', 'linenumber') | ||||||
|         self.sidebar_text.bind('<MouseWheel>', self.redirect_mousewheel_event) |         self.sidebar_text.config(takefocus=False, exportselection=False) | ||||||
|  |         self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT) | ||||||
| 
 | 
 | ||||||
|         # Redirect mouse button events to the main editor text widget, |         end = get_end_linenumber(self.text) | ||||||
|         # except for the left mouse button (1). |         self.update_sidebar_text(end) | ||||||
|         # |  | ||||||
|         # Note: X-11 sends Button-4 and Button-5 events for the scroll wheel. |  | ||||||
|         def bind_mouse_event(event_name, target_event_name): |  | ||||||
|             handler = functools.partial(self.redirect_mousebutton_event, |  | ||||||
|                                         event_name=target_event_name) |  | ||||||
|             self.sidebar_text.bind(event_name, handler) |  | ||||||
| 
 | 
 | ||||||
|         for button in [2, 3, 4, 5]: |         return self.sidebar_text | ||||||
|             for event_name in (f'<Button-{button}>', |  | ||||||
|                                f'<ButtonRelease-{button}>', |  | ||||||
|                                f'<B{button}-Motion>', |  | ||||||
|                                ): |  | ||||||
|                 bind_mouse_event(event_name, target_event_name=event_name) |  | ||||||
| 
 | 
 | ||||||
|             # Convert double- and triple-click events to normal click events, |     def grid(self): | ||||||
|             # since event_generate() doesn't allow generating such events. |         self.sidebar_text.grid(row=1, column=0, sticky=tk.NSEW) | ||||||
|             for event_name in (f'<Double-Button-{button}>', |  | ||||||
|                                f'<Triple-Button-{button}>', |  | ||||||
|                                ): |  | ||||||
|                 bind_mouse_event(event_name, |  | ||||||
|                                  target_event_name=f'<Button-{button}>') |  | ||||||
| 
 | 
 | ||||||
|         # This is set by b1_mousedown_handler() and read by |     def update_font(self): | ||||||
|         # drag_update_selection_and_insert_mark(), to know where dragging |         font = idleConf.GetFont(self.text, 'main', 'EditorWindow') | ||||||
|         # began. |         self.sidebar_text['font'] = font | ||||||
|         start_line = None |  | ||||||
|         # These are set by b1_motion_handler() and read by selection_handler(). |  | ||||||
|         # last_y is passed this way since the mouse Y-coordinate is not |  | ||||||
|         # available on selection event objects.  last_yview is passed this way |  | ||||||
|         # to recognize scrolling while the mouse isn't moving. |  | ||||||
|         last_y = last_yview = None |  | ||||||
| 
 |  | ||||||
|         def b1_mousedown_handler(event): |  | ||||||
|             # select the entire line |  | ||||||
|             lineno = int(float(self.sidebar_text.index(f"@0,{event.y}"))) |  | ||||||
|             self.text.tag_remove("sel", "1.0", "end") |  | ||||||
|             self.text.tag_add("sel", f"{lineno}.0", f"{lineno+1}.0") |  | ||||||
|             self.text.mark_set("insert", f"{lineno+1}.0") |  | ||||||
| 
 |  | ||||||
|             # remember this line in case this is the beginning of dragging |  | ||||||
|             nonlocal start_line |  | ||||||
|             start_line = lineno |  | ||||||
|         self.sidebar_text.bind('<Button-1>', b1_mousedown_handler) |  | ||||||
| 
 |  | ||||||
|         def b1_mouseup_handler(event): |  | ||||||
|             # On mouse up, we're no longer dragging.  Set the shared persistent |  | ||||||
|             # variables to None to represent this. |  | ||||||
|             nonlocal start_line |  | ||||||
|             nonlocal last_y |  | ||||||
|             nonlocal last_yview |  | ||||||
|             start_line = None |  | ||||||
|             last_y = None |  | ||||||
|             last_yview = None |  | ||||||
|         self.sidebar_text.bind('<ButtonRelease-1>', b1_mouseup_handler) |  | ||||||
| 
 |  | ||||||
|         def drag_update_selection_and_insert_mark(y_coord): |  | ||||||
|             """Helper function for drag and selection event handlers.""" |  | ||||||
|             lineno = int(float(self.sidebar_text.index(f"@0,{y_coord}"))) |  | ||||||
|             a, b = sorted([start_line, lineno]) |  | ||||||
|             self.text.tag_remove("sel", "1.0", "end") |  | ||||||
|             self.text.tag_add("sel", f"{a}.0", f"{b+1}.0") |  | ||||||
|             self.text.mark_set("insert", |  | ||||||
|                                f"{lineno if lineno == a else lineno + 1}.0") |  | ||||||
| 
 |  | ||||||
|         # Special handling of dragging with mouse button 1.  In "normal" text |  | ||||||
|         # widgets this selects text, but the line numbers text widget has |  | ||||||
|         # selection disabled.  Still, dragging triggers some selection-related |  | ||||||
|         # functionality under the hood.  Specifically, dragging to above or |  | ||||||
|         # below the text widget triggers scrolling, in a way that bypasses the |  | ||||||
|         # other scrolling synchronization mechanisms.i |  | ||||||
|         def b1_drag_handler(event, *args): |  | ||||||
|             nonlocal last_y |  | ||||||
|             nonlocal last_yview |  | ||||||
|             last_y = event.y |  | ||||||
|             last_yview = self.sidebar_text.yview() |  | ||||||
|             if not 0 <= last_y <= self.sidebar_text.winfo_height(): |  | ||||||
|                 self.text.yview_moveto(last_yview[0]) |  | ||||||
|             drag_update_selection_and_insert_mark(event.y) |  | ||||||
|         self.sidebar_text.bind('<B1-Motion>', b1_drag_handler) |  | ||||||
| 
 |  | ||||||
|         # With mouse-drag scrolling fixed by the above, there is still an edge- |  | ||||||
|         # case we need to handle: When drag-scrolling, scrolling can continue |  | ||||||
|         # while the mouse isn't moving, leading to the above fix not scrolling |  | ||||||
|         # properly. |  | ||||||
|         def selection_handler(event): |  | ||||||
|             if last_yview is None: |  | ||||||
|                 # This logic is only needed while dragging. |  | ||||||
|                 return |  | ||||||
|             yview = self.sidebar_text.yview() |  | ||||||
|             if yview != last_yview: |  | ||||||
|                 self.text.yview_moveto(yview[0]) |  | ||||||
|                 drag_update_selection_and_insert_mark(last_y) |  | ||||||
|         self.sidebar_text.bind('<<Selection>>', selection_handler) |  | ||||||
| 
 | 
 | ||||||
|     def update_colors(self): |     def update_colors(self): | ||||||
|         """Update the sidebar text colors, usually after config changes.""" |         """Update the sidebar text colors, usually after config changes.""" | ||||||
|         colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') |         colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') | ||||||
|         self._update_colors(foreground=colors['foreground'], |         foreground = colors['foreground'] | ||||||
|                             background=colors['background']) |         background = colors['background'] | ||||||
|  |         self.sidebar_text.config( | ||||||
|  |             fg=foreground, bg=background, | ||||||
|  |             selectforeground=foreground, selectbackground=background, | ||||||
|  |             inactiveselectbackground=background, | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|     def update_sidebar_text(self, end): |     def update_sidebar_text(self, end): | ||||||
|         """ |         """ | ||||||
|  | @ -319,6 +356,10 @@ def update_sidebar_text(self, end): | ||||||
| 
 | 
 | ||||||
|         self.prev_end = end |         self.prev_end = end | ||||||
| 
 | 
 | ||||||
|  |     def yscroll_event(self, *args, **kwargs): | ||||||
|  |         self.sidebar_text.yview_moveto(args[0]) | ||||||
|  |         return 'break' | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class WrappedLineHeightChangeDelegator(Delegator): | class WrappedLineHeightChangeDelegator(Delegator): | ||||||
|     def __init__(self, callback): |     def __init__(self, callback): | ||||||
|  | @ -361,22 +402,16 @@ def delete(self, index1, index2=None): | ||||||
|         self.callback() |         self.callback() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShellSidebar: | class ShellSidebar(BaseSideBar): | ||||||
|     """Sidebar for the PyShell window, for prompts etc.""" |     """Sidebar for the PyShell window, for prompts etc.""" | ||||||
|     def __init__(self, editwin): |     def __init__(self, editwin): | ||||||
|         self.editwin = editwin |         self.canvas = None | ||||||
|         self.parent = editwin.text_frame |         self.line_prompts = {} | ||||||
|         self.text = editwin.text |  | ||||||
| 
 | 
 | ||||||
|         self.canvas = tk.Canvas(self.parent, width=30, |         super().__init__(editwin) | ||||||
|                                 borderwidth=0, highlightthickness=0, |  | ||||||
|                                 takefocus=False) |  | ||||||
| 
 |  | ||||||
|         self.bind_events() |  | ||||||
| 
 | 
 | ||||||
|         change_delegator = \ |         change_delegator = \ | ||||||
|             WrappedLineHeightChangeDelegator(self.change_callback) |             WrappedLineHeightChangeDelegator(self.change_callback) | ||||||
| 
 |  | ||||||
|         # Insert the TextChangeDelegator after the last delegator, so that |         # Insert the TextChangeDelegator after the last delegator, so that | ||||||
|         # the sidebar reflects final changes to the text widget contents. |         # the sidebar reflects final changes to the text widget contents. | ||||||
|         d = self.editwin.per.top |         d = self.editwin.per.top | ||||||
|  | @ -385,16 +420,42 @@ def __init__(self, editwin): | ||||||
|                 d = d.delegate |                 d = d.delegate | ||||||
|         self.editwin.per.insertfilterafter(change_delegator, after=d) |         self.editwin.per.insertfilterafter(change_delegator, after=d) | ||||||
| 
 | 
 | ||||||
|         self.text['yscrollcommand'] = self.yscroll_event |  | ||||||
| 
 |  | ||||||
|         self.is_shown = False |  | ||||||
| 
 |  | ||||||
|         self.update_font() |  | ||||||
|         self.update_colors() |  | ||||||
|         self.update_sidebar() |  | ||||||
|         self.canvas.grid(row=1, column=0, sticky=tk.NSEW, padx=2, pady=0) |  | ||||||
|         self.is_shown = True |         self.is_shown = True | ||||||
| 
 | 
 | ||||||
|  |     def init_widgets(self): | ||||||
|  |         self.canvas = tk.Canvas(self.parent, width=30, | ||||||
|  |                                 borderwidth=0, highlightthickness=0, | ||||||
|  |                                 takefocus=False) | ||||||
|  |         self.update_sidebar() | ||||||
|  |         self.grid() | ||||||
|  |         return self.canvas | ||||||
|  | 
 | ||||||
|  |     def bind_events(self): | ||||||
|  |         super().bind_events() | ||||||
|  | 
 | ||||||
|  |         self.main_widget.bind( | ||||||
|  |             # AquaTk defines <2> as the right button, not <3>. | ||||||
|  |             "<Button-2>" if macosx.isAquaTk() else "<Button-3>", | ||||||
|  |             self.context_menu_event, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def context_menu_event(self, event): | ||||||
|  |         rmenu = tk.Menu(self.main_widget, tearoff=0) | ||||||
|  |         has_selection = bool(self.text.tag_nextrange('sel', '1.0')) | ||||||
|  |         def mkcmd(eventname): | ||||||
|  |             return lambda: self.text.event_generate(eventname) | ||||||
|  |         rmenu.add_command(label='Copy', | ||||||
|  |                           command=mkcmd('<<copy>>'), | ||||||
|  |                           state='normal' if has_selection else 'disabled') | ||||||
|  |         rmenu.add_command(label='Copy with prompts', | ||||||
|  |                           command=mkcmd('<<copy-with-prompts>>'), | ||||||
|  |                           state='normal' if has_selection else 'disabled') | ||||||
|  |         rmenu.tk_popup(event.x_root, event.y_root) | ||||||
|  |         return "break" | ||||||
|  | 
 | ||||||
|  |     def grid(self): | ||||||
|  |         self.canvas.grid(row=1, column=0, sticky=tk.NSEW, padx=2, pady=0) | ||||||
|  | 
 | ||||||
|     def change_callback(self): |     def change_callback(self): | ||||||
|         if self.is_shown: |         if self.is_shown: | ||||||
|             self.update_sidebar() |             self.update_sidebar() | ||||||
|  | @ -403,6 +464,7 @@ def update_sidebar(self): | ||||||
|         text = self.text |         text = self.text | ||||||
|         text_tagnames = text.tag_names |         text_tagnames = text.tag_names | ||||||
|         canvas = self.canvas |         canvas = self.canvas | ||||||
|  |         line_prompts = self.line_prompts = {} | ||||||
| 
 | 
 | ||||||
|         canvas.delete(tk.ALL) |         canvas.delete(tk.ALL) | ||||||
| 
 | 
 | ||||||
|  | @ -423,6 +485,8 @@ def update_sidebar(self): | ||||||
|             if prompt: |             if prompt: | ||||||
|                 canvas.create_text(2, y, anchor=tk.NW, text=prompt, |                 canvas.create_text(2, y, anchor=tk.NW, text=prompt, | ||||||
|                                    font=self.font, fill=self.colors[0]) |                                    font=self.font, fill=self.colors[0]) | ||||||
|  |                 lineno = get_lineno(text, index) | ||||||
|  |                 line_prompts[lineno] = prompt | ||||||
|             index = text.index(f'{index}+1line') |             index = text.index(f'{index}+1line') | ||||||
| 
 | 
 | ||||||
|     def yscroll_event(self, *args, **kwargs): |     def yscroll_event(self, *args, **kwargs): | ||||||
|  | @ -430,7 +494,6 @@ def yscroll_event(self, *args, **kwargs): | ||||||
| 
 | 
 | ||||||
|         The scroll bar is also updated. |         The scroll bar is also updated. | ||||||
|         """ |         """ | ||||||
|         self.editwin.vbar.set(*args) |  | ||||||
|         self.change_callback() |         self.change_callback() | ||||||
|         return 'break' |         return 'break' | ||||||
| 
 | 
 | ||||||
|  | @ -440,9 +503,6 @@ def update_font(self): | ||||||
|         tk_font = Font(self.text, font=font) |         tk_font = Font(self.text, font=font) | ||||||
|         char_width = max(tk_font.measure(char) for char in ['>', '.']) |         char_width = max(tk_font.measure(char) for char in ['>', '.']) | ||||||
|         self.canvas.configure(width=char_width * 3 + 4) |         self.canvas.configure(width=char_width * 3 + 4) | ||||||
|         self._update_font(font) |  | ||||||
| 
 |  | ||||||
|     def _update_font(self, font): |  | ||||||
|         self.font = font |         self.font = font | ||||||
|         self.change_callback() |         self.change_callback() | ||||||
| 
 | 
 | ||||||
|  | @ -450,65 +510,12 @@ def update_colors(self): | ||||||
|         """Update the sidebar text colors, usually after config changes.""" |         """Update the sidebar text colors, usually after config changes.""" | ||||||
|         linenumbers_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') |         linenumbers_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') | ||||||
|         prompt_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'console') |         prompt_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'console') | ||||||
|         self._update_colors(foreground=prompt_colors['foreground'], |         foreground = prompt_colors['foreground'] | ||||||
|                             background=linenumbers_colors['background']) |         background = linenumbers_colors['background'] | ||||||
| 
 |  | ||||||
|     def _update_colors(self, foreground, background): |  | ||||||
|         self.colors = (foreground, background) |         self.colors = (foreground, background) | ||||||
|         self.canvas.configure(background=self.colors[1]) |         self.canvas.configure(background=background) | ||||||
|         self.change_callback() |         self.change_callback() | ||||||
| 
 | 
 | ||||||
|     def redirect_focusin_event(self, event): |  | ||||||
|         """Redirect focus-in events to the main editor text widget.""" |  | ||||||
|         self.text.focus_set() |  | ||||||
|         return 'break' |  | ||||||
| 
 |  | ||||||
|     def redirect_mousebutton_event(self, event, event_name): |  | ||||||
|         """Redirect mouse button events to the main editor text widget.""" |  | ||||||
|         self.text.focus_set() |  | ||||||
|         self.text.event_generate(event_name, x=0, y=event.y) |  | ||||||
|         return 'break' |  | ||||||
| 
 |  | ||||||
|     def redirect_mousewheel_event(self, event): |  | ||||||
|         """Redirect mouse wheel events to the editwin text widget.""" |  | ||||||
|         self.text.event_generate('<MouseWheel>', |  | ||||||
|                                  x=0, y=event.y, delta=event.delta) |  | ||||||
|         return 'break' |  | ||||||
| 
 |  | ||||||
|     def bind_events(self): |  | ||||||
|         # Ensure focus is always redirected to the main editor text widget. |  | ||||||
|         self.canvas.bind('<FocusIn>', self.redirect_focusin_event) |  | ||||||
| 
 |  | ||||||
|         # Redirect mouse scrolling to the main editor text widget. |  | ||||||
|         # |  | ||||||
|         # Note that without this, scrolling with the mouse only scrolls |  | ||||||
|         # the line numbers. |  | ||||||
|         self.canvas.bind('<MouseWheel>', self.redirect_mousewheel_event) |  | ||||||
| 
 |  | ||||||
|         # Redirect mouse button events to the main editor text widget, |  | ||||||
|         # except for the left mouse button (1). |  | ||||||
|         # |  | ||||||
|         # Note: X-11 sends Button-4 and Button-5 events for the scroll wheel. |  | ||||||
|         def bind_mouse_event(event_name, target_event_name): |  | ||||||
|             handler = functools.partial(self.redirect_mousebutton_event, |  | ||||||
|                                         event_name=target_event_name) |  | ||||||
|             self.canvas.bind(event_name, handler) |  | ||||||
| 
 |  | ||||||
|         for button in [2, 3, 4, 5]: |  | ||||||
|             for event_name in (f'<Button-{button}>', |  | ||||||
|                                f'<ButtonRelease-{button}>', |  | ||||||
|                                f'<B{button}-Motion>', |  | ||||||
|                                ): |  | ||||||
|                 bind_mouse_event(event_name, target_event_name=event_name) |  | ||||||
| 
 |  | ||||||
|             # Convert double- and triple-click events to normal click events, |  | ||||||
|             # since event_generate() doesn't allow generating such events. |  | ||||||
|             for event_name in (f'<Double-Button-{button}>', |  | ||||||
|                                f'<Triple-Button-{button}>', |  | ||||||
|                                ): |  | ||||||
|                 bind_mouse_event(event_name, |  | ||||||
|                                  target_event_name=f'<Button-{button}>') |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| def _linenumbers_drag_scrolling(parent):  # htest # | def _linenumbers_drag_scrolling(parent):  # htest # | ||||||
|     from idlelib.idle_test.test_sidebar import Dummy_editwin |     from idlelib.idle_test.test_sidebar import Dummy_editwin | ||||||
|  |  | ||||||
|  | @ -0,0 +1,4 @@ | ||||||
|  | Add mouse actions to the shell sidebar.  Left click and optional drag | ||||||
|  | selects one or more lines, as with the editor line number sidebar.  Right | ||||||
|  | click after selecting raises a context menu with 'copy with prompts'.  This | ||||||
|  | zips together prompts from the sidebar with lines from the selected text. | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Tal Einat
						Tal Einat