Skip to content
Merged
13 changes: 8 additions & 5 deletions Lib/idlelib/configdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,17 +149,19 @@ def create_action_buttons(self):
else:
padding_args = {'padding': (6, 3)}
outer = Frame(self, padding=2)
buttons = Frame(outer, padding=2)
buttons_frame = Frame(outer, padding=2)
self.buttons = {}
for txt, cmd in (
('Ok', self.ok),
('Apply', self.apply),
('Cancel', self.cancel),
('Help', self.help)):
Button(buttons, text=txt, command=cmd, takefocus=FALSE,
**padding_args).pack(side=LEFT, padx=5)
self.buttons[txt] = Button(buttons_frame, text=txt, command=cmd,
takefocus=FALSE, **padding_args)
self.buttons[txt].pack(side=LEFT, padx=5)
# Add space above buttons.
Frame(outer, height=2, borderwidth=0).pack(side=TOP)
buttons.pack(side=BOTTOM)
buttons_frame.pack(side=BOTTOM)
return outer

def ok(self):
Expand Down Expand Up @@ -205,7 +207,6 @@ def help(self):

Attributes accessed:
note

Methods:
view_text: Method from textview module.
"""
Expand Down Expand Up @@ -852,6 +853,8 @@ def create_page_highlight(self):
text.configure(
font=('courier', 12, ''), cursor='hand2', width=1, height=1,
takefocus=FALSE, highlightthickness=0, wrap=NONE)
# Override to prevent double click and motion from propagating to
Comment thread
terryjreedy marked this conversation as resolved.
Resolved
# other handlers.
text.bind('<Double-Button-1>', lambda e: 'break')
text.bind('<B1-Motion>', lambda e: 'break')
string_tags=(
Expand Down
223 changes: 201 additions & 22 deletions Lib/idlelib/idle_test/test_configdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import unittest
from unittest import mock
from idlelib.idle_test.mock_idle import Func
from tkinter import Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL
from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED,
NORMAL, TclError)
from idlelib import config
from idlelib.configdialog import idleConf, changes, tracers

Expand All @@ -30,13 +31,15 @@
keyspage = changes['keys']
extpage = changes['extensions']


def setUpModule():
global root, dialog
idleConf.userCfg = testcfg
root = Tk()
# root.withdraw() # Comment out, see issue 30870
dialog = configdialog.ConfigDialog(root, 'Test', _utest=True)


def tearDownModule():
global root, dialog
idleConf.userCfg = usercfg
Expand All @@ -48,22 +51,56 @@ def tearDownModule():
root = dialog = None


class DialogTest(unittest.TestCase):
class ConfigDialogTest(unittest.TestCase):

def test_deactivate_current_config(self):
pass

def activate_config_changes(self):
pass


@mock.patch(__name__+'.dialog.destroy', new_callable=Func)
def test_cancel(self, destroy):
class ButtonTest(unittest.TestCase):

def test_click_ok(self):
d = dialog
apply = d.apply = mock.Mock()
destroy = d.destroy = mock.Mock()
d.buttons['Ok'].invoke()
apply.assert_called_once()
destroy.assert_called_once()
del d.destroy, d.apply

def test_click_apply(self):
d = dialog
deactivate = d.deactivate_current_config = mock.Mock()
save_ext = d.save_all_changed_extensions = mock.Mock()
activate = d.activate_config_changes = mock.Mock()
d.buttons['Apply'].invoke()
deactivate.assert_called_once()
save_ext.assert_called_once()
activate.assert_called_once()
del d.save_all_changed_extensions
del d.activate_config_changes, d.deactivate_current_config

def test_click_cancel(self):
d = dialog
d.destroy = Func()
changes['main']['something'] = 1
dialog.cancel()
d.buttons['Cancel'].invoke()
self.assertEqual(changes['main'], {})
self.assertEqual(destroy.called, 1)
self.assertEqual(d.destroy.called, 1)
del d.destroy

@mock.patch('idlelib.configdialog.view_text', new_callable=Func)
def test_help(self, view):
def test_click_help(self):
dialog.note.select(dialog.keyspage)
dialog.help()
s = view.kwds['contents']
self.assertTrue(s.startswith('When you click') and
s.endswith('a different name.\n'))
with mock.patch.object(configdialog, 'view_text',
new_callable=Func) as view:
dialog.buttons['Help'].invoke()
title, contents = view.kwds['title'], view.kwds['contents']
self.assertEqual(title, 'Help for IDLE preferences')
self.assertTrue(contents.startswith('When you click') and
contents.endswith('a different name.\n'))


class FontPageTest(unittest.TestCase):
Expand Down Expand Up @@ -438,6 +475,90 @@ def click_it(start):
eq(d.highlight_target.get(), elem[tag])
eq(d.set_highlight_target.called, count)

def test_highlight_sample_double_click(self):
# Test double click on highlight_sample.
eq = self.assertEqual
d = self.page

hs = d.highlight_sample
hs.focus_force()
hs.see(1.0)
hs.update_idletasks()

# Test binding from configdialog.
hs.event_generate('<Enter>', x=0, y=0)
Comment thread
terryjreedy marked this conversation as resolved.
Resolved
hs.event_generate('<Motion>', x=0, y=0)
# Double click is a sequence of two clicks in a row.
for _ in range(2):
hs.event_generate('<ButtonPress-1>', x=0, y=0)
hs.event_generate('<ButtonRelease-1>', x=0, y=0)

self.assertNotEqual(d.highlight_target.get(), 'text double')
Comment thread
terryjreedy marked this conversation as resolved.
Resolved
with self.assertRaises(TclError, msg='text doesn\'t contain any characters tagged with "sel"'):
hs.get('sel.first', 'sel.last')

# Change binding on double click. This will allow the event to
Comment thread
terryjreedy marked this conversation as resolved.
Resolved
# propagate to the other levels. The default binding for double
# click is to select the word that was double clicked.
hs.bind('<Double-Button-1>', lambda e: d.highlight_target.set('test double'))

hs.event_generate('<Enter>', x=0, y=0)
hs.event_generate('<Motion>', x=0, y=0)
# Double click is a sequence of two clicks in a row.
for _ in range(2):
hs.event_generate('<ButtonPress-1>', x=0, y=0)
hs.event_generate('<ButtonRelease-1>', x=0, y=0)

eq(d.highlight_target.get(), 'test double')
eq(hs.get('sel.first', 'sel.last'), ' 1 # Click selects item.\n')

# Remove selection and rebind.
hs.tag_remove('sel', '0.0', 'end')
hs.bind('<Double-Button-1>', lambda e: 'break')

def test_highlight_sample_b1_motion(self):
# Test button motion on highlight_sample.
eq = self.assertEqual
d = self.page

hs = d.highlight_sample
hs.focus_force()
hs.see(1.0)
hs.update_idletasks()

x, y, dx, dy, offset = hs.dlineinfo('1.0')

# Test binding from configdialog.
hs.event_generate('<Leave>')
hs.event_generate('<Enter>')
hs.event_generate('<Motion>', x=x, y=y)
hs.event_generate('<ButtonPress-1>', x=x, y=y)
hs.event_generate('<B1-Motion>', x=dx, y=dy)
hs.event_generate('<ButtonRelease-1>', x=dx, y=dy)

eq(hs.tag_ranges('sel'), ())

# Remove binding on button motion. This will allow the event to
# propagate to the other levels. The default binding for button
# motion is to select the text that was moused over while the button
# is held.
hs.unbind('<B1-Motion>')
x, y, dx, dy, offset = hs.dlineinfo('4.0')

hs.event_generate('<Leave>')
hs.event_generate('<Enter>')
hs.event_generate('<Motion>', x=x, y=y)
hs.event_generate('<ButtonPress-1>', x=x, y=y)
hs.event_generate('<B1-Motion>', x=x+dx, y=y+dy)
hs.event_generate('<ButtonRelease-1>', x=x+dx, y=y+dy)

self.assertNotEqual(hs.tag_ranges('sel'), ())
self.assertIn('def', hs.get('sel.first', 'sel.last'))

# Remove selection and rebind.
hs.tag_remove('sel', '0.0', 'end')
hs.bind('<B1-Motion>', lambda e: 'break')

def test_set_theme_type(self):
eq = self.assertEqual
d = self.page
Expand Down Expand Up @@ -568,6 +689,17 @@ def test_create_new_and_save_new(self):
self.assertFalse(d.theme_source.get()) # Use custom set.
eq(d.set_theme_type.called, 1)

# Call with same name as first time to make sure it adds it only once.
Comment thread
terryjreedy marked this conversation as resolved.
Resolved
d.theme_source.set(True)
d.builtin_name.set('IDLE Classic')
d.create_new(first_new)
eq(idleConf.GetSectionList('user', 'highlight'), [first_new])
eq(idleConf.GetThemeDict('default', 'IDLE Classic'),
idleConf.GetThemeDict('user', first_new))
eq(d.custom_name.get(), first_new)
self.assertFalse(d.theme_source.get()) # Use custom set.
eq(d.set_theme_type.called, 2)

# Test that changed targets are in new theme.
changes.add_option('highlight', first_new, 'hit-background', 'yellow')
self.assertNotIn(second_new, idleConf.userCfg)
Expand Down Expand Up @@ -666,16 +798,21 @@ def test_delete_custom(self):
idleConf.userCfg['highlight'].SetOption(theme_name, 'name', 'value')
highpage[theme_name] = {'option': 'True'}

theme_name2 = 'other theme'
idleConf.userCfg['highlight'].SetOption(theme_name2, 'name', 'value')
highpage[theme_name2] = {'option': 'False'}

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 and the corresponding additions for keys cover the failure of if not item_list and the else clause. I suspect it could be done in fewer lines, but I hope to someday factor out the common code, simpfiy the UI, and redo the tests anyway. So leave the additions alone.

# Force custom theme.
d.theme_source.set(False)
d.custom_theme_on.state(('!disabled',))
d.custom_theme_on.invoke()
d.custom_name.set(theme_name)

# Cancel deletion.
yesno.result = False
d.button_delete_custom.invoke()
eq(yesno.called, 1)
eq(highpage[theme_name], {'option': 'True'})
eq(idleConf.GetSectionList('user', 'highlight'), ['spam theme'])
eq(idleConf.GetSectionList('user', 'highlight'), [theme_name, theme_name2])
eq(dialog.deactivate_current_config.called, 0)
eq(dialog.activate_config_changes.called, 0)
eq(d.set_theme_type.called, 0)
Expand All @@ -685,13 +822,26 @@ def test_delete_custom(self):
d.button_delete_custom.invoke()
eq(yesno.called, 2)
self.assertNotIn(theme_name, highpage)
eq(idleConf.GetSectionList('user', 'highlight'), [])
eq(d.custom_theme_on.state(), ('disabled',))
eq(d.custom_name.get(), '- no custom themes -')
eq(idleConf.GetSectionList('user', 'highlight'), [theme_name2])
eq(d.custom_theme_on.state(), ())
eq(d.custom_name.get(), theme_name2)
eq(dialog.deactivate_current_config.called, 1)
eq(dialog.activate_config_changes.called, 1)
eq(d.set_theme_type.called, 1)

# Confirm deletion of second theme - empties list.
d.custom_name.set(theme_name2)
yesno.result = True
d.button_delete_custom.invoke()
eq(yesno.called, 3)
self.assertNotIn(theme_name, highpage)
eq(idleConf.GetSectionList('user', 'highlight'), [])
eq(d.custom_theme_on.state(), ('disabled',))
eq(d.custom_name.get(), '- no custom themes -')
eq(dialog.deactivate_current_config.called, 2)
eq(dialog.activate_config_changes.called, 2)
eq(d.set_theme_type.called, 2)

del dialog.activate_config_changes, dialog.deactivate_current_config
del d.askyesno

Expand Down Expand Up @@ -993,6 +1143,17 @@ def test_create_new_key_set_and_save_new_key_set(self):
self.assertFalse(d.keyset_source.get()) # Use custom set.
eq(d.set_keys_type.called, 1)

# Call with same name as first time to make sure it adds it only once.
d.keyset_source.set(True)
d.builtin_name.set('IDLE Classic Windows')
d.create_new_key_set(first_new)
eq(idleConf.GetSectionList('user', 'keys'), [first_new])
eq(idleConf.GetKeySet('IDLE Classic Windows'),
idleConf.GetKeySet(first_new))
eq(d.custom_name.get(), first_new)
self.assertFalse(d.keyset_source.get()) # Use custom set.
eq(d.set_keys_type.called, 2)

# Test that changed keybindings are in new keyset.
changes.add_option('keys', first_new, 'copy', '<Key-F11>')
self.assertNotIn(second_new, idleConf.userCfg)
Expand Down Expand Up @@ -1059,16 +1220,21 @@ def test_delete_custom_keys(self):
idleConf.userCfg['keys'].SetOption(keyset_name, 'name', 'value')
keyspage[keyset_name] = {'option': 'True'}

keyset_name2 = 'other key set'
idleConf.userCfg['keys'].SetOption(keyset_name2, 'name', 'value')
keyspage[keyset_name2] = {'option': 'False'}

# Force custom keyset.
d.keyset_source.set(False)
d.custom_keyset_on.state(('!disabled',))
d.custom_keyset_on.invoke()
d.custom_name.set(keyset_name)

# Cancel deletion.
yesno.result = False
d.button_delete_custom_keys.invoke()
eq(yesno.called, 1)
eq(keyspage[keyset_name], {'option': 'True'})
eq(idleConf.GetSectionList('user', 'keys'), ['spam key set'])
eq(idleConf.GetSectionList('user', 'keys'), [keyset_name, keyset_name2])
eq(dialog.deactivate_current_config.called, 0)
eq(dialog.activate_config_changes.called, 0)
eq(d.set_keys_type.called, 0)
Expand All @@ -1078,13 +1244,26 @@ def test_delete_custom_keys(self):
d.button_delete_custom_keys.invoke()
eq(yesno.called, 2)
self.assertNotIn(keyset_name, keyspage)
eq(idleConf.GetSectionList('user', 'keys'), [])
eq(d.custom_keyset_on.state(), ('disabled',))
eq(d.custom_name.get(), '- no custom keys -')
eq(idleConf.GetSectionList('user', 'keys'), [keyset_name2])
eq(d.custom_keyset_on.state(), ())
eq(d.custom_name.get(), keyset_name2)
eq(dialog.deactivate_current_config.called, 1)
eq(dialog.activate_config_changes.called, 1)
eq(d.set_keys_type.called, 1)

# Confirm deletion of second keyset - empties list.
d.custom_name.set(keyset_name2)
yesno.result = True
d.button_delete_custom_keys.invoke()
eq(yesno.called, 3)
self.assertNotIn(keyset_name, keyspage)
eq(idleConf.GetSectionList('user', 'keys'), [])
eq(d.custom_keyset_on.state(), ('disabled',))
eq(d.custom_name.get(), '- no custom keys -')
eq(dialog.deactivate_current_config.called, 2)
eq(dialog.activate_config_changes.called, 2)
eq(d.set_keys_type.called, 2)

del dialog.activate_config_changes, dialog.deactivate_current_config
del d.askyesno

Expand Down