mirror of https://github.com/python/cpython
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
|
@ -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 @@ TRIGGERS = f".{SEPS}"
|
||||||
|
|
||||||
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 @@ class AutoComplete:
|
||||||
"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 @@ DOUBLECLICK_SEQUENCE = "<B1-Double-ButtonRelease>"
|
||||||
|
|
||||||
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 @@ class AutoCompleteWindow:
|
||||||
"%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 @@ class EditorWindow:
|
||||||
|
|
||||||
# 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 @@ class AutoCompleteWindowTest(unittest.TestCase):
|
||||||
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 @@ class LineNumbersTest(unittest.TestCase):
|
||||||
|
|
||||||
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 @@ class ShellSidebarTest(unittest.TestCase):
|
||||||
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 @@ if TkVersion < 8.5:
|
||||||
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 @@ class PyShell(OutputWindow):
|
||||||
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 @@ class PyShell(OutputWindow):
|
||||||
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 @@ import tkinter as tk
|
||||||
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 @@ class BaseSideBar:
|
||||||
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 @@ class BaseSideBar:
|
||||||
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 @@ class EndLineDelegator(Delegator):
|
||||||
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 @@ class LineNumbers(BaseSideBar):
|
||||||
|
|
||||||
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 @@ class WrappedLineHeightChangeDelegator(Delegator):
|
||||||
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 @@ class ShellSidebar:
|
||||||
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 @@ class ShellSidebar:
|
||||||
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 @@ class ShellSidebar:
|
||||||
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 @@ class ShellSidebar:
|
||||||
|
|
||||||
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 @@ class ShellSidebar:
|
||||||
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 @@ class ShellSidebar:
|
||||||
"""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…
Reference in New Issue