bpo-33763: IDLE: Replace label widget with text widget in code context (GH-7367)

This commit is contained in:
Cheryl Sabella 2018-06-04 11:58:44 -04:00 committed by Terry Jan Reedy
parent d49dbd9acc
commit b609e687a0
3 changed files with 57 additions and 52 deletions

View File

@ -52,7 +52,7 @@ class CodeContext:
self.text is the editor window text widget. self.text is the editor window text widget.
self.textfont is the editor window font. self.textfont is the editor window font.
self.label displays the code context text above the editor text. self.context displays the code context text above the editor text.
Initially None, it is toggled via <<toggle-code-context>>. Initially None, it is toggled via <<toggle-code-context>>.
self.topvisible is the number of the top text line displayed. self.topvisible is the number of the top text line displayed.
self.info is a list of (line number, indent level, line text, self.info is a list of (line number, indent level, line text,
@ -67,7 +67,7 @@ class CodeContext:
self.text = editwin.text self.text = editwin.text
self.textfont = self.text["font"] self.textfont = self.text["font"]
self.contextcolors = CodeContext.colors self.contextcolors = CodeContext.colors
self.label = None self.context = None
self.topvisible = 1 self.topvisible = 1
self.info = [(0, -1, "", False)] self.info = [(0, -1, "", False)]
# Start two update cycles, one for context lines, one for font changes. # Start two update cycles, one for context lines, one for font changes.
@ -92,11 +92,11 @@ class CodeContext:
def toggle_code_context_event(self, event=None): def toggle_code_context_event(self, event=None):
"""Toggle code context display. """Toggle code context display.
If self.label doesn't exist, create it to match the size of the editor If self.context doesn't exist, create it to match the size of the editor
window text (toggle on). If it does exist, destroy it (toggle off). window text (toggle on). If it does exist, destroy it (toggle off).
Return 'break' to complete the processing of the binding. Return 'break' to complete the processing of the binding.
""" """
if not self.label: if not self.context:
# Calculate the border width and horizontal padding required to # Calculate the border width and horizontal padding required to
# align the context with the text in the main Text widget. # align the context with the text in the main Text widget.
# #
@ -110,20 +110,20 @@ class CodeContext:
padx += widget.tk.getint(widget.pack_info()['padx']) padx += widget.tk.getint(widget.pack_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.label = tkinter.Label( self.context = tkinter.Text(
self.editwin.top, text="", self.editwin.top, font=self.textfont,
anchor=W, justify=LEFT, font=self.textfont,
bg=self.contextcolors['background'], bg=self.contextcolors['background'],
fg=self.contextcolors['foreground'], fg=self.contextcolors['foreground'],
height=1,
width=1, # Don't request more than we get. width=1, # Don't request more than we get.
padx=padx, border=border, relief=SUNKEN) padx=padx, border=border, relief=SUNKEN, state='disabled')
# Pack the label widget before and above the text_frame widget, # Pack the context widget before and above the text_frame widget,
# thus ensuring that it will appear directly above text_frame. # thus ensuring that it will appear directly above text_frame.
self.label.pack(side=TOP, fill=X, expand=False, self.context.pack(side=TOP, fill=X, expand=False,
before=self.editwin.text_frame) before=self.editwin.text_frame)
else: else:
self.label.destroy() self.context.destroy()
self.label = None self.context = None
return "break" return "break"
def get_context(self, new_topvisible, stopline=1, stopindent=0): def get_context(self, new_topvisible, stopline=1, stopindent=0):
@ -161,9 +161,8 @@ class CodeContext:
No update is done if the text hasn't been scrolled. If the text No update is done if the text hasn't been scrolled. If the text
was scrolled, the lines that should be shown in the context will was scrolled, the lines that should be shown in the context will
be retrieved and the label widget will be updated with the code, be retrieved and the context area will be updated with the code,
padded with blank lines so that the code appears on the bottom of up to the number of maxlines.
the context label.
""" """
new_topvisible = int(self.text.index("@0,0").split('.')[0]) new_topvisible = int(self.text.index("@0,0").split('.')[0])
if self.topvisible == new_topvisible: # Haven't scrolled. if self.topvisible == new_topvisible: # Haven't scrolled.
@ -190,24 +189,29 @@ class CodeContext:
# Last context_depth context lines. # Last context_depth context lines.
context_strings = [x[2] for x in self.info[-self.context_depth:]] context_strings = [x[2] for x in self.info[-self.context_depth:]]
showfirst = 0 if context_strings[0] else 1 showfirst = 0 if context_strings[0] else 1
self.label["text"] = '\n'.join(context_strings[showfirst:]) # Update widget.
self.context['height'] = len(context_strings) - showfirst
self.context['state'] = 'normal'
self.context.delete('1.0', 'end')
self.context.insert('end', '\n'.join(context_strings[showfirst:]))
self.context['state'] = 'disabled'
def timer_event(self): def timer_event(self):
"Event on editor text widget triggered every UPDATEINTERVAL ms." "Event on editor text widget triggered every UPDATEINTERVAL ms."
if self.label: if self.context:
self.update_code_context() self.update_code_context()
self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event) self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
def config_timer_event(self): def config_timer_event(self):
"Event on editor text widget triggered every CONFIGUPDATEINTERVAL ms." "Event on editor text widget triggered every CONFIGUPDATEINTERVAL ms."
newtextfont = self.text["font"] newtextfont = self.text["font"]
if (self.label and (newtextfont != self.textfont or if (self.context and (newtextfont != self.textfont or
CodeContext.colors != self.contextcolors)): CodeContext.colors != self.contextcolors)):
self.textfont = newtextfont self.textfont = newtextfont
self.contextcolors = CodeContext.colors self.contextcolors = CodeContext.colors
self.label["font"] = self.textfont self.context["font"] = self.textfont
self.label['background'] = self.contextcolors['background'] self.context['background'] = self.contextcolors['background']
self.label['foreground'] = self.contextcolors['foreground'] self.context['foreground'] = self.contextcolors['foreground']
self.t2 = self.text.after(CONFIGUPDATEINTERVAL, self.config_timer_event) self.t2 = self.text.after(CONFIGUPDATEINTERVAL, self.config_timer_event)

View File

@ -56,7 +56,7 @@ class CodeContextTest(unittest.TestCase):
frame = cls.frame = Frame(root) frame = cls.frame = Frame(root)
text = cls.text = Text(frame) text = cls.text = Text(frame)
text.insert('1.0', code_sample) text.insert('1.0', code_sample)
# Need to pack for creation of code context label 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.pack(side='top', fill='both', expand=1)
cls.editor = DummyEditwin(root, frame, text) cls.editor = DummyEditwin(root, frame, text)
@ -75,11 +75,11 @@ class CodeContextTest(unittest.TestCase):
self.cc = codecontext.CodeContext(self.editor) self.cc = codecontext.CodeContext(self.editor)
def tearDown(self): def tearDown(self):
if self.cc.label: if self.cc.context:
self.cc.label.destroy() self.cc.context.destroy()
# Explicitly call __del__ to remove scheduled scripts. # Explicitly call __del__ to remove scheduled scripts.
self.cc.__del__() self.cc.__del__()
del self.cc.label, self.cc del self.cc.context, self.cc
def test_init(self): def test_init(self):
eq = self.assertEqual eq = self.assertEqual
@ -89,7 +89,7 @@ class CodeContextTest(unittest.TestCase):
eq(cc.editwin, ed) eq(cc.editwin, ed)
eq(cc.text, ed.text) eq(cc.text, ed.text)
eq(cc.textfont, ed.text['font']) eq(cc.textfont, ed.text['font'])
self.assertIsNone(cc.label) self.assertIsNone(cc.context)
eq(cc.info, [(0, -1, '', False)]) eq(cc.info, [(0, -1, '', False)])
eq(cc.topvisible, 1) eq(cc.topvisible, 1)
eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer') eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer')
@ -120,20 +120,20 @@ class CodeContextTest(unittest.TestCase):
toggle = cc.toggle_code_context_event toggle = cc.toggle_code_context_event
# Make sure code context is off. # Make sure code context is off.
if cc.label: if cc.context:
toggle() toggle()
# Toggle on. # Toggle on.
eq(toggle(), 'break') eq(toggle(), 'break')
self.assertIsNotNone(cc.label) self.assertIsNotNone(cc.context)
eq(cc.label['font'], cc.textfont) eq(cc.context['font'], cc.textfont)
eq(cc.label['fg'], cc.colors['foreground']) eq(cc.context['fg'], cc.colors['foreground'])
eq(cc.label['bg'], cc.colors['background']) eq(cc.context['bg'], cc.colors['background'])
eq(cc.label['text'], '') eq(cc.context.get('1.0', 'end-1c'), '')
# Toggle off. # Toggle off.
eq(toggle(), 'break') eq(toggle(), 'break')
self.assertIsNone(cc.label) self.assertIsNone(cc.context)
def test_get_context(self): def test_get_context(self):
eq = self.assertEqual eq = self.assertEqual
@ -187,7 +187,7 @@ class CodeContextTest(unittest.TestCase):
eq = self.assertEqual eq = self.assertEqual
cc = self.cc cc = self.cc
# Ensure code context is active. # Ensure code context is active.
if not cc.label: if not cc.context:
cc.toggle_code_context_event() cc.toggle_code_context_event()
# Invoke update_code_context without scrolling - nothing happens. # Invoke update_code_context without scrolling - nothing happens.
@ -200,21 +200,21 @@ class CodeContextTest(unittest.TestCase):
cc.update_code_context() cc.update_code_context()
eq(cc.info, [(0, -1, '', False)]) eq(cc.info, [(0, -1, '', False)])
eq(cc.topvisible, 2) eq(cc.topvisible, 2)
eq(cc.label['text'], '') eq(cc.context.get('1.0', 'end-1c'), '')
# Scroll down to line 2. # Scroll down to line 2.
cc.text.yview(2) cc.text.yview(2)
cc.update_code_context() cc.update_code_context()
eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1():', 'class')]) eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1():', 'class')])
eq(cc.topvisible, 3) eq(cc.topvisible, 3)
eq(cc.label['text'], 'class C1():') eq(cc.context.get('1.0', 'end-1c'), 'class C1():')
# Scroll down to line 3. Since it's a comment, nothing changes. # Scroll down to line 3. Since it's a comment, nothing changes.
cc.text.yview(3) cc.text.yview(3)
cc.update_code_context() cc.update_code_context()
eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1():', 'class')]) eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1():', 'class')])
eq(cc.topvisible, 4) eq(cc.topvisible, 4)
eq(cc.label['text'], 'class C1():') eq(cc.context.get('1.0', 'end-1c'), 'class C1():')
# Scroll down to line 4. # Scroll down to line 4.
cc.text.yview(4) cc.text.yview(4)
@ -223,7 +223,7 @@ class CodeContextTest(unittest.TestCase):
(2, 0, 'class C1():', 'class'), (2, 0, 'class C1():', 'class'),
(4, 4, ' def __init__(self, a, b):', 'def')]) (4, 4, ' def __init__(self, a, b):', 'def')])
eq(cc.topvisible, 5) eq(cc.topvisible, 5)
eq(cc.label['text'], 'class C1():\n' eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n'
' def __init__(self, a, b):') ' def __init__(self, a, b):')
# Scroll down to line 11. Last 'def' is removed. # Scroll down to line 11. Last 'def' is removed.
@ -235,7 +235,7 @@ class CodeContextTest(unittest.TestCase):
(8, 8, ' if a > b:', 'if'), (8, 8, ' if a > b:', 'if'),
(10, 8, ' elif a < b:', 'elif')]) (10, 8, ' elif a < b:', 'elif')])
eq(cc.topvisible, 12) eq(cc.topvisible, 12)
eq(cc.label['text'], 'class C1():\n' eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n'
' def compare(self):\n' ' def compare(self):\n'
' if a > b:\n' ' if a > b:\n'
' elif a < b:') ' elif a < b:')
@ -249,7 +249,7 @@ class CodeContextTest(unittest.TestCase):
(8, 8, ' if a > b:', 'if'), (8, 8, ' if a > b:', 'if'),
(10, 8, ' elif a < b:', 'elif')]) (10, 8, ' elif a < b:', 'elif')])
eq(cc.topvisible, 12) eq(cc.topvisible, 12)
eq(cc.label['text'], 'class C1():\n' eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n'
' def compare(self):\n' ' def compare(self):\n'
' if a > b:\n' ' if a > b:\n'
' elif a < b:') ' elif a < b:')
@ -262,12 +262,12 @@ class CodeContextTest(unittest.TestCase):
(4, 4, ' def __init__(self, a, b):', 'def')]) (4, 4, ' def __init__(self, a, b):', 'def')])
eq(cc.topvisible, 6) eq(cc.topvisible, 6)
# context_depth is 1. # context_depth is 1.
eq(cc.label['text'], ' def __init__(self, a, b):') eq(cc.context.get('1.0', 'end-1c'), ' def __init__(self, a, b):')
@mock.patch.object(codecontext.CodeContext, 'update_code_context') @mock.patch.object(codecontext.CodeContext, 'update_code_context')
def test_timer_event(self, mock_update): def test_timer_event(self, mock_update):
# Ensure code context is not active. # Ensure code context is not active.
if self.cc.label: if self.cc.context:
self.cc.toggle_code_context_event() self.cc.toggle_code_context_event()
self.cc.timer_event() self.cc.timer_event()
mock_update.assert_not_called() mock_update.assert_not_called()
@ -286,7 +286,7 @@ class CodeContextTest(unittest.TestCase):
test_colors = {'background': '#222222', 'foreground': '#ffff00'} test_colors = {'background': '#222222', 'foreground': '#ffff00'}
# Ensure code context is not active. # Ensure code context is not active.
if cc.label: if cc.context:
cc.toggle_code_context_event() cc.toggle_code_context_event()
# Nothing updates on inactive code context. # Nothing updates on inactive code context.
@ -303,18 +303,18 @@ class CodeContextTest(unittest.TestCase):
cc.config_timer_event() cc.config_timer_event()
eq(cc.textfont, save_font) eq(cc.textfont, save_font)
eq(cc.contextcolors, save_colors) eq(cc.contextcolors, save_colors)
eq(cc.label['font'], save_font) eq(cc.context['font'], save_font)
eq(cc.label['background'], save_colors['background']) eq(cc.context['background'], save_colors['background'])
eq(cc.label['foreground'], save_colors['foreground']) eq(cc.context['foreground'], save_colors['foreground'])
# Active code context, change font. # Active code context, change font.
cc.text['font'] = test_font cc.text['font'] = test_font
cc.config_timer_event() cc.config_timer_event()
eq(cc.textfont, test_font) eq(cc.textfont, test_font)
eq(cc.contextcolors, save_colors) eq(cc.contextcolors, save_colors)
eq(cc.label['font'], test_font) eq(cc.context['font'], test_font)
eq(cc.label['background'], save_colors['background']) eq(cc.context['background'], save_colors['background'])
eq(cc.label['foreground'], save_colors['foreground']) eq(cc.context['foreground'], save_colors['foreground'])
# Active code context, change color. # Active code context, change color.
cc.text['font'] = save_font cc.text['font'] = save_font
@ -322,9 +322,9 @@ class CodeContextTest(unittest.TestCase):
cc.config_timer_event() cc.config_timer_event()
eq(cc.textfont, save_font) eq(cc.textfont, save_font)
eq(cc.contextcolors, test_colors) eq(cc.contextcolors, test_colors)
eq(cc.label['font'], save_font) eq(cc.context['font'], save_font)
eq(cc.label['background'], test_colors['background']) eq(cc.context['background'], test_colors['background'])
eq(cc.label['foreground'], test_colors['foreground']) eq(cc.context['foreground'], test_colors['foreground'])
codecontext.CodeContext.colors = save_colors codecontext.CodeContext.colors = save_colors
cc.config_timer_event() cc.config_timer_event()

View File

@ -0,0 +1 @@
IDLE: Use read-only text widget for code context instead of label widget.