Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 47 additions & 16 deletions Lib/idlelib/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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 = {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like the doubling of the configuration code, but I have not thought of anything better yet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't make any changes for this.

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."
Expand All @@ -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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one more level than before. I don't know if we use more than l1. I may add some prints to the parser to fine out. We do use everything else.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean by 'one more level' than before? Before, it did levels l1, l2, l3, and l4. Debugging shows that down to l3 is used.

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."
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No default. Set self.base_size to base passed in.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the refactoring of the class for FontSizer, I removed self.base_size.

# 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."
Expand Down
72 changes: 71 additions & 1 deletion Lib/idlelib/idle_test/test_help.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
"Test help, coverage 87%."
"Test help, coverage 90%."

import sys

from idlelib import help
import unittest
from test.support import requires
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):
Expand All @@ -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 = (('<<increase_font_size>>', larger),
('<<decrease_font_size>>', base),
('<<decrease_font_size>>', smaller),
('<<increase_font_size>>', 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)
74 changes: 73 additions & 1 deletion Lib/idlelib/idle_test/test_textview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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('<<increase_font_size>>')
eq(font['size'], 31)
text.event_generate('<<increase_font_size>>')
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('<<decrease_font_size>>')
eq(font['size'], 29)
text.event_generate('<<decrease_font_size>>')
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('<<increase_font_size>>')
eq(text.tk.splitlist(text['font'])[1], '46')
text.event_generate('<<increase_font_size>>')
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('<<decrease_font_size>>')
eq(text.tk.splitlist(text['font'])[1], '44')
text.event_generate('<<decrease_font_size>>')
eq(text.tk.splitlist(text['font'])[1], '43')


if __name__ == '__main__':
unittest.main(verbosity=2)
78 changes: 77 additions & 1 deletion Lib/idlelib/textview.py
Original file line number Diff line number Diff line change
@@ -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(
'<<increase_font_size>>',
f'<{shortcut}-Key-equal>', f'<{shortcut}-Key-plus>')
self.text.bind('<<increase_font_size>>', self.increase_font_size)

self.text.event_add(
'<<decrease_font_size>>',
f'<{shortcut}-Key-minus>', f'<{shortcut}-Key-underscore>')
self.text.bind('<<decrease_font_size>>', self.decrease_font_size)

# Windows and Mac use MouseWheel.
self.text.bind('<Control-MouseWheel>', self.update_mousewheel)
# Linux uses Button 4 and Button 5 for the mousewheel.
self.text.bind('<Control-Button-4>', self.decrease_font_size)
self.text.bind('<Control-Button-5>', 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.
Expand Down Expand Up @@ -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."""
Expand Down
Original file line number Diff line number Diff line change
@@ -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.