From e5f4daa0835dab259dc106ac8fd80bd4034ba726 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Wed, 12 Jun 2019 22:05:05 +0300 Subject: [PATCH 01/50] apply Saimadhav's patch v3 (linenumber-text-widget-v3.diff) --- Doc/library/idle.rst | 4 + Lib/idlelib/LineNumber.py | 177 +++++++++++++++++++++++ Lib/idlelib/config-extensions.def | 10 ++ Lib/idlelib/idle_test/test_linenumber.py | 138 ++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 Lib/idlelib/LineNumber.py create mode 100644 Lib/idlelib/idle_test/test_linenumber.py diff --git a/Doc/library/idle.rst b/Doc/library/idle.rst index bd24695c728233d..be229acfc443403 100644 --- a/Doc/library/idle.rst +++ b/Doc/library/idle.rst @@ -291,6 +291,10 @@ Zoom/Restore Height defaults to 40 lines by 80 chars unless changed on the General tab of the Configure IDLE dialog. +Line Numbers (Editor Window only) + Open a column to the left of the edit window which shows the linenumber + of each line of text. + Window menu (Shell and Editor) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/Lib/idlelib/LineNumber.py b/Lib/idlelib/LineNumber.py new file mode 100644 index 000000000000000..91bba8deb3ba56d --- /dev/null +++ b/Lib/idlelib/LineNumber.py @@ -0,0 +1,177 @@ +"""Linenumbering implementation for IDLE as an extension. +Includes BaseSideBar which can be extended for other sidebar based extensions +""" +import tkinter as tk +from idlelib.Delegator import Delegator +from idlelib.configHandler import idleConf + +DISABLED = False +ENABLED = True + +get_end = lambda text: int(float(text.index('end-1c'))) + + +class BaseSideBar: + """ + The base class for extensions which require a sidebar. + """ + def __init__(self, editwin): + self.editwin = editwin + self.text = editwin.text + self.text.bind('<>', self.update_sidebar_text_font) + self.parent = self.text.nametowidget(self.text.winfo_parent()) + self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE) + self.sidebar_text.config(state=tk.DISABLED) + self.text['yscrollcommand'] = self.vbar_set + self.sidebar_text['yscrollcommand'] = self.vbar_set + + def update_sidebar_text_font(self, event=''): + """ + Implement in subclass to update font config values of sidebar_text + when font config values of editwin.text changes + """ + + def show_sidebar(self, side=tk.LEFT): + """ + side - Valid values are tk.LEFT, tk.RIGHT, tk.TOP, tk.BOTTOM + """ + try: + self.sidebar_text.pack(side=tk.LEFT, fill=tk.Y, before=self.text) + except tk.TclError: + self.sidebar_text.pack(side=tk.LEFT, fill=tk.Y) + self.state = ENABLED + + def hide_sidebar(self): + self.sidebar_text.pack_forget() + self.state = DISABLED + + def vbar_set(self, *args, **kwargs): + """Redirect scrollbar's set command to editwin.text and sidebar_text + """ + self.editwin.vbar.set(*args) + self.sidebar_text.yview_moveto(args[0]) + self.text.yview_moveto(args[0]) + + def redirect_event(self, event, event_name): + """Set focus to editwin.text and redirect 'event' to editwin.text. + """ + self.text.focus_set() + if event_name == '': + self.text.event_generate(event_name, x=event.x, y=event.y, + delta=event.delta) + else: + self.text.event_generate(event_name, x=event.x, y=event.y) + + +class EndLineDelegator(Delegator): + """Generate callbacks with the current end line number after + insert or delete operations""" + def __init__(self, changed_callback, end=1): + """ + changed_callback - Callable, will be called after insert + or delete operations with the current + end line number. + end - int, inital value of the end line number""" + Delegator.__init__(self) + self.changed_callback = changed_callback + self.changed_callback(end) + + def insert(self, index, chars, tags=None): + self.delegate.insert(index, chars, tags) + self.changed_callback(get_end(self.delegate)) + + def delete(self, index1, index2=None): + self.delegate.delete(index1, index2) + self.changed_callback(get_end(self.delegate)) + + +class LineNumber(BaseSideBar): + + menudefs = [ + ('options', [ + ("!Line Numbers", "<>"), + ]) + ] + + def __init__(self, editwin): + BaseSideBar.__init__(self, editwin) + self.prev_end = 1 + end = get_end(self.text) + self.update_sidebar_text_font() + 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) + for event_name in ('', '', '', + '', '', '', + '', '', + '', '', + '', '', '', + '<2>', '<3>', '', + ''): + self.sidebar_text.bind(event_name, + lambda event, event_name=event_name: + self.redirect_event(event, event_name)) + self.end_line_delegator = EndLineDelegator(self.update_sidebar_text, + end) + self.editwin.per.insertfilter(self.end_line_delegator) + self.state = idleConf.GetOption('extensions', 'LineNumber', 'visible', + type='bool') + # Note : We invert state here, and call toggle_linenumbering_event + # to get our desired state + self.state = not self.state + self.toggle_linenumbering_event('') + + def update_sidebar_text_font(self, event=''): + """Update the font of sidebar_text when font of editwin.font + changes + """ + bg = idleConf.GetOption('extensions', 'LineNumber', 'bgcolor') + fg = idleConf.GetOption('extensions', 'LineNumber', 'fgcolor') + self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT) + config = {'fg': fg, 'bg': bg, 'font': self.text['font'], + 'relief': tk.FLAT, 'selectforeground': fg, + 'selectbackground': bg} + if tk.TkVersion >= 8.5: + config['inactiveselectbackground'] = bg + self.sidebar_text.config(**config) + # The below lines below are required to allow tk to "catch up" with + # changes in font to the main text widget + # + sidebar_text = self.sidebar_text.get('1.0', 'end') + self.sidebar_text.delete('1.0', 'end') + self.sidebar_text.insert('1.0', sidebar_text) + self.text.update_idletasks() + self.sidebar_text.update_idletasks() + + def toggle_linenumbering_event(self, event): + self.show_sidebar() if self.state == DISABLED else self.hide_sidebar() + self.editwin.setvar('<>', self.state) + idleConf.SetOption('extensions', 'LineNumber', 'visible', + str(self.state)) + idleConf.SaveUserCfgFiles() + + 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)) + new_width = int(float(self.sidebar_text['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: + for i in range(self.prev_end + 1, end + 1): + self.sidebar_text.insert('{}.0'.format(i), '\n{}'.format(i), + 'linenumber') + else: + self.sidebar_text.delete('{}.0'.format(end+1), 'end') + self.sidebar_text.config(state=tk.DISABLED) + self.prev_end = end + +if __name__ == '__main__': + from unittest import main + main('idlelib.idle_test.test_linenumber', verbosity=2) diff --git a/Lib/idlelib/config-extensions.def b/Lib/idlelib/config-extensions.def index 7e23fb0a73d1d54..babf52b92e714c7 100644 --- a/Lib/idlelib/config-extensions.def +++ b/Lib/idlelib/config-extensions.def @@ -17,6 +17,16 @@ style= expression flash-delay= 500 bell= True +[LineNumber] +enable=1 +enable_editor=1 +# Should LineNumbering be visible on EditorWindow startup? +visible=0 +bgcolor=Gray +fgcolor=Black +[LineNumber_bindings] +toggle-linenumbering= + # IDLE reads several config files to determine user preferences. This # file is the default configuration file for IDLE extensions settings. # diff --git a/Lib/idlelib/idle_test/test_linenumber.py b/Lib/idlelib/idle_test/test_linenumber.py new file mode 100644 index 000000000000000..3a93a0af344b975 --- /dev/null +++ b/Lib/idlelib/idle_test/test_linenumber.py @@ -0,0 +1,138 @@ +"""Unittest for idlelib.LineNumber""" +import unittest +from test.support import requires +import tkinter as tk +from idlelib.Percolator import Percolator +from idlelib.LineNumber import LineNumber + + +class Dummy_editwin: + def __init__(self, text): + self.text = text + self.per = Percolator(text) + + +class LineNumberTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = tk.Tk() + cls.text = tk.Text(cls.root) + cls.editwin = Dummy_editwin(cls.text) + cls.editwin.vbar = tk.Scrollbar(cls.root) + + @classmethod + def tearDownClass(cls): + cls.editwin.per.close() + cls.text.destroy() + cls.root.destroy() + del cls.text, cls.root + + def setUp(self): + self.linenumber = LineNumber(self.editwin) + + def tearDown(self): + self.text.delete('1.0', 'end') + + def test_init_empty(self): + get = self.linenumber.sidebar_text.get + self.assertEqual(get('1.0', 'end'), '1\n') + + def test_init_not_empty(self): + self.text.insert('insert', 'foo bar\n'*3) + linenumber = LineNumber(self.editwin) + get = linenumber.sidebar_text.get + self.assertEqual(get('1.0', 'end'), '1\n2\n3\n4\n') + + def test_toggle_linenumbering(self): + ENABLED = True + DISABLED = False + + self.linenumber.state = ENABLED + self.linenumber.toggle_linenumbering_event('') + self.assertEqual(self.linenumber.state, DISABLED) + + self.linenumber.state = DISABLED + self.linenumber.toggle_linenumbering_event('') + self.assertEqual(self.linenumber.state, ENABLED) + + def test_insert(self): + text = self.editwin.text + get = self.linenumber.sidebar_text.get + config = self.linenumber.sidebar_text.config + equal = self.assertEqual + + text.insert('insert', 'foobar') + equal(get('1.0', 'end'), '1\n') + equal(config()['state'][-1], tk.DISABLED) + + text.insert('insert', '\nfoo') + equal(get('1.0', 'end'), '1\n2\n') + equal(config()['state'][-1], tk.DISABLED) + + text.insert('insert', 'hello\n'*2) + equal(get('1.0', 'end'), '1\n2\n3\n4\n') + equal(config()['state'][-1], tk.DISABLED) + + text.insert('insert', '\nworld') + equal(get('1.0', 'end'), '1\n2\n3\n4\n5\n') + equal(config()['state'][-1], tk.DISABLED) + + def test_delete(self): + text = self.editwin.text + get = self.linenumber.sidebar_text.get + config = self.linenumber.sidebar_text.config + equal = self.assertEqual + + text.insert('insert', 'foobar') + text.delete('1.1', '1.3') + equal(get('1.0', 'end'), '1\n') + equal(config()['state'][-1], tk.DISABLED) + + text.insert('insert', 'foo\n'*2) + text.delete('3.1') + text.delete('2.0', '2.end') + equal(get('1.0', 'end'), '1\n2\n') + equal(config()['state'][-1], tk.DISABLED) + + text.delete('1.3', 'end') + equal(get('1.0', 'end'), '1\n') + equal(config()['state'][-1], tk.DISABLED) + + text.delete('1.0', 'end') + equal(get('1.0', 'end'), '1\n') + equal(config()['state'][-1], tk.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] + text = self.text + equal = self.assertEqual + + equal(get_width(), 1) + + text.insert('insert', 'foo') + equal(get_width(), 1) + + text.insert('insert', 'foo\n'*10) + equal(get_width(), 2) + + text.insert('insert', 'foo\n'*100) + equal(get_width(), 3) + + text.delete('50.0', 'end') + equal(get_width(), 2) + + text.delete('5.0', 'end') + equal(get_width(), 1) + + text.delete('1.0', 'end') + equal(get_width(), 1) + +if __name__ == '__main__': + unittest.main(verbosity=2) From 5cda90dd09ecbcbe34e0d41225778da0d0cffde5 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Wed, 12 Jun 2019 23:15:13 +0300 Subject: [PATCH 02/50] updated into first working version * refactored from an extension into an integral part of IDLE * got tests working * various required fixes * some code cleanup and stylistic changes --- Lib/idlelib/config-extensions.def | 18 ++--- Lib/idlelib/editor.py | 18 ++++- ...test_linenumber.py => test_linenumbers.py} | 66 ++++++++++--------- Lib/idlelib/{LineNumber.py => linenumbers.py} | 55 +++++++--------- Lib/idlelib/mainmenu.py | 1 + Lib/idlelib/outwin.py | 2 + Lib/idlelib/pyshell.py | 2 + 7 files changed, 89 insertions(+), 73 deletions(-) rename Lib/idlelib/idle_test/{test_linenumber.py => test_linenumbers.py} (62%) rename Lib/idlelib/{LineNumber.py => linenumbers.py} (81%) diff --git a/Lib/idlelib/config-extensions.def b/Lib/idlelib/config-extensions.def index babf52b92e714c7..d5625dae8ee4c24 100644 --- a/Lib/idlelib/config-extensions.def +++ b/Lib/idlelib/config-extensions.def @@ -17,15 +17,15 @@ style= expression flash-delay= 500 bell= True -[LineNumber] -enable=1 -enable_editor=1 -# Should LineNumbering be visible on EditorWindow startup? -visible=0 -bgcolor=Gray -fgcolor=Black -[LineNumber_bindings] -toggle-linenumbering= +# [LineNumber] +# enable=1 +# enable_editor=1 +# # Should LineNumbering be visible on EditorWindow startup? +# visible=0 +# bgcolor=Gray +# fgcolor=Black +# [LineNumber_bindings] +# toggle-line-numbers= # IDLE reads several config files to determine user preferences. This # file is the default configuration file for IDLE extensions settings. diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index a6674728cd9374c..1819a385edd9ed1 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -54,6 +54,7 @@ class EditorWindow(object): from idlelib.calltip import Calltip from idlelib.codecontext import CodeContext from idlelib.paragraph import FormatParagraph + from idlelib.linenumbers import LineNumbers from idlelib.parenmatch import ParenMatch from idlelib.rstrip import Rstrip from idlelib.squeezer import Squeezer @@ -62,6 +63,9 @@ class EditorWindow(object): filesystemencoding = sys.getfilesystemencoding() # for file names help_url = None + allow_code_context = True + allow_line_numbers = True + def __init__(self, flist=None, filename=None, key=None, root=None): # Delay import: runscript imports pyshell imports EditorWindow. from idlelib.runscript import ScriptBinding @@ -311,8 +315,18 @@ def __init__(self, flist=None, filename=None, key=None, root=None): text.bind("<>", ctip.refresh_calltip_event) text.bind("<>", ctip.force_open_calltip_event) text.bind("<>", self.ZoomHeight(self).zoom_height_event) - text.bind("<>", - self.CodeContext(self).toggle_code_context_event) + if self.allow_code_context: + code_context = self.CodeContext(self) + text.bind("<>", + code_context.toggle_code_context_event) + else: + self.update_menu_state('options', '*Code Context', 'disabled') + if self.allow_line_numbers: + line_numbers = self.LineNumbers(self) + text.bind("<>", + line_numbers.toggle_line_numbers_event) + else: + self.update_menu_state('options', '*Line Numbers', 'disabled') def _filename_to_unicode(self, filename): """Return filename as BMP unicode so displayable in Tk.""" diff --git a/Lib/idlelib/idle_test/test_linenumber.py b/Lib/idlelib/idle_test/test_linenumbers.py similarity index 62% rename from Lib/idlelib/idle_test/test_linenumber.py rename to Lib/idlelib/idle_test/test_linenumbers.py index 3a93a0af344b975..123d855fd6a4933 100644 --- a/Lib/idlelib/idle_test/test_linenumber.py +++ b/Lib/idlelib/idle_test/test_linenumbers.py @@ -2,8 +2,9 @@ import unittest from test.support import requires import tkinter as tk -from idlelib.Percolator import Percolator -from idlelib.LineNumber import LineNumber + +from idlelib.percolator import Percolator +from idlelib.linenumbers import LineNumbers class Dummy_editwin: @@ -11,6 +12,9 @@ def __init__(self, text): self.text = text self.per = Percolator(text) + def setvar(self, name, value): + pass + class LineNumberTest(unittest.TestCase): @@ -30,7 +34,7 @@ def tearDownClass(cls): del cls.text, cls.root def setUp(self): - self.linenumber = LineNumber(self.editwin) + self.linenumber = LineNumbers(self.editwin) def tearDown(self): self.text.delete('1.0', 'end') @@ -41,7 +45,7 @@ def test_init_empty(self): def test_init_not_empty(self): self.text.insert('insert', 'foo bar\n'*3) - linenumber = LineNumber(self.editwin) + linenumber = LineNumbers(self.editwin) get = linenumber.sidebar_text.get self.assertEqual(get('1.0', 'end'), '1\n2\n3\n4\n') @@ -50,59 +54,57 @@ def test_toggle_linenumbering(self): DISABLED = False self.linenumber.state = ENABLED - self.linenumber.toggle_linenumbering_event('') + self.linenumber.toggle_line_numbers_event('') self.assertEqual(self.linenumber.state, DISABLED) self.linenumber.state = DISABLED - self.linenumber.toggle_linenumbering_event('') + self.linenumber.toggle_line_numbers_event('') self.assertEqual(self.linenumber.state, ENABLED) def test_insert(self): text = self.editwin.text get = self.linenumber.sidebar_text.get config = self.linenumber.sidebar_text.config - equal = self.assertEqual text.insert('insert', 'foobar') - equal(get('1.0', 'end'), '1\n') - equal(config()['state'][-1], tk.DISABLED) + self.assertEqual(get('1.0', 'end'), '1\n') + self.assertEqual(config()['state'][-1], tk.DISABLED) text.insert('insert', '\nfoo') - equal(get('1.0', 'end'), '1\n2\n') - equal(config()['state'][-1], tk.DISABLED) + self.assertEqual(get('1.0', 'end'), '1\n2\n') + self.assertEqual(config()['state'][-1], tk.DISABLED) text.insert('insert', 'hello\n'*2) - equal(get('1.0', 'end'), '1\n2\n3\n4\n') - equal(config()['state'][-1], tk.DISABLED) + self.assertEqual(get('1.0', 'end'), '1\n2\n3\n4\n') + self.assertEqual(config()['state'][-1], tk.DISABLED) text.insert('insert', '\nworld') - equal(get('1.0', 'end'), '1\n2\n3\n4\n5\n') - equal(config()['state'][-1], tk.DISABLED) + self.assertEqual(get('1.0', 'end'), '1\n2\n3\n4\n5\n') + self.assertEqual(config()['state'][-1], tk.DISABLED) def test_delete(self): text = self.editwin.text get = self.linenumber.sidebar_text.get config = self.linenumber.sidebar_text.config - equal = self.assertEqual text.insert('insert', 'foobar') text.delete('1.1', '1.3') - equal(get('1.0', 'end'), '1\n') - equal(config()['state'][-1], tk.DISABLED) + self.assertEqual(get('1.0', 'end'), '1\n') + self.assertEqual(config()['state'][-1], tk.DISABLED) text.insert('insert', 'foo\n'*2) text.delete('3.1') text.delete('2.0', '2.end') - equal(get('1.0', 'end'), '1\n2\n') - equal(config()['state'][-1], tk.DISABLED) + self.assertEqual(get('1.0', 'end'), '1\n2\n') + self.assertEqual(config()['state'][-1], tk.DISABLED) text.delete('1.3', 'end') - equal(get('1.0', 'end'), '1\n') - equal(config()['state'][-1], tk.DISABLED) + self.assertEqual(get('1.0', 'end'), '1\n') + self.assertEqual(config()['state'][-1], tk.DISABLED) text.delete('1.0', 'end') - equal(get('1.0', 'end'), '1\n') - equal(config()['state'][-1], tk.DISABLED) + self.assertEqual(get('1.0', 'end'), '1\n') + self.assertEqual(config()['state'][-1], tk.DISABLED) def test_sidebar_text_width(self): """ @@ -112,27 +114,27 @@ def test_sidebar_text_width(self): def get_width(): return self.linenumber.sidebar_text.config()['width'][-1] text = self.text - equal = self.assertEqual - equal(get_width(), 1) + self.assertEqual(get_width(), 1) text.insert('insert', 'foo') - equal(get_width(), 1) + self.assertEqual(get_width(), 1) text.insert('insert', 'foo\n'*10) - equal(get_width(), 2) + self.assertEqual(get_width(), 2) text.insert('insert', 'foo\n'*100) - equal(get_width(), 3) + self.assertEqual(get_width(), 3) text.delete('50.0', 'end') - equal(get_width(), 2) + self.assertEqual(get_width(), 2) text.delete('5.0', 'end') - equal(get_width(), 1) + self.assertEqual(get_width(), 1) text.delete('1.0', 'end') - equal(get_width(), 1) + self.assertEqual(get_width(), 1) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Lib/idlelib/LineNumber.py b/Lib/idlelib/linenumbers.py similarity index 81% rename from Lib/idlelib/LineNumber.py rename to Lib/idlelib/linenumbers.py index 91bba8deb3ba56d..d9de5db60a19c38 100644 --- a/Lib/idlelib/LineNumber.py +++ b/Lib/idlelib/linenumbers.py @@ -1,9 +1,9 @@ -"""Linenumbering implementation for IDLE as an extension. +"""Line numbering implementation for IDLE as an extension. Includes BaseSideBar which can be extended for other sidebar based extensions """ import tkinter as tk -from idlelib.Delegator import Delegator -from idlelib.configHandler import idleConf +from idlelib.config import idleConf +from idlelib.delegator import Delegator DISABLED = False ENABLED = True @@ -56,11 +56,10 @@ def redirect_event(self, event, event_name): """Set focus to editwin.text and redirect 'event' to editwin.text. """ self.text.focus_set() + kwargs = dict(x=event.x, y=event.y) if event_name == '': - self.text.event_generate(event_name, x=event.x, y=event.y, - delta=event.delta) - else: - self.text.event_generate(event_name, x=event.x, y=event.y) + kwargs.update(delta=event.delta) + self.text.event_generate(event_name, **kwargs) class EndLineDelegator(Delegator): @@ -85,14 +84,8 @@ def delete(self, index1, index2=None): self.changed_callback(get_end(self.delegate)) -class LineNumber(BaseSideBar): - - menudefs = [ - ('options', [ - ("!Line Numbers", "<>"), - ]) - ] - +class LineNumbers(BaseSideBar): + """Line numbers support for editor windows.""" def __init__(self, editwin): BaseSideBar.__init__(self, editwin) self.prev_end = 1 @@ -115,19 +108,19 @@ def __init__(self, editwin): self.end_line_delegator = EndLineDelegator(self.update_sidebar_text, end) self.editwin.per.insertfilter(self.end_line_delegator) - self.state = idleConf.GetOption('extensions', 'LineNumber', 'visible', - type='bool') - # Note : We invert state here, and call toggle_linenumbering_event + # self.state = idleConf.GetOption('extensions', 'LineNumber', 'visible', + # type='bool') + self.state = True # TODO: Read config + # Note : We invert state here, and call toggle_line_numbers_event # to get our desired state self.state = not self.state - self.toggle_linenumbering_event('') + self.toggle_line_numbers_event('') def update_sidebar_text_font(self, event=''): - """Update the font of sidebar_text when font of editwin.font - changes - """ - bg = idleConf.GetOption('extensions', 'LineNumber', 'bgcolor') - fg = idleConf.GetOption('extensions', 'LineNumber', 'fgcolor') + """Update the font when the editor window's font changes.""" + colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context') + bg = colors['background'] + fg = colors['foreground'] self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT) config = {'fg': fg, 'bg': bg, 'font': self.text['font'], 'relief': tk.FLAT, 'selectforeground': fg, @@ -138,18 +131,19 @@ def update_sidebar_text_font(self, event=''): # The below lines below are required to allow tk to "catch up" with # changes in font to the main text widget # + # TODO: validate the assertion above sidebar_text = self.sidebar_text.get('1.0', 'end') self.sidebar_text.delete('1.0', 'end') self.sidebar_text.insert('1.0', sidebar_text) self.text.update_idletasks() self.sidebar_text.update_idletasks() - def toggle_linenumbering_event(self, event): + def toggle_line_numbers_event(self, event): self.show_sidebar() if self.state == DISABLED else self.hide_sidebar() - self.editwin.setvar('<>', self.state) - idleConf.SetOption('extensions', 'LineNumber', 'visible', - str(self.state)) - idleConf.SaveUserCfgFiles() + self.editwin.setvar('<>', self.state) + # idleConf.SetOption('extensions', 'LineNumber', 'visible', + # str(self.state)) + # idleConf.SaveUserCfgFiles() def update_sidebar_text(self, end): """ @@ -172,6 +166,7 @@ def update_sidebar_text(self, end): self.sidebar_text.config(state=tk.DISABLED) self.prev_end = end + if __name__ == '__main__': from unittest import main - main('idlelib.idle_test.test_linenumber', verbosity=2) + main('idlelib.idle_test.test_linenumbers', verbosity=2) diff --git a/Lib/idlelib/mainmenu.py b/Lib/idlelib/mainmenu.py index f834220fc2bb753..866afced7bd42db 100644 --- a/Lib/idlelib/mainmenu.py +++ b/Lib/idlelib/mainmenu.py @@ -99,6 +99,7 @@ ('Configure _IDLE', '<>'), None, ('Show _Code Context', '<>'), + ("!Line Numbers", "<>"), ('Zoom Height', '<>'), ]), diff --git a/Lib/idlelib/outwin.py b/Lib/idlelib/outwin.py index ecc53ef0195dc64..2b927e0a8f0fabc 100644 --- a/Lib/idlelib/outwin.py +++ b/Lib/idlelib/outwin.py @@ -74,6 +74,8 @@ class OutputWindow(EditorWindow): ("Go to file/line", "<>", None), ] + allow_code_context = False + def __init__(self, *args): EditorWindow.__init__(self, *args) self.text.bind("<>", self.goto_file_line) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 6e0707d68bb6ede..03c78ca06f45eaf 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -861,6 +861,8 @@ class PyShell(OutputWindow): ("Squeeze", "<>"), ] + allow_line_numbers = False + # New classes from idlelib.history import History From d35acd07054cb44aa0c48b39272ab5317bf0fb87 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Wed, 12 Jun 2019 23:40:11 +0300 Subject: [PATCH 03/50] some refactoring --- Lib/idlelib/linenumbers.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/linenumbers.py index d9de5db60a19c38..1107dd350243a0e 100644 --- a/Lib/idlelib/linenumbers.py +++ b/Lib/idlelib/linenumbers.py @@ -1,6 +1,8 @@ """Line numbering implementation for IDLE as an extension. Includes BaseSideBar which can be extended for other sidebar based extensions """ +import itertools + import tkinter as tk from idlelib.config import idleConf from idlelib.delegator import Delegator @@ -65,15 +67,14 @@ def redirect_event(self, event, event_name): class EndLineDelegator(Delegator): """Generate callbacks with the current end line number after insert or delete operations""" - def __init__(self, changed_callback, end=1): + def __init__(self, changed_callback): """ changed_callback - Callable, will be called after insert or delete operations with the current end line number. - end - int, inital value of the end line number""" + """ Delegator.__init__(self) self.changed_callback = changed_callback - self.changed_callback(end) def insert(self, index, chars, tags=None): self.delegate.insert(index, chars, tags) @@ -89,7 +90,6 @@ class LineNumbers(BaseSideBar): def __init__(self, editwin): BaseSideBar.__init__(self, editwin) self.prev_end = 1 - end = get_end(self.text) self.update_sidebar_text_font() self._sidebar_width_type = type(self.sidebar_text['width']) self.sidebar_text.config(state=tk.NORMAL) @@ -105,8 +105,9 @@ def __init__(self, editwin): self.sidebar_text.bind(event_name, lambda event, event_name=event_name: self.redirect_event(event, event_name)) - self.end_line_delegator = EndLineDelegator(self.update_sidebar_text, - end) + self.end_line_delegator = EndLineDelegator(self.update_sidebar_text) + end = get_end(self.text) + self.update_sidebar_text(end) self.editwin.per.insertfilter(self.end_line_delegator) # self.state = idleConf.GetOption('extensions', 'LineNumber', 'visible', # type='bool') @@ -153,17 +154,24 @@ def update_sidebar_text(self, end): editwin.text contain the same number of lines""" if end == self.prev_end: return + width_difference = len(str(end)) - len(str(self.prev_end)) - new_width = int(float(self.sidebar_text['width'])) + width_difference - self.sidebar_text['width'] = self._sidebar_width_type(new_width) + 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: - for i in range(self.prev_end + 1, end + 1): - self.sidebar_text.insert('{}.0'.format(i), '\n{}'.format(i), - 'linenumber') + new_text = '\n'.join(itertools.chain( + [''], + map(str, range(self.prev_end + 1, end + 1)), + )) + self.sidebar_text.insert(f'{end+1:d}.0', new_text, 'linenumber') else: - self.sidebar_text.delete('{}.0'.format(end+1), 'end') + self.sidebar_text.delete(f'{end+1:d}.0', 'end') self.sidebar_text.config(state=tk.DISABLED) + self.prev_end = end From 89feac0c20b44df1db6787ba9936ffe37fb47faa Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Wed, 12 Jun 2019 23:47:02 +0300 Subject: [PATCH 04/50] fix integration with undo/redo --- Lib/idlelib/linenumbers.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/linenumbers.py index 1107dd350243a0e..5f16f748ea1ce06 100644 --- a/Lib/idlelib/linenumbers.py +++ b/Lib/idlelib/linenumbers.py @@ -105,10 +105,15 @@ def __init__(self, editwin): self.sidebar_text.bind(event_name, lambda event, event_name=event_name: self.redirect_event(event, event_name)) - self.end_line_delegator = EndLineDelegator(self.update_sidebar_text) end = get_end(self.text) self.update_sidebar_text(end) - self.editwin.per.insertfilter(self.end_line_delegator) + + 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.delegate = self.editwin.undo.delegate + self.editwin.undo.delegate = end_line_delegator + # self.state = idleConf.GetOption('extensions', 'LineNumber', 'visible', # type='bool') self.state = True # TODO: Read config From dd117968a2a480b08c0ec856faceabd41100d693 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Wed, 12 Jun 2019 23:52:51 +0300 Subject: [PATCH 05/50] give get_end a better name and a doc-string --- Lib/idlelib/linenumbers.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/linenumbers.py index 5f16f748ea1ce06..9a7821100c81329 100644 --- a/Lib/idlelib/linenumbers.py +++ b/Lib/idlelib/linenumbers.py @@ -10,7 +10,10 @@ DISABLED = False ENABLED = True -get_end = lambda text: int(float(text.index('end-1c'))) + +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'))) class BaseSideBar: @@ -78,11 +81,11 @@ def __init__(self, changed_callback): def insert(self, index, chars, tags=None): self.delegate.insert(index, chars, tags) - self.changed_callback(get_end(self.delegate)) + self.changed_callback(get_end_linenumber(self.delegate)) def delete(self, index1, index2=None): self.delegate.delete(index1, index2) - self.changed_callback(get_end(self.delegate)) + self.changed_callback(get_end_linenumber(self.delegate)) class LineNumbers(BaseSideBar): @@ -105,7 +108,7 @@ def __init__(self, editwin): self.sidebar_text.bind(event_name, lambda event, event_name=event_name: self.redirect_event(event, event_name)) - end = get_end(self.text) + end = get_end_linenumber(self.text) self.update_sidebar_text(end) end_line_delegator = EndLineDelegator(self.update_sidebar_text) From 62ce5dbc64444441b1f55faa4888a834f7306b83 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 13 Jun 2019 00:41:54 +0300 Subject: [PATCH 06/50] fix buggy insertion of a delegator into the middle of the percolator --- Lib/idlelib/idle_test/test_linenumbers.py | 3 +++ Lib/idlelib/linenumbers.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/idle_test/test_linenumbers.py b/Lib/idlelib/idle_test/test_linenumbers.py index 123d855fd6a4933..616ac99ea47fd60 100644 --- a/Lib/idlelib/idle_test/test_linenumbers.py +++ b/Lib/idlelib/idle_test/test_linenumbers.py @@ -3,6 +3,7 @@ from test.support import requires import tkinter as tk +from idlelib.delegator import Delegator from idlelib.percolator import Percolator from idlelib.linenumbers import LineNumbers @@ -11,6 +12,8 @@ class Dummy_editwin: def __init__(self, text): self.text = text self.per = Percolator(text) + self.undo = Delegator() + self.per.insertfilter(self.undo) def setvar(self, name, value): pass diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/linenumbers.py index 9a7821100c81329..73bd11ab8ffb9f4 100644 --- a/Lib/idlelib/linenumbers.py +++ b/Lib/idlelib/linenumbers.py @@ -114,8 +114,14 @@ def __init__(self, editwin): 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.delegate = self.editwin.undo.delegate - self.editwin.undo.delegate = end_line_delegator + 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.state = idleConf.GetOption('extensions', 'LineNumber', 'visible', # type='bool') From bd942da320be0afbe83da47809d835e38cdf12b8 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 13 Jun 2019 00:50:44 +0300 Subject: [PATCH 07/50] fix bug where BaseSideBar.show_sidebar() ignored the given side also refactor the state of BaseSideBar and LineNumbers --- Lib/idlelib/idle_test/test_linenumbers.py | 11 ++--- Lib/idlelib/linenumbers.py | 50 +++++++++++++++-------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/Lib/idlelib/idle_test/test_linenumbers.py b/Lib/idlelib/idle_test/test_linenumbers.py index 616ac99ea47fd60..547aefba0dbb7e9 100644 --- a/Lib/idlelib/idle_test/test_linenumbers.py +++ b/Lib/idlelib/idle_test/test_linenumbers.py @@ -53,16 +53,13 @@ def test_init_not_empty(self): self.assertEqual(get('1.0', 'end'), '1\n2\n3\n4\n') def test_toggle_linenumbering(self): - ENABLED = True - DISABLED = False - - self.linenumber.state = ENABLED + self.linenumber.side = tk.LEFT self.linenumber.toggle_line_numbers_event('') - self.assertEqual(self.linenumber.state, DISABLED) + self.assertEqual(self.linenumber.side, None) - self.linenumber.state = DISABLED + self.linenumber.side = None self.linenumber.toggle_line_numbers_event('') - self.assertEqual(self.linenumber.state, ENABLED) + self.assertEqual(self.linenumber.side, tk.LEFT) def test_insert(self): text = self.editwin.text diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/linenumbers.py index 73bd11ab8ffb9f4..25c86d371264e81 100644 --- a/Lib/idlelib/linenumbers.py +++ b/Lib/idlelib/linenumbers.py @@ -7,9 +7,6 @@ from idlelib.config import idleConf from idlelib.delegator import Delegator -DISABLED = False -ENABLED = True - def get_end_linenumber(text): """Utility to get the last line's number in a Tk text widget.""" @@ -30,25 +27,34 @@ def __init__(self, editwin): self.text['yscrollcommand'] = self.vbar_set self.sidebar_text['yscrollcommand'] = self.vbar_set + self.side = None + def update_sidebar_text_font(self, event=''): """ Implement in subclass to update font config values of sidebar_text when font config values of editwin.text changes """ - def show_sidebar(self, side=tk.LEFT): + def show_sidebar(self, side): """ - side - Valid values are tk.LEFT, tk.RIGHT, tk.TOP, tk.BOTTOM + side - Valid values are tk.LEFT, tk.RIGHT """ - try: - self.sidebar_text.pack(side=tk.LEFT, fill=tk.Y, before=self.text) - except tk.TclError: - self.sidebar_text.pack(side=tk.LEFT, fill=tk.Y) - self.state = ENABLED + if side not in {tk.LEFT, tk.RIGHT}: + raise ValueError( + 'side must be one of: ' + 'tk.LEFT = {tk.LEFT!r}; ' + 'tk.RIGHT = {tk.RIGHT!r}') + if side != self.side: + try: + self.sidebar_text.pack(side=side, fill=tk.Y, before=self.text) + except tk.TclError: + self.sidebar_text.pack(side=side, fill=tk.Y) + self.side = side def hide_sidebar(self): - self.sidebar_text.pack_forget() - self.state = DISABLED + if self.side is not None: + self.sidebar_text.pack_forget() + self.side = None def vbar_set(self, *args, **kwargs): """Redirect scrollbar's set command to editwin.text and sidebar_text @@ -123,14 +129,22 @@ def __init__(self, editwin): delegator.resetcache() delegator = delegator.delegate - # self.state = idleConf.GetOption('extensions', 'LineNumber', 'visible', - # type='bool') - self.state = True # TODO: Read config + self.is_shown = True # TODO: Read config # Note : We invert state here, and call toggle_line_numbers_event # to get our desired state - self.state = not self.state + self.is_shown = not self.is_shown self.toggle_line_numbers_event('') + @property + def is_shown(self): + return self.side is not None + + @is_shown.setter + def is_shown(self, value): + if not isinstance(value, bool): + raise TypeError('is_shown value must be boolean') + self.side = tk.LEFT if value else None + def update_sidebar_text_font(self, event=''): """Update the font when the editor window's font changes.""" colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context') @@ -154,8 +168,8 @@ def update_sidebar_text_font(self, event=''): self.sidebar_text.update_idletasks() def toggle_line_numbers_event(self, event): - self.show_sidebar() if self.state == DISABLED else self.hide_sidebar() - self.editwin.setvar('<>', self.state) + self.show_sidebar(tk.LEFT) if not self.is_shown else self.hide_sidebar() + self.editwin.setvar('<>', self.is_shown) # idleConf.SetOption('extensions', 'LineNumber', 'visible', # str(self.state)) # idleConf.SaveUserCfgFiles() From 954d122a9553627e606e69c299ef64bbd3c520aa Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 13 Jun 2019 01:07:36 +0300 Subject: [PATCH 08/50] add a NEWS entry --- Misc/NEWS.d/next/IDLE/2019-06-13-01-07-20.bpo-17535.K8i2St.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/IDLE/2019-06-13-01-07-20.bpo-17535.K8i2St.rst diff --git a/Misc/NEWS.d/next/IDLE/2019-06-13-01-07-20.bpo-17535.K8i2St.rst b/Misc/NEWS.d/next/IDLE/2019-06-13-01-07-20.bpo-17535.K8i2St.rst new file mode 100644 index 000000000000000..78ef99f503983aa --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2019-06-13-01-07-20.bpo-17535.K8i2St.rst @@ -0,0 +1 @@ +Add optional line numbers for IDLE editor windows. From 5f21e7f898653f07ac51c7d057d726c2cb7798a8 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 6 Jul 2019 00:00:41 +0300 Subject: [PATCH 09/50] update line numbers immediately upon highlighting config changes --- Lib/idlelib/editor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 1819a385edd9ed1..7955b1d2abee619 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -251,6 +251,7 @@ def __init__(self, flist=None, filename=None, key=None, root=None): self.good_load = False self.set_indentation_params(False) self.color = None # initialized below in self.ResetColorizer + self.line_numbers = None # optionally initialized later below if filename: if os.path.exists(filename) and not os.path.isdir(filename): if io.loadfile(filename): @@ -322,9 +323,10 @@ def __init__(self, flist=None, filename=None, key=None, root=None): else: self.update_menu_state('options', '*Code Context', 'disabled') if self.allow_line_numbers: - line_numbers = self.LineNumbers(self) + self.line_numbers = self.LineNumbers(self) text.bind("<>", - line_numbers.toggle_line_numbers_event) + self.line_numbers.toggle_line_numbers_event) + pass else: self.update_menu_state('options', '*Line Numbers', 'disabled') @@ -785,6 +787,8 @@ def ResetColorizer(self): self._rmcolorizer() self._addcolorizer() EditorWindow.color_config(self.text) + if self.line_numbers is not None: + self.line_numbers.update_sidebar_text_font() IDENTCHARS = string.ascii_letters + string.digits + "_" From 776aa4e2b777ea8d346d07bebf2f58b301db34c9 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 6 Jul 2019 00:37:33 +0300 Subject: [PATCH 10/50] optimize and improve scrolling * avoid duplicate scrolling events which degrade performance * reorganize event forwarding code * dragging on line numbers now selects entire lines * work around difficult to handle drag-scrolling --- Lib/idlelib/linenumbers.py | 117 +++++++++++++++++++++++++++++-------- 1 file changed, 94 insertions(+), 23 deletions(-) diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/linenumbers.py index 25c86d371264e81..36103e421b144bf 100644 --- a/Lib/idlelib/linenumbers.py +++ b/Lib/idlelib/linenumbers.py @@ -1,6 +1,7 @@ """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 @@ -19,13 +20,13 @@ class BaseSideBar: """ def __init__(self, editwin): self.editwin = editwin + self.parent = editwin.text_frame self.text = editwin.text + self.text.bind('<>', self.update_sidebar_text_font) - self.parent = self.text.nametowidget(self.text.winfo_parent()) self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE) self.sidebar_text.config(state=tk.DISABLED) - self.text['yscrollcommand'] = self.vbar_set - self.sidebar_text['yscrollcommand'] = self.vbar_set + self.text['yscrollcommand'] = self.redirect_yscroll_event self.side = None @@ -34,6 +35,7 @@ def update_sidebar_text_font(self, event=''): Implement in subclass to update font config values of sidebar_text when font config values of editwin.text changes """ + pass def show_sidebar(self, side): """ @@ -56,21 +58,31 @@ def hide_sidebar(self): self.sidebar_text.pack_forget() self.side = None - def vbar_set(self, *args, **kwargs): - """Redirect scrollbar's set command to editwin.text and sidebar_text + 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]) - self.text.yview_moveto(args[0]) + return 'break' - def redirect_event(self, event, event_name): - """Set focus to editwin.text and redirect 'event' to editwin.text. - """ + 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() - kwargs = dict(x=event.x, y=event.y) - if event_name == '': - kwargs.update(delta=event.delta) - self.text.event_generate(event_name, **kwargs) + self.text.event_generate(event_name or event, 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('', + x=0, y=event.y, delta=event.delta) + return 'break' class EndLineDelegator(Delegator): @@ -104,16 +116,10 @@ def __init__(self, editwin): self.sidebar_text.config(state=tk.NORMAL) self.sidebar_text.insert('insert', '1', 'linenumber') self.sidebar_text.config(state=tk.DISABLED) - for event_name in ('', '', '', - '', '', '', - '', '', - '', '', - '', '', '', - '<2>', '<3>', '', - ''): - self.sidebar_text.bind(event_name, - lambda event, event_name=event_name: - self.redirect_event(event, event_name)) + self.sidebar_text.config(takefocus=False, exportselection=False) + + self.bind_events() + end = get_end_linenumber(self.text) self.update_sidebar_text(end) @@ -135,6 +141,71 @@ def __init__(self, editwin): self.is_shown = not self.is_shown self.toggle_line_numbers_event('') + def bind_events(self): + # Ensure focus is always redirected to the main editor text widget. + self.sidebar_text.bind('', 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('', self.redirect_mousewheel_event) + + # Redirect mouse button events to the main editor text widget. + # + # Note that double- and triple-clicks must be replaced with normal + # clicks, since event_generate() doesn't allow generating them + # directly. + def bind_mouse_event(event_name, target_event_name=None): + target_event_name = target_event_name or event_name + handler = functools.partial(self.redirect_mousebutton_event, + event_name=target_event_name) + self.sidebar_text.bind(event_name, handler) + + for button in range(1, 5+1): + for event in (f'', + f'', + f'', + ): + bind_mouse_event(event, target_event_name=f'<{button}>') + for event in (f'', + f'', + ): + bind_mouse_event(event) + + # 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 + + # 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_motion_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]) + self.redirect_mousebutton_event(event, '') + self.sidebar_text.bind('', b1_motion_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=None): + yview = self.sidebar_text.yview() + if yview != last_yview: + self.text.yview_moveto(yview[0]) + self.text.event_generate('', x=0, y=last_y) + self.sidebar_text.bind('<>', selection_handler) + @property def is_shown(self): return self.side is not None From 86ebf063d08338b76b6b171792cd5b9dfb0fe249 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 6 Jul 2019 01:00:21 +0300 Subject: [PATCH 11/50] add a separate configuration setting for line number colors --- Lib/idlelib/config-highlight.def | 14 ++++++++++---- Lib/idlelib/config.py | 6 ++++-- Lib/idlelib/configdialog.py | 6 ++++++ Lib/idlelib/linenumbers.py | 2 +- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Lib/idlelib/config-highlight.def b/Lib/idlelib/config-highlight.def index aaa2b57a7f6d851..a7b0433831c533c 100644 --- a/Lib/idlelib/config-highlight.def +++ b/Lib/idlelib/config-highlight.def @@ -22,6 +22,10 @@ hit-foreground= #ffffff hit-background= #000000 error-foreground= #000000 error-background= #ff7777 +context-foreground= #000000 +context-background= lightgray +linenumber-foreground= gray +linenumber-background= #ffffff #cursor (only foreground can be set, restart IDLE) cursor-foreground= black #shell window @@ -31,8 +35,6 @@ stderr-foreground= red stderr-background= #ffffff console-foreground= #770000 console-background= #ffffff -context-foreground= #000000 -context-background= lightgray [IDLE New] normal-foreground= #000000 @@ -55,6 +57,10 @@ hit-foreground= #ffffff hit-background= #000000 error-foreground= #000000 error-background= #ff7777 +context-foreground= #000000 +context-background= lightgray +linenumber-foreground= gray +linenumber-background= #ffffff #cursor (only foreground can be set, restart IDLE) cursor-foreground= black #shell window @@ -64,8 +70,6 @@ stderr-foreground= red stderr-background= #ffffff console-foreground= #770000 console-background= #ffffff -context-foreground= #000000 -context-background= lightgray [IDLE Dark] comment-foreground = #dd0000 @@ -97,3 +101,5 @@ comment-background = #002240 break-foreground = #FFFFFF context-foreground= #ffffff context-background= #454545 +linenumber-foreground= gray +linenumber-background= #002240 diff --git a/Lib/idlelib/config.py b/Lib/idlelib/config.py index 12113c19c08672a..50ebbb6e242d457 100644 --- a/Lib/idlelib/config.py +++ b/Lib/idlelib/config.py @@ -336,6 +336,10 @@ def GetThemeDict(self, type, themeName): 'hit-background':'#000000', 'error-foreground':'#ffffff', 'error-background':'#000000', + 'context-foreground':'#000000', + 'context-background':'#ffffff', + 'linenumber-foreground':'#000000', + 'linenumber-background':'#ffffff', #cursor (only foreground can be set) 'cursor-foreground':'#000000', #shell window @@ -345,8 +349,6 @@ def GetThemeDict(self, type, themeName): 'stderr-background':'#ffffff', 'console-foreground':'#000000', 'console-background':'#ffffff', - 'context-foreground':'#000000', - 'context-background':'#ffffff', } for element in theme: if not cfgParser.has_option(themeName, element): diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 807ff60413d1b0f..f9f38a13092ceb9 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -817,6 +817,7 @@ def create_page_highlight(self): 'Shell Error Text': ('error', '12'), 'Shell Stdout Text': ('stdout', '13'), 'Shell Stderr Text': ('stderr', '14'), + 'Line Number': ('linenumber', '16'), } self.builtin_name = tracers.add( StringVar(self), self.var_changed_builtin_name) @@ -864,6 +865,11 @@ def create_page_highlight(self): ('stderr', 'stderr'), ('\n\n', 'normal')) for texttag in text_and_tags: 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: def tem(event, elem=element): # event.widget.winfo_top_level().highlight_target.set(elem) diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/linenumbers.py index 36103e421b144bf..3641368e406e86a 100644 --- a/Lib/idlelib/linenumbers.py +++ b/Lib/idlelib/linenumbers.py @@ -218,7 +218,7 @@ def is_shown(self, value): def update_sidebar_text_font(self, event=''): """Update the font when the editor window's font changes.""" - colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context') + colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') bg = colors['background'] fg = colors['foreground'] self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT) From 1cdfa2753fd84f277a6940ead1244df8b38a258f Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 6 Jul 2019 01:21:05 +0300 Subject: [PATCH 12/50] remove double- and triple- click conversion to single clicks This is apparently unnecessary. --- Lib/idlelib/linenumbers.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/linenumbers.py index 3641368e406e86a..bfabdeed060537d 100644 --- a/Lib/idlelib/linenumbers.py +++ b/Lib/idlelib/linenumbers.py @@ -152,23 +152,16 @@ def bind_events(self): self.sidebar_text.bind('', self.redirect_mousewheel_event) # Redirect mouse button events to the main editor text widget. - # - # Note that double- and triple-clicks must be replaced with normal - # clicks, since event_generate() doesn't allow generating them - # directly. - def bind_mouse_event(event_name, target_event_name=None): - target_event_name = target_event_name or event_name + def bind_mouse_event(event_name): handler = functools.partial(self.redirect_mousebutton_event, - event_name=target_event_name) + event_name=event_name) self.sidebar_text.bind(event_name, handler) for button in range(1, 5+1): for event in (f'', f'', f'', - ): - bind_mouse_event(event, target_event_name=f'<{button}>') - for event in (f'', + f'', f'', ): bind_mouse_event(event) From 083689e9a7f8f9dbd98e74de6e9c7cf3e1f43cbb Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 6 Jul 2019 01:38:55 +0300 Subject: [PATCH 13/50] replace hack to make font changes take effect immediately Taking advantage of the tighter integration with EditorWindow, each window now directly updates its line numbers sidebar upon font changes. --- Lib/idlelib/editor.py | 2 ++ Lib/idlelib/linenumbers.py | 29 ++++++++++------------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 7955b1d2abee619..7a44d5a65aed813 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -808,6 +808,8 @@ def ResetFont(self): # Called from configdialog.py self.text['font'] = idleConf.GetFont(self.root, 'main','EditorWindow') + if self.line_numbers is not None: + self.line_numbers.update_sidebar_text_font() def RemoveKeybindings(self): "Remove the keybindings before they are changed." diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/linenumbers.py index bfabdeed060537d..e0b4add87d73950 100644 --- a/Lib/idlelib/linenumbers.py +++ b/Lib/idlelib/linenumbers.py @@ -23,10 +23,11 @@ def __init__(self, editwin): self.parent = editwin.text_frame self.text = editwin.text - self.text.bind('<>', self.update_sidebar_text_font) - self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE) + self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE, + relief=tk.FLAT) self.sidebar_text.config(state=tk.DISABLED) self.text['yscrollcommand'] = self.redirect_yscroll_event + self.update_sidebar_text_font() self.side = None @@ -111,12 +112,12 @@ class LineNumbers(BaseSideBar): def __init__(self, editwin): BaseSideBar.__init__(self, editwin) self.prev_end = 1 - self.update_sidebar_text_font() 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() @@ -214,22 +215,12 @@ def update_sidebar_text_font(self, event=''): colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') bg = colors['background'] fg = colors['foreground'] - self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT) - config = {'fg': fg, 'bg': bg, 'font': self.text['font'], - 'relief': tk.FLAT, 'selectforeground': fg, - 'selectbackground': bg} - if tk.TkVersion >= 8.5: - config['inactiveselectbackground'] = bg - self.sidebar_text.config(**config) - # The below lines below are required to allow tk to "catch up" with - # changes in font to the main text widget - # - # TODO: validate the assertion above - sidebar_text = self.sidebar_text.get('1.0', 'end') - self.sidebar_text.delete('1.0', 'end') - self.sidebar_text.insert('1.0', sidebar_text) - self.text.update_idletasks() - self.sidebar_text.update_idletasks() + self.sidebar_text.config( + font=self.text['font'], + fg=fg, bg=bg, + selectforeground=fg, selectbackground=bg, + inactiveselectbackground=bg, + ) def toggle_line_numbers_event(self, event): self.show_sidebar(tk.LEFT) if not self.is_shown else self.hide_sidebar() From 8f02e7bdb79b76e725cccb675ae591bebf8ca176 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 6 Jul 2019 22:23:47 +0300 Subject: [PATCH 14/50] refactor and expand tests, also fixing test_delete --- Lib/idlelib/idle_test/test_linenumbers.py | 175 ++++++++++++++-------- 1 file changed, 116 insertions(+), 59 deletions(-) diff --git a/Lib/idlelib/idle_test/test_linenumbers.py b/Lib/idlelib/idle_test/test_linenumbers.py index 547aefba0dbb7e9..541a9e811b1a9c2 100644 --- a/Lib/idlelib/idle_test/test_linenumbers.py +++ b/Lib/idlelib/idle_test/test_linenumbers.py @@ -1,4 +1,5 @@ """Unittest for idlelib.LineNumber""" +from itertools import chain import unittest from test.support import requires import tkinter as tk @@ -11,6 +12,7 @@ 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) @@ -34,7 +36,7 @@ def tearDownClass(cls): cls.editwin.per.close() cls.text.destroy() cls.root.destroy() - del cls.text, cls.root + del cls.text, cls.editwin, cls.root def setUp(self): self.linenumber = LineNumbers(self.editwin) @@ -42,69 +44,85 @@ def setUp(self): def tearDown(self): self.text.delete('1.0', 'end') + 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): - get = self.linenumber.sidebar_text.get - self.assertEqual(get('1.0', 'end'), '1\n') + self.assert_sidebar_n_lines(1) def test_init_not_empty(self): self.text.insert('insert', 'foo bar\n'*3) - linenumber = LineNumbers(self.editwin) - get = linenumber.sidebar_text.get - self.assertEqual(get('1.0', 'end'), '1\n2\n3\n4\n') + self.assert_text_equals('foo bar\n'*3 + '\n') + self.assert_sidebar_n_lines(4) def test_toggle_linenumbering(self): - self.linenumber.side = tk.LEFT - self.linenumber.toggle_line_numbers_event('') + self.assertEqual(self.linenumber.side, tk.LEFT) + self.linenumber.toggle_line_numbers_event(None) self.assertEqual(self.linenumber.side, None) - - self.linenumber.side = None - self.linenumber.toggle_line_numbers_event('') + self.linenumber.toggle_line_numbers_event(None) self.assertEqual(self.linenumber.side, tk.LEFT) def test_insert(self): - text = self.editwin.text - get = self.linenumber.sidebar_text.get - config = self.linenumber.sidebar_text.config - - text.insert('insert', 'foobar') - self.assertEqual(get('1.0', 'end'), '1\n') - self.assertEqual(config()['state'][-1], tk.DISABLED) - - text.insert('insert', '\nfoo') - self.assertEqual(get('1.0', 'end'), '1\n2\n') - self.assertEqual(config()['state'][-1], tk.DISABLED) - - text.insert('insert', 'hello\n'*2) - self.assertEqual(get('1.0', 'end'), '1\n2\n3\n4\n') - self.assertEqual(config()['state'][-1], tk.DISABLED) - - text.insert('insert', '\nworld') - self.assertEqual(get('1.0', 'end'), '1\n2\n3\n4\n5\n') - self.assertEqual(config()['state'][-1], tk.DISABLED) + 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): - text = self.editwin.text - get = self.linenumber.sidebar_text.get - config = self.linenumber.sidebar_text.config - - text.insert('insert', 'foobar') - text.delete('1.1', '1.3') - self.assertEqual(get('1.0', 'end'), '1\n') - self.assertEqual(config()['state'][-1], tk.DISABLED) - - text.insert('insert', 'foo\n'*2) - text.delete('3.1') - text.delete('2.0', '2.end') - self.assertEqual(get('1.0', 'end'), '1\n2\n') - self.assertEqual(config()['state'][-1], tk.DISABLED) - - text.delete('1.3', 'end') - self.assertEqual(get('1.0', 'end'), '1\n') - self.assertEqual(config()['state'][-1], tk.DISABLED) - - text.delete('1.0', 'end') - self.assertEqual(get('1.0', 'end'), '1\n') - self.assertEqual(config()['state'][-1], tk.DISABLED) + 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): """ @@ -113,26 +131,65 @@ def test_sidebar_text_width(self): """ def get_width(): return self.linenumber.sidebar_text.config()['width'][-1] - text = self.text + 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) - text.insert('insert', 'foo') + 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) - text.insert('insert', 'foo\n'*10) + self.text.insert('insert', 'foo\n'*90) + self.assert_sidebar_n_lines(99) self.assertEqual(get_width(), 2) - text.insert('insert', 'foo\n'*100) + 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) - text.delete('50.0', 'end') + self.text.delete('insert -1l linestart', 'insert linestart') + self.assert_sidebar_n_lines(99) + self.assertEqual(get_width(), 2) + + self.text.delete('50.0', 'end') + self.assert_sidebar_n_lines(49) self.assertEqual(get_width(), 2) - text.delete('5.0', 'end') + self.text.delete('5.0', 'end') + self.assert_sidebar_n_lines(4) self.assertEqual(get_width(), 1) - text.delete('1.0', 'end') + # note: Text widgets always keep a single '\n' character at the end + self.text.delete('1.0', 'end') + self.assert_sidebar_n_lines(1) self.assertEqual(get_width(), 1) From b7faf1f6a31503ee11d590e6a654feaf71cbb279 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sun, 7 Jul 2019 10:47:50 +0300 Subject: [PATCH 15/50] attempt to fix tests failing on Linux There is apparently a minor inconsistency in Tk's Text widget. --- Lib/idlelib/idle_test/test_linenumbers.py | 6 +++--- Lib/idlelib/linenumbers.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/idlelib/idle_test/test_linenumbers.py b/Lib/idlelib/idle_test/test_linenumbers.py index 541a9e811b1a9c2..955bb17a64b0c9c 100644 --- a/Lib/idlelib/idle_test/test_linenumbers.py +++ b/Lib/idlelib/idle_test/test_linenumbers.py @@ -179,16 +179,16 @@ def get_width(): self.assert_sidebar_n_lines(99) self.assertEqual(get_width(), 2) - self.text.delete('50.0', 'end') + self.text.delete('50.0 -1c', 'end -1c') self.assert_sidebar_n_lines(49) self.assertEqual(get_width(), 2) - self.text.delete('5.0', 'end') + 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') + self.text.delete('1.0', 'end -1c') self.assert_sidebar_n_lines(1) self.assertEqual(get_width(), 1) diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/linenumbers.py index e0b4add87d73950..533d44c3ac07c67 100644 --- a/Lib/idlelib/linenumbers.py +++ b/Lib/idlelib/linenumbers.py @@ -250,9 +250,9 @@ def update_sidebar_text(self, end): [''], map(str, range(self.prev_end + 1, end + 1)), )) - self.sidebar_text.insert(f'{end+1:d}.0', new_text, 'linenumber') + self.sidebar_text.insert(f'end -1c', new_text, 'linenumber') else: - self.sidebar_text.delete(f'{end+1:d}.0', 'end') + self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c') self.sidebar_text.config(state=tk.DISABLED) self.prev_end = end From bbba1ea56166efb82b0fb1c766351113709c1d17 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Tue, 9 Jul 2019 12:40:22 +0300 Subject: [PATCH 16/50] remove sidebar text widget border on macOS --- Lib/idlelib/linenumbers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/linenumbers.py index 533d44c3ac07c67..6580deeea6deca4 100644 --- a/Lib/idlelib/linenumbers.py +++ b/Lib/idlelib/linenumbers.py @@ -24,7 +24,7 @@ def __init__(self, editwin): self.text = editwin.text self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE, - relief=tk.FLAT) + borderwidth=0, highlightthickness=0) self.sidebar_text.config(state=tk.DISABLED) self.text['yscrollcommand'] = self.redirect_yscroll_event self.update_sidebar_text_font() From 5cade34a4f8f496460aa7624d25e42c0c832c275 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Tue, 9 Jul 2019 12:41:29 +0300 Subject: [PATCH 17/50] mouse click and drag now select entire lines --- Lib/idlelib/linenumbers.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/linenumbers.py index 6580deeea6deca4..984043b83a3db5d 100644 --- a/Lib/idlelib/linenumbers.py +++ b/Lib/idlelib/linenumbers.py @@ -167,6 +167,18 @@ def bind_mouse_event(event_name): ): bind_mouse_event(event) + 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") + + # remember this line in case this is the beginning of dragging + nonlocal start_line + start_line = lineno + self.sidebar_text.bind('', 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 @@ -187,6 +199,12 @@ def b1_motion_handler(event, *args): if not 0 <= last_y <= self.sidebar_text.winfo_height(): self.text.yview_moveto(last_yview[0]) self.redirect_mousebutton_event(event, '') + + # update the selection + lineno = self.editwin.getlineno(f"@0,{event.y}") + 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.sidebar_text.bind('', b1_motion_handler) # With mouse-drag scrolling fixed by the above, there is still an edge- @@ -198,6 +216,12 @@ def selection_handler(event=None): if yview != last_yview: self.text.yview_moveto(yview[0]) self.text.event_generate('', x=0, y=last_y) + + # update the selection + lineno = self.editwin.getlineno(f"@0,{last_y}") + 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.sidebar_text.bind('<>', selection_handler) @property From 351c205ec1642288cc8b1448c0e3ac25c648e8a0 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Tue, 9 Jul 2019 16:25:47 +0300 Subject: [PATCH 18/50] fix layout when showing both line numbers and code context --- Lib/idlelib/codecontext.py | 31 ++++++++++++++++++++++++------- Lib/idlelib/editor.py | 4 ++-- Lib/idlelib/linenumbers.py | 38 ++++++++------------------------------ 3 files changed, 34 insertions(+), 39 deletions(-) diff --git a/Lib/idlelib/codecontext.py b/Lib/idlelib/codecontext.py index 2aed76de7fb6f87..a5d5219edf0673a 100644 --- a/Lib/idlelib/codecontext.py +++ b/Lib/idlelib/codecontext.py @@ -13,7 +13,7 @@ from sys import maxsize as INFINITY import tkinter -from tkinter.constants import TOP, X, SUNKEN +from tkinter.constants import NSEW, SUNKEN from idlelib.config import idleConf @@ -68,6 +68,7 @@ def __init__(self, editwin): self.textfont = self.text["font"] self.contextcolors = CodeContext.colors self.context = None + self.cell00 = None self.topvisible = 1 self.info = [(0, -1, "", False)] # Start two update cycles, one for context lines, one for font changes. @@ -107,25 +108,35 @@ def toggle_code_context_event(self, event=None): padx = 0 border = 0 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')) border += widget.tk.getint(widget.cget('border')) self.context = tkinter.Text( - self.editwin.top, font=self.textfont, + self.editwin.text_frame, font=self.textfont, bg=self.contextcolors['background'], fg=self.contextcolors['foreground'], height=1, width=1, # Don't request more than we get. + highlightthickness=0, padx=padx, border=border, relief=SUNKEN, state='disabled') self.context.bind('', self.jumptoline) - # Pack the context widget before and above the text_frame widget, - # thus ensuring that it will appear directly above text_frame. - self.context.pack(side=TOP, fill=X, expand=False, - before=self.editwin.text_frame) + # Grid the context widget above the text widget. + self.context.grid(row=0, column=1, sticky=NSEW) + + 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' else: self.context.destroy() self.context = None + self.cell00.destroy() + self.cell00 = None menu_status = 'Show' self.editwin.update_menu_label(menu='options', index='* Code Context', label=f'{menu_status} Code Context') @@ -231,6 +242,12 @@ def config_timer_event(self): self.context["font"] = self.textfont self.context['background'] = self.contextcolors['background'] self.context['foreground'] = self.contextcolors['foreground'] + + if self.cell00 is not None: + line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), + 'linenumber') + self.cell00.config(bg=line_number_colors['background']) + self.t2 = self.text.after(CONFIGUPDATEINTERVAL, self.config_timer_event) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 7a44d5a65aed813..4eab7df88ec4b74 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -200,11 +200,11 @@ def __init__(self, flist=None, filename=None, key=None, root=None): self.set_status_bar() vbar['command'] = self.handle_yview - vbar.pack(side=RIGHT, fill=Y) + vbar.grid(row=1, column=2, sticky=NSEW) text['yscrollcommand'] = vbar.set text['font'] = idleConf.GetFont(self.root, 'main', 'EditorWindow') text_frame.pack(side=LEFT, fill=BOTH, expand=1) - text.pack(side=TOP, fill=BOTH, expand=1) + text.grid(row=1, column=1, sticky=NSEW) text.focus_set() # usetabs true -> literal tab characters are used by indent and diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/linenumbers.py index 984043b83a3db5d..288ac567faf41f9 100644 --- a/Lib/idlelib/linenumbers.py +++ b/Lib/idlelib/linenumbers.py @@ -29,7 +29,7 @@ def __init__(self, editwin): self.text['yscrollcommand'] = self.redirect_yscroll_event self.update_sidebar_text_font() - self.side = None + self.is_shown = False def update_sidebar_text_font(self, event=''): """ @@ -38,26 +38,14 @@ def update_sidebar_text_font(self, event=''): """ pass - def show_sidebar(self, side): - """ - side - Valid values are tk.LEFT, tk.RIGHT - """ - if side not in {tk.LEFT, tk.RIGHT}: - raise ValueError( - 'side must be one of: ' - 'tk.LEFT = {tk.LEFT!r}; ' - 'tk.RIGHT = {tk.RIGHT!r}') - if side != self.side: - try: - self.sidebar_text.pack(side=side, fill=tk.Y, before=self.text) - except tk.TclError: - self.sidebar_text.pack(side=side, fill=tk.Y) - self.side = side + def show_sidebar(self): + self.sidebar_text.grid(row=1, column=0, sticky=tk.NSEW) + self.is_shown = True def hide_sidebar(self): - if self.side is not None: - self.sidebar_text.pack_forget() - self.side = None + if self.is_shown is not None: + 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. @@ -224,16 +212,6 @@ def selection_handler(event=None): self.text.tag_add("sel", f"{a}.0", f"{b+1}.0") self.sidebar_text.bind('<>', selection_handler) - @property - def is_shown(self): - return self.side is not None - - @is_shown.setter - def is_shown(self, value): - if not isinstance(value, bool): - raise TypeError('is_shown value must be boolean') - self.side = tk.LEFT if value else None - def update_sidebar_text_font(self, event=''): """Update the font when the editor window's font changes.""" colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') @@ -247,7 +225,7 @@ def update_sidebar_text_font(self, event=''): ) def toggle_line_numbers_event(self, event): - self.show_sidebar(tk.LEFT) if not self.is_shown else self.hide_sidebar() + self.show_sidebar() if not self.is_shown else self.hide_sidebar() self.editwin.setvar('<>', self.is_shown) # idleConf.SetOption('extensions', 'LineNumber', 'visible', # str(self.state)) From 7251403ce9a9c0ce7e99deedfdae22bd90cb7177 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Tue, 9 Jul 2019 19:02:51 +0300 Subject: [PATCH 19/50] fix test_toggle_linenumbering --- Lib/idlelib/idle_test/test_linenumbers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/idle_test/test_linenumbers.py b/Lib/idlelib/idle_test/test_linenumbers.py index 955bb17a64b0c9c..a73b8f99200017c 100644 --- a/Lib/idlelib/idle_test/test_linenumbers.py +++ b/Lib/idlelib/idle_test/test_linenumbers.py @@ -67,11 +67,11 @@ def test_init_not_empty(self): self.assert_sidebar_n_lines(4) def test_toggle_linenumbering(self): - self.assertEqual(self.linenumber.side, tk.LEFT) + self.assertEqual(self.linenumber.is_shown, True) self.linenumber.toggle_line_numbers_event(None) - self.assertEqual(self.linenumber.side, None) + self.assertEqual(self.linenumber.is_shown, False) self.linenumber.toggle_line_numbers_event(None) - self.assertEqual(self.linenumber.side, tk.LEFT) + self.assertEqual(self.linenumber.is_shown, True) def test_insert(self): self.text.insert('insert', 'foobar') From 335b78e5debba9e2d9d7a473b0a4198715a38043 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Tue, 9 Jul 2019 20:59:37 +0300 Subject: [PATCH 20/50] fix code context tests after pack->grid change --- Lib/idlelib/idle_test/test_codecontext.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/idle_test/test_codecontext.py b/Lib/idlelib/idle_test/test_codecontext.py index 6c6893580f42f6c..569398754ecf8c0 100644 --- a/Lib/idlelib/idle_test/test_codecontext.py +++ b/Lib/idlelib/idle_test/test_codecontext.py @@ -3,7 +3,7 @@ from idlelib import codecontext import unittest from test.support import requires -from tkinter import Tk, Frame, Text, TclError +from tkinter import NSEW, Tk, Frame, Text, TclError from unittest import mock import re @@ -58,7 +58,7 @@ def setUpClass(cls): text.insert('1.0', code_sample) # Need to pack for creation of code context text widget. 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) codecontext.idleConf.userCfg = testcfg From 76a15519883a1e90ddb002480fcec2cdbe1ab65d Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Wed, 10 Jul 2019 01:06:40 -0400 Subject: [PATCH 21/50] 10. Make text_frame resizeable. --- Lib/idlelib/editor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 5b6df69e2bdd56f..fc023d00b9c707e 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -199,11 +199,13 @@ def __init__(self, flist=None, filename=None, key=None, root=None): text.bind("<>", self.open_turtle_demo) 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.grid(row=1, column=2, sticky=NSEW) text['yscrollcommand'] = vbar.set 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.focus_set() From 83220c04ce7e60a41b5741da81df7849fea80645 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Wed, 10 Jul 2019 01:09:21 -0400 Subject: [PATCH 22/50] Remove unneeded addition to config-extensions.def. --- Lib/idlelib/config-extensions.def | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Lib/idlelib/config-extensions.def b/Lib/idlelib/config-extensions.def index d5625dae8ee4c24..7e23fb0a73d1d54 100644 --- a/Lib/idlelib/config-extensions.def +++ b/Lib/idlelib/config-extensions.def @@ -17,16 +17,6 @@ style= expression flash-delay= 500 bell= True -# [LineNumber] -# enable=1 -# enable_editor=1 -# # Should LineNumbering be visible on EditorWindow startup? -# visible=0 -# bgcolor=Gray -# fgcolor=Black -# [LineNumber_bindings] -# toggle-line-numbers= - # IDLE reads several config files to determine user preferences. This # file is the default configuration file for IDLE extensions settings. # From 5ea1326dad9c745fd2a1027eebaa24da9af395d6 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Wed, 10 Jul 2019 01:50:13 -0400 Subject: [PATCH 23/50] 1. Skip warning for new theme elements. --- Lib/idlelib/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/idlelib/config.py b/Lib/idlelib/config.py index 5a98652c939bf8a..48096a75f57ebc1 100644 --- a/Lib/idlelib/config.py +++ b/Lib/idlelib/config.py @@ -351,7 +351,9 @@ def GetThemeDict(self, type, themeName): 'console-background':'#ffffff', } for element in theme: - if not cfgParser.has_option(themeName, element): + if (not cfgParser.has_option(themeName, element) and + # Skip warning for new elements. + element[:element.index('-')] not in ['context', 'sidebar']): # Print warning that will return a default color warning = ('\n Warning: config.IdleConf.GetThemeDict' ' -\n problem retrieving theme element %r' From 857d5367af24dd673a15e2ee4d2362392edd7661 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Wed, 10 Jul 2019 02:01:11 -0400 Subject: [PATCH 24/50] Remane linenumbers.py to sidebar.py. --- Lib/idlelib/editor.py | 2 +- Lib/idlelib/{linenumbers.py => sidebar.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename Lib/idlelib/{linenumbers.py => sidebar.py} (100%) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index fc023d00b9c707e..5e1b01bb792ea9a 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -54,7 +54,7 @@ class EditorWindow(object): from idlelib.calltip import Calltip from idlelib.codecontext import CodeContext from idlelib.paragraph import FormatParagraph - from idlelib.linenumbers import LineNumbers + from idlelib.sidebar import LineNumbers from idlelib.parenmatch import ParenMatch from idlelib.rstrip import Rstrip from idlelib.squeezer import Squeezer diff --git a/Lib/idlelib/linenumbers.py b/Lib/idlelib/sidebar.py similarity index 100% rename from Lib/idlelib/linenumbers.py rename to Lib/idlelib/sidebar.py From 53abdb4e35b4fb3ad309396aeca4c2378e56bedc Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Wed, 10 Jul 2019 11:34:51 -0400 Subject: [PATCH 25/50] Rename test_linenumbers to test_sidebar, fix import. --- Lib/idlelib/idle_test/{test_linenumbers.py => test_sidebar.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Lib/idlelib/idle_test/{test_linenumbers.py => test_sidebar.py} (99%) diff --git a/Lib/idlelib/idle_test/test_linenumbers.py b/Lib/idlelib/idle_test/test_sidebar.py similarity index 99% rename from Lib/idlelib/idle_test/test_linenumbers.py rename to Lib/idlelib/idle_test/test_sidebar.py index a73b8f99200017c..55f21dc06340892 100644 --- a/Lib/idlelib/idle_test/test_linenumbers.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -6,7 +6,7 @@ from idlelib.delegator import Delegator from idlelib.percolator import Percolator -from idlelib.linenumbers import LineNumbers +from idlelib.sidebar import LineNumbers class Dummy_editwin: From 3bccd4660bf31edf83fd32a46892c3286e2fec47 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Wed, 10 Jul 2019 18:25:34 +0300 Subject: [PATCH 26/50] rename test class --- Lib/idlelib/idle_test/test_sidebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 55f21dc06340892..e61b6a647f6e6a0 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -21,7 +21,7 @@ def setvar(self, name, value): pass -class LineNumberTest(unittest.TestCase): +class LineNumbersTest(unittest.TestCase): @classmethod def setUpClass(cls): From 2c90d7a3d02be8df908d3cedc8d271664ee73a3a Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Wed, 10 Jul 2019 18:39:27 +0300 Subject: [PATCH 27/50] forward double- and triple-clicks events as normal click events --- Lib/idlelib/sidebar.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 288ac567faf41f9..65eb95dd15304a1 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -64,7 +64,7 @@ def redirect_focusin_event(self, event): 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 or event, x=0, y=event.y) + self.text.event_generate(event_name, x=0, y=event.y) return 'break' def redirect_mousewheel_event(self, event): @@ -141,19 +141,25 @@ def bind_events(self): self.sidebar_text.bind('', self.redirect_mousewheel_event) # Redirect mouse button events to the main editor text widget. - def bind_mouse_event(event_name): + def bind_mouse_event(event_name, target_event_name): handler = functools.partial(self.redirect_mousebutton_event, - event_name=event_name) + event_name=target_event_name) self.sidebar_text.bind(event_name, handler) for button in range(1, 5+1): - for event in (f'', - f'', - f'', - f'', - f'', - ): - bind_mouse_event(event) + for event_name in (f'', + f'', + f'', + ): + 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'', + f'', + ): + bind_mouse_event(event_name, + target_event_name=f'') start_line = None def b1_mousedown_handler(event): @@ -186,7 +192,7 @@ def b1_motion_handler(event, *args): last_yview = self.sidebar_text.yview() if not 0 <= last_y <= self.sidebar_text.winfo_height(): self.text.yview_moveto(last_yview[0]) - self.redirect_mousebutton_event(event, '') + self.redirect_mousebutton_event(event, event_name='') # update the selection lineno = self.editwin.getlineno(f"@0,{event.y}") From 0fad84fee95f3f2aba9401ffdd7ee21265c6028c Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Wed, 10 Jul 2019 22:39:34 +0300 Subject: [PATCH 28/50] line numbers off by default and set separately per window This is identical to how code context currently works, using the same implementation method. --- Lib/idlelib/mainmenu.py | 4 ++-- Lib/idlelib/sidebar.py | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Lib/idlelib/mainmenu.py b/Lib/idlelib/mainmenu.py index 39af03c31d52489..056301a1666d4e8 100644 --- a/Lib/idlelib/mainmenu.py +++ b/Lib/idlelib/mainmenu.py @@ -100,8 +100,8 @@ ('Configure _IDLE', '<>'), None, ('Show _Code Context', '<>'), - ("!Line Numbers", "<>"), - ('Zoom Height', '<>'), + ('Show _Line Numbers', '<>'), + ('_Zoom Height', '<>'), ]), ('window', [ diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 65eb95dd15304a1..c65f793800c19c0 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -124,7 +124,7 @@ def __init__(self, editwin): delegator.resetcache() delegator = delegator.delegate - self.is_shown = True # TODO: Read config + self.is_shown = False # TODO: Read config? # Note : We invert state here, and call toggle_line_numbers_event # to get our desired state self.is_shown = not self.is_shown @@ -232,10 +232,9 @@ def update_sidebar_text_font(self, event=''): def toggle_line_numbers_event(self, event): self.show_sidebar() if not self.is_shown else self.hide_sidebar() - self.editwin.setvar('<>', self.is_shown) - # idleConf.SetOption('extensions', 'LineNumber', 'visible', - # str(self.state)) - # idleConf.SaveUserCfgFiles() + menu_status = "Show" if not self.is_shown else "Hide" + self.editwin.update_menu_label(menu='options', index='* Line Numbers', + label=f'{menu_status} Line Numbers') def update_sidebar_text(self, end): """ From c2fb8f97b86dee0110eb26d7c5e99c0650477c91 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Wed, 10 Jul 2019 23:15:26 +0300 Subject: [PATCH 29/50] configurable default for whether to show line numbers in new windows --- Lib/idlelib/config-main.def | 5 +++-- Lib/idlelib/configdialog.py | 20 ++++++++++++++++++++ Lib/idlelib/sidebar.py | 4 +++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/config-main.def b/Lib/idlelib/config-main.def index 06e3c5adb0e35b6..b2be6250d021afe 100644 --- a/Lib/idlelib/config-main.def +++ b/Lib/idlelib/config-main.def @@ -36,7 +36,7 @@ # Additional help sources are listed in the [HelpFiles] section below # 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 -# menu. The pattern, and two examples, are +# menu. The pattern, and two examples, are: # # # 1 = IDLE;C:/Programs/Python36/Lib/idlelib/help.html @@ -46,7 +46,7 @@ # platform specific because of path separators, drive specs etc. # # 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. [General] @@ -65,6 +65,7 @@ font= TkFixedFont font-size= 10 font-bold= 0 encoding= none +line-numbers-default= 0 [PyShell] auto-squeeze-min-lines= 50 diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index f9f38a13092ceb9..426a9bc12b6ef0d 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -1822,6 +1822,9 @@ def create_page_general(self): frame_format: Frame format_width_title: Label (*)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 context_title: Label (*)context_int: Entry - context_lines @@ -1861,6 +1864,9 @@ def create_page_general(self): IntVar(self), ('main', 'General', 'autosave')) self.format_width = tracers.add( StringVar(self), ('extensions', 'FormatParagraph', 'max-width')) + self.line_numbers_default = tracers.add( + BooleanVar(self), + ('main', 'EditorWindow', 'line-numbers-default')) self.context_lines = tracers.add( StringVar(self), ('extensions', 'CodeContext', 'maxlines')) @@ -1930,6 +1936,14 @@ def create_page_general(self): self.format_width_int = Entry( frame_format, textvariable=self.format_width, width=4) + 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) context_title = Label(frame_context, text='Max Context Lines :') self.context_int = Entry( @@ -2003,6 +2017,10 @@ def create_page_general(self): frame_format.pack(side=TOP, padx=5, pady=0, fill=X) format_width_title.pack(side=LEFT, anchor=W, padx=5, 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.pack(side=TOP, padx=5, pady=0, fill=X) context_title.pack(side=LEFT, anchor=W, padx=5, pady=5) @@ -2045,6 +2063,8 @@ def load_general_cfg(self): 'main', 'General', 'autosave', default=0, type='bool')) self.format_width.set(idleConf.GetOption( '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( 'extensions', 'CodeContext', 'maxlines', type='int')) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index c65f793800c19c0..694758885351c7e 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -124,7 +124,8 @@ def __init__(self, editwin): delegator.resetcache() delegator = delegator.delegate - self.is_shown = False # TODO: Read config? + self.is_shown = idleConf.GetOption( + 'main', 'EditorWindow', 'line-numbers-default', type='bool') # Note : We invert state here, and call toggle_line_numbers_event # to get our desired state self.is_shown = not self.is_shown @@ -232,6 +233,7 @@ def update_sidebar_text_font(self, event=''): def toggle_line_numbers_event(self, event): self.show_sidebar() if not self.is_shown else self.hide_sidebar() + menu_status = "Show" if not self.is_shown else "Hide" self.editwin.update_menu_label(menu='options', index='* Line Numbers', label=f'{menu_status} Line Numbers') From 5cd21f44ef3f2cce55436d055c5c15be5a47847c Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Wed, 10 Jul 2019 23:57:06 +0300 Subject: [PATCH 30/50] fix state of menu option when opening IDLE with line numbers shown This also improves the separation of concerns between the editor window code and the sidebar code. --- Lib/idlelib/editor.py | 20 +++++++++++++++++--- Lib/idlelib/idle_test/test_sidebar.py | 6 ++++-- Lib/idlelib/outwin.py | 2 -- Lib/idlelib/sidebar.py | 14 +------------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 5e1b01bb792ea9a..bf173e08a71b9ad 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -327,9 +327,10 @@ def __init__(self, flist=None, filename=None, key=None, root=None): self.update_menu_state('options', '*Code Context', 'disabled') if self.allow_line_numbers: self.line_numbers = self.LineNumbers(self) - text.bind("<>", - self.line_numbers.toggle_line_numbers_event) - pass + if idleConf.GetOption('main', 'EditorWindow', + 'line-numbers-default', type='bool'): + self.toggle_line_numbers_event() + text.bind("<>", self.toggle_line_numbers_event) else: self.update_menu_state('options', '*Line Numbers', 'disabled') @@ -1603,6 +1604,19 @@ def guess_indent(self): indentsmall = indentlarge = 0 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 def index2line(index): return int(float(index)) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index e61b6a647f6e6a0..6224849d1114c0d 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -67,10 +67,12 @@ def test_init_not_empty(self): 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.toggle_line_numbers_event(None) + self.linenumber.hide_sidebar() self.assertEqual(self.linenumber.is_shown, False) - self.linenumber.toggle_line_numbers_event(None) + self.linenumber.show_sidebar() self.assertEqual(self.linenumber.is_shown, True) def test_insert(self): diff --git a/Lib/idlelib/outwin.py b/Lib/idlelib/outwin.py index 2b927e0a8f0fabc..90272b6feb4af6e 100644 --- a/Lib/idlelib/outwin.py +++ b/Lib/idlelib/outwin.py @@ -79,8 +79,6 @@ class OutputWindow(EditorWindow): def __init__(self, *args): EditorWindow.__init__(self, *args) self.text.bind("<>", self.goto_file_line) - self.text.unbind("<>") - self.update_menu_state('options', '*Code Context', 'disabled') # Customize EditorWindow def ispythonsource(self, filename): diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 694758885351c7e..de2993fe92d962e 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -124,12 +124,7 @@ def __init__(self, editwin): delegator.resetcache() delegator = delegator.delegate - self.is_shown = idleConf.GetOption( - 'main', 'EditorWindow', 'line-numbers-default', type='bool') - # Note : We invert state here, and call toggle_line_numbers_event - # to get our desired state - self.is_shown = not self.is_shown - self.toggle_line_numbers_event('') + self.is_shown = False def bind_events(self): # Ensure focus is always redirected to the main editor text widget. @@ -231,13 +226,6 @@ def update_sidebar_text_font(self, event=''): inactiveselectbackground=bg, ) - def toggle_line_numbers_event(self, event): - self.show_sidebar() if not self.is_shown else self.hide_sidebar() - - menu_status = "Show" if not self.is_shown else "Hide" - self.editwin.update_menu_label(menu='options', index='* Line Numbers', - label=f'{menu_status} Line Numbers') - def update_sidebar_text(self, end): """ Perform the following action: From d246688bcd1f24ab4311cc1a88354c61e4b72cc9 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 11 Jul 2019 13:08:39 +0300 Subject: [PATCH 31/50] fix and improve show_sidebar() and hide_sidebar() and their tests --- Lib/idlelib/idle_test/test_sidebar.py | 4 ++++ Lib/idlelib/sidebar.py | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 6224849d1114c0d..ab3418b556ee9c2 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -72,6 +72,10 @@ def test_toggle_linenumbering(self): 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) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index de2993fe92d962e..f774174c033986c 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -39,11 +39,12 @@ def update_sidebar_text_font(self, event=''): pass def show_sidebar(self): - self.sidebar_text.grid(row=1, column=0, sticky=tk.NSEW) - self.is_shown = True + 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 is not None: + if self.is_shown: self.sidebar_text.grid_forget() self.is_shown = False From cc294e93817df4ae2dee77537af436513c37a16a Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 11 Jul 2019 13:38:40 +0300 Subject: [PATCH 32/50] add tests for click and drag selections --- Lib/idlelib/idle_test/test_sidebar.py | 62 +++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index ab3418b556ee9c2..b4e909633ea37bc 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -20,6 +20,9 @@ def __init__(self, text): def setvar(self, name, value): pass + def getlineno(self, index): + return int(float(self.text.index(index))) + class LineNumbersTest(unittest.TestCase): @@ -27,16 +30,25 @@ class LineNumbersTest(unittest.TestCase): def setUpClass(cls): requires('gui') cls.root = tk.Tk() - cls.text = tk.Text(cls.root) + # cls.root.withdraw() + + 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) + cls.text.grid(row=1, column=1, sticky=tk.NSEW) + cls.editwin = Dummy_editwin(cls.text) - cls.editwin.vbar = tk.Scrollbar(cls.root) + cls.editwin.vbar = tk.Scrollbar(cls.text_frame) @classmethod def tearDownClass(cls): cls.editwin.per.close() - cls.text.destroy() + cls.root.update() cls.root.destroy() - del cls.text, cls.editwin, cls.root + del cls.text, cls.text_frame, cls.editwin, cls.root def setUp(self): self.linenumber = LineNumbers(self.editwin) @@ -44,6 +56,15 @@ def setUp(self): 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] + bbox[2] // 2 + y = bbox[1] + bbox[3] // 2 + return x, y + def assert_state_disabled(self): state = self.linenumber.sidebar_text.config()['state'] self.assertEqual(state[-1], tk.DISABLED) @@ -198,6 +219,39 @@ def get_width(): 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('', 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('', + x=start_x, y=start_y) + self.linenumber.sidebar_text.event_generate('', + x=start_x, y=start_y) + self.linenumber.sidebar_text.event_generate('', + x=end_x, y=end_y) + self.linenumber.sidebar_text.event_generate('', + x=end_x, y=end_y) + self.root.update() + + self.assertEqual(self.get_selection(), ('1.0', '4.0')) + if __name__ == '__main__': unittest.main(verbosity=2) From bd8fc827bbaf16f310748558c66797e95b4ddb78 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 11 Jul 2019 14:37:50 +0300 Subject: [PATCH 33/50] add a test for scrolling --- Lib/idlelib/idle_test/test_sidebar.py | 32 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index b4e909633ea37bc..37403df323a44b5 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -30,7 +30,6 @@ class LineNumbersTest(unittest.TestCase): def setUpClass(cls): requires('gui') cls.root = tk.Tk() - # cls.root.withdraw() cls.text_frame = tk.Frame(cls.root) cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) @@ -134,7 +133,7 @@ def test_delete(self): self.assert_sidebar_n_lines(3) self.assert_state_disabled() - # note: deleting up to "2.end" doesn't delete the final newline + # 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) @@ -145,7 +144,7 @@ def test_delete(self): self.assert_sidebar_n_lines(1) self.assert_state_disabled() - # note: Text widgets always keep a single '\n' character at the end + # 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) @@ -214,7 +213,7 @@ def get_width(): self.assert_sidebar_n_lines(4) self.assertEqual(get_width(), 1) - # note: Text widgets always keep a single '\n' character at the end + # 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) @@ -224,7 +223,7 @@ def test_click_selection(self): self.text.insert('1.0', 'one\ntwo\nthree\nfour\n') self.root.update() - # click on the second line + # Click on the second line. x, y = self.get_line_screen_position(2) self.linenumber.sidebar_text.event_generate('', x=x, y=y) self.linenumber.sidebar_text.update() @@ -237,7 +236,7 @@ def test_drag_selection(self): self.text.insert('1.0', 'one\ntwo\nthree\nfour\n') self.root.update() - # drag from the first line to the third line + # 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('', @@ -252,6 +251,27 @@ def test_drag_selection(self): 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('', + 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') + if __name__ == '__main__': unittest.main(verbosity=2) From f267df99209c601388be369dede01bf07652e4a0 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 11 Jul 2019 15:24:30 +0300 Subject: [PATCH 34/50] add an htest for drag-scrolling on the line numbers --- Lib/idlelib/idle_test/htest.py | 16 ++++++++++++++++ Lib/idlelib/idle_test/test_sidebar.py | 2 +- Lib/idlelib/sidebar.py | 26 +++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py index 20e5e9014ee73bc..42fa538ab73d994 100644 --- a/Lib/idlelib/idle_test/htest.py +++ b/Lib/idlelib/idle_test/htest.py @@ -67,6 +67,7 @@ def _wrapper(parent): # htest # import idlelib.pyshell # Set Windows DPI awareness before Tk(). from importlib import import_module +import textwrap import tkinter as tk from tkinter.ttk import Scrollbar tk.NoDefaultRoot() @@ -205,6 +206,21 @@ def _wrapper(parent): # htest # "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 = { 'file': 'multicall', 'kwds': {}, diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 37403df323a44b5..84cb4a1282f988c 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -36,7 +36,7 @@ def setUpClass(cls): 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) + 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) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index f774174c033986c..6ccd541118276ed 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -256,6 +256,30 @@ def update_sidebar_text(self, end): 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) + + text = tk.Text(text_frame, width=80, height=24, wrap=tk.NONE) + 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_linenumbers', verbosity=2) + main('idlelib.idle_test.test_sidebar', verbosity=2, exit=False) + + from idlelib.idle_test.htest import run + run(_linenumbers_drag_scrolling) From 7728110c4a49c195ab7ace2ccfc8a854038ea1c9 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 11 Jul 2019 15:41:42 +0300 Subject: [PATCH 35/50] fix string formatting in htest --- Lib/idlelib/idle_test/htest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py index 42fa538ab73d994..f2f37e161632a6a 100644 --- a/Lib/idlelib/idle_test/htest.py +++ b/Lib/idlelib/idle_test/htest.py @@ -214,11 +214,9 @@ def _wrapper(parent): # htest # 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. - """), + continuously."""), } _multi_call_spec = { From 8c133799a0122dd5cb7a7bf51638824b246f3792 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 11 Jul 2019 17:39:43 +0300 Subject: [PATCH 36/50] fix skipping warning for missing line number highlight config --- Lib/idlelib/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/config.py b/Lib/idlelib/config.py index 48096a75f57ebc1..731c7798297929e 100644 --- a/Lib/idlelib/config.py +++ b/Lib/idlelib/config.py @@ -353,7 +353,7 @@ def GetThemeDict(self, type, themeName): for element in theme: if (not cfgParser.has_option(themeName, element) and # Skip warning for new elements. - element[:element.index('-')] not in ['context', 'sidebar']): + not element.startswith(('context-', 'linenumber-')): # Print warning that will return a default color warning = ('\n Warning: config.IdleConf.GetThemeDict' ' -\n problem retrieving theme element %r' From 8d2b924daea4fdf8cadfb22bb5fcecbd14081561 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 11 Jul 2019 17:50:32 +0300 Subject: [PATCH 37/50] fix skipping warning for missing line number highlight config --- Lib/idlelib/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/config.py b/Lib/idlelib/config.py index 731c7798297929e..0f98158761bb628 100644 --- a/Lib/idlelib/config.py +++ b/Lib/idlelib/config.py @@ -351,9 +351,9 @@ def GetThemeDict(self, type, themeName): 'console-background':'#ffffff', } for element in theme: - if (not cfgParser.has_option(themeName, element) and - # Skip warning for new elements. - not element.startswith(('context-', 'linenumber-')): + 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 warning = ('\n Warning: config.IdleConf.GetThemeDict' ' -\n problem retrieving theme element %r' From a90e9f757929c0af5d564e98310f68fa2b0c6d80 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 11 Jul 2019 21:41:40 +0300 Subject: [PATCH 38/50] fix click and drag selection --- Lib/idlelib/sidebar.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 6ccd541118276ed..7f91720a361d685 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -137,13 +137,16 @@ def bind_events(self): # the line numbers. self.sidebar_text.bind('', self.redirect_mousewheel_event) - # Redirect mouse button events to the main editor text widget. + # 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 range(1, 5+1): + for button in [2, 3, 4, 5]: for event_name in (f'', f'', f'', @@ -164,6 +167,7 @@ def b1_mousedown_handler(event): 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 @@ -176,43 +180,40 @@ def b1_mousedown_handler(event): # 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_motion_handler(event, *args): + 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]) - self.redirect_mousebutton_event(event, event_name='') - - # update the selection - lineno = self.editwin.getlineno(f"@0,{event.y}") - 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.sidebar_text.bind('', b1_motion_handler) + drag_update_selection_and_insert_mark(event.y) + self.sidebar_text.bind('', 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=None): + def selection_handler(event): yview = self.sidebar_text.yview() if yview != last_yview: self.text.yview_moveto(yview[0]) - self.text.event_generate('', x=0, y=last_y) - - # update the selection - lineno = self.editwin.getlineno(f"@0,{last_y}") - 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") + drag_update_selection_and_insert_mark(last_y) self.sidebar_text.bind('<>', selection_handler) def update_sidebar_text_font(self, event=''): From 7c822b6f4b090b5fda9b4968ca29b0fd93bb4220 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Sat, 20 Jul 2019 19:46:49 -0400 Subject: [PATCH 39/50] Move line numbers down a pixel. --- Lib/idlelib/sidebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 7f91720a361d685..779cfd6290d8756 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -23,7 +23,7 @@ def __init__(self, editwin): self.parent = editwin.text_frame self.text = editwin.text - self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE, + self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE, pady=2, borderwidth=0, highlightthickness=0) self.sidebar_text.config(state=tk.DISABLED) self.text['yscrollcommand'] = self.redirect_yscroll_event From 46935c96c5ad9d2ac49495f95fe7d622fcd5154f Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Mon, 22 Jul 2019 21:50:08 +0300 Subject: [PATCH 40/50] properly ensure correct Y-axis padding of the sidebar widget --- Lib/idlelib/sidebar.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 779cfd6290d8756..bde16843066b364 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -14,6 +14,32 @@ def get_end_linenumber(text): 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. @@ -23,7 +49,9 @@ def __init__(self, editwin): self.parent = editwin.text_frame self.text = editwin.text - self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE, pady=2, + _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 From 6e937be9bd9c365ee04e6c6d4a02222e57e8756e Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Mon, 22 Jul 2019 23:26:47 +0300 Subject: [PATCH 41/50] fix line numbers update upon font config changes Also refactor the font and highlight config updating mechanism for both line numbers and code context. --- Lib/idlelib/codecontext.py | 6 ++-- Lib/idlelib/editor.py | 9 ++--- Lib/idlelib/idle_test/test_codecontext.py | 40 ++++++++++++--------- Lib/idlelib/sidebar.py | 43 ++++++++++++++--------- 4 files changed, 58 insertions(+), 40 deletions(-) diff --git a/Lib/idlelib/codecontext.py b/Lib/idlelib/codecontext.py index 0b477283ce1984a..4ce98136fe41759 100644 --- a/Lib/idlelib/codecontext.py +++ b/Lib/idlelib/codecontext.py @@ -113,11 +113,12 @@ def toggle_code_context_event(self, event=None): padx += widget.tk.getint(widget.cget('padx')) border += widget.tk.getint(widget.cget('border')) self.context = tkinter.Text( - self.editwin.text_frame, font=self.text['font'], + self.editwin.text_frame, height=1, width=1, # Don't request more than we get. highlightthickness=0, padx=padx, border=border, relief=SUNKEN, state='disabled') + self.update_font() self.update_highlight_colors() self.context.bind('', self.jumptoline) # Get the current context and initiate the recurring update event. @@ -233,8 +234,9 @@ def timer_event(self): self.update_code_context() 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: + font = idleConf.GetFont(self.text, 'main', 'EditorWindow') self.context['font'] = font def update_highlight_colors(self): diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 0217db443fb4fb7..497ee12f18140e6 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -798,7 +798,7 @@ def ResetColorizer(self): self.code_context.update_highlight_colors() if self.line_numbers is not None: - self.line_numbers.update_sidebar_text_font() + self.line_numbers.update_colors() IDENTCHARS = string.ascii_letters + string.digits + "_" @@ -817,15 +817,16 @@ def ResetFont(self): "Update the text widgets' font if it is changed" # Called from configdialog.py - new_font = idleConf.GetFont(self.root, 'main', 'EditorWindow') # Update the code context widget first, since its height affects # the height of the text widget. This avoids double re-rendering. if self.code_context is not None: - self.code_context.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_sidebar_text_font() + self.line_numbers.update_font() + # Finally, update the main text widget. + new_font = idleConf.GetFont(self.root, 'main', 'EditorWindow') self.text['font'] = new_font def RemoveKeybindings(self): diff --git a/Lib/idlelib/idle_test/test_codecontext.py b/Lib/idlelib/idle_test/test_codecontext.py index 9f13d2db4b615a3..a61e3a789ee13b6 100644 --- a/Lib/idlelib/idle_test/test_codecontext.py +++ b/Lib/idlelib/idle_test/test_codecontext.py @@ -77,6 +77,7 @@ def tearDownClass(cls): def setUp(self): self.text.yview(0) + self.text['font'] = 'TkFixedFont' self.cc = codecontext.CodeContext(self.editor) self.highlight_cfg = {"background": '#abcdef', @@ -86,10 +87,18 @@ def mock_idleconf_GetHighlight(theme, element): if element == 'context': return self.highlight_cfg return orig_idleConf_GetHighlight(theme, element) - patcher = unittest.mock.patch.object( + GetHighlight_patcher = unittest.mock.patch.object( codecontext.idleConf, 'GetHighlight', mock_idleconf_GetHighlight) - patcher.start() - self.addCleanup(patcher.stop) + 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( + codecontext.idleConf, 'GetFont', mock_idleconf_GetFont) + GetFont_patcher.start() + self.addCleanup(GetFont_patcher.stop) def tearDown(self): if self.cc.context: @@ -341,32 +350,29 @@ def test_font(self): cc = self.cc save_font = cc.text['font'] test_font = 'TkTextFont' + self.assertNotEqual(save_font, test_font) # Ensure code context is not active. if cc.context is not None: cc.toggle_code_context_event() + self.font_override = test_font # 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, 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) + eq(cc.context['font'], test_font) cc.toggle_code_context_event() + self.font_override = save_font + cc.toggle_code_context_event() + eq(cc.context['font'], save_font) - # Change text widget font and activate code context. - cc.text['font'] = test_font - cc.toggle_code_context_event(test_font) + # Call the font update, change is picked up. + self.font_override = test_font + cc.update_font() eq(cc.context['font'], test_font) - # Just call the font update. - cc.update_font(save_font) - eq(cc.context['font'], save_font) - cc.text['font'] = save_font - def test_highlight_colors(self): eq = self.assertEqual cc = self.cc diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index bde16843066b364..f54e2d6e67abd82 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -55,16 +55,31 @@ def __init__(self, editwin): borderwidth=0, highlightthickness=0) self.sidebar_text.config(state=tk.DISABLED) self.text['yscrollcommand'] = self.redirect_yscroll_event - self.update_sidebar_text_font() + self.update_font() + self.update_colors() self.is_shown = False - def update_sidebar_text_font(self, event=''): - """ - Implement in subclass to update font config values of sidebar_text - when font config values of editwin.text changes - """ - pass + 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: @@ -244,17 +259,11 @@ def selection_handler(event): drag_update_selection_and_insert_mark(last_y) self.sidebar_text.bind('<>', selection_handler) - def update_sidebar_text_font(self, event=''): - """Update the font when the editor window's font changes.""" + def update_colors(self): + """Update the sidebar text colors, usually after config changes.""" colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') - bg = colors['background'] - fg = colors['foreground'] - self.sidebar_text.config( - font=self.text['font'], - fg=fg, bg=bg, - selectforeground=fg, selectbackground=bg, - inactiveselectbackground=bg, - ) + self._update_colors(foreground=colors['foreground'], + background=colors['background']) def update_sidebar_text(self, end): """ From ab69da5df081aa7024923d688d75aea5e9ca1576 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Mon, 22 Jul 2019 23:27:23 +0300 Subject: [PATCH 42/50] fix fragile tests in test_sidebar --- Lib/idlelib/idle_test/test_sidebar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 84cb4a1282f988c..dc90c86abed0911 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -60,8 +60,8 @@ def get_selection(self): def get_line_screen_position(self, line): bbox = self.linenumber.sidebar_text.bbox(f'{line}.end -1c') - x = bbox[0] + bbox[2] // 2 - y = bbox[1] + bbox[3] // 2 + x = bbox[0] + 2 + y = bbox[1] + 2 return x, y def assert_state_disabled(self): From 60e1132fa9f53c809163dc06acd452828a619885 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Mon, 22 Jul 2019 23:22:00 -0400 Subject: [PATCH 43/50] Update test_sidebar docstring. --- Lib/idlelib/idle_test/test_sidebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index dc90c86abed0911..b33499ddbd797f2 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -1,4 +1,4 @@ -"""Unittest for idlelib.LineNumber""" +"""Test sidebar, coverage 93%""" from itertools import chain import unittest from test.support import requires From da95809c815b3808f4cc0b6ad20d0f3449c0c394 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Tue, 23 Jul 2019 00:02:36 -0400 Subject: [PATCH 44/50] Touchup docs. --- Doc/library/idle.rst | 11 +++++++---- Lib/idlelib/help.html | 11 ++++++++--- .../IDLE/2019-06-13-01-07-20.bpo-17535.K8i2St.rst | 5 ++++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Doc/library/idle.rst b/Doc/library/idle.rst index d062b9f1680681d..86e6af29818e81c 100644 --- a/Doc/library/idle.rst +++ b/Doc/library/idle.rst @@ -294,12 +294,19 @@ Configure IDLE configuration dialog by selecting Preferences in the application menu. For more, see :ref:`Setting 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) 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 :ref:`Code Context ` in the Editing and Navigation section below. +Line Numbers (Editor Window only) + 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 :ref:`Setting preferences `). + Zoom/Restore Height 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 @@ -308,10 +315,6 @@ Zoom/Restore Height Changing screen settings may invalidate the saved height. This toogle has no effect when a window is maximized. -Line Numbers (Editor Window only) - Open a column to the left of the edit window which shows the linenumber - of each line of text. - Window menu (Shell and Editor) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/Lib/idlelib/help.html b/Lib/idlelib/help.html index cee6887df68f9d5..49068df7a8b96c5 100644 --- a/Lib/idlelib/help.html +++ b/Lib/idlelib/help.html @@ -271,10 +271,15 @@

Options menu (Shell and Editor)Setting preferences under Help and preferences. +Setting 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)
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 Code Context in the Editing and Navigation section below.
+
Line Numbers (Editor Window only)
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 Setting preferences).
Zoom/Restore Height
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 Configure IDLE dialog. The maximum height for a screen is determined by @@ -607,7 +612,7 @@

Running user codesys.getrecursionlimit and -sys.setrecursionlimit to reduce their visibility.

+sys.setrecursionlimit to reduce the effect of the additional stack frames.

If sys is reset by user code, such as with importlib.reload(sys), IDLE’s changes are lost and input from the keyboard and output to the screen will not work correctly.

@@ -895,7 +900,7 @@

Navigation



- Last updated on Jul 04, 2019. + Last updated on Jul 23, 2019.
Found a bug?
diff --git a/Misc/NEWS.d/next/IDLE/2019-06-13-01-07-20.bpo-17535.K8i2St.rst b/Misc/NEWS.d/next/IDLE/2019-06-13-01-07-20.bpo-17535.K8i2St.rst index 78ef99f503983aa..201a413b42a354a 100644 --- a/Misc/NEWS.d/next/IDLE/2019-06-13-01-07-20.bpo-17535.K8i2St.rst +++ b/Misc/NEWS.d/next/IDLE/2019-06-13-01-07-20.bpo-17535.K8i2St.rst @@ -1 +1,4 @@ -Add optional line numbers for IDLE editor windows. +Add optional line numbers for IDLE editor windows. Windows +open without line numbers unless set otherwise in the General +tab of the configuration dialog. + From bb2a08ce8892fea10ba91b85d2149dcfab2d5155 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Tue, 23 Jul 2019 00:12:04 -0400 Subject: [PATCH 45/50] fix locally --- Lib/idlelib/idle_test/test_sidebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index dc90c86abed0911..b33499ddbd797f2 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -1,4 +1,4 @@ -"""Unittest for idlelib.LineNumber""" +"""Test sidebar, coverage 93%""" from itertools import chain import unittest from test.support import requires From 18dcbc99ebc7d981ffa58ca826fba9503bd65a8e Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Tue, 23 Jul 2019 11:37:22 +0300 Subject: [PATCH 46/50] more doc touch-ups: consistency, wording, typo --- Doc/library/idle.rst | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Doc/library/idle.rst b/Doc/library/idle.rst index 86e6af29818e81c..5975e3bb5806288 100644 --- a/Doc/library/idle.rst +++ b/Doc/library/idle.rst @@ -290,29 +290,31 @@ Options menu (Shell and Editor) Configure IDLE Open a configuration dialog and change preferences for the following: 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 - menu. For more, see + menu. For more details, see :ref:`Setting 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) 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 - :ref:`Code Context ` in the Editing and Navigation section below. + :ref:`Code Context ` in the Editing and Navigation section + below. -Line Numbers (Editor Window only) - 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 :ref:`Setting preferences `). +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 `). Zoom/Restore Height 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 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. - 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. Window menu (Shell and Editor) From af4473b65ddd19524f8753938421cd8153fd12ae Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Tue, 23 Jul 2019 12:15:41 +0300 Subject: [PATCH 47/50] add tests for font and color updates for line numbers Also cleaned up the parallel code context tests. --- Lib/idlelib/idle_test/test_codecontext.py | 47 ++++++-------- Lib/idlelib/idle_test/test_sidebar.py | 78 ++++++++++++++++++++++- 2 files changed, 96 insertions(+), 29 deletions(-) diff --git a/Lib/idlelib/idle_test/test_codecontext.py b/Lib/idlelib/idle_test/test_codecontext.py index a61e3a789ee13b6..3ec49e97af6f916 100644 --- a/Lib/idlelib/idle_test/test_codecontext.py +++ b/Lib/idlelib/idle_test/test_codecontext.py @@ -348,9 +348,10 @@ def test_timer_event(self, mock_update): def test_font(self): eq = self.assertEqual cc = self.cc - save_font = cc.text['font'] + + orig_font = cc.text['font'] test_font = 'TkTextFont' - self.assertNotEqual(save_font, test_font) + self.assertNotEqual(orig_font, test_font) # Ensure code context is not active. if cc.context is not None: @@ -360,54 +361,46 @@ def test_font(self): # Nothing breaks or changes with inactive code context. cc.update_font() - # Activate code context, font change is immediately effective. + # Activate code context, previous font change is immediately effective. cc.toggle_code_context_event() eq(cc.context['font'], test_font) - cc.toggle_code_context_event() - self.font_override = save_font - cc.toggle_code_context_event() - eq(cc.context['font'], save_font) # Call the font update, change is picked up. - self.font_override = test_font + self.font_override = orig_font cc.update_font() - eq(cc.context['font'], test_font) + eq(cc.context['font'], orig_font) def test_highlight_colors(self): eq = self.assertEqual cc = self.cc - save_colors = dict(self.highlight_cfg) + + orig_colors = dict(self.highlight_cfg) 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. if cc.context: cc.toggle_code_context_event() + self.highlight_cfg = test_colors # Nothing breaks with inactive code context. 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() - eq(cc.context['background'], save_colors['background']) - eq(cc.context['foreground'], save_colors['foreground']) + assert_colors_are_equal(test_colors) - # Call colors update, but no change to font. + # Call colors update with no change to the configured colors. cc.update_highlight_colors() - eq(cc.context['background'], save_colors['background']) - 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']) + assert_colors_are_equal(test_colors) - # Change colors and call highlight colors update. - self.highlight_cfg = save_colors + # Call the colors update with code context active, change is picked up. + self.highlight_cfg = orig_colors cc.update_highlight_colors() - eq(cc.context['background'], save_colors['background']) - eq(cc.context['foreground'], save_colors['foreground']) + assert_colors_are_equal(orig_colors) class HelperFunctionText(unittest.TestCase): diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index b33499ddbd797f2..8c98a0c0cbdecac 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -1,12 +1,13 @@ """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 -from idlelib.sidebar import LineNumbers +import idlelib.sidebar class Dummy_editwin: @@ -50,7 +51,27 @@ def tearDownClass(cls): del cls.text, cls.text_frame, cls.editwin, cls.root def setUp(self): - self.linenumber = LineNumbers(self.editwin) + 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') @@ -272,6 +293,59 @@ def test_scroll(self): 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) From 2a493ea92998e27b93a008c59919f4cb823f1f50 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Tue, 23 Jul 2019 12:16:00 +0300 Subject: [PATCH 48/50] ensure font consistency in line numbers htest --- Lib/idlelib/sidebar.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index f54e2d6e67abd82..8f9bf80b5260558 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -303,7 +303,8 @@ def _linenumbers_drag_scrolling(parent): # htest # text_frame.rowconfigure(1, weight=1) text_frame.columnconfigure(1, weight=1) - text = tk.Text(text_frame, width=80, height=24, wrap=tk.NONE) + 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) From 86d420e0875bbc652fd41a327d5e232904af67b0 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Tue, 23 Jul 2019 13:34:28 +0300 Subject: [PATCH 49/50] add "What's New" entries --- Doc/whatsnew/3.7.rst | 7 +++++++ Doc/whatsnew/3.8.rst | 5 +++++ Doc/whatsnew/3.9.rst | 8 ++++++++ 3 files changed, 20 insertions(+) diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index fc867ac15a5fe22..46834ec42d9cf33 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -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. +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 --------- diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 95f25cd1cc52a98..3f84e092827bd8e 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -515,6 +515,11 @@ for certain types of invalid or corrupt gzip files. 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. 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 diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 85e254f061eb9af..ea627f3a0b7d233 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -103,6 +103,14 @@ New Modules Improved Modules ================ +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`.) + threading --------- From 27e741fbd043c37328dc71caa181e60c03f0b7e6 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Tue, 23 Jul 2019 13:35:29 +0300 Subject: [PATCH 50/50] revert 3.9 "what's New" entry --- Doc/whatsnew/3.9.rst | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index ea627f3a0b7d233..85e254f061eb9af 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -103,14 +103,6 @@ New Modules Improved Modules ================ -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`.) - threading ---------