configHandler.py 31.5 KB
Newer Older
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
1 2 3 4 5 6
"""Provides access to stored IDLE configuration information.

Refer to the comments at the beginning of config-main.def for a description of
the available configuration files and the design implemented to update user
configuration information.  In particular, user configuration choices which
duplicate the defaults will be removed from the user's configuration files,
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
7
and if a file becomes empty, it will be deleted.
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
8 9 10 11 12 13 14 15 16 17

The contents of the user files may be altered using the Options/Configure IDLE
menu to access the configuration GUI (configDialog.py), or manually.

Throughout this module there is an emphasis on returning useable defaults
when a problem occurs in returning a requested configuration value back to
idle. This is to allow IDLE to continue to function in spite of errors in
the retrieval of config information. When a default is returned instead of
a requested config value, a message is printed to stderr to aid in
configuration problem notification and resolution.
18
"""
19 20
# TODOs added Oct 2014, tjr

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
21 22
import os
import sys
23

24
from configparser import ConfigParser
25
from tkinter import TkVersion
26
from tkinter.font import Font, nametofont
27

28 29 30 31 32
class InvalidConfigType(Exception): pass
class InvalidConfigSet(Exception): pass
class InvalidFgBg(Exception): pass
class InvalidTheme(Exception): pass

33 34 35 36 37 38 39 40
class IdleConfParser(ConfigParser):
    """
    A ConfigParser specialised for idle configuration file handling
    """
    def __init__(self, cfgFile, cfgDefaults=None):
        """
        cfgFile - string, fully specified configuration file name
        """
41
        self.file = cfgFile
42
        ConfigParser.__init__(self, defaults=cfgDefaults, strict=False)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
43

44
    def Get(self, section, option, type=None, default=None, raw=False):
45 46 47 48
        """
        Get an option value for given section/option or return default.
        If type is specified, return as type.
        """
49 50 51
        # TODO Use default as fallback, at least if not None
        # Should also print Warning(file, section, option).
        # Currently may raise ValueError
52 53
        if not self.has_option(section, option):
            return default
54
        if type == 'bool':
55
            return self.getboolean(section, option)
56
        elif type == 'int':
57
            return self.getint(section, option)
58
        else:
59
            return self.get(section, option, raw=raw)
60

61 62
    def GetOptionList(self, section):
        "Return a list of options for given section, else []."
63
        if self.has_section(section):
64 65 66 67 68
            return self.options(section)
        else:  #return a default value
            return []

    def Load(self):
69
        "Load the configuration file from disk."
70
        self.read(self.file)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
71

72 73
class IdleUserConfParser(IdleConfParser):
    """
74
    IdleConfigParser specialised for user configuration handling.
75
    """
76

77 78
    def AddSection(self, section):
        "If section doesn't exist, add it."
79 80
        if not self.has_section(section):
            self.add_section(section)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
81

82
    def RemoveEmptySections(self):
83
        "Remove any sections that have no options."
84 85
        for section in self.sections():
            if not self.GetOptionList(section):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
86 87
                self.remove_section(section)

88
    def IsEmpty(self):
89
        "Return True if no sections after removing empty sections."
90
        self.RemoveEmptySections()
91
        return not self.sections()
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
92

93 94 95 96
    def RemoveOption(self, section, option):
        """Return True if option is removed from section, else False.

        False if either section does not exist or did not have option.
97 98
        """
        if self.has_section(section):
99 100
            return self.remove_option(section, option)
        return False
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
101

102 103 104 105
    def SetOption(self, section, option, value):
        """Return True if option is added or changed to value, else False.

        Add section if required.  False means option already had value.
106
        """
107 108 109
        if self.has_option(section, option):
            if self.get(section, option) == value:
                return False
110
            else:
111 112
                self.set(section, option, value)
                return True
113 114 115
        else:
            if not self.has_section(section):
                self.add_section(section)
116 117
            self.set(section, option, value)
            return True
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
118

119
    def RemoveFile(self):
120
        "Remove user config file self.file from disk if it exists."
121
        if os.path.exists(self.file):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
122 123
            os.remove(self.file)

124
    def Save(self):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
125 126 127 128 129
        """Update user configuration file.

        Remove empty sections. If resulting config isn't empty, write the file
        to disk. If config is empty, remove the file from disk if it exists.

130
        """
131
        if not self.IsEmpty():
132 133 134
            fname = self.file
            try:
                cfgFile = open(fname, 'w')
135
            except OSError:
Christian Heimes's avatar
Christian Heimes committed
136
                os.unlink(fname)
137
                cfgFile = open(fname, 'w')
138 139
            with cfgFile:
                self.write(cfgFile)
140
        else:
141
            self.RemoveFile()
142 143

class IdleConf:
144 145 146 147 148 149 150 151 152
    """Hold config parsers for all idle config files in singleton instance.

    Default config files, self.defaultCfg --
        for config_type in self.config_types:
            (idle install dir)/config-{config-type}.def

    User config files, self.userCfg --
        for config_type in self.config_types:
        (user home dir)/.idlerc/config-{config-type}.cfg
153 154
    """
    def __init__(self):
155 156 157 158
        self.config_types = ('main', 'extensions', 'highlight', 'keys')
        self.defaultCfg = {}
        self.userCfg = {}
        self.cfg = {}  # TODO use to select userCfg vs defaultCfg
159 160
        self.CreateConfigHandlers()
        self.LoadCfgFiles()
161

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
162

163
    def CreateConfigHandlers(self):
164
        "Populate default and user config parser dictionaries."
165 166
        #build idle install path
        if __name__ != '__main__': # we were imported
167
            idleDir=os.path.dirname(__file__)
168
        else: # we were exec'ed (for testing only)
169 170
            idleDir=os.path.abspath(sys.path[0])
        userDir=self.GetUserCfgDir()
171 172 173 174 175 176 177 178 179 180 181 182

        defCfgFiles = {}
        usrCfgFiles = {}
        # TODO eliminate these temporaries by combining loops
        for cfgType in self.config_types: #build config file names
            defCfgFiles[cfgType] = os.path.join(
                    idleDir, 'config-' + cfgType + '.def')
            usrCfgFiles[cfgType] = os.path.join(
                    userDir, 'config-' + cfgType + '.cfg')
        for cfgType in self.config_types: #create config parsers
            self.defaultCfg[cfgType] = IdleConfParser(defCfgFiles[cfgType])
            self.userCfg[cfgType] = IdleUserConfParser(usrCfgFiles[cfgType])
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
183

184
    def GetUserCfgDir(self):
185
        """Return a filesystem directory for storing user config files.
Tim Peters's avatar
Tim Peters committed
186

187
        Creates it if required.
188
        """
189 190 191
        cfgDir = '.idlerc'
        userDir = os.path.expanduser('~')
        if userDir != '~': # expanduser() found user home dir
192
            if not os.path.exists(userDir):
193 194
                warn = ('\n Warning: os.path.expanduser("~") points to\n ' +
                        userDir + ',\n but the path does not exist.')
Christian Heimes's avatar
Christian Heimes committed
195
                try:
196
                    print(warn, file=sys.stderr)
197
                except OSError:
Christian Heimes's avatar
Christian Heimes committed
198
                    pass
199 200 201 202 203
                userDir = '~'
        if userDir == "~": # still no path to home!
            # traditionally IDLE has defaulted to os.getcwd(), is this adequate?
            userDir = os.getcwd()
        userDir = os.path.join(userDir, cfgDir)
204
        if not os.path.exists(userDir):
205
            try:
206
                os.mkdir(userDir)
207
            except OSError:
208 209
                warn = ('\n Warning: unable to create user config directory\n' +
                        userDir + '\n Check path and permissions.\n Exiting!\n')
210
                print(warn, file=sys.stderr)
211
                raise SystemExit
212
        # TODO continue without userDIr instead of exit
213
        return userDir
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
214

215
    def GetOption(self, configType, section, option, default=None, type=None,
216
                  warn_on_default=True, raw=False):
217 218 219 220 221 222 223
        """Return a value for configType section option, or default.

        If type is not None, return a value of that type.  Also pass raw
        to the config parser.  First try to return a valid value
        (including type) from a user configuration. If that fails, try
        the default configuration. If that fails, return default, with a
        default of None.
224

225 226
        Warn if either user or default configurations have an invalid value.
        Warn if default is returned and warn_on_default is True.
227
        """
228
        try:
229
            if self.userCfg[configType].has_option(section, option):
230 231 232 233 234
                return self.userCfg[configType].Get(section, option,
                                                    type=type, raw=raw)
        except ValueError:
            warning = ('\n Warning: configHandler.py - IdleConf.GetOption -\n'
                       ' invalid %r value for configuration option %r\n'
235
                       ' from section %r: %r' %
236
                       (type, option, section,
237
                       self.userCfg[configType].Get(section, option, raw=raw)))
238
            try:
239
                print(warning, file=sys.stderr)
240
            except OSError:
241 242 243
                pass
        try:
            if self.defaultCfg[configType].has_option(section,option):
244 245
                return self.defaultCfg[configType].Get(
                        section, option, type=type, raw=raw)
246 247 248 249 250 251 252
        except ValueError:
            pass
        #returning default, print warning
        if warn_on_default:
            warning = ('\n Warning: configHandler.py - IdleConf.GetOption -\n'
                       ' problem retrieving configuration option %r\n'
                       ' from section %r.\n'
253
                       ' returning default value: %r' %
254 255
                       (option, section, default))
            try:
256
                print(warning, file=sys.stderr)
257
            except OSError:
258 259
                pass
        return default
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
260

261
    def SetOption(self, configType, section, option, value):
262
        """Set section option to value in user config file."""
263 264
        self.userCfg[configType].SetOption(section, option, value)

265
    def GetSectionList(self, configSet, configType):
266 267
        """Return sections for configSet configType configuration.

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
268
        configSet must be either 'user' or 'default'
269
        configType must be in self.config_types.
270
        """
271
        if not (configType in self.config_types):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
272
            raise InvalidConfigType('Invalid configType specified')
273
        if configSet == 'user':
274
            cfgParser = self.userCfg[configType]
275 276 277
        elif configSet == 'default':
            cfgParser=self.defaultCfg[configType]
        else:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
278
            raise InvalidConfigSet('Invalid configSet specified')
279
        return cfgParser.sections()
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
280

281
    def GetHighlight(self, theme, element, fgBg=None):
282
        """Return individual theme element highlight color(s).
283

284 285 286 287 288
        fgBg - string ('fg' or 'bg') or None.
        If None, return a dictionary containing fg and bg colors with
        keys 'foreground' and 'background'.  Otherwise, only return
        fg or bg color, as specified.  Colors are intended to be
        appropriate for passing to Tkinter in, e.g., a tag_config call).
289
        """
290
        if self.defaultCfg['highlight'].has_section(theme):
291
            themeDict = self.GetThemeDict('default', theme)
292
        else:
293 294
            themeDict = self.GetThemeDict('user', theme)
        fore = themeDict[element + '-foreground']
295
        if element == 'cursor':  # There is no config value for cursor bg
296
            back = themeDict['normal-background']
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
297
        else:
298 299
            back = themeDict[element + '-background']
        highlight = {"foreground": fore, "background": back}
300
        if not fgBg:  # Return dict of both colors
301
            return highlight
302
        else:  # Return specified color only
303 304 305 306
            if fgBg == 'fg':
                return highlight["foreground"]
            if fgBg == 'bg':
                return highlight["background"]
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
307
            else:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
308
                raise InvalidFgBg('Invalid fgBg specified')
309

310 311 312
    def GetThemeDict(self, type, themeName):
        """Return {option:value} dict for elements in themeName.

313 314
        type - string, 'default' or 'user' theme type
        themeName - string, theme name
315 316
        Values are loaded over ultimate fallback defaults to guarantee
        that all theme elements are present in a newly created theme.
317 318
        """
        if type == 'user':
319
            cfgParser = self.userCfg['highlight']
320
        elif type == 'default':
321
            cfgParser = self.defaultCfg['highlight']
322
        else:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
323
            raise InvalidTheme('Invalid theme type specified')
324 325 326 327 328
        # Provide foreground and background colors for each theme
        # element (other than cursor) even though some values are not
        # yet used by idle, to allow for their use in the future.
        # Default values are generally black and white.
        # TODO copy theme from a class attribute.
329
        theme ={'normal-foreground':'#000000',
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
330 331 332
                'normal-background':'#ffffff',
                'keyword-foreground':'#000000',
                'keyword-background':'#ffffff',
333 334
                'builtin-foreground':'#000000',
                'builtin-background':'#ffffff',
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
335 336
                'comment-foreground':'#000000',
                'comment-background':'#ffffff',
337 338
                'string-foreground':'#000000',
                'string-background':'#ffffff',
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
339
                'definition-foreground':'#000000',
340 341 342 343 344 345 346 347
                'definition-background':'#ffffff',
                'hilite-foreground':'#000000',
                'hilite-background':'gray',
                'break-foreground':'#ffffff',
                'break-background':'#000000',
                'hit-foreground':'#ffffff',
                'hit-background':'#000000',
                'error-foreground':'#ffffff',
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
348 349 350
                'error-background':'#000000',
                #cursor (only foreground can be set)
                'cursor-foreground':'#000000',
351 352 353 354 355 356 357
                #shell window
                'stdout-foreground':'#000000',
                'stdout-background':'#ffffff',
                'stderr-foreground':'#000000',
                'stderr-background':'#ffffff',
                'console-foreground':'#000000',
                'console-background':'#ffffff' }
358
        for element in theme:
359
            if not cfgParser.has_option(themeName, element):
360 361
                # Print warning that will return a default color
                warning = ('\n Warning: configHandler.IdleConf.GetThemeDict'
362 363
                           ' -\n problem retrieving theme element %r'
                           '\n from theme %r.\n'
364
                           ' returning default color: %r' %
365
                           (element, themeName, theme[element]))
Christian Heimes's avatar
Christian Heimes committed
366
                try:
367
                    print(warning, file=sys.stderr)
368
                except OSError:
Christian Heimes's avatar
Christian Heimes committed
369
                    pass
370 371
            theme[element] = cfgParser.Get(
                    themeName, element, default=theme[element])
372
        return theme
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
373

374
    def CurrentTheme(self):
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400
        """Return the name of the currently active text color theme.

        idlelib.config-main.def includes this section
        [Theme]
        default= 1
        name= IDLE Classic
        name2=
        # name2 set in user config-main.cfg for themes added after 2015 Oct 1

        Item name2 is needed because setting name to a new builtin
        causes older IDLEs to display multiple error messages or quit.
        See https://bugs.python.org/issue25313.
        When default = True, name2 takes precedence over name,
        while older IDLEs will just use name.
        """
        default = self.GetOption('main', 'Theme', 'default',
                                 type='bool', default=True)
        if default:
            theme = self.GetOption('main', 'Theme', 'name2', default='')
        if default and not theme or not default:
            theme = self.GetOption('main', 'Theme', 'name', default='')
        source = self.defaultCfg if default else self.userCfg
        if source['highlight'].has_section(theme):
                return theme
        else:    
            return "IDLE Classic"
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
401

402
    def CurrentKeys(self):
403 404
        "Return the name of the currently active key set."
        return self.GetOption('main', 'Keys', 'name', default='')
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
405

406
    def GetExtensions(self, active_only=True, editor_only=False, shell_only=False):
407 408 409 410 411
        """Return extensions in default and user config-extensions files.

        If active_only True, only return active (enabled) extensions
        and optionally only editor or shell extensions.
        If active_only False, return all extensions.
412
        """
413 414 415 416
        extns = self.RemoveKeyBindNames(
                self.GetSectionList('default', 'extensions'))
        userExtns = self.RemoveKeyBindNames(
                self.GetSectionList('user', 'extensions'))
417 418
        for extn in userExtns:
            if extn not in extns: #user has added own extension
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
419
                extns.append(extn)
420
        if active_only:
421
            activeExtns = []
422
            for extn in extns:
423 424
                if self.GetOption('extensions', extn, 'enable', default=True,
                                  type='bool'):
425
                    #the extension is enabled
426
                    if editor_only or shell_only:  # TODO if both, contradictory
427 428 429 430 431 432 433 434 435 436
                        if editor_only:
                            option = "enable_editor"
                        else:
                            option = "enable_shell"
                        if self.GetOption('extensions', extn,option,
                                          default=True, type='bool',
                                          warn_on_default=False):
                            activeExtns.append(extn)
                    else:
                        activeExtns.append(extn)
437 438
            return activeExtns
        else:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
439
            return extns
440

441 442 443 444 445
    def RemoveKeyBindNames(self, extnNameList):
        "Return extnNameList with keybinding section names removed."
        # TODO Easier to return filtered copy with list comp
        names = extnNameList
        kbNameIndicies = []
446
        for name in names:
447
            if name.endswith(('_bindings', '_cfgBindings')):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
448
                kbNameIndicies.append(names.index(name))
449
        kbNameIndicies.sort(reverse=True)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
450
        for index in kbNameIndicies: #delete each keybinding section name
451 452
            del(names[index])
        return names
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
453

454 455 456 457 458
    def GetExtnNameForEvent(self, virtualEvent):
        """Return the name of the extension binding virtualEvent, or None.

        virtualEvent - string, name of the virtual event to test for,
                       without the enclosing '<< >>'
459
        """
460 461
        extName = None
        vEvent = '<<' + virtualEvent + '>>'
462
        for extn in self.GetExtensions(active_only=0):
463
            for event in self.GetExtensionKeys(extn):
464
                if event == vEvent:
465
                    extName = extn  # TODO return here?
466
        return extName
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
467

468 469 470 471 472 473
    def GetExtensionKeys(self, extensionName):
        """Return dict: {configurable extensionName event : active keybinding}.

        Events come from default config extension_cfgBindings section.
        Keybindings come from GetCurrentKeySet() active key dict,
        where previously used bindings are disabled.
474
        """
475 476 477
        keysName = extensionName + '_cfgBindings'
        activeKeys = self.GetCurrentKeySet()
        extKeys = {}
478
        if self.defaultCfg['extensions'].has_section(keysName):
479
            eventNames = self.defaultCfg['extensions'].GetOptionList(keysName)
480
            for eventName in eventNames:
481 482 483
                event = '<<' + eventName + '>>'
                binding = activeKeys[event]
                extKeys[event] = binding
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
484 485
        return extKeys

486
    def __GetRawExtensionKeys(self,extensionName):
487 488 489 490 491
        """Return dict {configurable extensionName event : keybinding list}.

        Events come from default config extension_cfgBindings section.
        Keybindings list come from the splitting of GetOption, which
        tries user config before default config.
492
        """
493 494
        keysName = extensionName+'_cfgBindings'
        extKeys = {}
495
        if self.defaultCfg['extensions'].has_section(keysName):
496
            eventNames = self.defaultCfg['extensions'].GetOptionList(keysName)
497
            for eventName in eventNames:
498 499 500 501
                binding = self.GetOption(
                        'extensions', keysName, eventName, default='').split()
                event = '<<' + eventName + '>>'
                extKeys[event] = binding
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
502 503
        return extKeys

504 505 506 507 508 509
    def GetExtensionBindings(self, extensionName):
        """Return dict {extensionName event : active or defined keybinding}.

        Augment self.GetExtensionKeys(extensionName) with mapping of non-
        configurable events (from default config) to GetOption splits,
        as in self.__GetRawExtensionKeys.
510
        """
511 512
        bindsName = extensionName + '_bindings'
        extBinds = self.GetExtensionKeys(extensionName)
513 514
        #add the non-configurable bindings
        if self.defaultCfg['extensions'].has_section(bindsName):
515
            eventNames = self.defaultCfg['extensions'].GetOptionList(bindsName)
516
            for eventName in eventNames:
517 518 519 520
                binding = self.GetOption(
                        'extensions', bindsName, eventName, default='').split()
                event = '<<' + eventName + '>>'
                extBinds[event] = binding
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
521 522 523

        return extBinds

524
    def GetKeyBinding(self, keySetName, eventStr):
525 526 527 528
        """Return the keybinding list for keySetName eventStr.

        keySetName - name of key binding set (config-keys section).
        eventStr - virtual event, including brackets, as in '<<event>>'.
529
        """
530 531
        eventName = eventStr[2:-2] #trim off the angle brackets
        binding = self.GetOption('keys', keySetName, eventName, default='').split()
532 533
        return binding

534
    def GetCurrentKeySet(self):
535
        "Return CurrentKeys with 'darwin' modifications."
536 537
        result = self.GetKeySet(self.CurrentKeys())

538 539 540 541
        if sys.platform == "darwin":
            # OS X Tk variants do not support the "Alt" keyboard modifier.
            # So replace all keybingings that use "Alt" with ones that
            # use the "Option" keyboard modifier.
542
            # TODO (Ned?): the "Option" modifier does not work properly for
543 544
            #        Cocoa Tk and XQuartz Tk so we should not use it
            #        in default OS X KeySets.
545 546 547 548 549 550
            for k, v in result.items():
                v2 = [ x.replace('<Alt-', '<Option-') for x in v ]
                if v != v2:
                    result[k] = v2

        return result
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
551

552 553 554 555 556
    def GetKeySet(self, keySetName):
        """Return event-key dict for keySetName core plus active extensions.

        If a binding defined in an extension is already in use, the
        extension binding is disabled by being set to ''
557
        """
558 559
        keySet = self.GetCoreKeys(keySetName)
        activeExtns = self.GetExtensions(active_only=1)
560
        for extn in activeExtns:
561
            extKeys = self.__GetRawExtensionKeys(extn)
562
            if extKeys: #the extension defines keybindings
563
                for event in extKeys:
564
                    if extKeys[event] in keySet.values():
565
                        #the binding is already in use
566 567
                        extKeys[event] = '' #disable this binding
                    keySet[event] = extKeys[event] #add binding
568 569
        return keySet

570 571 572 573 574
    def IsCoreBinding(self, virtualEvent):
        """Return True if the virtual event is one of the core idle key events.

        virtualEvent - string, name of the virtual event to test for,
                       without the enclosing '<< >>'
575
        """
576
        return ('<<'+virtualEvent+'>>') in self.GetCoreKeys()
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
577

578 579 580
# TODO make keyBindins a file or class attribute used for test above
# and copied in function below

581
    def GetCoreKeys(self, keySetName=None):
582 583 584 585 586 587 588
        """Return dict of core virtual-key keybindings for keySetName.

        The default keySetName None corresponds to the keyBindings base
        dict. If keySetName is not None, bindings from the config
        file(s) are loaded _over_ these defaults, so if there is a
        problem getting any core binding there will be an 'ultimate last
        resort fallback' to the CUA-ish bindings defined here.
589 590
        """
        keyBindings={
591 592 593
            '<<copy>>': ['<Control-c>', '<Control-C>'],
            '<<cut>>': ['<Control-x>', '<Control-X>'],
            '<<paste>>': ['<Control-v>', '<Control-V>'],
594 595 596 597
            '<<beginning-of-line>>': ['<Control-a>', '<Home>'],
            '<<center-insert>>': ['<Control-l>'],
            '<<close-all-windows>>': ['<Control-q>'],
            '<<close-window>>': ['<Alt-F4>'],
598
            '<<do-nothing>>': ['<Control-x>'],
599 600
            '<<end-of-file>>': ['<Control-d>'],
            '<<python-docs>>': ['<F1>'],
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
601
            '<<python-context-help>>': ['<Shift-F1>'],
602 603 604
            '<<history-next>>': ['<Alt-n>'],
            '<<history-previous>>': ['<Alt-p>'],
            '<<interrupt-execution>>': ['<Control-c>'],
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
605
            '<<view-restart>>': ['<F6>'],
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
606
            '<<restart-shell>>': ['<Control-F6>'],
607 608 609 610 611
            '<<open-class-browser>>': ['<Alt-c>'],
            '<<open-module>>': ['<Alt-m>'],
            '<<open-new-window>>': ['<Control-n>'],
            '<<open-window-from-file>>': ['<Control-o>'],
            '<<plain-newline-and-indent>>': ['<Control-j>'],
612
            '<<print-window>>': ['<Control-p>'],
613 614
            '<<redo>>': ['<Control-y>'],
            '<<remove-selection>>': ['<Escape>'],
615
            '<<save-copy-of-window-as-file>>': ['<Alt-Shift-S>'],
616 617 618 619
            '<<save-window-as-file>>': ['<Alt-s>'],
            '<<save-window>>': ['<Control-s>'],
            '<<select-all>>': ['<Alt-a>'],
            '<<toggle-auto-coloring>>': ['<Control-slash>'],
620 621 622 623 624 625
            '<<undo>>': ['<Control-z>'],
            '<<find-again>>': ['<Control-g>', '<F3>'],
            '<<find-in-files>>': ['<Alt-F3>'],
            '<<find-selection>>': ['<Control-F3>'],
            '<<find>>': ['<Control-f>'],
            '<<replace>>': ['<Control-h>'],
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
626
            '<<goto-line>>': ['<Alt-g>'],
627
            '<<smart-backspace>>': ['<Key-BackSpace>'],
628
            '<<newline-and-indent>>': ['<Key-Return>', '<Key-KP_Enter>'],
629 630 631 632 633 634 635 636
            '<<smart-indent>>': ['<Key-Tab>'],
            '<<indent-region>>': ['<Control-Key-bracketright>'],
            '<<dedent-region>>': ['<Control-Key-bracketleft>'],
            '<<comment-region>>': ['<Alt-Key-3>'],
            '<<uncomment-region>>': ['<Alt-Key-4>'],
            '<<tabify-region>>': ['<Alt-Key-5>'],
            '<<untabify-region>>': ['<Alt-Key-6>'],
            '<<toggle-tabs>>': ['<Alt-Key-t>'],
637 638 639
            '<<change-indentwidth>>': ['<Alt-Key-u>'],
            '<<del-word-left>>': ['<Control-Key-BackSpace>'],
            '<<del-word-right>>': ['<Control-Key-Delete>']
640
            }
641
        if keySetName:
642
            for event in keyBindings:
643
                binding = self.GetKeyBinding(keySetName, event)
644
                if binding:
645
                    keyBindings[event] = binding
646
                else: #we are going to return a default, print warning
647 648 649
                    warning=('\n Warning: configHandler.py - IdleConf.GetCoreKeys'
                               ' -\n problem retrieving key binding for event %r'
                               '\n from key set %r.\n'
650
                               ' returning default value: %r' %
651
                               (event, keySetName, keyBindings[event]))
Christian Heimes's avatar
Christian Heimes committed
652
                    try:
653
                        print(warning, file=sys.stderr)
654
                    except OSError:
Christian Heimes's avatar
Christian Heimes committed
655
                        pass
656
        return keyBindings
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
657

658 659
    def GetExtraHelpSourceList(self, configSet):
        """Return list of extra help sources from a given configSet.
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
660

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
661 662 663 664 665 666
        Valid configSets are 'user' or 'default'.  Return a list of tuples of
        the form (menu_item , path_to_help_file , option), or return the empty
        list.  'option' is the sequence number of the help resource.  'option'
        values determine the position of the menu items on the Help menu,
        therefore the returned list must be sorted by 'option'.

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
667
        """
668 669 670 671 672
        helpSources = []
        if configSet == 'user':
            cfgParser = self.userCfg['main']
        elif configSet == 'default':
            cfgParser = self.defaultCfg['main']
673
        else:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
674
            raise InvalidConfigSet('Invalid configSet specified')
675 676
        options=cfgParser.GetOptionList('HelpFiles')
        for option in options:
677 678 679 680
            value=cfgParser.Get('HelpFiles', option, default=';')
            if value.find(';') == -1: #malformed config entry with no ';'
                menuItem = '' #make these empty
                helpPath = '' #so value won't be added to list
681
            else: #config entry contains ';' as expected
682
                value=value.split(';')
683 684 685 686
                menuItem=value[0].strip()
                helpPath=value[1].strip()
            if menuItem and helpPath: #neither are empty strings
                helpSources.append( (menuItem,helpPath,option) )
687
        helpSources.sort(key=lambda x: x[2])
688 689 690
        return helpSources

    def GetAllExtraHelpSourcesList(self):
691 692 693
        """Return a list of the details of all additional help sources.

        Tuples in the list are those of GetExtraHelpSourceList.
694
        """
695
        allHelpSources = (self.GetExtraHelpSourceList('default') +
696
                self.GetExtraHelpSourceList('user') )
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
697 698
        return allHelpSources

699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715
    def GetFont(self, root, configType, section):
        """Retrieve a font from configuration (font, font-size, font-bold)
        Intercept the special value 'TkFixedFont' and substitute
        the actual font, factoring in some tweaks if needed for
        appearance sakes.

        The 'root' parameter can normally be any valid Tkinter widget.

        Return a tuple (family, size, weight) suitable for passing
        to tkinter.Font
        """
        family = self.GetOption(configType, section, 'font', default='courier')
        size = self.GetOption(configType, section, 'font-size', type='int',
                              default='10')
        bold = self.GetOption(configType, section, 'font-bold', default=0,
                              type='bool')
        if (family == 'TkFixedFont'):
716 717 718 719 720 721 722 723 724 725
            if TkVersion < 8.5:
                family = 'Courier'
            else:
                f = Font(name='TkFixedFont', exists=True, root=root)
                actualFont = Font.actual(f)
                family = actualFont['family']
                size = actualFont['size']
                if size < 0:
                    size = 10  # if font in pixels, ignore actual size
                bold = actualFont['weight']=='bold'
726 727
        return (family, size, 'bold' if bold else 'normal')

728
    def LoadCfgFiles(self):
729
        "Load all configuration files."
730
        for key in self.defaultCfg:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
731 732
            self.defaultCfg[key].Load()
            self.userCfg[key].Load() #same keys
733 734

    def SaveUserCfgFiles(self):
735
        "Write all loaded user configuration files to disk."
736
        for key in self.userCfg:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
737
            self.userCfg[key].Save()
738 739


740 741 742
idleConf = IdleConf()

# TODO Revise test output, write expanded unittest
743 744 745
### module test
if __name__ == '__main__':
    def dumpCfg(cfg):
746
        print('\n', cfg, '\n')
747
        for key in cfg:
748
            sections = cfg[key].sections()
749 750
            print(key)
            print(sections)
751
            for section in sections:
752
                options = cfg[key].options(section)
753 754
                print(section)
                print(options)
755
                for option in options:
756
                    print(option, '=', cfg[key].Get(section, option))
757 758
    dumpCfg(idleConf.defaultCfg)
    dumpCfg(idleConf.userCfg)
759
    print(idleConf.userCfg['main'].Get('Theme', 'name'))
760
    #print idleConf.userCfg['highlight'].GetDefHighlight('Foo','normal')