Issue #27380: IDLE: add base Query dialog, with ttk widgets and subclass
SectionName. These split class GetCfgSectionNameDialog from configSectionNameDialog.py, temporarily renamed config_sec.py in 3.7.9a2. More Query subclasses are planned.
This commit is contained in:
parent
754a5c1a1d
commit
68a53c5d39
|
@ -1,98 +0,0 @@
|
|||
"""
|
||||
Dialog that allows user to specify a new config file section name.
|
||||
Used to get new highlight theme and keybinding set names.
|
||||
The 'return value' for the dialog, used two placed in configdialog.py,
|
||||
is the .result attribute set in the Ok and Cancel methods.
|
||||
"""
|
||||
from tkinter import *
|
||||
import tkinter.messagebox as tkMessageBox
|
||||
|
||||
class GetCfgSectionNameDialog(Toplevel):
|
||||
def __init__(self, parent, title, message, used_names, _htest=False):
|
||||
"""
|
||||
message - string, informational message to display
|
||||
used_names - string collection, names already in use for validity check
|
||||
_htest - bool, change box location when running htest
|
||||
"""
|
||||
Toplevel.__init__(self, parent)
|
||||
self.configure(borderwidth=5)
|
||||
self.resizable(height=FALSE, width=FALSE)
|
||||
self.title(title)
|
||||
self.transient(parent)
|
||||
self.grab_set()
|
||||
self.protocol("WM_DELETE_WINDOW", self.Cancel)
|
||||
self.parent = parent
|
||||
self.message = message
|
||||
self.used_names = used_names
|
||||
self.create_widgets()
|
||||
self.withdraw() #hide while setting geometry
|
||||
self.update_idletasks()
|
||||
#needs to be done here so that the winfo_reqwidth is valid
|
||||
self.messageInfo.config(width=self.frameMain.winfo_reqwidth())
|
||||
self.geometry(
|
||||
"+%d+%d" % (
|
||||
parent.winfo_rootx() +
|
||||
(parent.winfo_width()/2 - self.winfo_reqwidth()/2),
|
||||
parent.winfo_rooty() +
|
||||
((parent.winfo_height()/2 - self.winfo_reqheight()/2)
|
||||
if not _htest else 100)
|
||||
) ) #centre dialog over parent (or below htest box)
|
||||
self.deiconify() #geometry set, unhide
|
||||
self.wait_window()
|
||||
|
||||
def create_widgets(self):
|
||||
self.name = StringVar(self.parent)
|
||||
self.fontSize = StringVar(self.parent)
|
||||
self.frameMain = Frame(self, borderwidth=2, relief=SUNKEN)
|
||||
self.frameMain.pack(side=TOP, expand=TRUE, fill=BOTH)
|
||||
self.messageInfo = Message(self.frameMain, anchor=W, justify=LEFT,
|
||||
padx=5, pady=5, text=self.message) #,aspect=200)
|
||||
entryName = Entry(self.frameMain, textvariable=self.name, width=30)
|
||||
entryName.focus_set()
|
||||
self.messageInfo.pack(padx=5, pady=5) #, expand=TRUE, fill=BOTH)
|
||||
entryName.pack(padx=5, pady=5)
|
||||
|
||||
frameButtons = Frame(self, pady=2)
|
||||
frameButtons.pack(side=BOTTOM)
|
||||
self.buttonOk = Button(frameButtons, text='Ok',
|
||||
width=8, command=self.Ok)
|
||||
self.buttonOk.pack(side=LEFT, padx=5)
|
||||
self.buttonCancel = Button(frameButtons, text='Cancel',
|
||||
width=8, command=self.Cancel)
|
||||
self.buttonCancel.pack(side=RIGHT, padx=5)
|
||||
|
||||
def name_ok(self):
|
||||
''' After stripping entered name, check that it is a sensible
|
||||
ConfigParser file section name. Return it if it is, '' if not.
|
||||
'''
|
||||
name = self.name.get().strip()
|
||||
if not name: #no name specified
|
||||
tkMessageBox.showerror(title='Name Error',
|
||||
message='No name specified.', parent=self)
|
||||
elif len(name)>30: #name too long
|
||||
tkMessageBox.showerror(title='Name Error',
|
||||
message='Name too long. It should be no more than '+
|
||||
'30 characters.', parent=self)
|
||||
name = ''
|
||||
elif name in self.used_names:
|
||||
tkMessageBox.showerror(title='Name Error',
|
||||
message='This name is already in use.', parent=self)
|
||||
name = ''
|
||||
return name
|
||||
|
||||
def Ok(self, event=None):
|
||||
name = self.name_ok()
|
||||
if name:
|
||||
self.result = name
|
||||
self.destroy()
|
||||
|
||||
def Cancel(self, event=None):
|
||||
self.result = ''
|
||||
self.destroy()
|
||||
|
||||
if __name__ == '__main__':
|
||||
import unittest
|
||||
unittest.main('idlelib.idle_test.test_config_name', verbosity=2, exit=False)
|
||||
|
||||
from idlelib.idle_test.htest import run
|
||||
run(GetCfgSectionNameDialog)
|
|
@ -18,7 +18,7 @@ import tkinter.font as tkFont
|
|||
from idlelib.config import idleConf
|
||||
from idlelib.dynoption import DynOptionMenu
|
||||
from idlelib.config_key import GetKeysDialog
|
||||
from idlelib.config_sec import GetCfgSectionNameDialog
|
||||
from idlelib.query import SectionName
|
||||
from idlelib.config_help import GetHelpSourceDialog
|
||||
from idlelib.tabbedpages import TabbedPageSet
|
||||
from idlelib.textview import view_text
|
||||
|
@ -684,7 +684,7 @@ class ConfigDialog(Toplevel):
|
|||
def GetNewKeysName(self, message):
|
||||
usedNames = (idleConf.GetSectionList('user', 'keys') +
|
||||
idleConf.GetSectionList('default', 'keys'))
|
||||
newKeySet = GetCfgSectionNameDialog(
|
||||
newKeySet = SectionName(
|
||||
self, 'New Custom Key Set', message, usedNames).result
|
||||
return newKeySet
|
||||
|
||||
|
@ -837,7 +837,7 @@ class ConfigDialog(Toplevel):
|
|||
def GetNewThemeName(self, message):
|
||||
usedNames = (idleConf.GetSectionList('user', 'highlight') +
|
||||
idleConf.GetSectionList('default', 'highlight'))
|
||||
newTheme = GetCfgSectionNameDialog(
|
||||
newTheme = SectionName(
|
||||
self, 'New Custom Theme', message, usedNames).result
|
||||
return newTheme
|
||||
|
||||
|
|
|
@ -137,18 +137,6 @@ _editor_window_spec = {
|
|||
"Best to close editor first."
|
||||
}
|
||||
|
||||
GetCfgSectionNameDialog_spec = {
|
||||
'file': 'config_sec',
|
||||
'kwds': {'title':'Get Name',
|
||||
'message':'Enter something',
|
||||
'used_names': {'abc'},
|
||||
'_htest': True},
|
||||
'msg': "After the text entered with [Ok] is stripped, <nothing>, "
|
||||
"'abc', or more that 30 chars are errors.\n"
|
||||
"Close 'Get Name' with a valid entry (printed to Shell), "
|
||||
"[Cancel], or [X]",
|
||||
}
|
||||
|
||||
GetHelpSourceDialog_spec = {
|
||||
'file': 'config_help',
|
||||
'kwds': {'title': 'Get helpsource',
|
||||
|
@ -245,6 +233,17 @@ _percolator_spec = {
|
|||
"Test for actions like text entry, and removal."
|
||||
}
|
||||
|
||||
Query_spec = {
|
||||
'file': 'query',
|
||||
'kwds': {'title':'Query',
|
||||
'message':'Enter something',
|
||||
'_htest': True},
|
||||
'msg': "Enter with <Return> or [Ok]. Print valid entry to Shell\n"
|
||||
"Blank line, after stripping, is ignored\n"
|
||||
"Close dialog with valid entry, [Cancel] or [X]",
|
||||
}
|
||||
|
||||
|
||||
_replace_dialog_spec = {
|
||||
'file': 'replace',
|
||||
'kwds': {},
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
"""Unit tests for idlelib.config_sec"""
|
||||
import unittest
|
||||
from idlelib.idle_test.mock_tk import Var, Mbox
|
||||
from idlelib import config_sec as name_dialog_module
|
||||
|
||||
name_dialog = name_dialog_module.GetCfgSectionNameDialog
|
||||
|
||||
class Dummy_name_dialog:
|
||||
# Mock for testing the following methods of name_dialog
|
||||
name_ok = name_dialog.name_ok
|
||||
Ok = name_dialog.Ok
|
||||
Cancel = name_dialog.Cancel
|
||||
# Attributes, constant or variable, needed for tests
|
||||
used_names = ['used']
|
||||
name = Var()
|
||||
result = None
|
||||
destroyed = False
|
||||
def destroy(self):
|
||||
self.destroyed = True
|
||||
|
||||
# name_ok calls Mbox.showerror if name is not ok
|
||||
orig_mbox = name_dialog_module.tkMessageBox
|
||||
showerror = Mbox.showerror
|
||||
|
||||
class ConfigNameTest(unittest.TestCase):
|
||||
dialog = Dummy_name_dialog()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
name_dialog_module.tkMessageBox = Mbox
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
name_dialog_module.tkMessageBox = orig_mbox
|
||||
|
||||
def test_blank_name(self):
|
||||
self.dialog.name.set(' ')
|
||||
self.assertEqual(self.dialog.name_ok(), '')
|
||||
self.assertEqual(showerror.title, 'Name Error')
|
||||
self.assertIn('No', showerror.message)
|
||||
|
||||
def test_used_name(self):
|
||||
self.dialog.name.set('used')
|
||||
self.assertEqual(self.dialog.name_ok(), '')
|
||||
self.assertEqual(showerror.title, 'Name Error')
|
||||
self.assertIn('use', showerror.message)
|
||||
|
||||
def test_long_name(self):
|
||||
self.dialog.name.set('good'*8)
|
||||
self.assertEqual(self.dialog.name_ok(), '')
|
||||
self.assertEqual(showerror.title, 'Name Error')
|
||||
self.assertIn('too long', showerror.message)
|
||||
|
||||
def test_good_name(self):
|
||||
self.dialog.name.set(' good ')
|
||||
showerror.title = 'No Error' # should not be called
|
||||
self.assertEqual(self.dialog.name_ok(), 'good')
|
||||
self.assertEqual(showerror.title, 'No Error')
|
||||
|
||||
def test_ok(self):
|
||||
self.dialog.destroyed = False
|
||||
self.dialog.name.set('good')
|
||||
self.dialog.Ok()
|
||||
self.assertEqual(self.dialog.result, 'good')
|
||||
self.assertTrue(self.dialog.destroyed)
|
||||
|
||||
def test_cancel(self):
|
||||
self.dialog.destroyed = False
|
||||
self.dialog.Cancel()
|
||||
self.assertEqual(self.dialog.result, '')
|
||||
self.assertTrue(self.dialog.destroyed)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2, exit=False)
|
|
@ -0,0 +1,164 @@
|
|||
"""Test idlelib.query.
|
||||
|
||||
Coverage: 100%.
|
||||
"""
|
||||
from test.support import requires
|
||||
from tkinter import Tk
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from idlelib.idle_test.mock_tk import Var, Mbox_func
|
||||
from idlelib import query
|
||||
Query, SectionName = query.Query, query.SectionName
|
||||
|
||||
class Dummy_Query:
|
||||
# Mock for testing the following methods Query
|
||||
entry_ok = Query.entry_ok
|
||||
ok = Query.ok
|
||||
cancel = Query.cancel
|
||||
# Attributes, constant or variable, needed for tests
|
||||
entry = Var()
|
||||
result = None
|
||||
destroyed = False
|
||||
def destroy(self):
|
||||
self.destroyed = True
|
||||
|
||||
# entry_ok calls modal messagebox.showerror if entry is not ok.
|
||||
# Mock showerrer returns, so don't need to click to continue.
|
||||
orig_showerror = query.showerror
|
||||
showerror = Mbox_func() # Instance has __call__ method.
|
||||
|
||||
def setUpModule():
|
||||
query.showerror = showerror
|
||||
|
||||
def tearDownModule():
|
||||
query.showerror = orig_showerror
|
||||
|
||||
|
||||
class QueryTest(unittest.TestCase):
|
||||
dialog = Dummy_Query()
|
||||
|
||||
def setUp(self):
|
||||
showerror.title = None
|
||||
self.dialog.result = None
|
||||
self.dialog.destroyed = False
|
||||
|
||||
def test_blank_entry(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
dialog.entry.set(' ')
|
||||
Equal(dialog.entry_ok(), '')
|
||||
Equal((dialog.result, dialog.destroyed), (None, False))
|
||||
Equal(showerror.title, 'Entry Error')
|
||||
self.assertIn('Blank', showerror.message)
|
||||
|
||||
def test_good_entry(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
dialog.entry.set(' good ')
|
||||
Equal(dialog.entry_ok(), 'good')
|
||||
Equal((dialog.result, dialog.destroyed), (None, False))
|
||||
Equal(showerror.title, None)
|
||||
|
||||
def test_ok(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
dialog.entry.set('good')
|
||||
Equal(dialog.ok(), None)
|
||||
Equal((dialog.result, dialog.destroyed), ('good', True))
|
||||
|
||||
def test_cancel(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
Equal(self.dialog.cancel(), None)
|
||||
Equal((dialog.result, dialog.destroyed), (None, True))
|
||||
|
||||
|
||||
class Dummy_SectionName:
|
||||
# Mock for testing the following method of Section_Name
|
||||
entry_ok = SectionName.entry_ok
|
||||
# Attributes, constant or variable, needed for tests
|
||||
used_names = ['used']
|
||||
entry = Var()
|
||||
|
||||
class SectionNameTest(unittest.TestCase):
|
||||
dialog = Dummy_SectionName()
|
||||
|
||||
|
||||
def setUp(self):
|
||||
showerror.title = None
|
||||
|
||||
def test_blank_name(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
dialog.entry.set(' ')
|
||||
Equal(dialog.entry_ok(), '')
|
||||
Equal(showerror.title, 'Name Error')
|
||||
self.assertIn('No', showerror.message)
|
||||
|
||||
def test_used_name(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
dialog.entry.set('used')
|
||||
Equal(self.dialog.entry_ok(), '')
|
||||
Equal(showerror.title, 'Name Error')
|
||||
self.assertIn('use', showerror.message)
|
||||
|
||||
def test_long_name(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
dialog.entry.set('good'*8)
|
||||
Equal(self.dialog.entry_ok(), '')
|
||||
Equal(showerror.title, 'Name Error')
|
||||
self.assertIn('too long', showerror.message)
|
||||
|
||||
def test_good_entry(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
dialog.entry.set(' good ')
|
||||
Equal(dialog.entry_ok(), 'good')
|
||||
Equal(showerror.title, None)
|
||||
|
||||
|
||||
class QueryGuiTest(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
requires('gui')
|
||||
cls.root = Tk()
|
||||
cls.dialog = Query(cls.root, 'TEST', 'test', _utest=True)
|
||||
cls.dialog.destroy = mock.Mock()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
del cls.dialog
|
||||
cls.root.destroy()
|
||||
del cls.root
|
||||
|
||||
def setUp(self):
|
||||
self.dialog.entry.delete(0, 'end')
|
||||
self.dialog.result = None
|
||||
self.dialog.destroy.reset_mock()
|
||||
|
||||
def test_click_ok(self):
|
||||
dialog = self.dialog
|
||||
dialog.entry.insert(0, 'abc')
|
||||
dialog.button_ok.invoke()
|
||||
self.assertEqual(dialog.result, 'abc')
|
||||
self.assertTrue(dialog.destroy.called)
|
||||
|
||||
def test_click_blank(self):
|
||||
dialog = self.dialog
|
||||
dialog.button_ok.invoke()
|
||||
self.assertEqual(dialog.result, None)
|
||||
self.assertFalse(dialog.destroy.called)
|
||||
|
||||
def test_click_cancel(self):
|
||||
dialog = self.dialog
|
||||
dialog.entry.insert(0, 'abc')
|
||||
dialog.button_cancel.invoke()
|
||||
self.assertEqual(dialog.result, None)
|
||||
self.assertTrue(dialog.destroy.called)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2, exit=False)
|
|
@ -0,0 +1,148 @@
|
|||
"""
|
||||
Dialogs that query users and verify the answer before accepting.
|
||||
Use ttk widgets, limiting use to tcl/tk 8.5+, as in IDLE 3.6+.
|
||||
|
||||
Query is the generic base class for a popup dialog.
|
||||
The user must either enter a valid answer or close the dialog.
|
||||
Entries are validated when <Return> is entered or [Ok] is clicked.
|
||||
Entries are ignored when [Cancel] or [X] are clicked.
|
||||
The 'return value' is .result set to either a valid answer or None.
|
||||
|
||||
Subclass SectionName gets a name for a new config file section.
|
||||
Configdialog uses it for new highlight theme and keybinding set names.
|
||||
"""
|
||||
# Query and Section name result from splitting GetCfgSectionNameDialog
|
||||
# of configSectionNameDialog.py (temporarily config_sec.py) into
|
||||
# generic and specific parts.
|
||||
|
||||
from tkinter import FALSE, TRUE, Toplevel
|
||||
from tkinter.messagebox import showerror
|
||||
from tkinter.ttk import Frame, Button, Entry, Label
|
||||
|
||||
class Query(Toplevel):
|
||||
"""Base class for getting verified answer from a user.
|
||||
|
||||
For this base class, accept any non-blank string.
|
||||
"""
|
||||
def __init__(self, parent, title, message,
|
||||
*, _htest=False, _utest=False): # Call from override.
|
||||
"""Create popup, do not return until tk widget destroyed.
|
||||
|
||||
Additional subclass init must be done before calling this.
|
||||
|
||||
title - string, title of popup dialog
|
||||
message - string, informational message to display
|
||||
_htest - bool, change box location when running htest
|
||||
_utest - bool, leave window hidden and not modal
|
||||
"""
|
||||
Toplevel.__init__(self, parent)
|
||||
self.configure(borderwidth=5)
|
||||
self.resizable(height=FALSE, width=FALSE)
|
||||
self.title(title)
|
||||
self.transient(parent)
|
||||
self.grab_set()
|
||||
self.bind('<Key-Return>', self.ok)
|
||||
self.protocol("WM_DELETE_WINDOW", self.cancel)
|
||||
self.parent = parent
|
||||
self.message = message
|
||||
self.create_widgets()
|
||||
self.update_idletasks()
|
||||
#needs to be done here so that the winfo_reqwidth is valid
|
||||
self.withdraw() # Hide while configuring, especially geometry.
|
||||
self.geometry(
|
||||
"+%d+%d" % (
|
||||
parent.winfo_rootx() +
|
||||
(parent.winfo_width()/2 - self.winfo_reqwidth()/2),
|
||||
parent.winfo_rooty() +
|
||||
((parent.winfo_height()/2 - self.winfo_reqheight()/2)
|
||||
if not _htest else 150)
|
||||
) ) #centre dialog over parent (or below htest box)
|
||||
if not _utest:
|
||||
self.deiconify() #geometry set, unhide
|
||||
self.wait_window()
|
||||
|
||||
def create_widgets(self): # Call from override, if any.
|
||||
frame = Frame(self, borderwidth=2, relief='sunken', )
|
||||
label = Label(frame, anchor='w', justify='left',
|
||||
text=self.message)
|
||||
self.entry = Entry(frame, width=30) # Bind name for entry_ok.
|
||||
self.entry.focus_set()
|
||||
|
||||
buttons = Frame(self) # Bind buttons for invoke in unittest.
|
||||
self.button_ok = Button(buttons, text='Ok',
|
||||
width=8, command=self.ok)
|
||||
self.button_cancel = Button(buttons, text='Cancel',
|
||||
width=8, command=self.cancel)
|
||||
|
||||
frame.pack(side='top', expand=TRUE, fill='both')
|
||||
label.pack(padx=5, pady=5)
|
||||
self.entry.pack(padx=5, pady=5)
|
||||
buttons.pack(side='bottom')
|
||||
self.button_ok.pack(side='left', padx=5)
|
||||
self.button_cancel.pack(side='right', padx=5)
|
||||
|
||||
def entry_ok(self): # Usually replace.
|
||||
"Check that entry not blank."
|
||||
entry = self.entry.get().strip()
|
||||
if not entry:
|
||||
showerror(title='Entry Error',
|
||||
message='Blank line.', parent=self)
|
||||
return entry
|
||||
|
||||
def ok(self, event=None): # Do not replace.
|
||||
'''If entry is valid, bind it to 'result' and destroy tk widget.
|
||||
|
||||
Otherwise leave dialog open for user to correct entry or cancel.
|
||||
'''
|
||||
entry = self.entry_ok()
|
||||
if entry:
|
||||
self.result = entry
|
||||
self.destroy()
|
||||
else:
|
||||
# [Ok] (but not <Return>) moves focus. Move it back.
|
||||
self.entry.focus_set()
|
||||
|
||||
def cancel(self, event=None): # Do not replace.
|
||||
"Set dialog result to None and destroy tk widget."
|
||||
self.result = None
|
||||
self.destroy()
|
||||
|
||||
|
||||
class SectionName(Query):
|
||||
"Get a name for a config file section name."
|
||||
|
||||
def __init__(self, parent, title, message, used_names,
|
||||
*, _htest=False, _utest=False):
|
||||
"used_names - collection of strings already in use"
|
||||
|
||||
self.used_names = used_names
|
||||
Query.__init__(self, parent, title, message,
|
||||
_htest=_htest, _utest=_utest)
|
||||
# This call does ot return until tk widget is destroyed.
|
||||
|
||||
def entry_ok(self):
|
||||
'''Stripping entered name, check that it is a sensible
|
||||
ConfigParser file section name. Return it if it is, '' if not.
|
||||
'''
|
||||
name = self.entry.get().strip()
|
||||
if not name:
|
||||
showerror(title='Name Error',
|
||||
message='No name specified.', parent=self)
|
||||
elif len(name)>30:
|
||||
showerror(title='Name Error',
|
||||
message='Name too long. It should be no more than '+
|
||||
'30 characters.', parent=self)
|
||||
name = ''
|
||||
elif name in self.used_names:
|
||||
showerror(title='Name Error',
|
||||
message='This name is already in use.', parent=self)
|
||||
name = ''
|
||||
return name
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import unittest
|
||||
unittest.main('idlelib.idle_test.test_query', verbosity=2, exit=False)
|
||||
|
||||
from idlelib.idle_test.htest import run
|
||||
run(Query)
|
Loading…
Reference in New Issue