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
1 change: 1 addition & 0 deletions Doc/library/tk.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ alternative `GUI frameworks and tools <https://wiki.python.org/moin/GuiProgrammi
tkinter.rst
tkinter.colorchooser.rst
tkinter.font.rst
tkinter.fontchooser.rst
dialog.rst
tkinter.messagebox.rst
tkinter.scrolledtext.rst
Expand Down
94 changes: 94 additions & 0 deletions Doc/library/tkinter.fontchooser.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
:mod:`!tkinter.fontchooser` --- Font selection dialog
=====================================================

.. module:: tkinter.fontchooser
:synopsis: Font selection dialog

.. versionadded:: next

**Source code:** :source:`Lib/tkinter/fontchooser.py`

--------------

The :mod:`!tkinter.fontchooser` module provides the :class:`FontChooser` class
as an interface to the native font selection dialog.

The font dialog is application-global:
there is a single font dialog per Tcl interpreter,
and all :class:`FontChooser` instances configure the same dialog.

Depending on the platform, the dialog may be modal or modeless,
so :meth:`~FontChooser.show` may return immediately.
The selected font is not returned:
it is passed as a :class:`~tkinter.font.Font` object to the callback
specified with the *command* option.

The dialog also generates two virtual events on the parent window
(see the *parent* option):

``<<TkFontchooserVisibility>>``
Generated when the dialog is shown or hidden.
Query the *visible* option to tell which.

``<<TkFontchooserFontChanged>>``
Generated when the selected font changes.

.. note::

The *command* callback is the only reliable way to obtain the
selected font.
On some platforms the *font* option is not updated to the user's
choice.

.. class:: FontChooser(master=None, *, parent=None, title=None, font=None, command=None)

The class implementing the font selection dialog.

*master* is the widget whose Tcl interpreter owns the dialog.
If omitted, it defaults to *parent* if that is given,
or to the default root window otherwise.

The supported configuration options are:

* *parent* --- the window to which the dialog and its virtual events
are related.
It defaults to the main window;
on macOS the dialog is shown as a sheet attached to it,
rather than as a free-standing panel.
* *title* --- the title of the dialog.
* *font* --- the font that is currently selected in the dialog.
* *command* --- a callback that is called with a
:class:`~tkinter.font.Font` object wrapping the selected font when
the user selects a font.
* *visible* --- whether the dialog is currently displayed (read-only).

The *font* option accepts the forms supported by
:class:`tkinter.font.Font`.

.. method:: configure(**options)
config(**options)

Query or modify the options of the font dialog.
With no arguments, return a dict of all option values.
With a string argument, return the value of that option.
Otherwise, set the given options.

.. method:: cget(option)

Return the value of the given option of the font dialog.

.. method:: show()

Display the font dialog.
Depending on the platform, this method may return immediately
or only once the dialog has been withdrawn.

.. method:: hide()

Hide the font dialog if it is displayed.


.. seealso::

Module :mod:`tkinter.font`
Tkinter font-handling utilities
3 changes: 3 additions & 0 deletions Doc/library/tkinter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ The modules that provide Tk support include:
:mod:`tkinter.font`
Utilities to help work with fonts.

:mod:`tkinter.fontchooser`
Dialog to let the user choose a font.

:mod:`tkinter.messagebox`
Access to standard Tk dialog boxes.

Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,11 @@ tkinter
:meth:`~tkinter.font.Font.measure` and :meth:`~tkinter.font.Font.metrics`.
(Contributed by Serhiy Storchaka in :gh:`143990`.)

* Added the :mod:`tkinter.fontchooser` module which provides the
:class:`~tkinter.fontchooser.FontChooser` class as an interface to the
native font selection dialog.
(Contributed by Serhiy Storchaka in :gh:`72880`.)

xml
---

Expand Down
145 changes: 145 additions & 0 deletions Lib/test/test_tkinter/test_fontchooser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import time
import unittest
import tkinter
from tkinter import font
from tkinter.fontchooser import FontChooser
from test import support
from test.support import requires
from test.test_tkinter.support import (AbstractTkTest,
AbstractDefaultRootTest,
setUpModule) # noqa: F401

requires('gui')


class FontChooserTest(AbstractTkTest, unittest.TestCase):

def setUp(self):
super().setUp()
self.fc = FontChooser(self.root)
self.addCleanup(self._reset)

def _reset(self):
# The font dialog is global for the interpreter, so restore
# a clean state for the next test.
try:
self.fc.configure(parent=self.root, title='', command=None)
self.fc.hide()
except tkinter.TclError:
pass

def _visible(self):
return self.root.tk.getboolean(self.fc.cget('visible'))

def _wait_visibility(self, expected, timeout=None):
# Bounded wait until the dialog visibility matches expected.
if timeout is None:
timeout = support.LOOPBACK_TIMEOUT
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
self.root.update()
if self._visible() == expected:
return True
time.sleep(0.01)
return False

def test_configure_query(self):
options = self.fc.configure()
self.assertIsInstance(options, dict)
self.assertLessEqual({'parent', 'title', 'font', 'command', 'visible'},
options.keys())

def test_configure(self):
self.fc.configure(title='Pick a font')
if self.root._windowingsystem == 'x11':
self.assertEqual(self.fc.cget('title'), 'Pick a font')
self.fc.configure(font='Courier 10')
self.assertTrue(self.fc.cget('font'))
if self.root._windowingsystem == 'x11':
self.assertEqual(str(self.fc.cget('font')), 'Courier 10')

def test_parent(self):
top = tkinter.Toplevel(self.root)
self.addCleanup(top.destroy)
# The constructor does not force -parent to the master: it stays
# the main window even when the master is another widget.
FontChooser(top)
self.assertEqual(self.fc.cget('parent'), str(self.root))
# -parent can be set explicitly.
self.fc.configure(parent=top)
self.assertEqual(self.fc.cget('parent'), str(top))

def test_configure_font_instance(self):
# A Font instance can be passed as the font, both to the
# constructor and to configure(). The dialog may store it as
# the font name or as a resolved description depending on the
# platform, so compare the actual attributes.
def actual(spec):
return font.Font(self.root, spec, exists=True).actual()
f = font.Font(self.root, family='Courier', size=14, weight='bold')
fc = FontChooser(self.root, font=f)
self.assertEqual(actual(fc.cget('font')), f.actual())
f2 = font.Font(self.root, family='Times', size=11)
fc.configure(font=f2)
self.assertEqual(actual(fc.cget('font')), f2.actual())

def test_configure_visible_readonly(self):
with self.assertRaises(tkinter.TclError):
self.fc.configure(visible=True)

def test_cget_visible(self):
self.assertFalse(self._visible())

def test_command(self):
result = []
self.fc.configure(command=result.append)
name = self.fc._command_name
self.assertTrue(name)
# The Tcl command is named after the wrapped callback.
self.assertTrue(name.endswith('append'), name)
# The callback receives a Font wrapping the selected font.
self.root.tk.call(name, 'Courier 10')
self.assertEqual(len(result), 1)
selected = result[0]
self.assertIsInstance(selected, font.Font)
# The description is wrapped without creating a named font.
self.assertEqual(str(selected), 'Courier 10')
self.assertFalse(selected.delete_font)
self.assertEqual(int(selected.actual('size')), 10)
# Replacing the callback deletes the old Tcl command.
self.fc.configure(command=lambda font: None)
self.assertNotEqual(self.fc._command_name, name)
self.assertFalse(self.root.tk.call('info', 'commands', name))
# Removing the callback deletes the Tcl command.
name = self.fc._command_name
self.fc.configure(command=None)
self.assertIsNone(self.fc._command_name)
self.assertFalse(self.root.tk.call('info', 'commands', name))
self.assertEqual(self.fc.cget('command'), '')

def test_show_hide(self):
if self.root._windowingsystem != 'x11':
self.skipTest('cannot safely drive the native font dialog')
events = []
self.root.bind('<<TkFontchooserVisibility>>', events.append)
self.fc.show()
if not self._wait_visibility(True):
self.skipTest('the font dialog was not mapped')
self.fc.hide()
self.assertTrue(self._wait_visibility(False))
self.assertTrue(events)


class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):

def test_fontchooser(self):
root = tkinter.Tk()
fc = FontChooser()
self.assertIs(fc.master, root)
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, FontChooser)


if __name__ == "__main__":
unittest.main()
118 changes: 118 additions & 0 deletions Lib/tkinter/fontchooser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Interface to the native font selection dialog.

The FontChooser class gives access to the "tk fontchooser" command.
The dialog is application-global: all instances configure the single
dialog of the Tcl interpreter.

The dialog may be modeless, so show() may return immediately.
"""

import tkinter
from tkinter.font import Font

__all__ = ["FontChooser"]


class FontChooser:
"""The font selection dialog.

Supported configuration options are:

parent: the window to which the dialog and its virtual
events are related (the main window by default)
title: the title of the dialog
font: the font that is currently selected in the dialog
command: a callback that is called with a tkinter.font.Font
object wrapping the selected font when the user
selects a font
visible: whether the dialog is currently displayed
(read-only)

The "font" option accepts the forms supported by
tkinter.font.Font(font=...).

The "command" callback is the only reliable way to obtain the
selected font. On some platforms the "font" option is not updated
to the user's choice.
"""

def __init__(self, master=None, *, parent=None, title=None, font=None,
command=None):
if master is None:
master = parent
if master is None:
master = tkinter._get_default_root('create a font chooser')
self.master = master
self._command_name = None
options = {}
if parent is not None:
options['parent'] = parent
if title is not None:
options['title'] = title
if font is not None:
options['font'] = font
if command is not None:
options['command'] = command
if options:
self.configure(options)

def configure(self, cnf=None, **kw):
"""Query or modify the options of the font dialog.

With no arguments, return a dict of all option values. With a
string argument, return the value of that option. Otherwise,
set the given options. The "visible" option is read-only.
"""
if kw:
cnf = tkinter._cnfmerge((cnf, kw))
elif cnf:
cnf = tkinter._cnfmerge(cnf)
master = self.master
if cnf is None:
items = master.tk.splitlist(master.tk.call(
'tk', 'fontchooser', 'configure'))
return {items[i][1:]: items[i+1] for i in range(0, len(items), 2)}
if isinstance(cnf, str):
return master.tk.call('tk', 'fontchooser', 'configure', '-' + cnf)
cnf = dict(cnf)
new_name = None
if 'command' in cnf:
command = cnf['command']
if command is None:
cnf['command'] = ''
elif callable(command):
# Pass the selected font to the callback as a Font object.
def callback(description):
command(Font(self.master, font=description, exists=True))
# Name the Tcl command after the callback.
callback.__func__ = command
new_name = cnf['command'] = master._register(callback)
try:
master.tk.call('tk', 'fontchooser', 'configure',
*master._options(cnf))
except tkinter.TclError:
if new_name is not None:
master.deletecommand(new_name)
raise
if 'command' in cnf:
if self._command_name is not None:
master.deletecommand(self._command_name)
self._command_name = new_name
config = configure

def cget(self, option):
"""Return the value of the given option of the font dialog."""
return self.master.tk.call('tk', 'fontchooser', 'configure',
'-' + option)

def show(self):
"""Display the font dialog.

Depending on the platform, may return immediately or only
once the dialog has been withdrawn.
"""
self.master.tk.call('tk', 'fontchooser', 'show')

def hide(self):
"""Hide the font dialog if it is displayed."""
self.master.tk.call('tk', 'fontchooser', 'hide')
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added the :mod:`tkinter.fontchooser` module which provides the
:class:`~tkinter.fontchooser.FontChooser` class as an interface to the
native font selection dialog.
Loading