From e36d9f5568093b3885da62a0bf0fdfbe3771672b Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Tue, 15 Aug 2017 18:26:23 -0400 Subject: [PATCH] bpo-31205: IDLE: Factor KeysPage class from ConfigDialog (#3096) The slightly modified tests continue to pass. Patch by Cheryl Sabella. --- Lib/idlelib/configdialog.py | 1544 +++++++++-------- Lib/idlelib/idle_test/test_configdialog.py | 61 +- .../2017-08-15-12-58-23.bpo-31205.iuziZ5.rst | 2 + 3 files changed, 810 insertions(+), 797 deletions(-) create mode 100644 Misc/NEWS.d/next/IDLE/2017-08-15-12-58-23.bpo-31205.iuziZ5.rst diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 66219f1820..e1ac82b7df 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -92,9 +92,9 @@ class ConfigDialog(Toplevel): note: Notebook highpage: self.create_page_highlight fontpage: FontPage - keyspage: self.create_page_keys + keyspage: KeysPage genpage: GenPage - extpageL self.create_page_extensions + extpage: self.create_page_extensions Methods: create_action_buttons @@ -104,7 +104,7 @@ class ConfigDialog(Toplevel): self.note = note = Notebook(self, width=450, height=450) self.highpage = self.create_page_highlight() self.fontpage = FontPage(note, self.highpage) - self.keyspage = self.create_page_keys() + self.keyspage = KeysPage(note) self.genpage = GenPage(note) self.extpage = self.create_page_extensions() note.add(self.fontpage, text='Fonts/Tabs') @@ -132,7 +132,7 @@ class ConfigDialog(Toplevel): #self.load_font_cfg() #self.load_tab_cfg() self.load_theme_cfg() - self.load_key_cfg() + # self.load_key_cfg() # self.load_general_cfg() # note: extension page handled separately @@ -791,863 +791,869 @@ class ConfigDialog(Toplevel): self.activate_config_changes() self.set_theme_type() + def deactivate_current_config(self): + """Remove current key bindings. - def create_page_keys(self): - """Return frame of widgets for Keys tab. + Iterate over window instances defined in parent and remove + the keybindings. + """ + # Before a config is saved, some cleanup of current + # config must be done - remove the previous keybindings. + win_instances = self.parent.instance_dict.keys() + for instance in win_instances: + instance.RemoveKeybindings() - Enable users to provisionally change both individual and sets of - keybindings (shortcut keys). Except for features implemented as - extensions, keybindings are stored in complete sets called - keysets. Built-in keysets in idlelib/config-keys.def are fixed - as far as the dialog is concerned. Any keyset can be used as the - base for a new custom keyset, stored in .idlerc/config-keys.cfg. + def activate_config_changes(self): + """Apply configuration changes to current windows. - Function load_key_cfg() initializes tk variables and keyset - lists and calls load_keys_list for the current keyset. - Radiobuttons builtin_keyset_on and custom_keyset_on toggle var - keyset_source, which controls if the current set of keybindings - are from a builtin or custom keyset. DynOptionMenus builtinlist - and customlist contain lists of the builtin and custom keysets, - respectively, and the current item from each list is stored in - vars builtin_name and custom_name. + Dynamically update the current parent window instances + with some of the configuration changes. + """ + win_instances = self.parent.instance_dict.keys() + for instance in win_instances: + instance.ResetColorizer() + instance.ResetFont() + instance.set_notabs_indentwidth() + instance.ApplyKeybindings() + instance.reset_help_menu_entries() - Button delete_custom_keys invokes delete_custom_keys() to delete - a custom keyset from idleConf.userCfg['keys'] and changes. Button - save_custom_keys invokes save_as_new_key_set() which calls - get_new_keys_name() and create_new_key_set() to save a custom keyset - and its keybindings to idleConf.userCfg['keys']. + def create_page_extensions(self): + """Part of the config dialog used for configuring IDLE extensions. - Listbox bindingslist contains all of the keybindings for the - selected keyset. The keybindings are loaded in load_keys_list() - and are pairs of (event, [keys]) where keys can be a list - of one or more key combinations to bind to the same event. - Mouse button 1 click invokes on_bindingslist_select(), which - allows button_new_keys to be clicked. + This code is generic - it works for any and all IDLE extensions. - So, an item is selected in listbindings, which activates - button_new_keys, and clicking button_new_keys calls function - get_new_keys(). Function get_new_keys() gets the key mappings from the - current keyset for the binding event item that was selected. The - function then displays another dialog, GetKeysDialog, with the - selected binding event and current keys and always new key sequences - to be entered for that binding event. If the keys aren't - changed, nothing happens. If the keys are changed and the keyset - is a builtin, function get_new_keys_name() will be called - for input of a custom keyset name. If no name is given, then the - change to the keybinding will abort and no updates will be made. If - a custom name is entered in the prompt or if the current keyset was - already custom (and thus didn't require a prompt), then - idleConf.userCfg['keys'] is updated in function create_new_key_set() - with the change to the event binding. The item listing in bindingslist - is updated with the new keys. Var keybinding is also set which invokes - the callback function, var_changed_keybinding, to add the change to - the 'keys' or 'extensions' changes tracker based on the binding type. + IDLE extensions save their configuration options using idleConf. + This code reads the current configuration using idleConf, supplies a + GUI interface to change the configuration values, and saves the + changes using idleConf. - Tk Variables: - keybinding: Action/key bindings. + Not all changes take effect immediately - some may require restarting IDLE. + This depends on each extension's implementation. - Methods: - load_keys_list: Reload active set. - create_new_key_set: Combine active keyset and changes. - set_keys_type: Command for keyset_source. - save_new_key_set: Save to idleConf.userCfg['keys'] (is function). - deactivate_current_config: Remove keys bindings in editors. + All values are treated as text, and it is up to the user to supply + reasonable values. The only exception to this are the 'enable*' options, + which are boolean, and can be toggled with a True/False button. - Widgets for keys page frame: (*) widgets bound to self - frame_key_sets: LabelFrame - frames[0]: Frame - (*)builtin_keyset_on: Radiobutton - var keyset_source - (*)custom_keyset_on: Radiobutton - var keyset_source - (*)builtinlist: DynOptionMenu - var builtin_name, - func keybinding_selected - (*)customlist: DynOptionMenu - var custom_name, - func keybinding_selected - (*)keys_message: Label - frames[1]: Frame - (*)button_delete_custom_keys: Button - delete_custom_keys - (*)button_save_custom_keys: Button - save_as_new_key_set - frame_custom: LabelFrame - frame_target: Frame - target_title: Label - scroll_target_y: Scrollbar - scroll_target_x: Scrollbar - (*)bindingslist: ListBox - on_bindingslist_select - (*)button_new_keys: Button - get_new_keys & ..._name + Methods: + load_extensions: + extension_selected: Handle selection from list. + create_extension_frame: Hold widgets for one extension. + set_extension_value: Set in userCfg['extensions']. + save_all_changed_extensions: Call extension page Save(). """ parent = self.parent - self.builtin_name = tracers.add( - StringVar(parent), self.var_changed_builtin_name) - self.custom_name = tracers.add( - StringVar(parent), self.var_changed_custom_name) - self.keyset_source = tracers.add( - BooleanVar(parent), self.var_changed_keyset_source) - self.keybinding = tracers.add( - StringVar(parent), self.var_changed_keybinding) - - # Widget creation: - # body and section frames. frame = Frame(self.note) - frame_custom = LabelFrame( - frame, borderwidth=2, relief=GROOVE, - text=' Custom Key Bindings ') - frame_key_sets = LabelFrame( - frame, borderwidth=2, relief=GROOVE, text=' Key Set ') - #frame_custom - frame_target = Frame(frame_custom) - target_title = Label(frame_target, text='Action - Key(s)') - scroll_target_y = Scrollbar(frame_target) - scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL) - self.bindingslist = Listbox( - frame_target, takefocus=FALSE, exportselection=FALSE) - self.bindingslist.bind('', - self.on_bindingslist_select) - scroll_target_y['command'] = self.bindingslist.yview - scroll_target_x['command'] = self.bindingslist.xview - self.bindingslist['yscrollcommand'] = scroll_target_y.set - self.bindingslist['xscrollcommand'] = scroll_target_x.set - self.button_new_keys = Button( - frame_custom, text='Get New Keys for Selection', - command=self.get_new_keys, state=DISABLED) - #frame_key_sets - frames = [Frame(frame_key_sets, padx=2, pady=2, borderwidth=0) - for i in range(2)] - self.builtin_keyset_on = Radiobutton( - frames[0], variable=self.keyset_source, value=1, - command=self.set_keys_type, text='Use a Built-in Key Set') - self.custom_keyset_on = Radiobutton( - frames[0], variable=self.keyset_source, value=0, - command=self.set_keys_type, text='Use a Custom Key Set') - self.builtinlist = DynOptionMenu( - frames[0], self.builtin_name, None, command=None) - self.customlist = DynOptionMenu( - frames[0], self.custom_name, None, command=None) - self.button_delete_custom_keys = Button( - frames[1], text='Delete Custom Key Set', - command=self.delete_custom_keys) - self.button_save_custom_keys = Button( - frames[1], text='Save as New Custom Key Set', - command=self.save_as_new_key_set) - self.keys_message = Label(frames[0], bd=2) + self.ext_defaultCfg = idleConf.defaultCfg['extensions'] + self.ext_userCfg = idleConf.userCfg['extensions'] + self.is_int = self.register(is_int) + self.load_extensions() + # Create widgets - a listbox shows all available extensions, with the + # controls for the extension selected in the listbox to the right. + self.extension_names = StringVar(self) + frame.rowconfigure(0, weight=1) + frame.columnconfigure(2, weight=1) + self.extension_list = Listbox(frame, listvariable=self.extension_names, + selectmode='browse') + self.extension_list.bind('<>', self.extension_selected) + scroll = Scrollbar(frame, command=self.extension_list.yview) + self.extension_list.yscrollcommand=scroll.set + self.details_frame = LabelFrame(frame, width=250, height=250) + self.extension_list.grid(column=0, row=0, sticky='nws') + scroll.grid(column=1, row=0, sticky='ns') + self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0]) + frame.configure(padx=10, pady=10) + self.config_frame = {} + self.current_extension = None + + self.outerframe = self # TEMPORARY + self.tabbed_page_set = self.extension_list # TEMPORARY + + # Create the frame holding controls for each extension. + ext_names = '' + for ext_name in sorted(self.extensions): + self.create_extension_frame(ext_name) + ext_names = ext_names + '{' + ext_name + '} ' + self.extension_names.set(ext_names) + self.extension_list.selection_set(0) + self.extension_selected(None) - ##widget packing - #body - frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH) - frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH) - #frame_custom - self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5) - frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) - #frame target - frame_target.columnconfigure(0, weight=1) - frame_target.rowconfigure(1, weight=1) - target_title.grid(row=0, column=0, columnspan=2, sticky=W) - self.bindingslist.grid(row=1, column=0, sticky=NSEW) - scroll_target_y.grid(row=1, column=1, sticky=NS) - scroll_target_x.grid(row=2, column=0, sticky=EW) - #frame_key_sets - self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS) - self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS) - self.builtinlist.grid(row=0, column=1, sticky=NSEW) - self.customlist.grid(row=1, column=1, sticky=NSEW) - self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5) - self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) - self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) - frames[0].pack(side=TOP, fill=BOTH, expand=True) - frames[1].pack(side=TOP, fill=X, expand=True, pady=2) return frame - def load_key_cfg(self): - "Load current configuration settings for the keybinding options." - # Set current keys type radiobutton. - self.keyset_source.set(idleConf.GetOption( - 'main', 'Keys', 'default', type='bool', default=1)) - # Set current keys. - current_option = idleConf.CurrentKeys() - # Load available keyset option menus. - if self.keyset_source.get(): # Default theme selected. - item_list = idleConf.GetSectionList('default', 'keys') - item_list.sort() - self.builtinlist.SetMenu(item_list, current_option) - item_list = idleConf.GetSectionList('user', 'keys') - item_list.sort() - if not item_list: - self.custom_keyset_on['state'] = DISABLED - self.custom_name.set('- no custom keys -') - else: - self.customlist.SetMenu(item_list, item_list[0]) - else: # User key set selected. - item_list = idleConf.GetSectionList('user', 'keys') - item_list.sort() - self.customlist.SetMenu(item_list, current_option) - item_list = idleConf.GetSectionList('default', 'keys') - item_list.sort() - self.builtinlist.SetMenu(item_list, idleConf.default_keys()) - self.set_keys_type() - # Load keyset element list. - keyset_name = idleConf.CurrentKeys() - self.load_keys_list(keyset_name) + def load_extensions(self): + "Fill self.extensions with data from the default and user configs." + self.extensions = {} + for ext_name in idleConf.GetExtensions(active_only=False): + self.extensions[ext_name] = [] - def var_changed_builtin_name(self, *params): - "Process selection of builtin key set." - old_keys = ( - 'IDLE Classic Windows', - 'IDLE Classic Unix', - 'IDLE Classic Mac', - 'IDLE Classic OSX', - ) - value = self.builtin_name.get() - if value not in old_keys: - if idleConf.GetOption('main', 'Keys', 'name') not in old_keys: - changes.add_option('main', 'Keys', 'name', old_keys[0]) - changes.add_option('main', 'Keys', 'name2', value) - self.keys_message['text'] = 'New key set, see Help' - self.keys_message['fg'] = '#500000' - else: - changes.add_option('main', 'Keys', 'name', value) - changes.add_option('main', 'Keys', 'name2', '') - self.keys_message['text'] = '' - self.keys_message['fg'] = 'black' - self.load_keys_list(value) + for ext_name in self.extensions: + opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name)) - def var_changed_custom_name(self, *params): - "Process selection of custom key set." - value = self.custom_name.get() - if value != '- no custom keys -': - changes.add_option('main', 'Keys', 'name', value) - self.load_keys_list(value) + # Bring 'enable' options to the beginning of the list. + enables = [opt_name for opt_name in opt_list + if opt_name.startswith('enable')] + for opt_name in enables: + opt_list.remove(opt_name) + opt_list = enables + opt_list - def var_changed_keyset_source(self, *params): - "Process toggle between builtin key set and custom key set." - value = self.keyset_source.get() - changes.add_option('main', 'Keys', 'default', value) - if value: - self.var_changed_builtin_name() - else: - self.var_changed_custom_name() + for opt_name in opt_list: + def_str = self.ext_defaultCfg.Get( + ext_name, opt_name, raw=True) + try: + def_obj = {'True':True, 'False':False}[def_str] + opt_type = 'bool' + except KeyError: + try: + def_obj = int(def_str) + opt_type = 'int' + except ValueError: + def_obj = def_str + opt_type = None + try: + value = self.ext_userCfg.Get( + ext_name, opt_name, type=opt_type, raw=True, + default=def_obj) + except ValueError: # Need this until .Get fixed. + value = def_obj # Bad values overwritten by entry. + var = StringVar(self) + var.set(str(value)) - def var_changed_keybinding(self, *params): - "Store change to a keybinding." - value = self.keybinding.get() - key_set = self.custom_name.get() - event = self.bindingslist.get(ANCHOR).split()[0] - if idleConf.IsCoreBinding(event): - changes.add_option('keys', key_set, event, value) - else: # Event is an extension binding. - ext_name = idleConf.GetExtnNameForEvent(event) - ext_keybind_section = ext_name + '_cfgBindings' - changes.add_option('extensions', ext_keybind_section, event, value) + self.extensions[ext_name].append({'name': opt_name, + 'type': opt_type, + 'default': def_str, + 'value': value, + 'var': var, + }) - def set_keys_type(self): - "Set available screen options based on builtin or custom key set." - if self.keyset_source.get(): - self.builtinlist['state'] = NORMAL - self.customlist['state'] = DISABLED - self.button_delete_custom_keys['state'] = DISABLED - else: - self.builtinlist['state'] = DISABLED - self.custom_keyset_on['state'] = NORMAL - self.customlist['state'] = NORMAL - self.button_delete_custom_keys['state'] = NORMAL + def extension_selected(self, event): + "Handle selection of an extension from the list." + newsel = self.extension_list.curselection() + if newsel: + newsel = self.extension_list.get(newsel) + if newsel is None or newsel != self.current_extension: + if self.current_extension: + self.details_frame.config(text='') + self.config_frame[self.current_extension].grid_forget() + self.current_extension = None + if newsel: + self.details_frame.config(text=newsel) + self.config_frame[newsel].grid(column=0, row=0, sticky='nsew') + self.current_extension = newsel - def get_new_keys(self): - """Handle event to change key binding for selected line. + def create_extension_frame(self, ext_name): + """Create a frame holding the widgets to configure one extension""" + f = VerticalScrolledFrame(self.details_frame, height=250, width=250) + self.config_frame[ext_name] = f + entry_area = f.interior + # Create an entry for each configuration option. + for row, opt in enumerate(self.extensions[ext_name]): + # Create a row with a label and entry/checkbutton. + label = Label(entry_area, text=opt['name']) + label.grid(row=row, column=0, sticky=NW) + var = opt['var'] + if opt['type'] == 'bool': + Checkbutton(entry_area, textvariable=var, variable=var, + onvalue='True', offvalue='False', + indicatoron=FALSE, selectcolor='', width=8 + ).grid(row=row, column=1, sticky=W, padx=7) + elif opt['type'] == 'int': + Entry(entry_area, textvariable=var, validate='key', + validatecommand=(self.is_int, '%P') + ).grid(row=row, column=1, sticky=NSEW, padx=7) - A selection of a key/binding in the list of current - bindings pops up a dialog to enter a new binding. If - the current key set is builtin and a binding has - changed, then a name for a custom key set needs to be - entered for the change to be applied. - """ - list_index = self.bindingslist.index(ANCHOR) - binding = self.bindingslist.get(list_index) - bind_name = binding.split()[0] - if self.keyset_source.get(): - current_key_set_name = self.builtin_name.get() - else: - current_key_set_name = self.custom_name.get() - current_bindings = idleConf.GetCurrentKeySet() - if current_key_set_name in changes['keys']: # unsaved changes - key_set_changes = changes['keys'][current_key_set_name] - for event in key_set_changes: - current_bindings[event] = key_set_changes[event].split() - current_key_sequences = list(current_bindings.values()) - new_keys = GetKeysDialog(self, 'Get New Keys', bind_name, - current_key_sequences).result - if new_keys: - if self.keyset_source.get(): # Current key set is a built-in. - message = ('Your changes will be saved as a new Custom Key Set.' - ' Enter a name for your new Custom Key Set below.') - new_keyset = self.get_new_keys_name(message) - if not new_keyset: # User cancelled custom key set creation. - self.bindingslist.select_set(list_index) - self.bindingslist.select_anchor(list_index) - return - else: # Create new custom key set based on previously active key set. - self.create_new_key_set(new_keyset) - self.bindingslist.delete(list_index) - self.bindingslist.insert(list_index, bind_name+' - '+new_keys) - self.bindingslist.select_set(list_index) - self.bindingslist.select_anchor(list_index) - self.keybinding.set(new_keys) - else: - self.bindingslist.select_set(list_index) - self.bindingslist.select_anchor(list_index) + else: + Entry(entry_area, textvariable=var + ).grid(row=row, column=1, sticky=NSEW, padx=7) + return - def get_new_keys_name(self, message): - "Return new key set name from query popup." - used_names = (idleConf.GetSectionList('user', 'keys') + - idleConf.GetSectionList('default', 'keys')) - new_keyset = SectionName( - self, 'New Custom Key Set', message, used_names).result - return new_keyset + def set_extension_value(self, section, opt): + """Return True if the configuration was added or changed. - def save_as_new_key_set(self): - "Prompt for name of new key set and save changes using that name." - new_keys_name = self.get_new_keys_name('New Key Set Name:') - if new_keys_name: - self.create_new_key_set(new_keys_name) + If the value is the same as the default, then remove it + from user config file. + """ + name = opt['name'] + default = opt['default'] + value = opt['var'].get().strip() or default + opt['var'].set(value) + # if self.defaultCfg.has_section(section): + # Currently, always true; if not, indent to return. + if (value == default): + return self.ext_userCfg.RemoveOption(section, name) + # Set the option. + return self.ext_userCfg.SetOption(section, name, value) - def on_bindingslist_select(self, event): - "Activate button to assign new keys to selected action." - self.button_new_keys['state'] = NORMAL + def save_all_changed_extensions(self): + """Save configuration changes to the user config file. - def create_new_key_set(self, new_key_set_name): - """Create a new custom key set with the given name. + Attributes accessed: + extensions - Copy the bindings/keys from the previously active keyset - to the new keyset and activate the new custom keyset. + Methods: + set_extension_value """ - if self.keyset_source.get(): - prev_key_set_name = self.builtin_name.get() - else: - prev_key_set_name = self.custom_name.get() - prev_keys = idleConf.GetCoreKeys(prev_key_set_name) - new_keys = {} - for event in prev_keys: # Add key set to changed items. - event_name = event[2:-2] # Trim off the angle brackets. - binding = ' '.join(prev_keys[event]) - new_keys[event_name] = binding - # Handle any unsaved changes to prev key set. - if prev_key_set_name in changes['keys']: - key_set_changes = changes['keys'][prev_key_set_name] - for event in key_set_changes: - new_keys[event] = key_set_changes[event] - # Save the new key set. - self.save_new_key_set(new_key_set_name, new_keys) - # Change GUI over to the new key set. - custom_key_list = idleConf.GetSectionList('user', 'keys') - custom_key_list.sort() - self.customlist.SetMenu(custom_key_list, new_key_set_name) - self.keyset_source.set(0) - self.set_keys_type() + has_changes = False + for ext_name in self.extensions: + options = self.extensions[ext_name] + for opt in options: + if self.set_extension_value(ext_name, opt): + has_changes = True + if has_changes: + self.ext_userCfg.Save() - def load_keys_list(self, keyset_name): - """Reload the list of action/key binding pairs for the active key set. - An action/key binding can be selected to change the key binding. - """ - reselect = False - if self.bindingslist.curselection(): - reselect = True - list_index = self.bindingslist.index(ANCHOR) - keyset = idleConf.GetKeySet(keyset_name) - bind_names = list(keyset.keys()) - bind_names.sort() - self.bindingslist.delete(0, END) - for bind_name in bind_names: - key = ' '.join(keyset[bind_name]) - bind_name = bind_name[2:-2] # Trim off the angle brackets. - if keyset_name in changes['keys']: - # Handle any unsaved changes to this key set. - if bind_name in changes['keys'][keyset_name]: - key = changes['keys'][keyset_name][bind_name] - self.bindingslist.insert(END, bind_name+' - '+key) - if reselect: - self.bindingslist.see(list_index) - self.bindingslist.select_set(list_index) - self.bindingslist.select_anchor(list_index) - - def save_new_key_set(self, keyset_name, keyset): - """Save a newly created core key set. - - Add keyset to idleConf.userCfg['keys'], not to disk. - If the keyset doesn't exist, it is created. The - binding/keys are taken from the keyset argument. - - keyset_name - string, the name of the new key set - keyset - dictionary containing the new keybindings - """ - if not idleConf.userCfg['keys'].has_section(keyset_name): - idleConf.userCfg['keys'].add_section(keyset_name) - for event in keyset: - value = keyset[event] - idleConf.userCfg['keys'].SetOption(keyset_name, event, value) +# class TabPage(Frame): # A template for Page classes. +# def __init__(self, master): +# super().__init__(master) +# self.create_page_tab() +# self.load_tab_cfg() +# def create_page_tab(self): +# # Define tk vars and register var and callback with tracers. +# # Create subframes and widgets. +# # Pack widgets. +# def load_tab_cfg(self): +# # Initialize widgets with data from idleConf. +# def var_changed_var_name(): +# # For each tk var that needs other than default callback. +# def other_methods(): +# # Define tab-specific behavior. - def delete_custom_keys(self): - """Handle event to delete a custom key set. - Applying the delete deactivates the current configuration and - reverts to the default. The custom key set is permanently - deleted from the config file. - """ - keyset_name=self.custom_name.get() - delmsg = 'Are you sure you wish to delete the key set %r ?' - if not tkMessageBox.askyesno( - 'Delete Key Set', delmsg % keyset_name, parent=self): - return - self.deactivate_current_config() - # Remove key set from changes, config, and file. - changes.delete_section('keys', keyset_name) - # Reload user key set list. - item_list = idleConf.GetSectionList('user', 'keys') - item_list.sort() - if not item_list: - self.custom_keyset_on['state'] = DISABLED - self.customlist.SetMenu(item_list, '- no custom keys -') - else: - self.customlist.SetMenu(item_list, item_list[0]) - # Revert to default key set. - self.keyset_source.set(idleConf.defaultCfg['main'] - .Get('Keys', 'default')) - self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name') - or idleConf.default_keys()) - # User can't back out of these changes, they must be applied now. - changes.save_all() - self.save_all_changed_extensions() - self.activate_config_changes() - self.set_keys_type() +class FontPage(Frame): - def deactivate_current_config(self): - """Remove current key bindings. + def __init__(self, master, highpage): + super().__init__(master) + self.highlight_sample = highpage.highlight_sample + self.create_page_font_tab() + self.load_font_cfg() + self.load_tab_cfg() - Iterate over window instances defined in parent and remove - the keybindings. - """ - # Before a config is saved, some cleanup of current - # config must be done - remove the previous keybindings. - win_instances = self.parent.instance_dict.keys() - for instance in win_instances: - instance.RemoveKeybindings() + def create_page_font_tab(self): + """Return frame of widgets for Font/Tabs tab. - def activate_config_changes(self): - """Apply configuration changes to current windows. + Fonts: Enable users to provisionally change font face, size, or + boldness and to see the consequence of proposed choices. Each + action set 3 options in changes structuree and changes the + corresponding aspect of the font sample on this page and + highlight sample on highlight page. - Dynamically update the current parent window instances - with some of the configuration changes. - """ - win_instances = self.parent.instance_dict.keys() - for instance in win_instances: - instance.ResetColorizer() - instance.ResetFont() - instance.set_notabs_indentwidth() - instance.ApplyKeybindings() - instance.reset_help_menu_entries() + Function load_font_cfg initializes font vars and widgets from + idleConf entries and tk. - def create_page_extensions(self): - """Part of the config dialog used for configuring IDLE extensions. + Fontlist: mouse button 1 click or up or down key invoke + on_fontlist_select(), which sets var font_name. - This code is generic - it works for any and all IDLE extensions. + Sizelist: clicking the menubutton opens the dropdown menu. A + mouse button 1 click or return key sets var font_size. - IDLE extensions save their configuration options using idleConf. - This code reads the current configuration using idleConf, supplies a - GUI interface to change the configuration values, and saves the - changes using idleConf. + Bold_toggle: clicking the box toggles var font_bold. - Not all changes take effect immediately - some may require restarting IDLE. - This depends on each extension's implementation. + Changing any of the font vars invokes var_changed_font, which + adds all 3 font options to changes and calls set_samples. + Set_samples applies a new font constructed from the font vars to + font_sample and to highlight_sample on the hightlight page. - All values are treated as text, and it is up to the user to supply - reasonable values. The only exception to this are the 'enable*' options, - which are boolean, and can be toggled with a True/False button. + Tabs: Enable users to change spaces entered for indent tabs. + Changing indent_scale value with the mouse sets Var space_num, + which invokes the default callback to add an entry to + changes. Load_tab_cfg initializes space_num to default. - Methods: - load_extensions: - extension_selected: Handle selection from list. - create_extension_frame: Hold widgets for one extension. - set_extension_value: Set in userCfg['extensions']. - save_all_changed_extensions: Call extension page Save(). + Widgets for FontPage(Frame): (*) widgets bound to self + frame_font: LabelFrame + frame_font_name: Frame + font_name_title: Label + (*)fontlist: ListBox - font_name + scroll_font: Scrollbar + frame_font_param: Frame + font_size_title: Label + (*)sizelist: DynOptionMenu - font_size + (*)bold_toggle: Checkbutton - font_bold + frame_font_sample: Frame + (*)font_sample: Label + frame_indent: LabelFrame + indent_title: Label + (*)indent_scale: Scale - space_num """ - parent = self.parent - frame = Frame(self.note) - self.ext_defaultCfg = idleConf.defaultCfg['extensions'] - self.ext_userCfg = idleConf.userCfg['extensions'] - self.is_int = self.register(is_int) - self.load_extensions() - # Create widgets - a listbox shows all available extensions, with the - # controls for the extension selected in the listbox to the right. - self.extension_names = StringVar(self) - frame.rowconfigure(0, weight=1) - frame.columnconfigure(2, weight=1) - self.extension_list = Listbox(frame, listvariable=self.extension_names, - selectmode='browse') - self.extension_list.bind('<>', self.extension_selected) - scroll = Scrollbar(frame, command=self.extension_list.yview) - self.extension_list.yscrollcommand=scroll.set - self.details_frame = LabelFrame(frame, width=250, height=250) - self.extension_list.grid(column=0, row=0, sticky='nws') - scroll.grid(column=1, row=0, sticky='ns') - self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0]) - frame.configure(padx=10, pady=10) - self.config_frame = {} - self.current_extension = None + self.font_name = tracers.add(StringVar(self), self.var_changed_font) + self.font_size = tracers.add(StringVar(self), self.var_changed_font) + self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font) + self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces')) - self.outerframe = self # TEMPORARY - self.tabbed_page_set = self.extension_list # TEMPORARY + # Create widgets: + # body and body section frames. + frame_font = LabelFrame( + self, borderwidth=2, relief=GROOVE, text=' Base Editor Font ') + frame_indent = LabelFrame( + self, borderwidth=2, relief=GROOVE, text=' Indentation Width ') + # frame_font. + frame_font_name = Frame(frame_font) + frame_font_param = Frame(frame_font) + font_name_title = Label( + frame_font_name, justify=LEFT, text='Font Face :') + self.fontlist = Listbox(frame_font_name, height=5, + takefocus=True, exportselection=FALSE) + self.fontlist.bind('', self.on_fontlist_select) + self.fontlist.bind('', self.on_fontlist_select) + self.fontlist.bind('', self.on_fontlist_select) + scroll_font = Scrollbar(frame_font_name) + scroll_font.config(command=self.fontlist.yview) + self.fontlist.config(yscrollcommand=scroll_font.set) + font_size_title = Label(frame_font_param, text='Size :') + self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None) + self.bold_toggle = Checkbutton( + frame_font_param, variable=self.font_bold, + onvalue=1, offvalue=0, text='Bold') + frame_font_sample = Frame(frame_font, relief=SOLID, borderwidth=1) + temp_font = tkFont.Font(self, ('courier', 10, 'normal')) + self.font_sample = Label( + frame_font_sample, justify=LEFT, font=temp_font, + text='AaBbCcDdEe\nFfGgHhIiJj\n1234567890\n#:+=(){}[]') + # frame_indent. + indent_title = Label( + frame_indent, justify=LEFT, + text='Python Standard: 4 Spaces!') + self.indent_scale = Scale( + frame_indent, variable=self.space_num, + orient='horizontal', tickinterval=2, from_=2, to=16) - # Create the frame holding controls for each extension. - ext_names = '' - for ext_name in sorted(self.extensions): - self.create_extension_frame(ext_name) - ext_names = ext_names + '{' + ext_name + '} ' - self.extension_names.set(ext_names) - self.extension_list.selection_set(0) - self.extension_selected(None) + # Pack widgets: + # body. + frame_font.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) + frame_indent.pack(side=LEFT, padx=5, pady=5, fill=Y) + # frame_font. + frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X) + frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X) + font_name_title.pack(side=TOP, anchor=W) + self.fontlist.pack(side=LEFT, expand=TRUE, fill=X) + scroll_font.pack(side=LEFT, fill=Y) + font_size_title.pack(side=LEFT, anchor=W) + self.sizelist.pack(side=LEFT, anchor=W) + self.bold_toggle.pack(side=LEFT, anchor=W, padx=20) + frame_font_sample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) + self.font_sample.pack(expand=TRUE, fill=BOTH) + # frame_indent. + frame_indent.pack(side=TOP, fill=X) + indent_title.pack(side=TOP, anchor=W, padx=5) + self.indent_scale.pack(side=TOP, padx=5, fill=X) - return frame + def load_font_cfg(self): + """Load current configuration settings for the font options. - def load_extensions(self): - "Fill self.extensions with data from the default and user configs." - self.extensions = {} - for ext_name in idleConf.GetExtensions(active_only=False): - self.extensions[ext_name] = [] + Retrieve current font with idleConf.GetFont and font families + from tk. Setup fontlist and set font_name. Setup sizelist, + which sets font_size. Set font_bold. Call set_samples. + """ + configured_font = idleConf.GetFont(self, 'main', 'EditorWindow') + font_name = configured_font[0].lower() + font_size = configured_font[1] + font_bold = configured_font[2]=='bold' - for ext_name in self.extensions: - opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name)) - - # Bring 'enable' options to the beginning of the list. - enables = [opt_name for opt_name in opt_list - if opt_name.startswith('enable')] - for opt_name in enables: - opt_list.remove(opt_name) - opt_list = enables + opt_list - - for opt_name in opt_list: - def_str = self.ext_defaultCfg.Get( - ext_name, opt_name, raw=True) - try: - def_obj = {'True':True, 'False':False}[def_str] - opt_type = 'bool' - except KeyError: - try: - def_obj = int(def_str) - opt_type = 'int' - except ValueError: - def_obj = def_str - opt_type = None - try: - value = self.ext_userCfg.Get( - ext_name, opt_name, type=opt_type, raw=True, - default=def_obj) - except ValueError: # Need this until .Get fixed. - value = def_obj # Bad values overwritten by entry. - var = StringVar(self) - var.set(str(value)) + # Set editor font selection list and font_name. + fonts = list(tkFont.families(self)) + fonts.sort() + for font in fonts: + self.fontlist.insert(END, font) + self.font_name.set(font_name) + lc_fonts = [s.lower() for s in fonts] + try: + current_font_index = lc_fonts.index(font_name) + self.fontlist.see(current_font_index) + self.fontlist.select_set(current_font_index) + self.fontlist.select_anchor(current_font_index) + self.fontlist.activate(current_font_index) + except ValueError: + pass + # Set font size dropdown. + self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14', + '16', '18', '20', '22', '25', '29', '34', '40'), + font_size) + # Set font weight. + self.font_bold.set(font_bold) + self.set_samples() - self.extensions[ext_name].append({'name': opt_name, - 'type': opt_type, - 'default': def_str, - 'value': value, - 'var': var, - }) + def var_changed_font(self, *params): + """Store changes to font attributes. - def extension_selected(self, event): - "Handle selection of an extension from the list." - newsel = self.extension_list.curselection() - if newsel: - newsel = self.extension_list.get(newsel) - if newsel is None or newsel != self.current_extension: - if self.current_extension: - self.details_frame.config(text='') - self.config_frame[self.current_extension].grid_forget() - self.current_extension = None - if newsel: - self.details_frame.config(text=newsel) - self.config_frame[newsel].grid(column=0, row=0, sticky='nsew') - self.current_extension = newsel + When one font attribute changes, save them all, as they are + not independent from each other. In particular, when we are + overriding the default font, we need to write out everything. + """ + value = self.font_name.get() + changes.add_option('main', 'EditorWindow', 'font', value) + value = self.font_size.get() + changes.add_option('main', 'EditorWindow', 'font-size', value) + value = self.font_bold.get() + changes.add_option('main', 'EditorWindow', 'font-bold', value) + self.set_samples() - def create_extension_frame(self, ext_name): - """Create a frame holding the widgets to configure one extension""" - f = VerticalScrolledFrame(self.details_frame, height=250, width=250) - self.config_frame[ext_name] = f - entry_area = f.interior - # Create an entry for each configuration option. - for row, opt in enumerate(self.extensions[ext_name]): - # Create a row with a label and entry/checkbutton. - label = Label(entry_area, text=opt['name']) - label.grid(row=row, column=0, sticky=NW) - var = opt['var'] - if opt['type'] == 'bool': - Checkbutton(entry_area, textvariable=var, variable=var, - onvalue='True', offvalue='False', - indicatoron=FALSE, selectcolor='', width=8 - ).grid(row=row, column=1, sticky=W, padx=7) - elif opt['type'] == 'int': - Entry(entry_area, textvariable=var, validate='key', - validatecommand=(self.is_int, '%P') - ).grid(row=row, column=1, sticky=NSEW, padx=7) + def on_fontlist_select(self, event): + """Handle selecting a font from the list. - else: - Entry(entry_area, textvariable=var - ).grid(row=row, column=1, sticky=NSEW, padx=7) - return + Event can result from either mouse click or Up or Down key. + Set font_name and example displays to selection. + """ + font = self.fontlist.get( + ACTIVE if event.type.name == 'KeyRelease' else ANCHOR) + self.font_name.set(font.lower()) - def set_extension_value(self, section, opt): - """Return True if the configuration was added or changed. + def set_samples(self, event=None): + """Update update both screen samples with the font settings. - If the value is the same as the default, then remove it - from user config file. + Called on font initialization and change events. + Accesses font_name, font_size, and font_bold Variables. + Updates font_sample and hightlight page highlight_sample. """ - name = opt['name'] - default = opt['default'] - value = opt['var'].get().strip() or default - opt['var'].set(value) - # if self.defaultCfg.has_section(section): - # Currently, always true; if not, indent to return. - if (value == default): - return self.ext_userCfg.RemoveOption(section, name) - # Set the option. - return self.ext_userCfg.SetOption(section, name, value) - - def save_all_changed_extensions(self): - """Save configuration changes to the user config file. + font_name = self.font_name.get() + font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL + new_font = (font_name, self.font_size.get(), font_weight) + self.font_sample['font'] = new_font + self.highlight_sample['font'] = new_font - Attributes accessed: - extensions + def load_tab_cfg(self): + """Load current configuration settings for the tab options. - Methods: - set_extension_value + Attributes updated: + space_num: Set to value from idleConf. """ - has_changes = False - for ext_name in self.extensions: - options = self.extensions[ext_name] - for opt in options: - if self.set_extension_value(ext_name, opt): - has_changes = True - if has_changes: - self.ext_userCfg.Save() - + # Set indent sizes. + space_num = idleConf.GetOption( + 'main', 'Indent', 'num-spaces', default=4, type='int') + self.space_num.set(space_num) -# class TabPage(Frame): # A template for Page classes. -# def __init__(self, master): -# super().__init__(master) -# self.create_page_tab() -# self.load_tab_cfg() -# def create_page_tab(self): -# # Define tk vars and register var and callback with tracers. -# # Create subframes and widgets. -# # Pack widgets. -# def load_tab_cfg(self): -# # Initialize widgets with data from idleConf. -# def var_changed_var_name(): -# # For each tk var that needs other than default callback. -# def other_methods(): -# # Define tab-specific behavior. + def var_changed_space_num(self, *params): + "Store change to indentation size." + value = self.space_num.get() + changes.add_option('main', 'Indent', 'num-spaces', value) -class FontPage(Frame): +class KeysPage(Frame): - def __init__(self, master, highpage): + def __init__(self, master): super().__init__(master) - self.highlight_sample = highpage.highlight_sample - self.create_page_font_tab() - self.load_font_cfg() - self.load_tab_cfg() + self.cd = master.master + self.create_page_keys() + self.load_key_cfg() - def create_page_font_tab(self): - """Return frame of widgets for Font/Tabs tab. + def create_page_keys(self): + """Return frame of widgets for Keys tab. - Fonts: Enable users to provisionally change font face, size, or - boldness and to see the consequence of proposed choices. Each - action set 3 options in changes structuree and changes the - corresponding aspect of the font sample on this page and - highlight sample on highlight page. + Enable users to provisionally change both individual and sets of + keybindings (shortcut keys). Except for features implemented as + extensions, keybindings are stored in complete sets called + keysets. Built-in keysets in idlelib/config-keys.def are fixed + as far as the dialog is concerned. Any keyset can be used as the + base for a new custom keyset, stored in .idlerc/config-keys.cfg. - Function load_font_cfg initializes font vars and widgets from - idleConf entries and tk. + Function load_key_cfg() initializes tk variables and keyset + lists and calls load_keys_list for the current keyset. + Radiobuttons builtin_keyset_on and custom_keyset_on toggle var + keyset_source, which controls if the current set of keybindings + are from a builtin or custom keyset. DynOptionMenus builtinlist + and customlist contain lists of the builtin and custom keysets, + respectively, and the current item from each list is stored in + vars builtin_name and custom_name. - Fontlist: mouse button 1 click or up or down key invoke - on_fontlist_select(), which sets var font_name. + Button delete_custom_keys invokes delete_custom_keys() to delete + a custom keyset from idleConf.userCfg['keys'] and changes. Button + save_custom_keys invokes save_as_new_key_set() which calls + get_new_keys_name() and create_new_key_set() to save a custom keyset + and its keybindings to idleConf.userCfg['keys']. - Sizelist: clicking the menubutton opens the dropdown menu. A - mouse button 1 click or return key sets var font_size. + Listbox bindingslist contains all of the keybindings for the + selected keyset. The keybindings are loaded in load_keys_list() + and are pairs of (event, [keys]) where keys can be a list + of one or more key combinations to bind to the same event. + Mouse button 1 click invokes on_bindingslist_select(), which + allows button_new_keys to be clicked. - Bold_toggle: clicking the box toggles var font_bold. + So, an item is selected in listbindings, which activates + button_new_keys, and clicking button_new_keys calls function + get_new_keys(). Function get_new_keys() gets the key mappings from the + current keyset for the binding event item that was selected. The + function then displays another dialog, GetKeysDialog, with the + selected binding event and current keys and always new key sequences + to be entered for that binding event. If the keys aren't + changed, nothing happens. If the keys are changed and the keyset + is a builtin, function get_new_keys_name() will be called + for input of a custom keyset name. If no name is given, then the + change to the keybinding will abort and no updates will be made. If + a custom name is entered in the prompt or if the current keyset was + already custom (and thus didn't require a prompt), then + idleConf.userCfg['keys'] is updated in function create_new_key_set() + with the change to the event binding. The item listing in bindingslist + is updated with the new keys. Var keybinding is also set which invokes + the callback function, var_changed_keybinding, to add the change to + the 'keys' or 'extensions' changes tracker based on the binding type. - Changing any of the font vars invokes var_changed_font, which - adds all 3 font options to changes and calls set_samples. - Set_samples applies a new font constructed from the font vars to - font_sample and to highlight_sample on the hightlight page. + Tk Variables: + keybinding: Action/key bindings. - Tabs: Enable users to change spaces entered for indent tabs. - Changing indent_scale value with the mouse sets Var space_num, - which invokes the default callback to add an entry to - changes. Load_tab_cfg initializes space_num to default. + Methods: + load_keys_list: Reload active set. + create_new_key_set: Combine active keyset and changes. + set_keys_type: Command for keyset_source. + save_new_key_set: Save to idleConf.userCfg['keys'] (is function). + deactivate_current_config: Remove keys bindings in editors. - Widgets for FontPage(Frame): (*) widgets bound to self - frame_font: LabelFrame - frame_font_name: Frame - font_name_title: Label - (*)fontlist: ListBox - font_name - scroll_font: Scrollbar - frame_font_param: Frame - font_size_title: Label - (*)sizelist: DynOptionMenu - font_size - (*)bold_toggle: Checkbutton - font_bold - frame_font_sample: Frame - (*)font_sample: Label - frame_indent: LabelFrame - indent_title: Label - (*)indent_scale: Scale - space_num + Widgets for KeysPage(frame): (*) widgets bound to self + frame_key_sets: LabelFrame + frames[0]: Frame + (*)builtin_keyset_on: Radiobutton - var keyset_source + (*)custom_keyset_on: Radiobutton - var keyset_source + (*)builtinlist: DynOptionMenu - var builtin_name, + func keybinding_selected + (*)customlist: DynOptionMenu - var custom_name, + func keybinding_selected + (*)keys_message: Label + frames[1]: Frame + (*)button_delete_custom_keys: Button - delete_custom_keys + (*)button_save_custom_keys: Button - save_as_new_key_set + frame_custom: LabelFrame + frame_target: Frame + target_title: Label + scroll_target_y: Scrollbar + scroll_target_x: Scrollbar + (*)bindingslist: ListBox - on_bindingslist_select + (*)button_new_keys: Button - get_new_keys & ..._name """ - self.font_name = tracers.add(StringVar(self), self.var_changed_font) - self.font_size = tracers.add(StringVar(self), self.var_changed_font) - self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font) - self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces')) + self.builtin_name = tracers.add( + StringVar(self), self.var_changed_builtin_name) + self.custom_name = tracers.add( + StringVar(self), self.var_changed_custom_name) + self.keyset_source = tracers.add( + BooleanVar(self), self.var_changed_keyset_source) + self.keybinding = tracers.add( + StringVar(self), self.var_changed_keybinding) # Create widgets: - # body and body section frames. - frame_font = LabelFrame( - self, borderwidth=2, relief=GROOVE, text=' Base Editor Font ') - frame_indent = LabelFrame( - self, borderwidth=2, relief=GROOVE, text=' Indentation Width ') - # frame_font. - frame_font_name = Frame(frame_font) - frame_font_param = Frame(frame_font) - font_name_title = Label( - frame_font_name, justify=LEFT, text='Font Face :') - self.fontlist = Listbox(frame_font_name, height=5, - takefocus=True, exportselection=FALSE) - self.fontlist.bind('', self.on_fontlist_select) - self.fontlist.bind('', self.on_fontlist_select) - self.fontlist.bind('', self.on_fontlist_select) - scroll_font = Scrollbar(frame_font_name) - scroll_font.config(command=self.fontlist.yview) - self.fontlist.config(yscrollcommand=scroll_font.set) - font_size_title = Label(frame_font_param, text='Size :') - self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None) - self.bold_toggle = Checkbutton( - frame_font_param, variable=self.font_bold, - onvalue=1, offvalue=0, text='Bold') - frame_font_sample = Frame(frame_font, relief=SOLID, borderwidth=1) - temp_font = tkFont.Font(self, ('courier', 10, 'normal')) - self.font_sample = Label( - frame_font_sample, justify=LEFT, font=temp_font, - text='AaBbCcDdEe\nFfGgHhIiJj\n1234567890\n#:+=(){}[]') - # frame_indent. - indent_title = Label( - frame_indent, justify=LEFT, - text='Python Standard: 4 Spaces!') - self.indent_scale = Scale( - frame_indent, variable=self.space_num, - orient='horizontal', tickinterval=2, from_=2, to=16) + # body and section frames. + frame_custom = LabelFrame( + self, borderwidth=2, relief=GROOVE, + text=' Custom Key Bindings ') + frame_key_sets = LabelFrame( + self, borderwidth=2, relief=GROOVE, text=' Key Set ') + # frame_custom. + frame_target = Frame(frame_custom) + target_title = Label(frame_target, text='Action - Key(s)') + scroll_target_y = Scrollbar(frame_target) + scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL) + self.bindingslist = Listbox( + frame_target, takefocus=FALSE, exportselection=FALSE) + self.bindingslist.bind('', + self.on_bindingslist_select) + scroll_target_y['command'] = self.bindingslist.yview + scroll_target_x['command'] = self.bindingslist.xview + self.bindingslist['yscrollcommand'] = scroll_target_y.set + self.bindingslist['xscrollcommand'] = scroll_target_x.set + self.button_new_keys = Button( + frame_custom, text='Get New Keys for Selection', + command=self.get_new_keys, state=DISABLED) + # frame_key_sets. + frames = [Frame(frame_key_sets, padx=2, pady=2, borderwidth=0) + for i in range(2)] + self.builtin_keyset_on = Radiobutton( + frames[0], variable=self.keyset_source, value=1, + command=self.set_keys_type, text='Use a Built-in Key Set') + self.custom_keyset_on = Radiobutton( + frames[0], variable=self.keyset_source, value=0, + command=self.set_keys_type, text='Use a Custom Key Set') + self.builtinlist = DynOptionMenu( + frames[0], self.builtin_name, None, command=None) + self.customlist = DynOptionMenu( + frames[0], self.custom_name, None, command=None) + self.button_delete_custom_keys = Button( + frames[1], text='Delete Custom Key Set', + command=self.delete_custom_keys) + self.button_save_custom_keys = Button( + frames[1], text='Save as New Custom Key Set', + command=self.save_as_new_key_set) + self.keys_message = Label(frames[0], bd=2) + + # Pack widgets: + # body. + frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH) + frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH) + # frame_custom. + self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5) + frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) + # frame_target. + frame_target.columnconfigure(0, weight=1) + frame_target.rowconfigure(1, weight=1) + target_title.grid(row=0, column=0, columnspan=2, sticky=W) + self.bindingslist.grid(row=1, column=0, sticky=NSEW) + scroll_target_y.grid(row=1, column=1, sticky=NS) + scroll_target_x.grid(row=2, column=0, sticky=EW) + # frame_key_sets. + self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS) + self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS) + self.builtinlist.grid(row=0, column=1, sticky=NSEW) + self.customlist.grid(row=1, column=1, sticky=NSEW) + self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5) + self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) + self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) + frames[0].pack(side=TOP, fill=BOTH, expand=True) + frames[1].pack(side=TOP, fill=X, expand=True, pady=2) + + def load_key_cfg(self): + "Load current configuration settings for the keybinding options." + # Set current keys type radiobutton. + self.keyset_source.set(idleConf.GetOption( + 'main', 'Keys', 'default', type='bool', default=1)) + # Set current keys. + current_option = idleConf.CurrentKeys() + # Load available keyset option menus. + if self.keyset_source.get(): # Default theme selected. + item_list = idleConf.GetSectionList('default', 'keys') + item_list.sort() + self.builtinlist.SetMenu(item_list, current_option) + item_list = idleConf.GetSectionList('user', 'keys') + item_list.sort() + if not item_list: + self.custom_keyset_on['state'] = DISABLED + self.custom_name.set('- no custom keys -') + else: + self.customlist.SetMenu(item_list, item_list[0]) + else: # User key set selected. + item_list = idleConf.GetSectionList('user', 'keys') + item_list.sort() + self.customlist.SetMenu(item_list, current_option) + item_list = idleConf.GetSectionList('default', 'keys') + item_list.sort() + self.builtinlist.SetMenu(item_list, idleConf.default_keys()) + self.set_keys_type() + # Load keyset element list. + keyset_name = idleConf.CurrentKeys() + self.load_keys_list(keyset_name) + + def var_changed_builtin_name(self, *params): + "Process selection of builtin key set." + old_keys = ( + 'IDLE Classic Windows', + 'IDLE Classic Unix', + 'IDLE Classic Mac', + 'IDLE Classic OSX', + ) + value = self.builtin_name.get() + if value not in old_keys: + if idleConf.GetOption('main', 'Keys', 'name') not in old_keys: + changes.add_option('main', 'Keys', 'name', old_keys[0]) + changes.add_option('main', 'Keys', 'name2', value) + self.keys_message['text'] = 'New key set, see Help' + self.keys_message['fg'] = '#500000' + else: + changes.add_option('main', 'Keys', 'name', value) + changes.add_option('main', 'Keys', 'name2', '') + self.keys_message['text'] = '' + self.keys_message['fg'] = 'black' + self.load_keys_list(value) + + def var_changed_custom_name(self, *params): + "Process selection of custom key set." + value = self.custom_name.get() + if value != '- no custom keys -': + changes.add_option('main', 'Keys', 'name', value) + self.load_keys_list(value) + + def var_changed_keyset_source(self, *params): + "Process toggle between builtin key set and custom key set." + value = self.keyset_source.get() + changes.add_option('main', 'Keys', 'default', value) + if value: + self.var_changed_builtin_name() + else: + self.var_changed_custom_name() + + def var_changed_keybinding(self, *params): + "Store change to a keybinding." + value = self.keybinding.get() + key_set = self.custom_name.get() + event = self.bindingslist.get(ANCHOR).split()[0] + if idleConf.IsCoreBinding(event): + changes.add_option('keys', key_set, event, value) + else: # Event is an extension binding. + ext_name = idleConf.GetExtnNameForEvent(event) + ext_keybind_section = ext_name + '_cfgBindings' + changes.add_option('extensions', ext_keybind_section, event, value) - # Pack widgets: - # body. - frame_font.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) - frame_indent.pack(side=LEFT, padx=5, pady=5, fill=Y) - # frame_font. - frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X) - frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X) - font_name_title.pack(side=TOP, anchor=W) - self.fontlist.pack(side=LEFT, expand=TRUE, fill=X) - scroll_font.pack(side=LEFT, fill=Y) - font_size_title.pack(side=LEFT, anchor=W) - self.sizelist.pack(side=LEFT, anchor=W) - self.bold_toggle.pack(side=LEFT, anchor=W, padx=20) - frame_font_sample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) - self.font_sample.pack(expand=TRUE, fill=BOTH) - # frame_indent. - frame_indent.pack(side=TOP, fill=X) - indent_title.pack(side=TOP, anchor=W, padx=5) - self.indent_scale.pack(side=TOP, padx=5, fill=X) + def set_keys_type(self): + "Set available screen options based on builtin or custom key set." + if self.keyset_source.get(): + self.builtinlist['state'] = NORMAL + self.customlist['state'] = DISABLED + self.button_delete_custom_keys['state'] = DISABLED + else: + self.builtinlist['state'] = DISABLED + self.custom_keyset_on['state'] = NORMAL + self.customlist['state'] = NORMAL + self.button_delete_custom_keys['state'] = NORMAL - def load_font_cfg(self): - """Load current configuration settings for the font options. + def get_new_keys(self): + """Handle event to change key binding for selected line. - Retrieve current font with idleConf.GetFont and font families - from tk. Setup fontlist and set font_name. Setup sizelist, - which sets font_size. Set font_bold. Call set_samples. + A selection of a key/binding in the list of current + bindings pops up a dialog to enter a new binding. If + the current key set is builtin and a binding has + changed, then a name for a custom key set needs to be + entered for the change to be applied. """ - configured_font = idleConf.GetFont(self, 'main', 'EditorWindow') - font_name = configured_font[0].lower() - font_size = configured_font[1] - font_bold = configured_font[2]=='bold' + list_index = self.bindingslist.index(ANCHOR) + binding = self.bindingslist.get(list_index) + bind_name = binding.split()[0] + if self.keyset_source.get(): + current_key_set_name = self.builtin_name.get() + else: + current_key_set_name = self.custom_name.get() + current_bindings = idleConf.GetCurrentKeySet() + if current_key_set_name in changes['keys']: # unsaved changes + key_set_changes = changes['keys'][current_key_set_name] + for event in key_set_changes: + current_bindings[event] = key_set_changes[event].split() + current_key_sequences = list(current_bindings.values()) + new_keys = GetKeysDialog(self, 'Get New Keys', bind_name, + current_key_sequences).result + if new_keys: + if self.keyset_source.get(): # Current key set is a built-in. + message = ('Your changes will be saved as a new Custom Key Set.' + ' Enter a name for your new Custom Key Set below.') + new_keyset = self.get_new_keys_name(message) + if not new_keyset: # User cancelled custom key set creation. + self.bindingslist.select_set(list_index) + self.bindingslist.select_anchor(list_index) + return + else: # Create new custom key set based on previously active key set. + self.create_new_key_set(new_keyset) + self.bindingslist.delete(list_index) + self.bindingslist.insert(list_index, bind_name+' - '+new_keys) + self.bindingslist.select_set(list_index) + self.bindingslist.select_anchor(list_index) + self.keybinding.set(new_keys) + else: + self.bindingslist.select_set(list_index) + self.bindingslist.select_anchor(list_index) - # Set editor font selection list and font_name. - fonts = list(tkFont.families(self)) - fonts.sort() - for font in fonts: - self.fontlist.insert(END, font) - self.font_name.set(font_name) - lc_fonts = [s.lower() for s in fonts] - try: - current_font_index = lc_fonts.index(font_name) - self.fontlist.see(current_font_index) - self.fontlist.select_set(current_font_index) - self.fontlist.select_anchor(current_font_index) - self.fontlist.activate(current_font_index) - except ValueError: - pass - # Set font size dropdown. - self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14', - '16', '18', '20', '22', '25', '29', '34', '40'), - font_size) - # Set font weight. - self.font_bold.set(font_bold) - self.set_samples() + def get_new_keys_name(self, message): + "Return new key set name from query popup." + used_names = (idleConf.GetSectionList('user', 'keys') + + idleConf.GetSectionList('default', 'keys')) + new_keyset = SectionName( + self, 'New Custom Key Set', message, used_names).result + return new_keyset - def var_changed_font(self, *params): - """Store changes to font attributes. + def save_as_new_key_set(self): + "Prompt for name of new key set and save changes using that name." + new_keys_name = self.get_new_keys_name('New Key Set Name:') + if new_keys_name: + self.create_new_key_set(new_keys_name) - When one font attribute changes, save them all, as they are - not independent from each other. In particular, when we are - overriding the default font, we need to write out everything. - """ - value = self.font_name.get() - changes.add_option('main', 'EditorWindow', 'font', value) - value = self.font_size.get() - changes.add_option('main', 'EditorWindow', 'font-size', value) - value = self.font_bold.get() - changes.add_option('main', 'EditorWindow', 'font-bold', value) - self.set_samples() + def on_bindingslist_select(self, event): + "Activate button to assign new keys to selected action." + self.button_new_keys['state'] = NORMAL - def on_fontlist_select(self, event): - """Handle selecting a font from the list. + def create_new_key_set(self, new_key_set_name): + """Create a new custom key set with the given name. - Event can result from either mouse click or Up or Down key. - Set font_name and example displays to selection. + Copy the bindings/keys from the previously active keyset + to the new keyset and activate the new custom keyset. """ - font = self.fontlist.get( - ACTIVE if event.type.name == 'KeyRelease' else ANCHOR) - self.font_name.set(font.lower()) + if self.keyset_source.get(): + prev_key_set_name = self.builtin_name.get() + else: + prev_key_set_name = self.custom_name.get() + prev_keys = idleConf.GetCoreKeys(prev_key_set_name) + new_keys = {} + for event in prev_keys: # Add key set to changed items. + event_name = event[2:-2] # Trim off the angle brackets. + binding = ' '.join(prev_keys[event]) + new_keys[event_name] = binding + # Handle any unsaved changes to prev key set. + if prev_key_set_name in changes['keys']: + key_set_changes = changes['keys'][prev_key_set_name] + for event in key_set_changes: + new_keys[event] = key_set_changes[event] + # Save the new key set. + self.save_new_key_set(new_key_set_name, new_keys) + # Change GUI over to the new key set. + custom_key_list = idleConf.GetSectionList('user', 'keys') + custom_key_list.sort() + self.customlist.SetMenu(custom_key_list, new_key_set_name) + self.keyset_source.set(0) + self.set_keys_type() - def set_samples(self, event=None): - """Update update both screen samples with the font settings. + def load_keys_list(self, keyset_name): + """Reload the list of action/key binding pairs for the active key set. - Called on font initialization and change events. - Accesses font_name, font_size, and font_bold Variables. - Updates font_sample and hightlight page highlight_sample. + An action/key binding can be selected to change the key binding. """ - font_name = self.font_name.get() - font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL - new_font = (font_name, self.font_size.get(), font_weight) - self.font_sample['font'] = new_font - self.highlight_sample['font'] = new_font + reselect = False + if self.bindingslist.curselection(): + reselect = True + list_index = self.bindingslist.index(ANCHOR) + keyset = idleConf.GetKeySet(keyset_name) + bind_names = list(keyset.keys()) + bind_names.sort() + self.bindingslist.delete(0, END) + for bind_name in bind_names: + key = ' '.join(keyset[bind_name]) + bind_name = bind_name[2:-2] # Trim off the angle brackets. + if keyset_name in changes['keys']: + # Handle any unsaved changes to this key set. + if bind_name in changes['keys'][keyset_name]: + key = changes['keys'][keyset_name][bind_name] + self.bindingslist.insert(END, bind_name+' - '+key) + if reselect: + self.bindingslist.see(list_index) + self.bindingslist.select_set(list_index) + self.bindingslist.select_anchor(list_index) - def load_tab_cfg(self): - """Load current configuration settings for the tab options. + @staticmethod + def save_new_key_set(keyset_name, keyset): + """Save a newly created core key set. - Attributes updated: - space_num: Set to value from idleConf. + Add keyset to idleConf.userCfg['keys'], not to disk. + If the keyset doesn't exist, it is created. The + binding/keys are taken from the keyset argument. + + keyset_name - string, the name of the new key set + keyset - dictionary containing the new keybindings """ - # Set indent sizes. - space_num = idleConf.GetOption( - 'main', 'Indent', 'num-spaces', default=4, type='int') - self.space_num.set(space_num) + if not idleConf.userCfg['keys'].has_section(keyset_name): + idleConf.userCfg['keys'].add_section(keyset_name) + for event in keyset: + value = keyset[event] + idleConf.userCfg['keys'].SetOption(keyset_name, event, value) - def var_changed_space_num(self, *params): - "Store change to indentation size." - value = self.space_num.get() - changes.add_option('main', 'Indent', 'num-spaces', value) + def delete_custom_keys(self): + """Handle event to delete a custom key set. + + Applying the delete deactivates the current configuration and + reverts to the default. The custom key set is permanently + deleted from the config file. + """ + keyset_name = self.custom_name.get() + delmsg = 'Are you sure you wish to delete the key set %r ?' + if not tkMessageBox.askyesno( + 'Delete Key Set', delmsg % keyset_name, parent=self): + return + self.cd.deactivate_current_config() + # Remove key set from changes, config, and file. + changes.delete_section('keys', keyset_name) + # Reload user key set list. + item_list = idleConf.GetSectionList('user', 'keys') + item_list.sort() + if not item_list: + self.custom_keyset_on['state'] = DISABLED + self.customlist.SetMenu(item_list, '- no custom keys -') + else: + self.customlist.SetMenu(item_list, item_list[0]) + # Revert to default key set. + self.keyset_source.set(idleConf.defaultCfg['main'] + .Get('Keys', 'default')) + self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name') + or idleConf.default_keys()) + # User can't back out of these changes, they must be applied now. + changes.save_all() + self.cd.save_all_changed_extensions() + self.cd.activate_config_changes() + self.set_keys_type() class GenPage(Frame): diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py index b07a65cf56..964784508f 100644 --- a/Lib/idlelib/idle_test/test_configdialog.py +++ b/Lib/idlelib/idle_test/test_configdialog.py @@ -232,22 +232,27 @@ class HighlightTest(unittest.TestCase): changes.clear() -class KeyTest(unittest.TestCase): +class KeysPageTest(unittest.TestCase): + """Test that keys tab widgets enable users to make changes. + + Test that widget actions set vars, that var changes add + options to changes and that key sets works correctly. + """ @classmethod def setUpClass(cls): - d = dialog - dialog.note.select(d.keyspage) - d.set_keys_type = Func() - d.load_keys_list = Func() + page = cls.page = dialog.keyspage + dialog.note.select(page) + page.set_keys_type = Func() + page.load_keys_list = Func() @classmethod def tearDownClass(cls): - d = dialog - del d.set_keys_type, d.load_keys_list + page = cls.page + del page.set_keys_type, page.load_keys_list def setUp(self): - d = dialog + d = self.page # The following is needed for test_load_key_cfg, _delete_custom_keys. # This may indicate a defect in some test or function. for section in idleConf.GetSectionList('user', 'keys'): @@ -258,7 +263,7 @@ class KeyTest(unittest.TestCase): def test_load_key_cfg(self): tracers.detach() - d = dialog + d = self.page eq = self.assertEqual # Use builtin keyset with no user keysets created. @@ -300,7 +305,7 @@ class KeyTest(unittest.TestCase): def test_keyset_source(self): eq = self.assertEqual - d = dialog + d = self.page # Test these separately. d.var_changed_builtin_name = Func() d.var_changed_custom_name = Func() @@ -321,7 +326,7 @@ class KeyTest(unittest.TestCase): def test_builtin_name(self): eq = self.assertEqual - d = dialog + d = self.page idleConf.userCfg['main'].remove_section('Keys') item_list = ['IDLE Classic Windows', 'IDLE Classic OSX', 'IDLE Modern UNIX'] @@ -352,7 +357,7 @@ class KeyTest(unittest.TestCase): eq(d.load_keys_list.args, ('IDLE Classic OSX', )) def test_custom_name(self): - d = dialog + d = self.page # If no selections, doesn't get added. d.customlist.SetMenu([], '- no custom keys -') @@ -366,7 +371,7 @@ class KeyTest(unittest.TestCase): self.assertEqual(d.load_keys_list.called, 1) def test_keybinding(self): - d = dialog + d = self.page d.custom_name.set('my custom keys') d.bindingslist.delete(0, 'end') d.bindingslist.insert(0, 'copy') @@ -386,7 +391,7 @@ class KeyTest(unittest.TestCase): def test_set_keys_type(self): eq = self.assertEqual - d = dialog + d = self.page del d.set_keys_type # Builtin keyset selected. @@ -407,7 +412,7 @@ class KeyTest(unittest.TestCase): def test_get_new_keys(self): eq = self.assertEqual - d = dialog + d = self.page orig_getkeysdialog = configdialog.GetKeysDialog gkd = configdialog.GetKeysDialog = Func(return_self=True) gnkn = d.get_new_keys_name = Func() @@ -456,7 +461,7 @@ class KeyTest(unittest.TestCase): def test_get_new_keys_name(self): orig_sectionname = configdialog.SectionName sn = configdialog.SectionName = Func(return_self=True) - d = dialog + d = self.page sn.result = 'New Keys' self.assertEqual(d.get_new_keys_name(''), 'New Keys') @@ -464,7 +469,7 @@ class KeyTest(unittest.TestCase): configdialog.SectionName = orig_sectionname def test_save_as_new_key_set(self): - d = dialog + d = self.page gnkn = d.get_new_keys_name = Func() d.keyset_source.set(True) @@ -482,7 +487,7 @@ class KeyTest(unittest.TestCase): del d.get_new_keys_name def test_on_bindingslist_select(self): - d = dialog + d = self.page b = d.bindingslist b.delete(0, 'end') b.insert(0, 'copy') @@ -504,7 +509,7 @@ class KeyTest(unittest.TestCase): def test_create_new_key_set_and_save_new_key_set(self): eq = self.assertEqual - d = dialog + d = self.page # Use default as previously active keyset. d.keyset_source.set(True) @@ -535,7 +540,7 @@ class KeyTest(unittest.TestCase): def test_load_keys_list(self): eq = self.assertEqual - d = dialog + d = self.page gks = idleConf.GetKeySet = Func() del d.load_keys_list b = d.bindingslist @@ -578,11 +583,11 @@ class KeyTest(unittest.TestCase): def test_delete_custom_keys(self): eq = self.assertEqual - d = dialog + d = self.page d.button_delete_custom_keys['state'] = NORMAL yesno = configdialog.tkMessageBox.askyesno = Func() - d.deactivate_current_config = Func() - d.activate_config_changes = Func() + dialog.deactivate_current_config = Func() + dialog.activate_config_changes = Func() keyset_name = 'spam key set' idleConf.userCfg['keys'].SetOption(keyset_name, 'name', 'value') @@ -598,8 +603,8 @@ class KeyTest(unittest.TestCase): eq(yesno.called, 1) eq(keyspage[keyset_name], {'option': 'True'}) eq(idleConf.GetSectionList('user', 'keys'), ['spam key set']) - eq(d.deactivate_current_config.called, 0) - eq(d.activate_config_changes.called, 0) + eq(dialog.deactivate_current_config.called, 0) + eq(dialog.activate_config_changes.called, 0) eq(d.set_keys_type.called, 0) # Confirm deletion. @@ -610,11 +615,11 @@ class KeyTest(unittest.TestCase): eq(idleConf.GetSectionList('user', 'keys'), []) eq(d.custom_keyset_on['state'], DISABLED) eq(d.custom_name.get(), '- no custom keys -') - eq(d.deactivate_current_config.called, 1) - eq(d.activate_config_changes.called, 1) + eq(dialog.deactivate_current_config.called, 1) + eq(dialog.activate_config_changes.called, 1) eq(d.set_keys_type.called, 1) - del d.activate_config_changes, d.deactivate_current_config + del dialog.activate_config_changes, dialog.deactivate_current_config del configdialog.tkMessageBox.askyesno diff --git a/Misc/NEWS.d/next/IDLE/2017-08-15-12-58-23.bpo-31205.iuziZ5.rst b/Misc/NEWS.d/next/IDLE/2017-08-15-12-58-23.bpo-31205.iuziZ5.rst new file mode 100644 index 0000000000..007a2e2fc5 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2017-08-15-12-58-23.bpo-31205.iuziZ5.rst @@ -0,0 +1,2 @@ +IDLE: Factor KeysPage(Frame) class from ConfigDialog. The slightly +modified tests continue to pass. Patch by Cheryl Sabella. -- 2.40.0