bpo-17535: IDLE editor line numbers (GH-14030)
This commit is contained in:
parent
1ebee37dde
commit
7123ea009b
|
@ -290,22 +290,31 @@ Options menu (Shell and Editor)
|
||||||
Configure IDLE
|
Configure IDLE
|
||||||
Open a configuration dialog and change preferences for the following:
|
Open a configuration dialog and change preferences for the following:
|
||||||
fonts, indentation, keybindings, text color themes, startup windows and
|
fonts, indentation, keybindings, text color themes, startup windows and
|
||||||
size, additional help sources, and extensions. On macOS, open the
|
size, additional help sources, and extensions. On macOS, open the
|
||||||
configuration dialog by selecting Preferences in the application
|
configuration dialog by selecting Preferences in the application
|
||||||
menu. For more, see
|
menu. For more details, see
|
||||||
:ref:`Setting preferences <preferences>` under Help and preferences.
|
:ref:`Setting preferences <preferences>` under Help and preferences.
|
||||||
|
|
||||||
|
Most configuration options apply to all windows or all future windows.
|
||||||
|
The option items below only apply to the active window.
|
||||||
|
|
||||||
Show/Hide Code Context (Editor Window only)
|
Show/Hide Code Context (Editor Window only)
|
||||||
Open a pane at the top of the edit window which shows the block context
|
Open a pane at the top of the edit window which shows the block context
|
||||||
of the code which has scrolled above the top of the window. See
|
of the code which has scrolled above the top of the window. See
|
||||||
:ref:`Code Context <code-context>` in the Editing and Navigation section below.
|
:ref:`Code Context <code-context>` in the Editing and Navigation section
|
||||||
|
below.
|
||||||
|
|
||||||
|
Show/Hide Line Numbers (Editor Window only)
|
||||||
|
Open a column to the left of the edit window which shows the number
|
||||||
|
of each line of text. The default is off, which may be changed in the
|
||||||
|
preferences (see :ref:`Setting preferences <preferences>`).
|
||||||
|
|
||||||
Zoom/Restore Height
|
Zoom/Restore Height
|
||||||
Toggles the window between normal size and maximum height. The initial size
|
Toggles the window between normal size and maximum height. The initial size
|
||||||
defaults to 40 lines by 80 chars unless changed on the General tab of the
|
defaults to 40 lines by 80 chars unless changed on the General tab of the
|
||||||
Configure IDLE dialog. The maximum height for a screen is determined by
|
Configure IDLE dialog. The maximum height for a screen is determined by
|
||||||
momentarily maximizing a window the first time one is zoomed on the screen.
|
momentarily maximizing a window the first time one is zoomed on the screen.
|
||||||
Changing screen settings may invalidate the saved height. This toogle has
|
Changing screen settings may invalidate the saved height. This toggle has
|
||||||
no effect when a window is maximized.
|
no effect when a window is maximized.
|
||||||
|
|
||||||
Window menu (Shell and Editor)
|
Window menu (Shell and Editor)
|
||||||
|
|
|
@ -1017,6 +1017,13 @@ by right-clicking the button. (Contributed by Tal Einat in :issue:`1529353`.)
|
||||||
|
|
||||||
The changes above have been backported to 3.6 maintenance releases.
|
The changes above have been backported to 3.6 maintenance releases.
|
||||||
|
|
||||||
|
New in 3.7.5:
|
||||||
|
|
||||||
|
Add optional line numbers for IDLE editor windows. Windows
|
||||||
|
open without line numbers unless set otherwise in the General
|
||||||
|
tab of the configuration dialog.
|
||||||
|
(Contributed by Tal Einat and Saimadhav Heblikar in :issue:`17535`.)
|
||||||
|
|
||||||
|
|
||||||
importlib
|
importlib
|
||||||
---------
|
---------
|
||||||
|
|
|
@ -515,6 +515,11 @@ for certain types of invalid or corrupt gzip files.
|
||||||
idlelib and IDLE
|
idlelib and IDLE
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
Add optional line numbers for IDLE editor windows. Windows
|
||||||
|
open without line numbers unless set otherwise in the General
|
||||||
|
tab of the configuration dialog.
|
||||||
|
(Contributed by Tal Einat and Saimadhav Heblikar in :issue:`17535`.)
|
||||||
|
|
||||||
Output over N lines (50 by default) is squeezed down to a button.
|
Output over N lines (50 by default) is squeezed down to a button.
|
||||||
N can be changed in the PyShell section of the General page of the
|
N can be changed in the PyShell section of the General page of the
|
||||||
Settings dialog. Fewer, but possibly extra long, lines can be squeezed by
|
Settings dialog. Fewer, but possibly extra long, lines can be squeezed by
|
||||||
|
|
|
@ -13,7 +13,7 @@ import re
|
||||||
from sys import maxsize as INFINITY
|
from sys import maxsize as INFINITY
|
||||||
|
|
||||||
import tkinter
|
import tkinter
|
||||||
from tkinter.constants import TOP, X, SUNKEN
|
from tkinter.constants import NSEW, SUNKEN
|
||||||
|
|
||||||
from idlelib.config import idleConf
|
from idlelib.config import idleConf
|
||||||
|
|
||||||
|
@ -67,6 +67,7 @@ class CodeContext:
|
||||||
|
|
||||||
def _reset(self):
|
def _reset(self):
|
||||||
self.context = None
|
self.context = None
|
||||||
|
self.cell00 = None
|
||||||
self.t1 = None
|
self.t1 = None
|
||||||
self.topvisible = 1
|
self.topvisible = 1
|
||||||
self.info = [(0, -1, "", False)]
|
self.info = [(0, -1, "", False)]
|
||||||
|
@ -105,25 +106,37 @@ class CodeContext:
|
||||||
padx = 0
|
padx = 0
|
||||||
border = 0
|
border = 0
|
||||||
for widget in widgets:
|
for widget in widgets:
|
||||||
padx += widget.tk.getint(widget.pack_info()['padx'])
|
info = (widget.grid_info()
|
||||||
|
if widget is self.editwin.text
|
||||||
|
else widget.pack_info())
|
||||||
|
padx += widget.tk.getint(info['padx'])
|
||||||
padx += widget.tk.getint(widget.cget('padx'))
|
padx += widget.tk.getint(widget.cget('padx'))
|
||||||
border += widget.tk.getint(widget.cget('border'))
|
border += widget.tk.getint(widget.cget('border'))
|
||||||
self.context = tkinter.Text(
|
self.context = tkinter.Text(
|
||||||
self.editwin.top, font=self.text['font'],
|
self.editwin.text_frame,
|
||||||
height=1,
|
height=1,
|
||||||
width=1, # Don't request more than we get.
|
width=1, # Don't request more than we get.
|
||||||
|
highlightthickness=0,
|
||||||
padx=padx, border=border, relief=SUNKEN, state='disabled')
|
padx=padx, border=border, relief=SUNKEN, state='disabled')
|
||||||
|
self.update_font()
|
||||||
self.update_highlight_colors()
|
self.update_highlight_colors()
|
||||||
self.context.bind('<ButtonRelease-1>', self.jumptoline)
|
self.context.bind('<ButtonRelease-1>', self.jumptoline)
|
||||||
# Get the current context and initiate the recurring update event.
|
# Get the current context and initiate the recurring update event.
|
||||||
self.timer_event()
|
self.timer_event()
|
||||||
# Pack the context widget before and above the text_frame widget,
|
# Grid the context widget above the text widget.
|
||||||
# thus ensuring that it will appear directly above text_frame.
|
self.context.grid(row=0, column=1, sticky=NSEW)
|
||||||
self.context.pack(side=TOP, fill=X, expand=False,
|
|
||||||
before=self.editwin.text_frame)
|
line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(),
|
||||||
|
'linenumber')
|
||||||
|
self.cell00 = tkinter.Frame(self.editwin.text_frame,
|
||||||
|
bg=line_number_colors['background'])
|
||||||
|
self.cell00.grid(row=0, column=0, sticky=NSEW)
|
||||||
menu_status = 'Hide'
|
menu_status = 'Hide'
|
||||||
else:
|
else:
|
||||||
self.context.destroy()
|
self.context.destroy()
|
||||||
|
self.context = None
|
||||||
|
self.cell00.destroy()
|
||||||
|
self.cell00 = None
|
||||||
self.text.after_cancel(self.t1)
|
self.text.after_cancel(self.t1)
|
||||||
self._reset()
|
self._reset()
|
||||||
menu_status = 'Show'
|
menu_status = 'Show'
|
||||||
|
@ -221,8 +234,9 @@ class CodeContext:
|
||||||
self.update_code_context()
|
self.update_code_context()
|
||||||
self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event)
|
self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event)
|
||||||
|
|
||||||
def update_font(self, font):
|
def update_font(self):
|
||||||
if self.context is not None:
|
if self.context is not None:
|
||||||
|
font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
|
||||||
self.context['font'] = font
|
self.context['font'] = font
|
||||||
|
|
||||||
def update_highlight_colors(self):
|
def update_highlight_colors(self):
|
||||||
|
@ -231,6 +245,11 @@ class CodeContext:
|
||||||
self.context['background'] = colors['background']
|
self.context['background'] = colors['background']
|
||||||
self.context['foreground'] = colors['foreground']
|
self.context['foreground'] = colors['foreground']
|
||||||
|
|
||||||
|
if self.cell00 is not None:
|
||||||
|
line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(),
|
||||||
|
'linenumber')
|
||||||
|
self.cell00.config(bg=line_number_colors['background'])
|
||||||
|
|
||||||
|
|
||||||
CodeContext.reload()
|
CodeContext.reload()
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,10 @@ hit-foreground= #ffffff
|
||||||
hit-background= #000000
|
hit-background= #000000
|
||||||
error-foreground= #000000
|
error-foreground= #000000
|
||||||
error-background= #ff7777
|
error-background= #ff7777
|
||||||
|
context-foreground= #000000
|
||||||
|
context-background= lightgray
|
||||||
|
linenumber-foreground= gray
|
||||||
|
linenumber-background= #ffffff
|
||||||
#cursor (only foreground can be set, restart IDLE)
|
#cursor (only foreground can be set, restart IDLE)
|
||||||
cursor-foreground= black
|
cursor-foreground= black
|
||||||
#shell window
|
#shell window
|
||||||
|
@ -31,8 +35,6 @@ stderr-foreground= red
|
||||||
stderr-background= #ffffff
|
stderr-background= #ffffff
|
||||||
console-foreground= #770000
|
console-foreground= #770000
|
||||||
console-background= #ffffff
|
console-background= #ffffff
|
||||||
context-foreground= #000000
|
|
||||||
context-background= lightgray
|
|
||||||
|
|
||||||
[IDLE New]
|
[IDLE New]
|
||||||
normal-foreground= #000000
|
normal-foreground= #000000
|
||||||
|
@ -55,6 +57,10 @@ hit-foreground= #ffffff
|
||||||
hit-background= #000000
|
hit-background= #000000
|
||||||
error-foreground= #000000
|
error-foreground= #000000
|
||||||
error-background= #ff7777
|
error-background= #ff7777
|
||||||
|
context-foreground= #000000
|
||||||
|
context-background= lightgray
|
||||||
|
linenumber-foreground= gray
|
||||||
|
linenumber-background= #ffffff
|
||||||
#cursor (only foreground can be set, restart IDLE)
|
#cursor (only foreground can be set, restart IDLE)
|
||||||
cursor-foreground= black
|
cursor-foreground= black
|
||||||
#shell window
|
#shell window
|
||||||
|
@ -64,8 +70,6 @@ stderr-foreground= red
|
||||||
stderr-background= #ffffff
|
stderr-background= #ffffff
|
||||||
console-foreground= #770000
|
console-foreground= #770000
|
||||||
console-background= #ffffff
|
console-background= #ffffff
|
||||||
context-foreground= #000000
|
|
||||||
context-background= lightgray
|
|
||||||
|
|
||||||
[IDLE Dark]
|
[IDLE Dark]
|
||||||
comment-foreground = #dd0000
|
comment-foreground = #dd0000
|
||||||
|
@ -97,3 +101,5 @@ comment-background = #002240
|
||||||
break-foreground = #FFFFFF
|
break-foreground = #FFFFFF
|
||||||
context-foreground= #ffffff
|
context-foreground= #ffffff
|
||||||
context-background= #454545
|
context-background= #454545
|
||||||
|
linenumber-foreground= gray
|
||||||
|
linenumber-background= #002240
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
# Additional help sources are listed in the [HelpFiles] section below
|
# Additional help sources are listed in the [HelpFiles] section below
|
||||||
# and should be viewable by a web browser (or the Windows Help viewer in
|
# and should be viewable by a web browser (or the Windows Help viewer in
|
||||||
# the case of .chm files). These sources will be listed on the Help
|
# the case of .chm files). These sources will be listed on the Help
|
||||||
# menu. The pattern, and two examples, are
|
# menu. The pattern, and two examples, are:
|
||||||
#
|
#
|
||||||
# <sequence_number = menu item;/path/to/help/source>
|
# <sequence_number = menu item;/path/to/help/source>
|
||||||
# 1 = IDLE;C:/Programs/Python36/Lib/idlelib/help.html
|
# 1 = IDLE;C:/Programs/Python36/Lib/idlelib/help.html
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
# platform specific because of path separators, drive specs etc.
|
# platform specific because of path separators, drive specs etc.
|
||||||
#
|
#
|
||||||
# The default files should not be edited except to add new sections to
|
# The default files should not be edited except to add new sections to
|
||||||
# config-extensions.def for added extensions . The user files should be
|
# config-extensions.def for added extensions. The user files should be
|
||||||
# modified through the Settings dialog.
|
# modified through the Settings dialog.
|
||||||
|
|
||||||
[General]
|
[General]
|
||||||
|
@ -65,6 +65,7 @@ font= TkFixedFont
|
||||||
font-size= 10
|
font-size= 10
|
||||||
font-bold= 0
|
font-bold= 0
|
||||||
encoding= none
|
encoding= none
|
||||||
|
line-numbers-default= 0
|
||||||
|
|
||||||
[PyShell]
|
[PyShell]
|
||||||
auto-squeeze-min-lines= 50
|
auto-squeeze-min-lines= 50
|
||||||
|
|
|
@ -319,6 +319,10 @@ class IdleConf:
|
||||||
'hit-background':'#000000',
|
'hit-background':'#000000',
|
||||||
'error-foreground':'#ffffff',
|
'error-foreground':'#ffffff',
|
||||||
'error-background':'#000000',
|
'error-background':'#000000',
|
||||||
|
'context-foreground':'#000000',
|
||||||
|
'context-background':'#ffffff',
|
||||||
|
'linenumber-foreground':'#000000',
|
||||||
|
'linenumber-background':'#ffffff',
|
||||||
#cursor (only foreground can be set)
|
#cursor (only foreground can be set)
|
||||||
'cursor-foreground':'#000000',
|
'cursor-foreground':'#000000',
|
||||||
#shell window
|
#shell window
|
||||||
|
@ -328,11 +332,11 @@ class IdleConf:
|
||||||
'stderr-background':'#ffffff',
|
'stderr-background':'#ffffff',
|
||||||
'console-foreground':'#000000',
|
'console-foreground':'#000000',
|
||||||
'console-background':'#ffffff',
|
'console-background':'#ffffff',
|
||||||
'context-foreground':'#000000',
|
|
||||||
'context-background':'#ffffff',
|
|
||||||
}
|
}
|
||||||
for element in theme:
|
for element in theme:
|
||||||
if not cfgParser.has_option(themeName, element):
|
if not (cfgParser.has_option(themeName, element) or
|
||||||
|
# Skip warning for new elements.
|
||||||
|
element.startswith(('context-', 'linenumber-'))):
|
||||||
# Print warning that will return a default color
|
# Print warning that will return a default color
|
||||||
warning = ('\n Warning: config.IdleConf.GetThemeDict'
|
warning = ('\n Warning: config.IdleConf.GetThemeDict'
|
||||||
' -\n problem retrieving theme element %r'
|
' -\n problem retrieving theme element %r'
|
||||||
|
|
|
@ -819,6 +819,7 @@ class HighPage(Frame):
|
||||||
'Shell Error Text': ('error', '12'),
|
'Shell Error Text': ('error', '12'),
|
||||||
'Shell Stdout Text': ('stdout', '13'),
|
'Shell Stdout Text': ('stdout', '13'),
|
||||||
'Shell Stderr Text': ('stderr', '14'),
|
'Shell Stderr Text': ('stderr', '14'),
|
||||||
|
'Line Number': ('linenumber', '16'),
|
||||||
}
|
}
|
||||||
self.builtin_name = tracers.add(
|
self.builtin_name = tracers.add(
|
||||||
StringVar(self), self.var_changed_builtin_name)
|
StringVar(self), self.var_changed_builtin_name)
|
||||||
|
@ -866,6 +867,11 @@ class HighPage(Frame):
|
||||||
('stderr', 'stderr'), ('\n\n', 'normal'))
|
('stderr', 'stderr'), ('\n\n', 'normal'))
|
||||||
for texttag in text_and_tags:
|
for texttag in text_and_tags:
|
||||||
text.insert(END, texttag[0], texttag[1])
|
text.insert(END, texttag[0], texttag[1])
|
||||||
|
n_lines = len(text.get('1.0', END).splitlines())
|
||||||
|
for lineno in range(1, n_lines + 1):
|
||||||
|
text.insert(f'{lineno}.0',
|
||||||
|
f'{lineno:{len(str(n_lines))}d} ',
|
||||||
|
'linenumber')
|
||||||
for element in self.theme_elements:
|
for element in self.theme_elements:
|
||||||
def tem(event, elem=element):
|
def tem(event, elem=element):
|
||||||
# event.widget.winfo_top_level().highlight_target.set(elem)
|
# event.widget.winfo_top_level().highlight_target.set(elem)
|
||||||
|
@ -1827,6 +1833,9 @@ class GenPage(Frame):
|
||||||
frame_format: Frame
|
frame_format: Frame
|
||||||
format_width_title: Label
|
format_width_title: Label
|
||||||
(*)format_width_int: Entry - format_width
|
(*)format_width_int: Entry - format_width
|
||||||
|
frame_line_numbers_default: Frame
|
||||||
|
line_numbers_default_title: Label
|
||||||
|
(*)line_numbers_default_bool: Checkbutton - line_numbers_default
|
||||||
frame_context: Frame
|
frame_context: Frame
|
||||||
context_title: Label
|
context_title: Label
|
||||||
(*)context_int: Entry - context_lines
|
(*)context_int: Entry - context_lines
|
||||||
|
@ -1866,6 +1875,9 @@ class GenPage(Frame):
|
||||||
IntVar(self), ('main', 'General', 'autosave'))
|
IntVar(self), ('main', 'General', 'autosave'))
|
||||||
self.format_width = tracers.add(
|
self.format_width = tracers.add(
|
||||||
StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
|
StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
|
||||||
|
self.line_numbers_default = tracers.add(
|
||||||
|
BooleanVar(self),
|
||||||
|
('main', 'EditorWindow', 'line-numbers-default'))
|
||||||
self.context_lines = tracers.add(
|
self.context_lines = tracers.add(
|
||||||
StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
|
StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
|
||||||
|
|
||||||
|
@ -1944,6 +1956,14 @@ class GenPage(Frame):
|
||||||
validatecommand=self.digits_only, validate='key',
|
validatecommand=self.digits_only, validate='key',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
frame_line_numbers_default = Frame(frame_editor, borderwidth=0)
|
||||||
|
line_numbers_default_title = Label(
|
||||||
|
frame_line_numbers_default, text='Show line numbers in new windows')
|
||||||
|
self.line_numbers_default_bool = Checkbutton(
|
||||||
|
frame_line_numbers_default,
|
||||||
|
variable=self.line_numbers_default,
|
||||||
|
width=1)
|
||||||
|
|
||||||
frame_context = Frame(frame_editor, borderwidth=0)
|
frame_context = Frame(frame_editor, borderwidth=0)
|
||||||
context_title = Label(frame_context, text='Max Context Lines :')
|
context_title = Label(frame_context, text='Max Context Lines :')
|
||||||
self.context_int = Entry(
|
self.context_int = Entry(
|
||||||
|
@ -2021,6 +2041,10 @@ class GenPage(Frame):
|
||||||
frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
|
frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
|
||||||
format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
|
format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
|
||||||
self.format_width_int.pack(side=TOP, padx=10, pady=5)
|
self.format_width_int.pack(side=TOP, padx=10, pady=5)
|
||||||
|
# frame_line_numbers_default.
|
||||||
|
frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X)
|
||||||
|
line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
|
||||||
|
self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5)
|
||||||
# frame_context.
|
# frame_context.
|
||||||
frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
|
frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
|
||||||
context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
|
context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
|
||||||
|
@ -2063,6 +2087,8 @@ class GenPage(Frame):
|
||||||
'main', 'General', 'autosave', default=0, type='bool'))
|
'main', 'General', 'autosave', default=0, type='bool'))
|
||||||
self.format_width.set(idleConf.GetOption(
|
self.format_width.set(idleConf.GetOption(
|
||||||
'extensions', 'FormatParagraph', 'max-width', type='int'))
|
'extensions', 'FormatParagraph', 'max-width', type='int'))
|
||||||
|
self.line_numbers_default.set(idleConf.GetOption(
|
||||||
|
'main', 'EditorWindow', 'line-numbers-default', type='bool'))
|
||||||
self.context_lines.set(idleConf.GetOption(
|
self.context_lines.set(idleConf.GetOption(
|
||||||
'extensions', 'CodeContext', 'maxlines', type='int'))
|
'extensions', 'CodeContext', 'maxlines', type='int'))
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ class EditorWindow(object):
|
||||||
from idlelib.autoexpand import AutoExpand
|
from idlelib.autoexpand import AutoExpand
|
||||||
from idlelib.calltip import Calltip
|
from idlelib.calltip import Calltip
|
||||||
from idlelib.codecontext import CodeContext
|
from idlelib.codecontext import CodeContext
|
||||||
|
from idlelib.sidebar import LineNumbers
|
||||||
from idlelib.format import FormatParagraph, FormatRegion, Indents, Rstrip
|
from idlelib.format import FormatParagraph, FormatRegion, Indents, Rstrip
|
||||||
from idlelib.parenmatch import ParenMatch
|
from idlelib.parenmatch import ParenMatch
|
||||||
from idlelib.squeezer import Squeezer
|
from idlelib.squeezer import Squeezer
|
||||||
|
@ -61,7 +62,8 @@ class EditorWindow(object):
|
||||||
filesystemencoding = sys.getfilesystemencoding() # for file names
|
filesystemencoding = sys.getfilesystemencoding() # for file names
|
||||||
help_url = None
|
help_url = None
|
||||||
|
|
||||||
allow_codecontext = True
|
allow_code_context = True
|
||||||
|
allow_line_numbers = True
|
||||||
|
|
||||||
def __init__(self, flist=None, filename=None, key=None, root=None):
|
def __init__(self, flist=None, filename=None, key=None, root=None):
|
||||||
# Delay import: runscript imports pyshell imports EditorWindow.
|
# Delay import: runscript imports pyshell imports EditorWindow.
|
||||||
|
@ -198,12 +200,14 @@ class EditorWindow(object):
|
||||||
text.bind("<<open-turtle-demo>>", self.open_turtle_demo)
|
text.bind("<<open-turtle-demo>>", self.open_turtle_demo)
|
||||||
|
|
||||||
self.set_status_bar()
|
self.set_status_bar()
|
||||||
|
text_frame.pack(side=LEFT, fill=BOTH, expand=1)
|
||||||
|
text_frame.rowconfigure(1, weight=1)
|
||||||
|
text_frame.columnconfigure(1, weight=1)
|
||||||
vbar['command'] = self.handle_yview
|
vbar['command'] = self.handle_yview
|
||||||
vbar.pack(side=RIGHT, fill=Y)
|
vbar.grid(row=1, column=2, sticky=NSEW)
|
||||||
text['yscrollcommand'] = vbar.set
|
text['yscrollcommand'] = vbar.set
|
||||||
text['font'] = idleConf.GetFont(self.root, 'main', 'EditorWindow')
|
text['font'] = idleConf.GetFont(self.root, 'main', 'EditorWindow')
|
||||||
text_frame.pack(side=LEFT, fill=BOTH, expand=1)
|
text.grid(row=1, column=1, sticky=NSEW)
|
||||||
text.pack(side=TOP, fill=BOTH, expand=1)
|
|
||||||
text.focus_set()
|
text.focus_set()
|
||||||
|
|
||||||
# usetabs true -> literal tab characters are used by indent and
|
# usetabs true -> literal tab characters are used by indent and
|
||||||
|
@ -250,7 +254,8 @@ class EditorWindow(object):
|
||||||
self.good_load = False
|
self.good_load = False
|
||||||
self.set_indentation_params(False)
|
self.set_indentation_params(False)
|
||||||
self.color = None # initialized below in self.ResetColorizer
|
self.color = None # initialized below in self.ResetColorizer
|
||||||
self.codecontext = None
|
self.code_context = None # optionally initialized later below
|
||||||
|
self.line_numbers = None # optionally initialized later below
|
||||||
if filename:
|
if filename:
|
||||||
if os.path.exists(filename) and not os.path.isdir(filename):
|
if os.path.exists(filename) and not os.path.isdir(filename):
|
||||||
if io.loadfile(filename):
|
if io.loadfile(filename):
|
||||||
|
@ -316,10 +321,20 @@ class EditorWindow(object):
|
||||||
text.bind("<<refresh-calltip>>", ctip.refresh_calltip_event)
|
text.bind("<<refresh-calltip>>", ctip.refresh_calltip_event)
|
||||||
text.bind("<<force-open-calltip>>", ctip.force_open_calltip_event)
|
text.bind("<<force-open-calltip>>", ctip.force_open_calltip_event)
|
||||||
text.bind("<<zoom-height>>", self.ZoomHeight(self).zoom_height_event)
|
text.bind("<<zoom-height>>", self.ZoomHeight(self).zoom_height_event)
|
||||||
if self.allow_codecontext:
|
if self.allow_code_context:
|
||||||
self.codecontext = self.CodeContext(self)
|
self.code_context = self.CodeContext(self)
|
||||||
text.bind("<<toggle-code-context>>",
|
text.bind("<<toggle-code-context>>",
|
||||||
self.codecontext.toggle_code_context_event)
|
self.code_context.toggle_code_context_event)
|
||||||
|
else:
|
||||||
|
self.update_menu_state('options', '*Code Context', 'disabled')
|
||||||
|
if self.allow_line_numbers:
|
||||||
|
self.line_numbers = self.LineNumbers(self)
|
||||||
|
if idleConf.GetOption('main', 'EditorWindow',
|
||||||
|
'line-numbers-default', type='bool'):
|
||||||
|
self.toggle_line_numbers_event()
|
||||||
|
text.bind("<<toggle-line-numbers>>", self.toggle_line_numbers_event)
|
||||||
|
else:
|
||||||
|
self.update_menu_state('options', '*Line Numbers', 'disabled')
|
||||||
|
|
||||||
def _filename_to_unicode(self, filename):
|
def _filename_to_unicode(self, filename):
|
||||||
"""Return filename as BMP unicode so displayable in Tk."""
|
"""Return filename as BMP unicode so displayable in Tk."""
|
||||||
|
@ -779,8 +794,11 @@ class EditorWindow(object):
|
||||||
self._addcolorizer()
|
self._addcolorizer()
|
||||||
EditorWindow.color_config(self.text)
|
EditorWindow.color_config(self.text)
|
||||||
|
|
||||||
if self.codecontext is not None:
|
if self.code_context is not None:
|
||||||
self.codecontext.update_highlight_colors()
|
self.code_context.update_highlight_colors()
|
||||||
|
|
||||||
|
if self.line_numbers is not None:
|
||||||
|
self.line_numbers.update_colors()
|
||||||
|
|
||||||
IDENTCHARS = string.ascii_letters + string.digits + "_"
|
IDENTCHARS = string.ascii_letters + string.digits + "_"
|
||||||
|
|
||||||
|
@ -799,11 +817,16 @@ class EditorWindow(object):
|
||||||
"Update the text widgets' font if it is changed"
|
"Update the text widgets' font if it is changed"
|
||||||
# Called from configdialog.py
|
# Called from configdialog.py
|
||||||
|
|
||||||
new_font = idleConf.GetFont(self.root, 'main', 'EditorWindow')
|
|
||||||
# Update the code context widget first, since its height affects
|
# Update the code context widget first, since its height affects
|
||||||
# the height of the text widget. This avoids double re-rendering.
|
# the height of the text widget. This avoids double re-rendering.
|
||||||
if self.codecontext is not None:
|
if self.code_context is not None:
|
||||||
self.codecontext.update_font(new_font)
|
self.code_context.update_font()
|
||||||
|
# Next, update the line numbers widget, since its width affects
|
||||||
|
# the width of the text widget.
|
||||||
|
if self.line_numbers is not None:
|
||||||
|
self.line_numbers.update_font()
|
||||||
|
# Finally, update the main text widget.
|
||||||
|
new_font = idleConf.GetFont(self.root, 'main', 'EditorWindow')
|
||||||
self.text['font'] = new_font
|
self.text['font'] = new_font
|
||||||
|
|
||||||
def RemoveKeybindings(self):
|
def RemoveKeybindings(self):
|
||||||
|
@ -1467,6 +1490,19 @@ class EditorWindow(object):
|
||||||
indentsmall = indentlarge = 0
|
indentsmall = indentlarge = 0
|
||||||
return indentlarge - indentsmall
|
return indentlarge - indentsmall
|
||||||
|
|
||||||
|
def toggle_line_numbers_event(self, event=None):
|
||||||
|
if self.line_numbers is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.line_numbers.is_shown:
|
||||||
|
self.line_numbers.hide_sidebar()
|
||||||
|
menu_label = "Show"
|
||||||
|
else:
|
||||||
|
self.line_numbers.show_sidebar()
|
||||||
|
menu_label = "Hide"
|
||||||
|
self.update_menu_label(menu='options', index='*Line Numbers',
|
||||||
|
label=f'{menu_label} Line Numbers')
|
||||||
|
|
||||||
# "line.col" -> line, as an int
|
# "line.col" -> line, as an int
|
||||||
def index2line(index):
|
def index2line(index):
|
||||||
return int(float(index))
|
return int(float(index))
|
||||||
|
|
|
@ -271,10 +271,15 @@ fonts, indentation, keybindings, text color themes, startup windows and
|
||||||
size, additional help sources, and extensions. On macOS, open the
|
size, additional help sources, and extensions. On macOS, open the
|
||||||
configuration dialog by selecting Preferences in the application
|
configuration dialog by selecting Preferences in the application
|
||||||
menu. For more, see
|
menu. For more, see
|
||||||
<a class="reference internal" href="#preferences"><span class="std std-ref">Setting preferences</span></a> under Help and preferences.</dd>
|
<a class="reference internal" href="#preferences"><span class="std std-ref">Setting preferences</span></a> under Help and preferences.
|
||||||
|
Most configuration options apply to all windows or all future windows.
|
||||||
|
The option items below only apply to the active window.</dd>
|
||||||
<dt>Show/Hide Code Context (Editor Window only)</dt><dd>Open a pane at the top of the edit window which shows the block context
|
<dt>Show/Hide Code Context (Editor Window only)</dt><dd>Open a pane at the top of the edit window which shows the block context
|
||||||
of the code which has scrolled above the top of the window. See
|
of the code which has scrolled above the top of the window. See
|
||||||
<a class="reference internal" href="#code-context"><span class="std std-ref">Code Context</span></a> in the Editing and Navigation section below.</dd>
|
<a class="reference internal" href="#code-context"><span class="std std-ref">Code Context</span></a> in the Editing and Navigation section below.</dd>
|
||||||
|
<dt>Line Numbers (Editor Window only)</dt><dd>Open a column to the left of the edit window which shows the linenumber
|
||||||
|
of each line of text. The default is off unless configured on
|
||||||
|
(see <a class="reference internal" href="#preferences"><span class="std std-ref">Setting preferences</span></a>).</dd>
|
||||||
<dt>Zoom/Restore Height</dt><dd>Toggles the window between normal size and maximum height. The initial size
|
<dt>Zoom/Restore Height</dt><dd>Toggles the window between normal size and maximum height. The initial size
|
||||||
defaults to 40 lines by 80 chars unless changed on the General tab of the
|
defaults to 40 lines by 80 chars unless changed on the General tab of the
|
||||||
Configure IDLE dialog. The maximum height for a screen is determined by
|
Configure IDLE dialog. The maximum height for a screen is determined by
|
||||||
|
@ -607,7 +612,7 @@ IDLE should be started in a command line window. The secondary subprocess
|
||||||
will then be attached to that window for input and output.</p>
|
will then be attached to that window for input and output.</p>
|
||||||
<p>The IDLE code running in the execution process adds frames to the call stack
|
<p>The IDLE code running in the execution process adds frames to the call stack
|
||||||
that would not be there otherwise. IDLE wraps <code class="docutils literal notranslate"><span class="pre">sys.getrecursionlimit</span></code> and
|
that would not be there otherwise. IDLE wraps <code class="docutils literal notranslate"><span class="pre">sys.getrecursionlimit</span></code> and
|
||||||
<code class="docutils literal notranslate"><span class="pre">sys.setrecursionlimit</span></code> to reduce their visibility.</p>
|
<code class="docutils literal notranslate"><span class="pre">sys.setrecursionlimit</span></code> to reduce the effect of the additional stack frames.</p>
|
||||||
<p>If <code class="docutils literal notranslate"><span class="pre">sys</span></code> is reset by user code, such as with <code class="docutils literal notranslate"><span class="pre">importlib.reload(sys)</span></code>,
|
<p>If <code class="docutils literal notranslate"><span class="pre">sys</span></code> is reset by user code, such as with <code class="docutils literal notranslate"><span class="pre">importlib.reload(sys)</span></code>,
|
||||||
IDLE’s changes are lost and input from the keyboard and output to the screen
|
IDLE’s changes are lost and input from the keyboard and output to the screen
|
||||||
will not work correctly.</p>
|
will not work correctly.</p>
|
||||||
|
@ -895,7 +900,7 @@ also used for testing.</p>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
Last updated on Jul 04, 2019.
|
Last updated on Jul 23, 2019.
|
||||||
<a href="https://docs.python.org/3/bugs.html">Found a bug</a>?
|
<a href="https://docs.python.org/3/bugs.html">Found a bug</a>?
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,7 @@ outwin.OutputWindow (indirectly being tested with grep test)
|
||||||
|
|
||||||
import idlelib.pyshell # Set Windows DPI awareness before Tk().
|
import idlelib.pyshell # Set Windows DPI awareness before Tk().
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
import textwrap
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter.ttk import Scrollbar
|
from tkinter.ttk import Scrollbar
|
||||||
tk.NoDefaultRoot()
|
tk.NoDefaultRoot()
|
||||||
|
@ -205,6 +206,19 @@ _io_binding_spec = {
|
||||||
"Check that changes were saved by opening the file elsewhere."
|
"Check that changes were saved by opening the file elsewhere."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_linenumbers_drag_scrolling_spec = {
|
||||||
|
'file': 'sidebar',
|
||||||
|
'kwds': {},
|
||||||
|
'msg': textwrap.dedent("""\
|
||||||
|
Click on the line numbers and drag down below the edge of the
|
||||||
|
window, moving the mouse a bit and then leaving it there for a while.
|
||||||
|
The text and line numbers should gradually scroll down, with the
|
||||||
|
selection updated continuously.
|
||||||
|
Do the same as above, dragging to above the window. The text and line
|
||||||
|
numbers should gradually scroll up, with the selection updated
|
||||||
|
continuously."""),
|
||||||
|
}
|
||||||
|
|
||||||
_multi_call_spec = {
|
_multi_call_spec = {
|
||||||
'file': 'multicall',
|
'file': 'multicall',
|
||||||
'kwds': {},
|
'kwds': {},
|
||||||
|
|
|
@ -4,7 +4,7 @@ from idlelib import codecontext
|
||||||
import unittest
|
import unittest
|
||||||
import unittest.mock
|
import unittest.mock
|
||||||
from test.support import requires
|
from test.support import requires
|
||||||
from tkinter import Tk, Frame, Text, TclError
|
from tkinter import NSEW, Tk, Frame, Text, TclError
|
||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
import re
|
import re
|
||||||
|
@ -62,7 +62,7 @@ class CodeContextTest(unittest.TestCase):
|
||||||
text.insert('1.0', code_sample)
|
text.insert('1.0', code_sample)
|
||||||
# Need to pack for creation of code context text widget.
|
# Need to pack for creation of code context text widget.
|
||||||
frame.pack(side='left', fill='both', expand=1)
|
frame.pack(side='left', fill='both', expand=1)
|
||||||
text.pack(side='top', fill='both', expand=1)
|
text.grid(row=1, column=1, sticky=NSEW)
|
||||||
cls.editor = DummyEditwin(root, frame, text)
|
cls.editor = DummyEditwin(root, frame, text)
|
||||||
codecontext.idleConf.userCfg = testcfg
|
codecontext.idleConf.userCfg = testcfg
|
||||||
|
|
||||||
|
@ -77,6 +77,7 @@ class CodeContextTest(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.text.yview(0)
|
self.text.yview(0)
|
||||||
|
self.text['font'] = 'TkFixedFont'
|
||||||
self.cc = codecontext.CodeContext(self.editor)
|
self.cc = codecontext.CodeContext(self.editor)
|
||||||
|
|
||||||
self.highlight_cfg = {"background": '#abcdef',
|
self.highlight_cfg = {"background": '#abcdef',
|
||||||
|
@ -86,10 +87,18 @@ class CodeContextTest(unittest.TestCase):
|
||||||
if element == 'context':
|
if element == 'context':
|
||||||
return self.highlight_cfg
|
return self.highlight_cfg
|
||||||
return orig_idleConf_GetHighlight(theme, element)
|
return orig_idleConf_GetHighlight(theme, element)
|
||||||
patcher = unittest.mock.patch.object(
|
GetHighlight_patcher = unittest.mock.patch.object(
|
||||||
codecontext.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
|
codecontext.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
|
||||||
patcher.start()
|
GetHighlight_patcher.start()
|
||||||
self.addCleanup(patcher.stop)
|
self.addCleanup(GetHighlight_patcher.stop)
|
||||||
|
|
||||||
|
self.font_override = 'TkFixedFont'
|
||||||
|
def mock_idleconf_GetFont(root, configType, section):
|
||||||
|
return self.font_override
|
||||||
|
GetFont_patcher = unittest.mock.patch.object(
|
||||||
|
codecontext.idleConf, 'GetFont', mock_idleconf_GetFont)
|
||||||
|
GetFont_patcher.start()
|
||||||
|
self.addCleanup(GetFont_patcher.stop)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
if self.cc.context:
|
if self.cc.context:
|
||||||
|
@ -339,69 +348,59 @@ class CodeContextTest(unittest.TestCase):
|
||||||
def test_font(self):
|
def test_font(self):
|
||||||
eq = self.assertEqual
|
eq = self.assertEqual
|
||||||
cc = self.cc
|
cc = self.cc
|
||||||
save_font = cc.text['font']
|
|
||||||
|
orig_font = cc.text['font']
|
||||||
test_font = 'TkTextFont'
|
test_font = 'TkTextFont'
|
||||||
|
self.assertNotEqual(orig_font, test_font)
|
||||||
|
|
||||||
# Ensure code context is not active.
|
# Ensure code context is not active.
|
||||||
if cc.context is not None:
|
if cc.context is not None:
|
||||||
cc.toggle_code_context_event()
|
cc.toggle_code_context_event()
|
||||||
|
|
||||||
|
self.font_override = test_font
|
||||||
# Nothing breaks or changes with inactive code context.
|
# Nothing breaks or changes with inactive code context.
|
||||||
cc.update_font(test_font)
|
cc.update_font()
|
||||||
|
|
||||||
# Activate code context, but no change to font.
|
# Activate code context, previous font change is immediately effective.
|
||||||
cc.toggle_code_context_event()
|
cc.toggle_code_context_event()
|
||||||
eq(cc.context['font'], save_font)
|
|
||||||
# Call font update with the existing font.
|
|
||||||
cc.update_font(save_font)
|
|
||||||
eq(cc.context['font'], save_font)
|
|
||||||
cc.toggle_code_context_event()
|
|
||||||
|
|
||||||
# Change text widget font and activate code context.
|
|
||||||
cc.text['font'] = test_font
|
|
||||||
cc.toggle_code_context_event(test_font)
|
|
||||||
eq(cc.context['font'], test_font)
|
eq(cc.context['font'], test_font)
|
||||||
|
|
||||||
# Just call the font update.
|
# Call the font update, change is picked up.
|
||||||
cc.update_font(save_font)
|
self.font_override = orig_font
|
||||||
eq(cc.context['font'], save_font)
|
cc.update_font()
|
||||||
cc.text['font'] = save_font
|
eq(cc.context['font'], orig_font)
|
||||||
|
|
||||||
def test_highlight_colors(self):
|
def test_highlight_colors(self):
|
||||||
eq = self.assertEqual
|
eq = self.assertEqual
|
||||||
cc = self.cc
|
cc = self.cc
|
||||||
save_colors = dict(self.highlight_cfg)
|
|
||||||
|
orig_colors = dict(self.highlight_cfg)
|
||||||
test_colors = {'background': '#222222', 'foreground': '#ffff00'}
|
test_colors = {'background': '#222222', 'foreground': '#ffff00'}
|
||||||
|
|
||||||
|
def assert_colors_are_equal(colors):
|
||||||
|
eq(cc.context['background'], colors['background'])
|
||||||
|
eq(cc.context['foreground'], colors['foreground'])
|
||||||
|
|
||||||
# Ensure code context is not active.
|
# Ensure code context is not active.
|
||||||
if cc.context:
|
if cc.context:
|
||||||
cc.toggle_code_context_event()
|
cc.toggle_code_context_event()
|
||||||
|
|
||||||
|
self.highlight_cfg = test_colors
|
||||||
# Nothing breaks with inactive code context.
|
# Nothing breaks with inactive code context.
|
||||||
cc.update_highlight_colors()
|
cc.update_highlight_colors()
|
||||||
|
|
||||||
# Activate code context, but no change to colors.
|
# Activate code context, previous colors change is immediately effective.
|
||||||
cc.toggle_code_context_event()
|
cc.toggle_code_context_event()
|
||||||
eq(cc.context['background'], save_colors['background'])
|
assert_colors_are_equal(test_colors)
|
||||||
eq(cc.context['foreground'], save_colors['foreground'])
|
|
||||||
|
|
||||||
# Call colors update, but no change to font.
|
# Call colors update with no change to the configured colors.
|
||||||
cc.update_highlight_colors()
|
cc.update_highlight_colors()
|
||||||
eq(cc.context['background'], save_colors['background'])
|
assert_colors_are_equal(test_colors)
|
||||||
eq(cc.context['foreground'], save_colors['foreground'])
|
|
||||||
cc.toggle_code_context_event()
|
|
||||||
|
|
||||||
# Change colors and activate code context.
|
# Call the colors update with code context active, change is picked up.
|
||||||
self.highlight_cfg = test_colors
|
self.highlight_cfg = orig_colors
|
||||||
cc.toggle_code_context_event()
|
|
||||||
eq(cc.context['background'], test_colors['background'])
|
|
||||||
eq(cc.context['foreground'], test_colors['foreground'])
|
|
||||||
|
|
||||||
# Change colors and call highlight colors update.
|
|
||||||
self.highlight_cfg = save_colors
|
|
||||||
cc.update_highlight_colors()
|
cc.update_highlight_colors()
|
||||||
eq(cc.context['background'], save_colors['background'])
|
assert_colors_are_equal(orig_colors)
|
||||||
eq(cc.context['foreground'], save_colors['foreground'])
|
|
||||||
|
|
||||||
|
|
||||||
class HelperFunctionText(unittest.TestCase):
|
class HelperFunctionText(unittest.TestCase):
|
||||||
|
|
|
@ -0,0 +1,351 @@
|
||||||
|
"""Test sidebar, coverage 93%"""
|
||||||
|
from itertools import chain
|
||||||
|
import unittest
|
||||||
|
import unittest.mock
|
||||||
|
from test.support import requires
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
from idlelib.delegator import Delegator
|
||||||
|
from idlelib.percolator import Percolator
|
||||||
|
import idlelib.sidebar
|
||||||
|
|
||||||
|
|
||||||
|
class Dummy_editwin:
|
||||||
|
def __init__(self, text):
|
||||||
|
self.text = text
|
||||||
|
self.text_frame = self.text.master
|
||||||
|
self.per = Percolator(text)
|
||||||
|
self.undo = Delegator()
|
||||||
|
self.per.insertfilter(self.undo)
|
||||||
|
|
||||||
|
def setvar(self, name, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getlineno(self, index):
|
||||||
|
return int(float(self.text.index(index)))
|
||||||
|
|
||||||
|
|
||||||
|
class LineNumbersTest(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
requires('gui')
|
||||||
|
cls.root = tk.Tk()
|
||||||
|
|
||||||
|
cls.text_frame = tk.Frame(cls.root)
|
||||||
|
cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
cls.text_frame.rowconfigure(1, weight=1)
|
||||||
|
cls.text_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
cls.text = tk.Text(cls.text_frame, width=80, height=24, wrap=tk.NONE)
|
||||||
|
cls.text.grid(row=1, column=1, sticky=tk.NSEW)
|
||||||
|
|
||||||
|
cls.editwin = Dummy_editwin(cls.text)
|
||||||
|
cls.editwin.vbar = tk.Scrollbar(cls.text_frame)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
cls.editwin.per.close()
|
||||||
|
cls.root.update()
|
||||||
|
cls.root.destroy()
|
||||||
|
del cls.text, cls.text_frame, cls.editwin, cls.root
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.linenumber = idlelib.sidebar.LineNumbers(self.editwin)
|
||||||
|
|
||||||
|
self.highlight_cfg = {"background": '#abcdef',
|
||||||
|
"foreground": '#123456'}
|
||||||
|
orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight
|
||||||
|
def mock_idleconf_GetHighlight(theme, element):
|
||||||
|
if element == 'linenumber':
|
||||||
|
return self.highlight_cfg
|
||||||
|
return orig_idleConf_GetHighlight(theme, element)
|
||||||
|
GetHighlight_patcher = unittest.mock.patch.object(
|
||||||
|
idlelib.sidebar.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
|
||||||
|
GetHighlight_patcher.start()
|
||||||
|
self.addCleanup(GetHighlight_patcher.stop)
|
||||||
|
|
||||||
|
self.font_override = 'TkFixedFont'
|
||||||
|
def mock_idleconf_GetFont(root, configType, section):
|
||||||
|
return self.font_override
|
||||||
|
GetFont_patcher = unittest.mock.patch.object(
|
||||||
|
idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont)
|
||||||
|
GetFont_patcher.start()
|
||||||
|
self.addCleanup(GetFont_patcher.stop)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.text.delete('1.0', 'end')
|
||||||
|
|
||||||
|
def get_selection(self):
|
||||||
|
return tuple(map(str, self.text.tag_ranges('sel')))
|
||||||
|
|
||||||
|
def get_line_screen_position(self, line):
|
||||||
|
bbox = self.linenumber.sidebar_text.bbox(f'{line}.end -1c')
|
||||||
|
x = bbox[0] + 2
|
||||||
|
y = bbox[1] + 2
|
||||||
|
return x, y
|
||||||
|
|
||||||
|
def assert_state_disabled(self):
|
||||||
|
state = self.linenumber.sidebar_text.config()['state']
|
||||||
|
self.assertEqual(state[-1], tk.DISABLED)
|
||||||
|
|
||||||
|
def get_sidebar_text_contents(self):
|
||||||
|
return self.linenumber.sidebar_text.get('1.0', tk.END)
|
||||||
|
|
||||||
|
def assert_sidebar_n_lines(self, n_lines):
|
||||||
|
expected = '\n'.join(chain(map(str, range(1, n_lines + 1)), ['']))
|
||||||
|
self.assertEqual(self.get_sidebar_text_contents(), expected)
|
||||||
|
|
||||||
|
def assert_text_equals(self, expected):
|
||||||
|
return self.assertEqual(self.text.get('1.0', 'end'), expected)
|
||||||
|
|
||||||
|
def test_init_empty(self):
|
||||||
|
self.assert_sidebar_n_lines(1)
|
||||||
|
|
||||||
|
def test_init_not_empty(self):
|
||||||
|
self.text.insert('insert', 'foo bar\n'*3)
|
||||||
|
self.assert_text_equals('foo bar\n'*3 + '\n')
|
||||||
|
self.assert_sidebar_n_lines(4)
|
||||||
|
|
||||||
|
def test_toggle_linenumbering(self):
|
||||||
|
self.assertEqual(self.linenumber.is_shown, False)
|
||||||
|
self.linenumber.show_sidebar()
|
||||||
|
self.assertEqual(self.linenumber.is_shown, True)
|
||||||
|
self.linenumber.hide_sidebar()
|
||||||
|
self.assertEqual(self.linenumber.is_shown, False)
|
||||||
|
self.linenumber.hide_sidebar()
|
||||||
|
self.assertEqual(self.linenumber.is_shown, False)
|
||||||
|
self.linenumber.show_sidebar()
|
||||||
|
self.assertEqual(self.linenumber.is_shown, True)
|
||||||
|
self.linenumber.show_sidebar()
|
||||||
|
self.assertEqual(self.linenumber.is_shown, True)
|
||||||
|
|
||||||
|
def test_insert(self):
|
||||||
|
self.text.insert('insert', 'foobar')
|
||||||
|
self.assert_text_equals('foobar\n')
|
||||||
|
self.assert_sidebar_n_lines(1)
|
||||||
|
self.assert_state_disabled()
|
||||||
|
|
||||||
|
self.text.insert('insert', '\nfoo')
|
||||||
|
self.assert_text_equals('foobar\nfoo\n')
|
||||||
|
self.assert_sidebar_n_lines(2)
|
||||||
|
self.assert_state_disabled()
|
||||||
|
|
||||||
|
self.text.insert('insert', 'hello\n'*2)
|
||||||
|
self.assert_text_equals('foobar\nfoohello\nhello\n\n')
|
||||||
|
self.assert_sidebar_n_lines(4)
|
||||||
|
self.assert_state_disabled()
|
||||||
|
|
||||||
|
self.text.insert('insert', '\nworld')
|
||||||
|
self.assert_text_equals('foobar\nfoohello\nhello\n\nworld\n')
|
||||||
|
self.assert_sidebar_n_lines(5)
|
||||||
|
self.assert_state_disabled()
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
self.text.insert('insert', 'foobar')
|
||||||
|
self.assert_text_equals('foobar\n')
|
||||||
|
self.text.delete('1.1', '1.3')
|
||||||
|
self.assert_text_equals('fbar\n')
|
||||||
|
self.assert_sidebar_n_lines(1)
|
||||||
|
self.assert_state_disabled()
|
||||||
|
|
||||||
|
self.text.insert('insert', 'foo\n'*2)
|
||||||
|
self.assert_text_equals('fbarfoo\nfoo\n\n')
|
||||||
|
self.assert_sidebar_n_lines(3)
|
||||||
|
self.assert_state_disabled()
|
||||||
|
|
||||||
|
# Note: deleting up to "2.end" doesn't delete the final newline.
|
||||||
|
self.text.delete('2.0', '2.end')
|
||||||
|
self.assert_text_equals('fbarfoo\n\n\n')
|
||||||
|
self.assert_sidebar_n_lines(3)
|
||||||
|
self.assert_state_disabled()
|
||||||
|
|
||||||
|
self.text.delete('1.3', 'end')
|
||||||
|
self.assert_text_equals('fba\n')
|
||||||
|
self.assert_sidebar_n_lines(1)
|
||||||
|
self.assert_state_disabled()
|
||||||
|
|
||||||
|
# Note: Text widgets always keep a single '\n' character at the end.
|
||||||
|
self.text.delete('1.0', 'end')
|
||||||
|
self.assert_text_equals('\n')
|
||||||
|
self.assert_sidebar_n_lines(1)
|
||||||
|
self.assert_state_disabled()
|
||||||
|
|
||||||
|
def test_sidebar_text_width(self):
|
||||||
|
"""
|
||||||
|
Test that linenumber text widget is always at the minimum
|
||||||
|
width
|
||||||
|
"""
|
||||||
|
def get_width():
|
||||||
|
return self.linenumber.sidebar_text.config()['width'][-1]
|
||||||
|
|
||||||
|
self.assert_sidebar_n_lines(1)
|
||||||
|
self.assertEqual(get_width(), 1)
|
||||||
|
|
||||||
|
self.text.insert('insert', 'foo')
|
||||||
|
self.assert_sidebar_n_lines(1)
|
||||||
|
self.assertEqual(get_width(), 1)
|
||||||
|
|
||||||
|
self.text.insert('insert', 'foo\n'*8)
|
||||||
|
self.assert_sidebar_n_lines(9)
|
||||||
|
self.assertEqual(get_width(), 1)
|
||||||
|
|
||||||
|
self.text.insert('insert', 'foo\n')
|
||||||
|
self.assert_sidebar_n_lines(10)
|
||||||
|
self.assertEqual(get_width(), 2)
|
||||||
|
|
||||||
|
self.text.insert('insert', 'foo\n')
|
||||||
|
self.assert_sidebar_n_lines(11)
|
||||||
|
self.assertEqual(get_width(), 2)
|
||||||
|
|
||||||
|
self.text.delete('insert -1l linestart', 'insert linestart')
|
||||||
|
self.assert_sidebar_n_lines(10)
|
||||||
|
self.assertEqual(get_width(), 2)
|
||||||
|
|
||||||
|
self.text.delete('insert -1l linestart', 'insert linestart')
|
||||||
|
self.assert_sidebar_n_lines(9)
|
||||||
|
self.assertEqual(get_width(), 1)
|
||||||
|
|
||||||
|
self.text.insert('insert', 'foo\n'*90)
|
||||||
|
self.assert_sidebar_n_lines(99)
|
||||||
|
self.assertEqual(get_width(), 2)
|
||||||
|
|
||||||
|
self.text.insert('insert', 'foo\n')
|
||||||
|
self.assert_sidebar_n_lines(100)
|
||||||
|
self.assertEqual(get_width(), 3)
|
||||||
|
|
||||||
|
self.text.insert('insert', 'foo\n')
|
||||||
|
self.assert_sidebar_n_lines(101)
|
||||||
|
self.assertEqual(get_width(), 3)
|
||||||
|
|
||||||
|
self.text.delete('insert -1l linestart', 'insert linestart')
|
||||||
|
self.assert_sidebar_n_lines(100)
|
||||||
|
self.assertEqual(get_width(), 3)
|
||||||
|
|
||||||
|
self.text.delete('insert -1l linestart', 'insert linestart')
|
||||||
|
self.assert_sidebar_n_lines(99)
|
||||||
|
self.assertEqual(get_width(), 2)
|
||||||
|
|
||||||
|
self.text.delete('50.0 -1c', 'end -1c')
|
||||||
|
self.assert_sidebar_n_lines(49)
|
||||||
|
self.assertEqual(get_width(), 2)
|
||||||
|
|
||||||
|
self.text.delete('5.0 -1c', 'end -1c')
|
||||||
|
self.assert_sidebar_n_lines(4)
|
||||||
|
self.assertEqual(get_width(), 1)
|
||||||
|
|
||||||
|
# Note: Text widgets always keep a single '\n' character at the end.
|
||||||
|
self.text.delete('1.0', 'end -1c')
|
||||||
|
self.assert_sidebar_n_lines(1)
|
||||||
|
self.assertEqual(get_width(), 1)
|
||||||
|
|
||||||
|
def test_click_selection(self):
|
||||||
|
self.linenumber.show_sidebar()
|
||||||
|
self.text.insert('1.0', 'one\ntwo\nthree\nfour\n')
|
||||||
|
self.root.update()
|
||||||
|
|
||||||
|
# Click on the second line.
|
||||||
|
x, y = self.get_line_screen_position(2)
|
||||||
|
self.linenumber.sidebar_text.event_generate('<Button-1>', x=x, y=y)
|
||||||
|
self.linenumber.sidebar_text.update()
|
||||||
|
self.root.update()
|
||||||
|
|
||||||
|
self.assertEqual(self.get_selection(), ('2.0', '3.0'))
|
||||||
|
|
||||||
|
def test_drag_selection(self):
|
||||||
|
self.linenumber.show_sidebar()
|
||||||
|
self.text.insert('1.0', 'one\ntwo\nthree\nfour\n')
|
||||||
|
self.root.update()
|
||||||
|
|
||||||
|
# Drag from the first line to the third line.
|
||||||
|
start_x, start_y = self.get_line_screen_position(1)
|
||||||
|
end_x, end_y = self.get_line_screen_position(3)
|
||||||
|
self.linenumber.sidebar_text.event_generate('<Button-1>',
|
||||||
|
x=start_x, y=start_y)
|
||||||
|
self.linenumber.sidebar_text.event_generate('<B1-Motion>',
|
||||||
|
x=start_x, y=start_y)
|
||||||
|
self.linenumber.sidebar_text.event_generate('<B1-Motion>',
|
||||||
|
x=end_x, y=end_y)
|
||||||
|
self.linenumber.sidebar_text.event_generate('<ButtonRelease-1>',
|
||||||
|
x=end_x, y=end_y)
|
||||||
|
self.root.update()
|
||||||
|
|
||||||
|
self.assertEqual(self.get_selection(), ('1.0', '4.0'))
|
||||||
|
|
||||||
|
def test_scroll(self):
|
||||||
|
self.linenumber.show_sidebar()
|
||||||
|
self.text.insert('1.0', 'line\n' * 100)
|
||||||
|
self.root.update()
|
||||||
|
|
||||||
|
# Scroll down 10 lines.
|
||||||
|
self.text.yview_scroll(10, 'unit')
|
||||||
|
self.root.update()
|
||||||
|
self.assertEqual(self.text.index('@0,0'), '11.0')
|
||||||
|
self.assertEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0')
|
||||||
|
|
||||||
|
# Generate a mouse-wheel event and make sure it scrolled up or down.
|
||||||
|
# The meaning of the "delta" is OS-dependant, so this just checks for
|
||||||
|
# any change.
|
||||||
|
self.linenumber.sidebar_text.event_generate('<MouseWheel>',
|
||||||
|
x=0, y=0,
|
||||||
|
delta=10)
|
||||||
|
self.root.update()
|
||||||
|
self.assertNotEqual(self.text.index('@0,0'), '11.0')
|
||||||
|
self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0')
|
||||||
|
|
||||||
|
def test_font(self):
|
||||||
|
ln = self.linenumber
|
||||||
|
|
||||||
|
orig_font = ln.sidebar_text['font']
|
||||||
|
test_font = 'TkTextFont'
|
||||||
|
self.assertNotEqual(orig_font, test_font)
|
||||||
|
|
||||||
|
# Ensure line numbers aren't shown.
|
||||||
|
ln.hide_sidebar()
|
||||||
|
|
||||||
|
self.font_override = test_font
|
||||||
|
# Nothing breaks when line numbers aren't shown.
|
||||||
|
ln.update_font()
|
||||||
|
|
||||||
|
# Activate line numbers, previous font change is immediately effective.
|
||||||
|
ln.show_sidebar()
|
||||||
|
self.assertEqual(ln.sidebar_text['font'], test_font)
|
||||||
|
|
||||||
|
# Call the font update with line numbers shown, change is picked up.
|
||||||
|
self.font_override = orig_font
|
||||||
|
ln.update_font()
|
||||||
|
self.assertEqual(ln.sidebar_text['font'], orig_font)
|
||||||
|
|
||||||
|
def test_highlight_colors(self):
|
||||||
|
ln = self.linenumber
|
||||||
|
|
||||||
|
orig_colors = dict(self.highlight_cfg)
|
||||||
|
test_colors = {'background': '#222222', 'foreground': '#ffff00'}
|
||||||
|
|
||||||
|
def assert_colors_are_equal(colors):
|
||||||
|
self.assertEqual(ln.sidebar_text['background'], colors['background'])
|
||||||
|
self.assertEqual(ln.sidebar_text['foreground'], colors['foreground'])
|
||||||
|
|
||||||
|
# Ensure line numbers aren't shown.
|
||||||
|
ln.hide_sidebar()
|
||||||
|
|
||||||
|
self.highlight_cfg = test_colors
|
||||||
|
# Nothing breaks with inactive code context.
|
||||||
|
ln.update_colors()
|
||||||
|
|
||||||
|
# Show line numbers, previous colors change is immediately effective.
|
||||||
|
ln.show_sidebar()
|
||||||
|
assert_colors_are_equal(test_colors)
|
||||||
|
|
||||||
|
# Call colors update with no change to the configured colors.
|
||||||
|
ln.update_colors()
|
||||||
|
assert_colors_are_equal(test_colors)
|
||||||
|
|
||||||
|
# Call the colors update with line numbers shown, change is picked up.
|
||||||
|
self.highlight_cfg = orig_colors
|
||||||
|
ln.update_colors()
|
||||||
|
assert_colors_are_equal(orig_colors)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main(verbosity=2)
|
|
@ -100,7 +100,8 @@ menudefs = [
|
||||||
('Configure _IDLE', '<<open-config-dialog>>'),
|
('Configure _IDLE', '<<open-config-dialog>>'),
|
||||||
None,
|
None,
|
||||||
('Show _Code Context', '<<toggle-code-context>>'),
|
('Show _Code Context', '<<toggle-code-context>>'),
|
||||||
('Zoom Height', '<<zoom-height>>'),
|
('Show _Line Numbers', '<<toggle-line-numbers>>'),
|
||||||
|
('_Zoom Height', '<<zoom-height>>'),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
('window', [
|
('window', [
|
||||||
|
|
|
@ -74,13 +74,11 @@ class OutputWindow(EditorWindow):
|
||||||
("Go to file/line", "<<goto-file-line>>", None),
|
("Go to file/line", "<<goto-file-line>>", None),
|
||||||
]
|
]
|
||||||
|
|
||||||
allow_codecontext = False
|
allow_code_context = False
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
EditorWindow.__init__(self, *args)
|
EditorWindow.__init__(self, *args)
|
||||||
self.text.bind("<<goto-file-line>>", self.goto_file_line)
|
self.text.bind("<<goto-file-line>>", self.goto_file_line)
|
||||||
self.text.unbind("<<toggle-code-context>>")
|
|
||||||
self.update_menu_state('options', '*Code Context', 'disabled')
|
|
||||||
|
|
||||||
# Customize EditorWindow
|
# Customize EditorWindow
|
||||||
def ispythonsource(self, filename):
|
def ispythonsource(self, filename):
|
||||||
|
|
|
@ -861,6 +861,8 @@ class PyShell(OutputWindow):
|
||||||
("Squeeze", "<<squeeze-current-text>>"),
|
("Squeeze", "<<squeeze-current-text>>"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
allow_line_numbers = False
|
||||||
|
|
||||||
# New classes
|
# New classes
|
||||||
from idlelib.history import History
|
from idlelib.history import History
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,324 @@
|
||||||
|
"""Line numbering implementation for IDLE as an extension.
|
||||||
|
Includes BaseSideBar which can be extended for other sidebar based extensions
|
||||||
|
"""
|
||||||
|
import functools
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from idlelib.config import idleConf
|
||||||
|
from idlelib.delegator import Delegator
|
||||||
|
|
||||||
|
|
||||||
|
def get_end_linenumber(text):
|
||||||
|
"""Utility to get the last line's number in a Tk text widget."""
|
||||||
|
return int(float(text.index('end-1c')))
|
||||||
|
|
||||||
|
|
||||||
|
def get_widget_padding(widget):
|
||||||
|
"""Get the total padding of a Tk widget, including its border."""
|
||||||
|
# TODO: use also in codecontext.py
|
||||||
|
manager = widget.winfo_manager()
|
||||||
|
if manager == 'pack':
|
||||||
|
info = widget.pack_info()
|
||||||
|
elif manager == 'grid':
|
||||||
|
info = widget.grid_info()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported geometry manager: {manager}")
|
||||||
|
|
||||||
|
# All values are passed through getint(), since some
|
||||||
|
# values may be pixel objects, which can't simply be added to ints.
|
||||||
|
padx = sum(map(widget.tk.getint, [
|
||||||
|
info['padx'],
|
||||||
|
widget.cget('padx'),
|
||||||
|
widget.cget('border'),
|
||||||
|
]))
|
||||||
|
pady = sum(map(widget.tk.getint, [
|
||||||
|
info['pady'],
|
||||||
|
widget.cget('pady'),
|
||||||
|
widget.cget('border'),
|
||||||
|
]))
|
||||||
|
return padx, pady
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSideBar:
|
||||||
|
"""
|
||||||
|
The base class for extensions which require a sidebar.
|
||||||
|
"""
|
||||||
|
def __init__(self, editwin):
|
||||||
|
self.editwin = editwin
|
||||||
|
self.parent = editwin.text_frame
|
||||||
|
self.text = editwin.text
|
||||||
|
|
||||||
|
_padx, pady = get_widget_padding(self.text)
|
||||||
|
self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE,
|
||||||
|
padx=0, pady=pady,
|
||||||
|
borderwidth=0, highlightthickness=0)
|
||||||
|
self.sidebar_text.config(state=tk.DISABLED)
|
||||||
|
self.text['yscrollcommand'] = self.redirect_yscroll_event
|
||||||
|
self.update_font()
|
||||||
|
self.update_colors()
|
||||||
|
|
||||||
|
self.is_shown = False
|
||||||
|
|
||||||
|
def update_font(self):
|
||||||
|
"""Update the sidebar text font, usually after config changes."""
|
||||||
|
font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
|
||||||
|
self._update_font(font)
|
||||||
|
|
||||||
|
def _update_font(self, font):
|
||||||
|
self.sidebar_text['font'] = font
|
||||||
|
|
||||||
|
def update_colors(self):
|
||||||
|
"""Update the sidebar text colors, usually after config changes."""
|
||||||
|
colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'normal')
|
||||||
|
self._update_colors(foreground=colors['foreground'],
|
||||||
|
background=colors['background'])
|
||||||
|
|
||||||
|
def _update_colors(self, foreground, background):
|
||||||
|
self.sidebar_text.config(
|
||||||
|
fg=foreground, bg=background,
|
||||||
|
selectforeground=foreground, selectbackground=background,
|
||||||
|
inactiveselectbackground=background,
|
||||||
|
)
|
||||||
|
|
||||||
|
def show_sidebar(self):
|
||||||
|
if not self.is_shown:
|
||||||
|
self.sidebar_text.grid(row=1, column=0, sticky=tk.NSEW)
|
||||||
|
self.is_shown = True
|
||||||
|
|
||||||
|
def hide_sidebar(self):
|
||||||
|
if self.is_shown:
|
||||||
|
self.sidebar_text.grid_forget()
|
||||||
|
self.is_shown = False
|
||||||
|
|
||||||
|
def redirect_yscroll_event(self, *args, **kwargs):
|
||||||
|
"""Redirect vertical scrolling to the main editor text widget.
|
||||||
|
|
||||||
|
The scroll bar is also updated.
|
||||||
|
"""
|
||||||
|
self.editwin.vbar.set(*args)
|
||||||
|
self.sidebar_text.yview_moveto(args[0])
|
||||||
|
return 'break'
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
class EndLineDelegator(Delegator):
|
||||||
|
"""Generate callbacks with the current end line number after
|
||||||
|
insert or delete operations"""
|
||||||
|
def __init__(self, changed_callback):
|
||||||
|
"""
|
||||||
|
changed_callback - Callable, will be called after insert
|
||||||
|
or delete operations with the current
|
||||||
|
end line number.
|
||||||
|
"""
|
||||||
|
Delegator.__init__(self)
|
||||||
|
self.changed_callback = changed_callback
|
||||||
|
|
||||||
|
def insert(self, index, chars, tags=None):
|
||||||
|
self.delegate.insert(index, chars, tags)
|
||||||
|
self.changed_callback(get_end_linenumber(self.delegate))
|
||||||
|
|
||||||
|
def delete(self, index1, index2=None):
|
||||||
|
self.delegate.delete(index1, index2)
|
||||||
|
self.changed_callback(get_end_linenumber(self.delegate))
|
||||||
|
|
||||||
|
|
||||||
|
class LineNumbers(BaseSideBar):
|
||||||
|
"""Line numbers support for editor windows."""
|
||||||
|
def __init__(self, editwin):
|
||||||
|
BaseSideBar.__init__(self, 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)
|
||||||
|
# Insert the delegator after the undo delegator, so that line numbers
|
||||||
|
# are properly updated after undo and redo actions.
|
||||||
|
end_line_delegator.setdelegate(self.editwin.undo.delegate)
|
||||||
|
self.editwin.undo.setdelegate(end_line_delegator)
|
||||||
|
# Reset the delegator caches of the delegators "above" the
|
||||||
|
# end line delegator we just inserted.
|
||||||
|
delegator = self.editwin.per.top
|
||||||
|
while delegator is not end_line_delegator:
|
||||||
|
delegator.resetcache()
|
||||||
|
delegator = delegator.delegate
|
||||||
|
|
||||||
|
self.is_shown = False
|
||||||
|
|
||||||
|
def bind_events(self):
|
||||||
|
# Ensure focus is always redirected to the main editor text widget.
|
||||||
|
self.sidebar_text.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.sidebar_text.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.sidebar_text.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 = None
|
||||||
|
def b1_mousedown_handler(event):
|
||||||
|
# select the entire line
|
||||||
|
lineno = self.editwin.getlineno(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)
|
||||||
|
|
||||||
|
# These are set by b1_motion_handler() and read by selection_handler();
|
||||||
|
# see below. 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 drag_update_selection_and_insert_mark(y_coord):
|
||||||
|
"""Helper function for drag and selection event handlers."""
|
||||||
|
lineno = self.editwin.getlineno(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):
|
||||||
|
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):
|
||||||
|
"""Update the sidebar text colors, usually after config changes."""
|
||||||
|
colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber')
|
||||||
|
self._update_colors(foreground=colors['foreground'],
|
||||||
|
background=colors['background'])
|
||||||
|
|
||||||
|
def update_sidebar_text(self, end):
|
||||||
|
"""
|
||||||
|
Perform the following action:
|
||||||
|
Each line sidebar_text contains the linenumber for that line
|
||||||
|
Synchronize with editwin.text so that both sidebar_text and
|
||||||
|
editwin.text contain the same number of lines"""
|
||||||
|
if end == self.prev_end:
|
||||||
|
return
|
||||||
|
|
||||||
|
width_difference = len(str(end)) - len(str(self.prev_end))
|
||||||
|
if width_difference:
|
||||||
|
cur_width = int(float(self.sidebar_text['width']))
|
||||||
|
new_width = cur_width + width_difference
|
||||||
|
self.sidebar_text['width'] = self._sidebar_width_type(new_width)
|
||||||
|
|
||||||
|
self.sidebar_text.config(state=tk.NORMAL)
|
||||||
|
if end > self.prev_end:
|
||||||
|
new_text = '\n'.join(itertools.chain(
|
||||||
|
[''],
|
||||||
|
map(str, range(self.prev_end + 1, end + 1)),
|
||||||
|
))
|
||||||
|
self.sidebar_text.insert(f'end -1c', new_text, 'linenumber')
|
||||||
|
else:
|
||||||
|
self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c')
|
||||||
|
self.sidebar_text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
self.prev_end = end
|
||||||
|
|
||||||
|
|
||||||
|
def _linenumbers_drag_scrolling(parent): # htest #
|
||||||
|
from idlelib.idle_test.test_sidebar import Dummy_editwin
|
||||||
|
|
||||||
|
toplevel = tk.Toplevel(parent)
|
||||||
|
text_frame = tk.Frame(toplevel)
|
||||||
|
text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
text_frame.rowconfigure(1, weight=1)
|
||||||
|
text_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
font = idleConf.GetFont(toplevel, 'main', 'EditorWindow')
|
||||||
|
text = tk.Text(text_frame, width=80, height=24, wrap=tk.NONE, font=font)
|
||||||
|
text.grid(row=1, column=1, sticky=tk.NSEW)
|
||||||
|
|
||||||
|
editwin = Dummy_editwin(text)
|
||||||
|
editwin.vbar = tk.Scrollbar(text_frame)
|
||||||
|
|
||||||
|
linenumbers = LineNumbers(editwin)
|
||||||
|
linenumbers.show_sidebar()
|
||||||
|
|
||||||
|
text.insert('1.0', '\n'.join('a'*i for i in range(1, 101)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from unittest import main
|
||||||
|
main('idlelib.idle_test.test_sidebar', verbosity=2, exit=False)
|
||||||
|
|
||||||
|
from idlelib.idle_test.htest import run
|
||||||
|
run(_linenumbers_drag_scrolling)
|
|
@ -0,0 +1,4 @@
|
||||||
|
Add optional line numbers for IDLE editor windows. Windows
|
||||||
|
open without line numbers unless set otherwise in the General
|
||||||
|
tab of the configuration dialog.
|
||||||
|
|
Loading…
Reference in New Issue