Commit 7123ea00 authored by Tal Einat's avatar Tal Einat Committed by GitHub

bpo-17535: IDLE editor line numbers (GH-14030)

parent 1ebee37d
...@@ -290,22 +290,31 @@ Options menu (Shell and Editor) ...@@ -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`.) ...@@ -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. ...@@ -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 ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 ...@@ -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: ...@@ -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: ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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 ...@@ -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 ...@@ -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> ...@@ -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) ...@@ -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 = { ...@@ -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 ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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()
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() 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.
self.highlight_cfg = test_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. # Call the colors update with code context active, change is picked up.
self.highlight_cfg = save_colors self.highlight_cfg = orig_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):
......
This diff is collapsed.
...@@ -100,7 +100,8 @@ menudefs = [ ...@@ -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): ...@@ -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): ...@@ -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
......
This diff is collapsed.
Add optional line numbers for IDLE editor windows. Windows
open without line numbers unless set otherwise in the General
tab of the configuration dialog.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment