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
22 changes: 19 additions & 3 deletions Doc/library/dialog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,19 @@ These do not emulate the native look-and-feel of the platform.
.. note:: The *FileDialog* class should be subclassed for custom event
handling and behaviour.

.. class:: FileDialog(master, title=None)
.. class:: FileDialog(master, title=None, *, use_ttk=True)

Create a basic file selection dialog.
Its layout -- a filter entry, side-by-side directory and file lists, and a
selection entry -- follows the classic Motif file selection dialog.
When *use_ttk* is true (the default), the dialog is built from the themed
:mod:`tkinter.ttk` widgets; when false, from the classic :mod:`tkinter`
widgets.

.. versionchanged:: next
The dialog is now built from the themed :mod:`tkinter.ttk` widgets by
default, instead of the classic :mod:`tkinter` widgets.
Added the *use_ttk* parameter.

.. method:: cancel_command(event=None)

Expand Down Expand Up @@ -280,21 +290,27 @@ These do not emulate the native look-and-feel of the platform.
Update the current file selection to *file*.


.. class:: LoadFileDialog(master, title=None)
.. class:: LoadFileDialog(master, title=None, *, use_ttk=True)

A subclass of FileDialog that creates a dialog window for selecting an
existing file.

.. versionchanged:: next
Added the *use_ttk* parameter.

.. method:: ok_command()

Test that a file is provided and that the selection indicates an
already existing file.

.. class:: SaveFileDialog(master, title=None)
.. class:: SaveFileDialog(master, title=None, *, use_ttk=True)

A subclass of FileDialog that creates a dialog window for selecting a
destination file.

.. versionchanged:: next
Added the *use_ttk* parameter.

.. method:: ok_command()

Test whether or not the selection points to a valid file that is not a
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,14 @@ tkinter
ttk version, and accepts mappings of button options as *buttons* entries.
(Contributed by Serhiy Storchaka in :gh:`59396`.)

* The :class:`!tkinter.filedialog.FileDialog` dialog and its
:class:`!tkinter.filedialog.LoadFileDialog` and
:class:`!tkinter.filedialog.SaveFileDialog` subclasses are now built from the
themed :mod:`tkinter.ttk` widgets by default instead of the classic
:mod:`tkinter` widgets, and gained a *use_ttk* parameter to select between
them.
(Contributed by Serhiy Storchaka in :gh:`59396`.)

xml
---

Expand Down
10 changes: 8 additions & 2 deletions Lib/idlelib/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@
import tkinter # Use tcl and, if startup fails, messagebox.
if not hasattr(sys.modules['idlelib.run'], 'firstrun'):
# Undo modifications of tkinter by idlelib imports; see bpo-25507.
# Which of these submodules got imported (and thus added as a tkinter
# attribute) depends on what idlelib pulled in, so tolerate missing
# ones rather than assuming a fixed set; see gh-59396.
for mod in ('simpledialog', 'messagebox', 'font',
'dialog', 'filedialog', 'commondialog',
'ttk'):
delattr(tkinter, mod)
del sys.modules['tkinter.' + mod]
try:
delattr(tkinter, mod)
del sys.modules['tkinter.' + mod]
except (AttributeError, KeyError):
pass
# Avoid AttributeError if run again; see bpo-37038.
sys.modules['idlelib.run'].firstrun = False

Expand Down
100 changes: 100 additions & 0 deletions Lib/test/test_tkinter/test_filedialog.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
import unittest
import tkinter
from tkinter import filedialog
from tkinter import ttk
from tkinter.commondialog import Dialog
from test.support import requires, swap_attr
from test.test_tkinter.support import setUpModule # noqa: F401
Expand Down Expand Up @@ -68,6 +70,104 @@ def test_subclasses(self):
self.assertIsInstance(d, filedialog.FileDialog)
self.assertEqual(d.top.title(), cls.title)

# --- Themed widgets and keyboard (modernization) ---

def open(self, **kw):
d = filedialog.FileDialog(self.root, **kw)
self.addCleanup(lambda: d.top.winfo_exists() and d.top.destroy())
d.top.deiconify() # __init__ leaves the dialog withdrawn until go()
d.top.update()
return d

def test_use_ttk(self):
# The dialog uses the themed (ttk) widgets by default.
d = self.open()
self.assertEqual(d.ok_button.winfo_class(), 'TButton')
self.assertEqual(d.selection.winfo_class(), 'TEntry')

def test_use_classic(self):
# use_ttk=False uses the classic Tk widgets.
d = self.open(use_ttk=False)
self.assertEqual(d.ok_button.winfo_class(), 'Button')
self.assertEqual(d.selection.winfo_class(), 'Entry')
if d.top._windowingsystem == 'x11':
self.assertEqual(str(d.botframe.cget('relief')), 'raised')

def test_background(self):
# The ttk dialog adopts the ttk background, even a customized one, while
# the classic dialog keeps the default Toplevel background.
style = ttk.Style(self.root)
old = style.lookup('.', 'background')
style.configure('.', background='#123456')
self.addCleanup(style.configure, '.', background=old)
d = self.open()
self.assertEqual(str(d.top.cget('background')), '#123456')
d = self.open(use_ttk=False)
ref = tkinter.Toplevel(self.root)
self.addCleanup(ref.destroy)
self.assertEqual(str(d.top.cget('background')),
str(ref.cget('background')))

def test_button_accelerator(self):
# The buttons' "&" accelerators are parsed.
d = self.open()
self.assertEqual(str(d.ok_button.cget('text')), 'OK')
self.assertEqual(int(d.ok_button.cget('underline')), 0)

def test_default_ring(self):
# The default ring follows the keyboard focus among the buttons.
d = self.open()
self.assertEqual(str(d.cancel_button.cget('default')), 'normal')
d.cancel_button.focus_force()
d.top.update()
self.assertEqual(str(d.cancel_button.cget('default')), 'active')
d.ok_button.focus_force()
d.top.update()
self.assertEqual(str(d.cancel_button.cget('default')), 'normal')

def test_alt_key(self):
# Alt + the underlined letter invokes the matching button.
d = self.open()
invoked = []
d.cancel_button.configure(command=lambda: invoked.append(True))
d.top.focus_force()
d.top.update()
d.top.event_generate('<Alt-c>') # "&Cancel"
d.top.update()
self.assertTrue(invoked)

def test_escape_cancels(self):
# The Escape key cancels the dialog.
d = self.open()
d.how = 'spam'
d.top.focus_force()
d.top.update()
d.top.event_generate('<Escape>')
d.top.update()
self.assertIsNone(d.how)

def test_horizontal_scrollbars(self):
# Each list has a horizontal scrollbar besides the vertical one.
d = self.open()
self.assertEqual(str(d.dirshbar.cget('orient')), 'horizontal')
self.assertEqual(str(d.fileshbar.cget('orient')), 'horizontal')
self.assertTrue(d.dirs.cget('xscrollcommand'))
self.assertTrue(d.files.cget('xscrollcommand'))

def test_type_ahead(self):
# Typing characters over a list jumps to a matching entry.
d = self.open()
d.directory = os.getcwd() # browsing the match fills the selection entry
d.files.delete(0, 'end')
for name in ('alpha', 'bravo', 'charlie'):
d.files.insert('end', name)
d.files.focus_force()
d.top.update()
d.files.event_generate('<Key>', keysym='c')
d.top.update()
sel = d.files.curselection()
self.assertEqual([d.files.get(i) for i in sel], ['charlie'])


if __name__ == "__main__":
unittest.main()
Loading
Loading