diff --git a/Lib/idlelib/help.py b/Lib/idlelib/help.py index f420d40fb9ea40..3a3f1343148003 100644 --- a/Lib/idlelib/help.py +++ b/Lib/idlelib/help.py @@ -28,17 +28,19 @@ from os.path import abspath, dirname, isfile, join from platform import python_version -from tkinter import Toplevel, Text, Menu +from tkinter import Toplevel, Text, Menu, EventType from tkinter.ttk import Frame, Menubutton, Scrollbar, Style from tkinter import font as tkfont from idlelib.config import idleConf +from idlelib.textview import FontSizer ## About IDLE ## ## IDLE Help ## + class HelpParser(HTMLParser): """Render help.html into a text widget. @@ -67,7 +69,7 @@ def __init__(self, text): def indent(self, amt=1): "Change indent (+1, 0, -1) and tags." self.level += amt - self.tags = '' if self.level == 0 else 'l'+str(self.level) + self.tags = '' if self.level == 0 else f'l{self.level}' def handle_starttag(self, tag, attrs): "Handle starttags in help.html." @@ -175,26 +177,37 @@ def __init__(self, parent, filename): Text.__init__(self, parent, wrap='word', highlightthickness=0, padx=5, borderwidth=0, width=uwide, height=uhigh) - normalfont = self.findfont(['TkDefaultFont', 'arial', 'helvetica']) - fixedfont = self.findfont(['TkFixedFont', 'monaco', 'courier']) - self['font'] = (normalfont, 12) - self.tag_configure('em', font=(normalfont, 12, 'italic')) - self.tag_configure('h1', font=(normalfont, 20, 'bold')) - self.tag_configure('h2', font=(normalfont, 18, 'bold')) - self.tag_configure('h3', font=(normalfont, 15, 'bold')) - self.tag_configure('pre', font=(fixedfont, 12), background='#f6f6ff') - self.tag_configure('preblock', font=(fixedfont, 10), lmargin1=25, - borderwidth=1, relief='solid', background='#eeffcc') - self.tag_configure('l1', lmargin1=25, lmargin2=25) - self.tag_configure('l2', lmargin1=50, lmargin2=50) - self.tag_configure('l3', lmargin1=75, lmargin2=75) - self.tag_configure('l4', lmargin1=100, lmargin2=100) + self.create_fonts() + self.configure_tags() self.parser = HelpParser(self) with open(filename, encoding='utf-8') as f: contents = f.read() self.parser.feed(contents) + self['state'] = 'disabled' + self.focus_set() + + def create_fonts(self): + "Create fonts to be used with tags." + base_size = idleConf.GetOption('main', 'EditorWindow', + 'font-size', type='int') + normalfont = self.findfont(['TkDefaultFont', 'arial', 'helvetica']) + fixedfont = self.findfont(['TkFixedFont', 'monaco', 'courier']) + + self.base_font = tkfont.Font(self, (normalfont, base_size)) + self['font'] = self.base_font + + # Define styling for each font tag used in html. + self.fonts = fonts = {} + fonts['em'] = tkfont.Font(self, family=normalfont, slant='italic') + for tag in ('h3', 'h2', 'h1'): + fonts[tag] = tkfont.Font(self, family=normalfont, weight='bold') + for tag in ('pre', 'preblock'): + fonts[tag] = tkfont.Font(self, family=fixedfont) + self.scale_tagfonts(base_size) + + FontSizer(self) def findfont(self, names): "Return name of first font family derived from names." @@ -206,6 +219,24 @@ def findfont(self, names): for x in tkfont.families(root=self)): return name + def configure_tags(self): + "Configure tags used in parsing." + for tag in ('em', 'h1', 'h2', 'h3', 'pre', 'preblock'): + self.tag_configure(tag, font=self.fonts[tag]) + self.tag_configure('pre', background='#f6f6ff') + self.tag_configure('preblock', lmargin1=25, borderwidth=1, + relief='solid', background='#eeffcc') + for level in range(1, 5): + self.tag_configure(f'l{level}', lmargin1=25*level, lmargin2=25*level) + + def scale_tagfonts(self, base): + "Scale tag sizes based on the size of normal text." + # Scale percentages are from Sphinx classic.css. + scale = {'h3': 1.2, 'h2': 1.4, 'h1': 1.6, + 'em': 1.0, 'pre': 1.0, 'preblock': 0.9} + for tag in self.fonts: + self.fonts[tag]['size'] = int(base * scale[tag]) + class HelpFrame(Frame): "Display html text, scrollbar, and toc." diff --git a/Lib/idlelib/idle_test/test_help.py b/Lib/idlelib/idle_test/test_help.py index b542659981894d..d676d95b79ff60 100644 --- a/Lib/idlelib/idle_test/test_help.py +++ b/Lib/idlelib/idle_test/test_help.py @@ -1,4 +1,6 @@ -"Test help, coverage 87%." +"Test help, coverage 90%." + +import sys from idlelib import help import unittest @@ -6,6 +8,16 @@ requires('gui') from os.path import abspath, dirname, join from tkinter import Tk +from idlelib import config + +darwin = sys.platform == 'darwin' +usercfg = help.idleConf.userCfg +testcfg = { + 'main': config.IdleUserConfParser(''), + 'highlight': config.IdleUserConfParser(''), + 'keys': config.IdleUserConfParser(''), + 'extensions': config.IdleUserConfParser(''), +} class HelpFrameTest(unittest.TestCase): @@ -30,5 +42,63 @@ def test_line1(self): self.assertEqual(text.get('1.0', '1.end'), ' IDLE ') +class HelpTextTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + help.idleConf.userCfg = testcfg + testcfg['main'].SetOption('EditorWindow', 'font-size', '12') + cls.root = root = Tk() + root.withdraw() + + @classmethod + def tearDownClass(cls): + help.idleConf.userCfg = usercfg + cls.root.update_idletasks() + cls.root.destroy() + del cls.root + + def setUp(self): + helpfile = join(dirname(dirname(abspath(__file__))), 'help.html') + self.text = help.HelpText(self.root, helpfile) + self.tags = ('h3', 'h2', 'h1', 'em', 'pre', 'preblock') + + def tearDown(self): + del self.text, self.tags + + def get_sizes(self): + return [self.text.fonts[tag]['size'] for tag in self.tags] + + def test_scale_tagfonts(self): + text = self.text + eq = self.assertEqual + + text.scale_tagfonts(12) + eq(self.get_sizes(), [14, 16, 19, 12, 12, 10]) + + text.scale_tagfonts(21) + eq(self.get_sizes(), [25, 29, 33, 21, 21, 18]) + + def test_resizing_callback(self): + text = self.text + eq = self.assertEqual + + base = [14, 16, 19, 12, 12, 10] + larger = [15, 17, 20, 13, 13, 11] + smaller = [13, 15, 18, 11, 11, 9] + + tests = (('<>', larger), + ('<>', base), + ('<>', smaller), + ('<>', base)) + + eq(self.get_sizes(), base) + + for event, result in tests: + with self.subTest(event=event): + text.event_generate(event) + eq(self.get_sizes(), result) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_textview.py b/Lib/idlelib/idle_test/test_textview.py index 7189378ab3dd61..89a89056e17f66 100644 --- a/Lib/idlelib/idle_test/test_textview.py +++ b/Lib/idlelib/idle_test/test_textview.py @@ -11,8 +11,9 @@ import os import unittest -from tkinter import Tk, TclError, CHAR, NONE, WORD +from tkinter import Tk, Text, TclError, CHAR, NONE, WORD from tkinter.ttk import Button +from tkinter import font as tkfont from idlelib.idle_test.mock_idle import Func from idlelib.idle_test.mock_tk import Mbox_func @@ -229,5 +230,76 @@ def _command(): self.assertEqual(get('3.0', '3.end'), f.readline().strip()) +class FontSizerTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.text = Text(root) + + @classmethod + def tearDownClass(cls): + del cls.text + + def setUp(self): + text = self.text + text.insert('end', 'Test Text') + self.sizer = tv.FontSizer(text) + + def tearDown(self): + del self.sizer + + def test_increase_font_size(self): + text = self.text + font = tkfont.Font(text, ('courier', 30)) + text['font'] = font + eq = self.assertEqual + text.focus_set() + + eq(font['size'], 30) + text.event_generate('<>') + eq(font['size'], 31) + text.event_generate('<>') + eq(font['size'], 32) + + def test_decrease_font_size(self): + text = self.text + font = tkfont.Font(text, ('courier', 30)) + text['font'] = font + eq = self.assertEqual + text.focus_set() + + eq(font['size'], 30) + text.event_generate('<>') + eq(font['size'], 29) + text.event_generate('<>') + eq(font['size'], 28) + + def test_increase_font_size_tuple(self): + text = self.text + font = ('Arial', 45, 'bold italic') + text['font'] = font + eq = self.assertEqual + text.focus_set() + + eq(text.tk.splitlist(text['font'])[1], '45') + text.event_generate('<>') + eq(text.tk.splitlist(text['font'])[1], '46') + text.event_generate('<>') + eq(text.tk.splitlist(text['font'])[1], '47') + + def test_decrease_font_size_tuple(self): + text = self.text + font = ('Arial', 45) + text['font'] = font + eq = self.assertEqual + text.focus_set() + + eq(text.tk.splitlist(text['font'])[1], '45') + text.event_generate('<>') + eq(text.tk.splitlist(text['font'])[1], '44') + text.event_generate('<>') + eq(text.tk.splitlist(text['font'])[1], '43') + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py index a66c1a4309a617..0bdffded61ebfd 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -1,13 +1,89 @@ """Simple text browser for IDLE """ +import sys from tkinter import Toplevel, Text, TclError,\ HORIZONTAL, VERTICAL, NS, EW, NSEW, NONE, WORD, SUNKEN +from tkinter.font import Font from tkinter.ttk import Frame, Scrollbar, Button from tkinter.messagebox import showerror from idlelib.colorizer import color_config +darwin = sys.platform == 'darwin' +MINIMUM_FONT_SIZE = 6 +MAXIMUM_FONT_SIZE = 100 + + +class FontSizer: + "Support dynamic text font resizing." + def __init__(self, text): + """"Add font resizing functionality to text widget. + + Args: + text: Tk widget with font attribute to size. + """ + self.text = text + self.bind_events() + + def bind_events(self): + "Bind events to the widget." + shortcut = 'Command' if darwin else 'Control' + # Bind to keys with or without shift. + self.text.event_add( + '<>', + f'<{shortcut}-Key-equal>', f'<{shortcut}-Key-plus>') + self.text.bind('<>', self.increase_font_size) + + self.text.event_add( + '<>', + f'<{shortcut}-Key-minus>', f'<{shortcut}-Key-underscore>') + self.text.bind('<>', self.decrease_font_size) + + # Windows and Mac use MouseWheel. + self.text.bind('', self.update_mousewheel) + # Linux uses Button 4 and Button 5 for the mousewheel. + self.text.bind('', self.decrease_font_size) + self.text.bind('', self.increase_font_size) + + def set_text_fontsize(new_size): + def sizer(self, event=None): + "Set the font size for this widget and its tags." + def resize(fontname): + try: + font = Font(self.text, name=fontname, exists=True) + font['size'] = new_size(font['size']) + except TclError: + font = list(self.text.tk.splitlist(fontname)) + if len(font) > 1: + font[1] = new_size(int(font[1])) + return font + + self.text['font'] = resize(self.text['font']) + for tag in self.text.tag_names(): + tag_font = self.text.tag_cget(tag, 'font') + if tag_font: + tag_font = resize(tag_font) + return 'break' + return sizer + + @set_text_fontsize + def increase_font_size(fontsize): + "Make font size larger." + return min(fontsize + 1, MAXIMUM_FONT_SIZE) + + @set_text_fontsize + def decrease_font_size(fontsize): + "Make font size smaller." + return max(fontsize - 1, MINIMUM_FONT_SIZE) + + def update_mousewheel(self, event): + "Adjust font size based on mouse wheel direction." + if (event.delta < 0) == (not darwin): + return self.decrease_font_size() + else: + return self.increase_font_size() + class AutoHideScrollbar(Scrollbar): """A scrollbar that is automatically hidden when not needed. @@ -94,8 +170,8 @@ def __init__(self, parent, contents, wrap='word'): self.button_ok = button_ok = Button( self, text='Close', command=self.ok, takefocus=False) - self.textframe.pack(side='top', expand=True, fill='both') button_ok.pack(side='bottom') + self.textframe.pack(side='top', expand=True, fill='both') def ok(self, event=None): """Dismiss text viewer dialog.""" diff --git a/Misc/NEWS.d/next/IDLE/2018-04-30-19-11-09.bpo-25198.yzJ3SL.rst b/Misc/NEWS.d/next/IDLE/2018-04-30-19-11-09.bpo-25198.yzJ3SL.rst new file mode 100644 index 00000000000000..c29acc97c38e5e --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2018-04-30-19-11-09.bpo-25198.yzJ3SL.rst @@ -0,0 +1,2 @@ +Use user preferences to set font size in IDLE Textview and Help and allow dynamic font +sizing using keybindings or mouse wheel.