Coverage for C:\leo.repo\leo-editor\leo\core\leoConfig.py : 36%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#@+leo-ver=5-thin
2#@+node:ekr.20130925160837.11429: * @file leoConfig.py
3"""Configuration classes for Leo."""
4# pylint: disable=unsubscriptable-object
5#@+<< imports >>
6#@+node:ekr.20041227063801: ** << imports >> (leoConfig)
7import os
8import sys
9import re
10import textwrap
11from typing import Any, Dict, List, Tuple, Union
12from leo.core.leoCommands import Commands as Cmdr
13from leo.plugins.mod_scripting import build_rclick_tree
14from leo.core import leoGlobals as g
15#@-<< imports >>
16#@+<< class ParserBaseClass >>
17#@+node:ekr.20041119203941.2: ** << class ParserBaseClass >>
18class ParserBaseClass:
19 """The base class for settings parsers."""
20 #@+<< ParserBaseClass data >>
21 #@+node:ekr.20041121130043: *3* << ParserBaseClass data >>
22 # These are the canonicalized names.
23 # Case is ignored, as are '_' and '-' characters.
24 basic_types = [
25 # Headlines have the form @kind name = var
26 'bool',
27 'color',
28 'directory',
29 'int',
30 'ints',
31 'float',
32 'path',
33 'ratio',
34 'string',
35 'strings',
36 ]
37 control_types = [
38 'buttons',
39 'commands',
40 'data',
41 'enabledplugins',
42 'font',
43 'ifenv',
44 'ifhostname',
45 'ifplatform',
46 'ignore',
47 'menus',
48 'mode',
49 'menuat',
50 'openwith',
51 'outlinedata',
52 'popup',
53 'settings',
54 'shortcuts',
55 ]
56 # Keys are settings names, values are (type,value) tuples.
57 settingsDict: Dict[str, Tuple[str, Union[g.TypedDict, g.GeneralSetting]]] = {}
58 #@-<< ParserBaseClass data >>
59 #@+others
60 #@+node:ekr.20041119204700: *3* pbc.ctor
61 #@@nobeautify
63 def __init__ (self,c,localFlag):
64 """Ctor for the ParserBaseClass class."""
65 self.c = c
66 self.clipBoard = []
67 self.localFlag = localFlag
68 # True if this is the .leo file being opened,
69 # as opposed to myLeoSettings.leo or leoSettings.leo.
70 self.shortcutsDict = g.TypedDict( # was TypedDictOfLists.
71 name='parser.shortcutsDict',
72 keyType=type('shortcutName'),
73 valType=g.BindingInfo,
74 )
75 self.openWithList = []
76 # A list of dicts containing 'name','shortcut','command' keys.
77 # Keys are canonicalized names.
78 self.dispatchDict = {
79 'bool': self.doBool,
80 'buttons': self.doButtons, # New in 4.4.4
81 'color': self.doColor,
82 'commands': self.doCommands, # New in 4.4.8.
83 'data': self.doData, # New in 4.4.6
84 'directory': self.doDirectory,
85 'enabledplugins': self.doEnabledPlugins,
86 'font': self.doFont,
87 'ifenv': self.doIfEnv, # New in 5.2 b1.
88 'ifhostname': self.doIfHostname,
89 'ifplatform': self.doIfPlatform,
90 'ignore': self.doIgnore,
91 'int': self.doInt,
92 'ints': self.doInts,
93 'float': self.doFloat,
94 'menus': self.doMenus, # New in 4.4.4
95 'menuat': self.doMenuat,
96 'popup': self.doPopup, # New in 4.4.8
97 'mode': self.doMode, # New in 4.4b1.
98 'openwith': self.doOpenWith, # New in 4.4.3 b1.
99 'outlinedata': self.doOutlineData, # New in 4.11.1.
100 'path': self.doPath,
101 'ratio': self.doRatio,
102 'shortcuts': self.doShortcuts,
103 'string': self.doString,
104 'strings': self.doStrings,
105 }
106 self.debug_count = 0
107 #@+node:ekr.20080514084054.4: *3* pbc.computeModeName
108 def computeModeName(self, name):
109 s = name.strip().lower()
110 j = s.find(' ')
111 if j > -1:
112 s = s[:j]
113 if s.endswith('mode'):
114 s = s[:-4].strip()
115 if s.endswith('-'):
116 s = s[:-1]
117 i = s.find('::')
118 if i > -1:
119 # The actual mode name is everything up to the "::"
120 # The prompt is everything after the prompt.
121 s = s[:i]
122 modeName = s + '-mode'
123 return modeName
124 #@+node:ekr.20060102103625: *3* pbc.createModeCommand
125 def createModeCommand(self, modeName, name, modeDict):
126 modeName = 'enter-' + modeName.replace(' ', '-')
127 i = name.find('::')
128 if i > -1:
129 # The prompt is everything after the '::'
130 prompt = name[i + 2 :].strip()
131 modeDict['*command-prompt*'] = g.BindingInfo(kind=prompt)
132 # Save the info for k.finishCreate and k.makeAllBindings.
133 d = g.app.config.modeCommandsDict
134 # New in 4.4.1 b2: silently allow redefinitions of modes.
135 d[modeName] = modeDict
136 #@+node:ekr.20041120103012: *3* pbc.error
137 def error(self, s):
138 g.pr(s)
139 # Does not work at present because we are using a null Gui.
140 g.blue(s)
141 #@+node:ekr.20041120094940: *3* pbc.kind handlers
142 #@+node:ekr.20041120094940.1: *4* pbc.doBool
143 def doBool(self, p, kind, name, val):
144 if val in ('True', 'true', '1'):
145 self.set(p, kind, name, True)
146 elif val in ('False', 'false', '0'):
147 self.set(p, kind, name, False)
148 else:
149 self.valueError(p, kind, name, val)
150 #@+node:ekr.20070925144337: *4* pbc.doButtons
151 def doButtons(self, p, kind, name, val):
152 """Create buttons for each @button node in an @buttons tree."""
153 c, tag = self.c, '@button'
154 aList, seen = [], []
155 after = p.nodeAfterTree()
156 while p and p != after:
157 if p.v in seen:
158 p.moveToNodeAfterTree()
159 elif p.isAtIgnoreNode():
160 seen.append(p.v)
161 p.moveToNodeAfterTree()
162 else:
163 seen.append(p.v)
164 if g.match_word(p.h, 0, tag):
165 # We can not assume that p will be valid when it is used.
166 script = g.getScript(
167 c,
168 p,
169 useSelectedText=False,
170 forcePythonSentinels=True,
171 useSentinels=True)
172 # #2011: put rclicks in aList. Do not inject into command_p.
173 command_p = p.copy()
174 rclicks = build_rclick_tree(command_p, top_level=True)
175 aList.append((command_p, script, rclicks))
176 p.moveToThreadNext()
177 # This setting is handled differently from most other settings,
178 # because the last setting must be retrieved before any commander exists.
179 if aList:
180 g.app.config.atCommonButtonsList.extend(aList)
181 # Bug fix: 2011/11/24: Extend the list, don't replace it.
182 g.app.config.buttonsFileName = (
183 c.shortFileName() if c else '<no settings file>'
184 )
185 #@+node:ekr.20041120094940.2: *4* pbc.doColor
186 def doColor(self, p, kind, name, val):
187 # At present no checking is done.
188 val = val.lstrip('"').rstrip('"')
189 val = val.lstrip("'").rstrip("'")
190 self.set(p, kind, name, val)
191 #@+node:ekr.20080312071248.6: *4* pbc.doCommands
192 def doCommands(self, p, kind, name, val):
193 """Handle an @commands tree."""
194 c = self.c
195 aList = []
196 tag = '@command'
197 seen = []
198 after = p.nodeAfterTree()
199 while p and p != after:
200 if p.v in seen:
201 p.moveToNodeAfterTree()
202 elif p.isAtIgnoreNode():
203 seen.append(p.v)
204 p.moveToNodeAfterTree()
205 else:
206 seen.append(p.v)
207 if g.match_word(p.h, 0, tag):
208 # We can not assume that p will be valid when it is used.
209 script = g.getScript(c, p,
210 useSelectedText=False,
211 forcePythonSentinels=True,
212 useSentinels=True)
213 aList.append((p.copy(), script),)
214 p.moveToThreadNext()
215 # This setting is handled differently from most other settings,
216 # because the last setting must be retrieved before any commander exists.
217 if aList:
218 g.app.config.atCommonCommandsList.extend(aList)
219 # Bug fix: 2011/11/24: Extend the list, don't replace it.
220 #@+node:ekr.20071214140900: *4* pbc.doData
221 def doData(self, p, kind, name, val):
222 # New in Leo 4.11: do not strip lines.
223 # New in Leo 4.12.1: strip *nothing* here.
224 # New in Leo 4.12.1: allow composition of nodes:
225 # - Append all text in descendants in outline order.
226 # - Ensure all fragments end with a newline.
227 data = g.splitLines(p.b)
228 for p2 in p.subtree():
229 if p2.b and not p2.h.startswith('@'):
230 data.extend(g.splitLines(p2.b))
231 if not p2.b.endswith('\n'):
232 data.append('\n')
233 self.set(p, kind, name, data)
234 #@+node:ekr.20131114051702.16545: *4* pbc.doOutlineData & helper
235 def doOutlineData(self, p, kind, name, val):
236 # New in Leo 4.11: do not strip lines.
237 data = self.getOutlineDataHelper(p)
238 self.set(p, kind, name, data)
239 return 'skip'
240 #@+node:ekr.20131114051702.16546: *5* pbc.getOutlineDataHelper
241 def getOutlineDataHelper(self, p):
242 c = self.c
243 if not p:
244 return None
245 try:
246 # Copy the entire tree to s.
247 c.fileCommands.leo_file_encoding = 'utf-8'
248 s = c.fileCommands.outline_to_clipboard_string(p)
249 s = g.toUnicode(s, encoding='utf-8')
250 except Exception:
251 g.es_exception()
252 s = None
253 return s
254 #@+node:ekr.20041120094940.3: *4* pbc.doDirectory & doPath
255 def doDirectory(self, p, kind, name, val):
256 # At present no checking is done.
257 self.set(p, kind, name, val)
259 doPath = doDirectory
260 #@+node:ekr.20070224075914: *4* pbc.doEnabledPlugins
261 def doEnabledPlugins(self, p, kind, name, val):
262 c = self.c
263 s = p.b
264 # This setting is handled differently from all other settings,
265 # because the last setting must be retrieved before any commander exists.
266 # 2011/09/04: Remove comments, comment lines and blank lines.
267 aList, lines = [], g.splitLines(s)
268 for s in lines:
269 i = s.find('#')
270 if i > -1:
271 s = s[:i] + '\n' # 2011/09/29: must add newline back in.
272 if s.strip():
273 aList.append(s.lstrip())
274 s = ''.join(aList)
275 # Set the global config ivars.
276 g.app.config.enabledPluginsString = s
277 g.app.config.enabledPluginsFileName = c.shortFileName(
278 ) if c else '<no settings file>'
279 #@+node:ekr.20041120094940.6: *4* pbc.doFloat
280 def doFloat(self, p, kind, name, val):
281 try:
282 val = float(val)
283 self.set(p, kind, name, val)
284 except ValueError:
285 self.valueError(p, kind, name, val)
286 #@+node:ekr.20041120094940.4: *4* pbc.doFont
287 def doFont(self, p, kind, name, val):
288 """Handle an @font node. Such nodes affect syntax coloring *only*."""
289 d = self.parseFont(p)
290 # Set individual settings.
291 for key in ('family', 'size', 'slant', 'weight'):
292 data = d.get(key)
293 if data is not None:
294 name, val = data
295 setKind = key
296 self.set(p, setKind, name, val)
297 #@+node:ekr.20150426034813.1: *4* pbc.doIfEnv
298 def doIfEnv(self, p, kind, name, val):
299 """
300 Support @ifenv in @settings trees.
302 Enable descendant settings if the value of os.getenv is in any of the names.
303 """
304 aList = name.split(',')
305 if not aList:
306 return 'skip'
307 name = aList[0]
308 env = os.getenv(name)
309 env = env.lower().strip() if env else 'none'
310 for s in aList[1:]:
311 if s.lower().strip() == env:
312 return None
313 return 'skip'
314 #@+node:dan.20080410121257.2: *4* pbc.doIfHostname
315 def doIfHostname(self, p, kind, name, val):
316 """
317 Support @ifhostname in @settings trees.
319 Examples: Let h = os.environ('HOSTNAME')
321 @ifhostname bob
322 Enable descendant settings if h == 'bob'
323 @ifhostname !harry
324 Enable descendant settings if h != 'harry'
325 """
326 lm = g.app.loadManager
327 h = lm.computeMachineName().strip()
328 s = name.strip()
329 if s.startswith('!'):
330 if h == s[1:]:
331 return 'skip'
332 elif h != s:
333 return 'skip'
334 return None
335 #@+node:ekr.20041120104215: *4* pbc.doIfPlatform
336 def doIfPlatform(self, p, kind, name, val):
337 """Support @ifplatform in @settings trees."""
338 platform = sys.platform.lower()
339 for s in name.split(','):
340 if platform == s.lower():
341 return None
342 return "skip"
343 #@+node:ekr.20041120104215.1: *4* pbc.doIgnore
344 def doIgnore(self, p, kind, name, val):
345 return "skip"
346 #@+node:ekr.20041120094940.5: *4* pbc.doInt
347 def doInt(self, p, kind, name, val):
348 try:
349 val = int(val)
350 self.set(p, kind, name, val)
351 except ValueError:
352 self.valueError(p, kind, name, val)
353 #@+node:ekr.20041217132253: *4* pbc.doInts
354 def doInts(self, p, kind, name, val):
355 """
356 We expect either:
357 @ints [val1,val2,...]aName=val
358 @ints aName[val1,val2,...]=val
359 """
360 name = name.strip() # The name indicates the valid values.
361 i = name.find('[')
362 j = name.find(']')
363 if -1 < i < j:
364 items = name[i + 1 : j]
365 items = items.split(',')
366 name = name[:i] + name[j + 1 :].strip()
367 try:
368 items = [int(item.strip()) for item in items]
369 except ValueError:
370 items = []
371 self.valueError(p, 'ints[]', name, val)
372 return
373 kind = f"ints[{','.join([str(item) for item in items])}]"
374 try:
375 val = int(val)
376 except ValueError:
377 self.valueError(p, 'int', name, val)
378 return
379 if val not in items:
380 self.error(f"{val} is not in {kind} in {name}")
381 return
382 # At present no checking is done.
383 self.set(p, kind, name, val)
384 #@+node:tbrown.20080514112857.124: *4* pbc.doMenuat
385 def doMenuat(self, p, kind, name, val):
386 """Handle @menuat setting."""
387 if g.app.config.menusList:
388 # get the patch fragment
389 patch: List[Any] = []
390 if p.hasChildren():
391 # self.doMenus(p.copy().firstChild(),kind,name,val,storeIn=patch)
392 self.doItems(p.copy(), patch)
393 # setup
394 parts = name.split()
395 if len(parts) != 3:
396 parts.append('subtree')
397 targetPath, mode, source = parts
398 if not targetPath.startswith('/'):
399 targetPath = '/' + targetPath
400 ans = self.patchMenuTree(g.app.config.menusList, targetPath)
401 if ans:
402 # pylint: disable=unpacking-non-sequence
403 list_, idx = ans
404 if mode not in ('copy', 'cut'):
405 if source != 'clipboard':
406 use = patch # [0][1]
407 else:
408 if isinstance(self.clipBoard, list):
409 use = self.clipBoard
410 else:
411 use = [self.clipBoard]
412 if mode == 'replace':
413 list_[idx] = use.pop(0)
414 while use:
415 idx += 1
416 list_.insert(idx, use.pop(0))
417 elif mode == 'before':
418 while use:
419 list_.insert(idx, use.pop())
420 elif mode == 'after':
421 while use:
422 list_.insert(idx + 1, use.pop())
423 elif mode == 'cut':
424 self.clipBoard = list_[idx]
425 del list_[idx]
426 elif mode == 'copy':
427 self.clipBoard = list_[idx]
428 else: # append
429 list_.extend(use)
430 else:
431 g.es_print("ERROR: didn't find menu path " + targetPath)
432 elif g.app.inBridge:
433 pass # #48: Not an error.
434 else:
435 g.es_print("ERROR: @menuat found but no menu tree to patch")
436 #@+node:tbrown.20080514180046.9: *5* pbc.getName
437 def getName(self, val, val2=None):
438 if val2 and val2.strip():
439 val = val2
440 val = val.split('\n', 1)[0]
441 for i in "*.-& \t\n":
442 val = val.replace(i, '')
443 return val.lower()
444 #@+node:tbrown.20080514180046.2: *5* pbc.dumpMenuTree
445 def dumpMenuTree(self, aList, level=0, path=''):
446 for z in aList:
447 kind, val, val2 = z
448 pad = ' ' * level
449 if kind == '@item':
450 name = self.getName(val, val2)
451 g.es_print(f"{pad} {val} ({val2}) [{path + '/' + name}]")
452 else:
453 name = self.getName(kind.replace('@menu ', ''))
454 g.es_print(f"{pad} {kind}... [{path + '/' + name}]")
455 self.dumpMenuTree(val, level + 1, path=path + '/' + name)
456 #@+node:tbrown.20080514180046.8: *5* pbc.patchMenuTree
457 def patchMenuTree(self, orig, targetPath, path=''):
459 for n, z in enumerate(orig):
460 kind, val, val2 = z
461 if kind == '@item':
462 name = self.getName(val, val2)
463 curPath = path + '/' + name
464 if curPath == targetPath:
465 return orig, n
466 else:
467 name = self.getName(kind.replace('@menu ', ''))
468 curPath = path + '/' + name
469 if curPath == targetPath:
470 return orig, n
471 ans = self.patchMenuTree(val, targetPath, path=path + '/' + name)
472 if ans:
473 return ans
474 return None
475 #@+node:ekr.20070925144337.2: *4* pbc.doMenus & helper
476 def doMenus(self, p, kind, name, val):
478 c = self.c
479 p = p.copy()
480 aList: List[Any] = [] # This entire logic is mysterious, and likely buggy.
481 after = p.nodeAfterTree()
482 while p and p != after:
483 self.debug_count += 1
484 h = p.h
485 if g.match_word(h, 0, '@menu'):
486 name = h[len('@menu') :].strip()
487 if name:
488 for z in aList:
489 name2, junk, junk = z
490 if name2 == name:
491 self.error(f"Replacing previous @menu {name}")
492 break
493 aList2: List[Any] = [] # Huh?
494 kind = f"{'@menu'} {name}"
495 self.doItems(p, aList2)
496 aList.append((kind, aList2, None),)
497 p.moveToNodeAfterTree()
498 else:
499 p.moveToThreadNext()
500 else:
501 p.moveToThreadNext()
502 if self.localFlag:
503 self.set(p, kind='menus', name='menus', val=aList)
504 else:
505 g.app.config.menusList = aList
506 name = c.shortFileName() if c else '<no settings file>'
507 g.app.config.menusFileName = name
508 #@+node:ekr.20070926141716: *5* pbc.doItems
509 def doItems(self, p, aList):
511 p = p.copy()
512 after = p.nodeAfterTree()
513 p.moveToThreadNext()
514 while p and p != after:
515 self.debug_count += 1
516 h = p.h
517 for tag in ('@menu', '@item', '@ifplatform'):
518 if g.match_word(h, 0, tag):
519 itemName = h[len(tag) :].strip()
520 if itemName:
521 lines = [z for z in g.splitLines(p.b) if
522 z.strip() and not z.strip().startswith('#')]
523 body = lines[0].strip() if lines else ''
524 # Only the first body line is significant.
525 # This allows following comment lines.
526 if tag == '@menu':
527 aList2: List[Any] = [] # Huh?
528 kind = f"{tag} {itemName}"
529 self.doItems(p, aList2) # Huh?
530 aList.append((kind, aList2, body),)
531 # #848: Body was None.
532 p.moveToNodeAfterTree()
533 break
534 else:
535 kind = tag
536 head = itemName
537 # Wrong: we must not clean non-unicode characters!
538 # # Fix #1117: Similar to cleanButtonText in mod_scripting.py.
539 # s = ''.join([ch if ch in chars else '' for ch in g.toUnicode(head)])
540 # head2 = s.replace('--', '-').lower()
541 # aList.append((kind, head2, body),)
542 aList.append((kind, head, body),)
543 p.moveToThreadNext()
544 break
545 else:
546 p.moveToThreadNext()
547 #@+node:ekr.20060102103625.1: *4* pbc.doMode
548 def doMode(self, p, kind, name, val):
549 """Parse an @mode node and create the enter-<name>-mode command."""
550 c = self.c
551 name1 = name
552 modeName = self.computeModeName(name)
553 d = g.TypedDict(
554 name=f"modeDict for {modeName}",
555 keyType=type('commandName'),
556 valType=g.BindingInfo)
557 s = p.b
558 lines = g.splitLines(s)
559 for line in lines:
560 line = line.strip()
561 if line and not g.match(line, 0, '#'):
562 name, bi = self.parseShortcutLine('*mode-setting*', line)
563 if not name:
564 # An entry command: put it in the special *entry-commands* key.
565 d.add_to_list('*entry-commands*', bi)
566 elif bi is not None:
567 # A regular shortcut.
568 bi.pane = modeName
569 aList = d.get(name, [])
570 # Important: use previous bindings if possible.
571 key2, aList2 = c.config.getShortcut(name)
572 aList3 = [z for z in aList2 if z.pane != modeName]
573 if aList3:
574 aList.extend(aList3)
575 aList.append(bi)
576 d[name] = aList
577 # Restore the global shortcutsDict.
578 # Create the command, but not any bindings to it.
579 self.createModeCommand(modeName, name1, d)
580 #@+node:ekr.20070411101643.1: *4* pbc.doOpenWith
581 def doOpenWith(self, p, kind, name, val):
583 d = self.parseOpenWith(p)
584 d['name'] = name
585 d['shortcut'] = val
586 name = kind = 'openwithtable'
587 self.openWithList.append(d)
588 self.set(p, kind, name, self.openWithList)
589 #@+node:bobjack.20080324141020.4: *4* pbc.doPopup & helper
590 def doPopup(self, p, kind, name, val):
591 """
592 Handle @popup menu items in @settings trees.
593 """
594 popupName = name
595 # popupType = val
596 aList: List[Any] = []
597 p = p.copy()
598 self.doPopupItems(p, aList)
599 if not hasattr(g.app.config, 'context_menus'):
600 g.app.config.context_menus = {}
601 g.app.config.context_menus[popupName] = aList
602 #@+node:bobjack.20080324141020.5: *5* pbc.doPopupItems
603 def doPopupItems(self, p, aList):
604 p = p.copy()
605 after = p.nodeAfterTree()
606 p.moveToThreadNext()
607 while p and p != after:
608 h = p.h
609 for tag in ('@menu', '@item'):
610 if g.match_word(h, 0, tag):
611 itemName = h[len(tag) :].strip()
612 if itemName:
613 if tag == '@menu':
614 aList2: List[Any] = []
615 kind = f"{itemName}"
616 body = p.b
617 self.doPopupItems(p, aList2) # Huh?
618 aList.append((kind + '\n' + body, aList2),)
619 p.moveToNodeAfterTree()
620 break
621 else:
622 kind = tag
623 head = itemName
624 body = p.b
625 aList.append((head, body),)
626 p.moveToThreadNext()
627 break
628 else:
629 p.moveToThreadNext()
630 #@+node:ekr.20041121125741: *4* pbc.doRatio
631 def doRatio(self, p, kind, name, val):
632 try:
633 val = float(val)
634 if 0.0 <= val <= 1.0:
635 self.set(p, kind, name, val)
636 else:
637 self.valueError(p, kind, name, val)
638 except ValueError:
639 self.valueError(p, kind, name, val)
640 #@+node:ekr.20041120105609: *4* pbc.doShortcuts
641 def doShortcuts(self, p, kind, junk_name, junk_val, s=None):
642 """Handle an @shortcut or @shortcuts node."""
643 c, d = self.c, self.shortcutsDict
644 if s is None:
645 s = p.b
646 fn = d.name()
647 for line in g.splitLines(s):
648 line = line.strip()
649 if line and not g.match(line, 0, '#'):
650 commandName, bi = self.parseShortcutLine(fn, line)
651 if bi is None: # Fix #718.
652 print(f"\nWarning: bad shortcut specifier: {line!r}\n")
653 else:
654 if bi and bi.stroke not in (None, 'none', 'None'):
655 self.doOneShortcut(bi, commandName, p)
656 else:
657 # New in Leo 5.7: Add local assignments to None to c.k.killedBindings.
658 if c.config.isLocalSettingsFile():
659 c.k.killedBindings.append(commandName)
660 #@+node:ekr.20111020144401.9585: *5* pbc.doOneShortcut
661 def doOneShortcut(self, bi, commandName, p):
662 """Handle a regular shortcut."""
663 d = self.shortcutsDict
664 aList = d.get(commandName, [])
665 aList.append(bi)
666 d[commandName] = aList
667 #@+node:ekr.20041217132028: *4* pbc.doString
668 def doString(self, p, kind, name, val):
669 # At present no checking is done.
670 self.set(p, kind, name, val)
671 #@+node:ekr.20041120094940.8: *4* pbc.doStrings
672 def doStrings(self, p, kind, name, val):
673 """
674 We expect one of the following:
675 @strings aName[val1,val2...]=val
676 @strings [val1,val2,...]aName=val
677 """
678 name = name.strip()
679 i = name.find('[')
680 j = name.find(']')
681 if -1 < i < j:
682 items = name[i + 1 : j]
683 items = items.split(',')
684 items = [item.strip() for item in items]
685 name = name[:i] + name[j + 1 :].strip()
686 kind = f"strings[{','.join(items)}]"
687 # At present no checking is done.
688 self.set(p, kind, name, val)
689 #@+node:ekr.20041124063257: *3* pbc.munge
690 def munge(self, s):
691 return g.app.config.canonicalizeSettingName(s)
692 #@+node:ekr.20041119204700.2: *3* pbc.oops
693 def oops(self):
694 g.pr("ParserBaseClass oops:",
695 g.callers(),
696 "must be overridden in subclass")
697 #@+node:ekr.20041213082558: *3* pbc.parsers
698 #@+node:ekr.20041213082558.1: *4* pbc.parseFont & helper
699 def parseFont(self, p):
700 d: Dict[str, Any] = {
701 'comments': [],
702 'family': None,
703 'size': None,
704 'slant': None,
705 'weight': None,
706 }
707 s = p.b
708 lines = g.splitLines(s)
709 for line in lines:
710 self.parseFontLine(line, d)
711 comments = d.get('comments')
712 d['comments'] = '\n'.join(comments)
713 return d
714 #@+node:ekr.20041213082558.2: *5* pbc.parseFontLine
715 def parseFontLine(self, line, d):
716 s = line.strip()
717 if not s:
718 return
719 try:
720 s = str(s)
721 except UnicodeError:
722 pass
723 if g.match(s, 0, '#'):
724 s = s[1:].strip()
725 comments = d.get('comments')
726 comments.append(s)
727 d['comments'] = comments
728 return
729 # name is everything up to '='
730 i = s.find('=')
731 if i == -1:
732 name = s
733 val = None
734 else:
735 name = s[:i].strip()
736 val = s[i + 1 :].strip().strip('"').strip("'")
737 for tag in ('_family', '_size', '_slant', '_weight'):
738 if name.endswith(tag):
739 kind = tag[1:]
740 d[kind] = name, val # Used only by doFont.
741 return
742 #@+node:ekr.20041119205148: *4* pbc.parseHeadline
743 def parseHeadline(self, s):
744 """
745 Parse a headline of the form @kind:name=val
746 Return (kind,name,val).
747 Leo 4.11.1: Ignore everything after @data name.
748 """
749 kind = name = val = None
750 if g.match(s, 0, '@'):
751 i = g.skip_id(s, 1, chars='-')
752 i = g.skip_ws(s, i)
753 kind = s[1:i].strip()
754 if kind:
755 # name is everything up to '='
756 if kind == 'data':
757 # i = g.skip_ws(s,i)
758 j = s.find(' ', i)
759 if j == -1:
760 name = s[i:].strip()
761 else:
762 name = s[i:j].strip()
763 else:
764 j = s.find('=', i)
765 if j == -1:
766 name = s[i:].strip()
767 else:
768 name = s[i:j].strip()
769 # val is everything after the '='
770 val = s[j + 1 :].strip()
771 return kind, name, val
772 #@+node:ekr.20070411101643.2: *4* pbc.parseOpenWith & helper
773 def parseOpenWith(self, p):
775 d = {'command': None}
776 # d contains args, kind, etc tags.
777 for line in g.splitLines(p.b):
778 self.parseOpenWithLine(line, d)
779 return d
780 #@+node:ekr.20070411101643.4: *5* pbc.parseOpenWithLine
781 def parseOpenWithLine(self, line, d):
782 s = line.strip()
783 if not s:
784 return
785 i = g.skip_ws(s, 0)
786 if g.match(s, i, '#'):
787 return
788 # try:
789 # s = str(s)
790 # except UnicodeError:
791 # pass
792 if 1: # new code
793 j = g.skip_c_id(s, i)
794 tag = s[i:j].strip()
795 if not tag:
796 g.es_print(f"@openwith lines must start with a tag: {s}")
797 return
798 i = g.skip_ws(s, j)
799 if not g.match(s, i, ':'):
800 g.es_print(f"colon must follow @openwith tag: {s}")
801 return
802 i += 1
803 val = s[i:].strip() or ''
804 # An empty val is valid.
805 if tag == 'arg':
806 aList = d.get('args', [])
807 aList.append(val)
808 d['args'] = aList
809 elif d.get(tag):
810 g.es_print(f"ignoring duplicate definition of {tag} {s}")
811 else:
812 d[tag] = val
813 else:
814 d['command'] = s
815 #@+node:ekr.20041120112043: *4* pbc.parseShortcutLine
816 def parseShortcutLine(self, kind, s):
817 """Parse a shortcut line. Valid forms:
819 --> entry-command
820 settingName = shortcut
821 settingName ! paneName = shortcut
822 command-name --> mode-name = binding
823 command-name --> same = binding
824 """
825 # c = self.c
826 s = s.replace('\x7f', '')
827 # Can happen on MacOS. Very weird.
828 name = val = nextMode = None
829 nextMode = 'none'
830 i = g.skip_ws(s, 0)
831 if g.match(s, i, '-->'): # New in 4.4.1 b1: allow mode-entry commands.
832 j = g.skip_ws(s, i + 3)
833 i = g.skip_id(s, j, '-')
834 entryCommandName = s[j:i]
835 return None, g.BindingInfo('*entry-command*', commandName=entryCommandName)
836 j = i
837 i = g.skip_id(s, j, '-@') # #718.
838 name = s[j:i]
839 # #718: Allow @button- and @command- prefixes.
840 for tag in ('@button-', '@command-'):
841 if name.startswith(tag):
842 name = name[len(tag) :]
843 break
844 if not name:
845 return None, None
846 # New in Leo 4.4b2.
847 i = g.skip_ws(s, i)
848 if g.match(s, i, '->'): # New in 4.4: allow pane-specific shortcuts.
849 j = g.skip_ws(s, i + 2)
850 i = g.skip_id(s, j)
851 nextMode = s[j:i]
852 i = g.skip_ws(s, i)
853 if g.match(s, i, '!'): # New in 4.4: allow pane-specific shortcuts.
854 j = g.skip_ws(s, i + 1)
855 i = g.skip_id(s, j)
856 pane = s[j:i]
857 if not pane.strip():
858 pane = 'all'
859 else: pane = 'all'
860 i = g.skip_ws(s, i)
861 if g.match(s, i, '='):
862 i = g.skip_ws(s, i + 1)
863 val = s[i:]
864 # New in 4.4: Allow comments after the shortcut.
865 # Comments must be preceded by whitespace.
866 if val:
867 i = val.find('#')
868 if i > 0 and val[i - 1] in (' ', '\t'):
869 val = val[:i].strip()
870 if not val:
871 return name, None
872 stroke = g.KeyStroke(binding=val) if val else None
873 bi = g.BindingInfo(kind=kind, nextMode=nextMode, pane=pane, stroke=stroke)
874 return name, bi
875 #@+node:ekr.20041120094940.9: *3* pbc.set
876 def set(self, p, kind, name, val):
877 """Init the setting for name to val."""
878 c = self.c
879 # Note: when kind is 'shortcut', name is a command name.
880 key = self.munge(name)
881 if key is None:
882 g.es_print('Empty setting name in', p.h in c.fileName())
883 parent = p.parent()
884 while parent:
885 g.trace('parent', parent.h)
886 parent.moveToParent()
887 return
888 d = self.settingsDict
889 gs = d.get(key)
890 if gs:
891 assert isinstance(gs, g.GeneralSetting), gs
892 path = gs.path
893 if g.os_path_finalize(c.mFileName) != g.os_path_finalize(path):
894 g.es("over-riding setting:", name, "from", path) # 1341
895 # Important: we can't use c here: it may be destroyed!
896 d[key] = g.GeneralSetting(kind, # type:ignore
897 path=c.mFileName,
898 tag='setting',
899 unl=(p and p.get_UNL()),
900 val=val,
901 )
902 #@+node:ekr.20041119204700.1: *3* pbc.traverse
903 def traverse(self):
904 """Traverse the entire settings tree."""
905 c = self.c
906 self.settingsDict = g.TypedDict( # type:ignore
907 name=f"settingsDict for {c.shortFileName()}",
908 keyType=type('settingName'),
909 valType=g.GeneralSetting)
910 self.shortcutsDict = g.TypedDict( # was TypedDictOfLists.
911 name=f"shortcutsDict for {c.shortFileName()}",
912 keyType=str,
913 valType=g.BindingInfo)
914 # This must be called after the outline has been inited.
915 p = c.config.settingsRoot()
916 if not p:
917 # c.rootPosition() doesn't exist yet.
918 # This is not an error.
919 return self.shortcutsDict, self.settingsDict
920 after = p.nodeAfterTree()
921 while p and p != after:
922 result = self.visitNode(p)
923 if result == "skip":
924 # g.warning('skipping settings in',p.h)
925 p.moveToNodeAfterTree()
926 else:
927 p.moveToThreadNext()
928 # Return the raw dict, unmerged.
929 return self.shortcutsDict, self.settingsDict
930 #@+node:ekr.20041120094940.10: *3* pbc.valueError
931 def valueError(self, p, kind, name, val):
932 """Give an error: val is not valid for kind."""
933 self.error(f"{val} is not a valid {kind} for {name}")
934 #@+node:ekr.20041119204700.3: *3* pbc.visitNode (must be overwritten in subclasses)
935 def visitNode(self, p):
936 self.oops()
937 #@-others
938#@-<< class ParserBaseClass >>
939#@+others
940#@+node:ekr.20190905091614.1: ** class ActiveSettingsOutline
941class ActiveSettingsOutline:
943 def __init__(self, c):
945 self.c = c
946 self.start()
947 self.create_outline()
948 #@+others
949 #@+node:ekr.20190905091614.2: *3* aso.start & helpers
950 def start(self):
951 """Do everything except populating the new outline."""
952 # Copy settings.
953 c = self.c
954 settings = c.config.settingsDict
955 shortcuts = c.config.shortcutsDict
956 assert isinstance(settings, g.TypedDict), repr(settings)
957 assert isinstance(shortcuts, g.TypedDict), repr(shortcuts)
958 settings_copy = settings.copy()
959 shortcuts_copy = shortcuts.copy()
960 # Create the new commander.
961 self.commander = self.new_commander()
962 # Open hidden commanders for non-local settings files.
963 self.load_hidden_commanders()
964 # Create the ordered list of commander tuples, including the local .leo file.
965 self.create_commanders_list()
966 # Jam the old settings into the new commander.
967 self.commander.config.settingsDict = settings_copy
968 self.commander.config.shortcutsDict = shortcuts_copy
969 #@+node:ekr.20190905091614.3: *4* aso.create_commanders_list
970 def create_commanders_list(self):
972 """Create the commanders list. Order matters."""
973 lm = g.app.loadManager
974 # The first element of each tuple must match the return values of c.config.getSource.
975 # "local_file", "theme_file", "myLeoSettings", "leoSettings"
977 self.commanders = [
978 ('leoSettings', lm.leo_settings_c),
979 ('myLeoSettings', lm.my_settings_c),
980 ]
981 if lm.theme_c:
982 self.commanders.append(('theme_file', lm.theme_c),)
983 if self.c.config.settingsRoot():
984 self.commanders.append(('local_file', self.c),)
985 #@+node:ekr.20190905091614.4: *4* aso.load_hidden_commanders
986 def load_hidden_commanders(self):
987 """
988 Open hidden commanders for leoSettings.leo, myLeoSettings.leo and theme.leo.
989 """
990 lm = g.app.loadManager
991 lm.readGlobalSettingsFiles()
992 # Make sure to reload the local file.
993 c = g.app.commanders()[0]
994 fn = c.fileName()
995 if fn:
996 self.local_c = lm.openSettingsFile(fn)
997 #@+node:ekr.20190905091614.5: *4* aso.new_commander
998 def new_commander(self):
999 """Create the new commander, and load all settings files."""
1000 lm = g.app.loadManager
1001 old_c = self.c
1002 # Save any changes so they can be seen.
1003 if old_c.isChanged():
1004 old_c.save()
1005 old_c.outerUpdate()
1006 # From file-new...
1007 g.app.disable_redraw = True
1008 g.app.setLog(None)
1009 g.app.lockLog()
1010 # Switch to the new commander. Do *not* use previous settings.
1011 fileName = f"{old_c.fileName()}-active-settings"
1012 g.es(fileName, color='red')
1013 c = g.app.newCommander(fileName=fileName)
1014 # Restore the layout, if we have ever saved this file.
1015 if not old_c:
1016 c.frame.setInitialWindowGeometry()
1017 # #1340: Don't do this. It is no longer needed.
1018 # g.app.restoreWindowState(c)
1019 c.frame.resizePanesToRatio(c.frame.ratio, c.frame.secondary_ratio)
1020 # From file-new...
1021 g.app.unlockLog()
1022 lm.createMenu(c)
1023 lm.finishOpen(c)
1024 g.app.writeWaitingLog(c)
1025 c.setLog()
1026 c.clearChanged() # Clears all dirty bits.
1027 g.app.disable_redraw = False
1028 return c
1029 #@+node:ekr.20190905091614.6: *3* aso.create_outline & helper
1030 def create_outline(self):
1031 """Create the summary outline"""
1032 c = self.commander
1033 #
1034 # Create the root node, with the legend in the body text.
1035 root = c.rootPosition()
1036 root.h = f"Legend for {self.c.shortFileName()}"
1037 root.b = self.legend()
1038 #
1039 # Create all the inner settings outlines.
1040 for kind, commander in self.commanders:
1041 p = root.insertAfter()
1042 p.h = g.shortFileName(commander.fileName())
1043 p.b = '@language rest\n@wrap\n'
1044 self.create_inner_outline(commander, kind, p)
1045 #
1046 # Clean all dirty/changed bits, so closing this outline won't prompt for a save.
1047 for v in c.all_nodes():
1048 v.clearDirty()
1049 c.setChanged()
1050 c.redraw()
1051 #@+node:ekr.20190905091614.7: *4* aso.legend
1052 def legend(self):
1053 """Compute legend for self.c"""
1054 c, lm = self.c, g.app.loadManager
1055 legend = f'''\
1056 @language rest
1058 legend:
1060 leoSettings.leo
1061 @ @button, @command, @mode
1062 [D] default settings
1063 [F] local file: {c.shortFileName()}
1064 [M] myLeoSettings.leo
1065 '''
1066 if lm.theme_path:
1067 legend = legend + f"[T] theme file: {g.shortFileName(lm.theme_path)}\n"
1068 return textwrap.dedent(legend)
1069 #@+node:ekr.20190905091614.8: *3* aso.create_inner_outline
1070 def create_inner_outline(self, c, kind, root):
1071 """
1072 Create the outline for the given hidden commander, as descendants of root.
1073 """
1074 # Find the settings tree
1075 settings_root = c.config.settingsRoot()
1076 if not settings_root:
1077 # This should not be called if the local file has no @settings node.
1078 g.trace('no @settings node!!', c.shortFileName())
1079 return
1080 # Unify all settings.
1081 self.create_unified_settings(kind, root, settings_root)
1082 self.clean(root)
1083 #@+node:ekr.20190905091614.9: *3* aso.create_unified_settings
1084 def create_unified_settings(self, kind, root, settings_root):
1085 """Create the active settings tree under root."""
1086 c = self.commander
1087 lm = g.app.loadManager
1088 settings_pat = re.compile(r'^(@[\w-]+)(\s+[\w\-\.]+)?')
1089 valid_list = [
1090 '@bool', '@color', '@directory', '@encoding',
1091 '@int', '@float', '@ratio', '@string',
1092 ]
1093 d = self.filter_settings(kind)
1094 ignore, outline_data = None, None
1095 self.parents = [root]
1096 self.level = settings_root.level()
1097 for p in settings_root.subtree():
1098 #@+<< continue if we should ignore p >>
1099 #@+node:ekr.20190905091614.10: *4* << continue if we should ignore p >>
1100 if ignore:
1101 if p == ignore:
1102 ignore = None
1103 else:
1104 # g.trace('IGNORE', p.h)
1105 continue
1106 if outline_data:
1107 if p == outline_data:
1108 outline_data = None
1109 else:
1110 self.add(p)
1111 continue
1112 #@-<< continue if we should ignore p >>
1113 m = settings_pat.match(p.h)
1114 if not m:
1115 self.add(p, h='ORG:' + p.h)
1116 continue
1117 if m.group(2) and m.group(1) in valid_list:
1118 #@+<< handle a real setting >>
1119 #@+node:ekr.20190905091614.11: *4* << handle a real setting >>
1120 key = g.app.config.munge(m.group(2).strip())
1121 val = d.get(key)
1122 if isinstance(val, g.GeneralSetting):
1123 self.add(p)
1124 else:
1125 # Look at all the settings to discover where the setting is defined.
1126 val = c.config.settingsDict.get(key)
1127 if isinstance(val, g.GeneralSetting):
1128 # Use self.c, not self.commander.
1129 letter = lm.computeBindingLetter(self.c, val.path)
1130 p.h = f"[{letter}] INACTIVE: {p.h}"
1131 p.h = f"UNUSED: {p.h}"
1132 self.add(p)
1133 #@-<< handle a real setting >>
1134 continue
1135 # Not a setting. Handle special cases.
1136 if m.group(1) == '@ignore':
1137 ignore = p.nodeAfterTree()
1138 elif m.group(1) in ('@data', '@outline-data'):
1139 outline_data = p.nodeAfterTree()
1140 self.add(p)
1141 else:
1142 self.add(p)
1143 #@+node:ekr.20190905091614.12: *3* aso.add
1144 def add(self, p, h=None):
1145 """
1146 Add a node for p.
1148 We must *never* alter p in any way.
1149 Instead, the org flag tells whether the "ORG:" prefix.
1150 """
1151 if 0:
1152 pad = ' ' * p.level()
1153 print(pad, p.h)
1154 p_level = p.level()
1155 if p_level > self.level + 1:
1156 g.trace('OOPS', p.v.context.shortFileName(), self.level, p_level, p.h)
1157 return
1158 while p_level < self.level + 1 and len(self.parents) > 1:
1159 self.parents.pop()
1160 self.level -= 1
1161 parent = self.parents[-1]
1162 child = parent.insertAsLastChild()
1163 child.h = h or p.h
1164 child.b = p.b
1165 self.parents.append(child)
1166 self.level += 1
1167 #@+node:ekr.20190905091614.13: *3* aso.clean
1168 def clean(self, root):
1169 """
1170 Remove all unnecessary nodes.
1171 Remove the "ORG:" prefix from remaining nodes.
1172 """
1173 self.clean_node(root)
1175 def clean_node(self, p):
1176 """Remove p if it contains no children after cleaning its children."""
1177 tag = 'ORG:'
1178 # There are no clones, so deleting children in reverse preserves positions.
1179 for child in reversed(list(p.children())):
1180 self.clean_node(child)
1181 if p.h.startswith(tag):
1182 if p.hasChildren():
1183 p.h = p.h.lstrip(tag).strip()
1184 else:
1185 p.doDelete()
1186 #@+node:ekr.20190905091614.14: *3* aso.filter_settings
1187 def filter_settings(self, target_kind):
1188 """Return a dict containing only settings defined in the file given by kind."""
1189 # Crucial: Always use the newly-created commander.
1190 # It's settings are guaranteed to be correct.
1191 c = self.commander
1192 valid_kinds = ('local_file', 'theme_file', 'myLeoSettings', 'leoSettings')
1193 assert target_kind in valid_kinds, repr(target_kind)
1194 d = c.config.settingsDict
1195 result = {}
1196 for key in d.keys():
1197 gs = d.get(key)
1198 assert isinstance(gs, g.GeneralSetting), repr(gs)
1199 if not gs.kind:
1200 g.trace('OOPS: no kind', repr(gs))
1201 continue
1202 kind = c.config.getSource(setting=gs)
1203 if kind == 'ignore':
1204 g.trace('IGNORE:', kind, key)
1205 continue
1206 if kind == 'error': # 2021/09/18.
1207 g.trace('ERROR:', kind, key)
1208 continue
1209 if kind == target_kind:
1210 result[key] = gs
1211 return result
1212 #@-others
1213#@+node:ekr.20041119203941: ** class GlobalConfigManager
1214class GlobalConfigManager:
1215 """A class to manage configuration settings."""
1216 # Class data...
1217 #@+<< gcm.defaultsDict >>
1218 #@+node:ekr.20041117062717.1: *3* << gcm.defaultsDict >>
1219 #@+at This contains only the "interesting" defaults.
1220 # Ints and bools default to 0, floats to 0.0 and strings to "".
1221 #@@c
1222 defaultBodyFontSize = 12 # 9 if sys.platform == "win32" else 12
1223 defaultLogFontSize = 12 # 8 if sys.platform == "win32" else 12
1224 defaultMenuFontSize = 12 # 9 if sys.platform == "win32" else 12
1225 defaultTreeFontSize = 12 # 9 if sys.platform == "win32" else 12
1226 defaultsDict = g.TypedDict(
1227 name='g.app.config.defaultsDict',
1228 keyType=str,
1229 valType=g.GeneralSetting,
1230 )
1231 defaultsData = (
1232 # compare options...
1233 ("ignore_blank_lines", "bool", True),
1234 ("limit_count", "int", 9),
1235 ("print_mismatching_lines", "bool", True),
1236 ("print_trailing_lines", "bool", True),
1237 # find/change options...
1238 ("search_body", "bool", True),
1239 ("whole_word", "bool", True),
1240 # Prefs panel.
1241 # ("default_target_language","language","python"),
1242 ("target_language", "language", "python"), # Bug fix: 6/20,2005.
1243 ("tab_width", "int", -4),
1244 ("page_width", "int", 132),
1245 ("output_doc_chunks", "bool", True),
1246 ("tangle_outputs_header", "bool", True),
1247 # Syntax coloring options...
1248 # Defaults for colors are handled by leoColor.py.
1249 ("color_directives_in_plain_text", "bool", True),
1250 ("underline_undefined_section_names", "bool", True),
1251 # Window options...
1252 ("body_pane_wraps", "bool", True),
1253 ("body_text_font_family", "family", "Courier"),
1254 ("body_text_font_size", "size", defaultBodyFontSize),
1255 ("body_text_font_slant", "slant", "roman"),
1256 ("body_text_font_weight", "weight", "normal"),
1257 ("enable_drag_messages", "bool", True),
1258 ("headline_text_font_family", "string", None),
1259 ("headline_text_font_size", "size", defaultLogFontSize),
1260 ("headline_text_font_slant", "slant", "roman"),
1261 ("headline_text_font_weight", "weight", "normal"),
1262 ("log_text_font_family", "string", None),
1263 ("log_text_font_size", "size", defaultLogFontSize),
1264 ("log_text_font_slant", "slant", "roman"),
1265 ("log_text_font_weight", "weight", "normal"),
1266 ("initial_window_height", "int", 600),
1267 ("initial_window_width", "int", 800),
1268 ("initial_window_left", "int", 10),
1269 ("initial_window_top", "int", 10),
1270 ("initial_split_orientation", "string", "vertical"), # was initial_splitter_orientation.
1271 ("initial_vertical_ratio", "ratio", 0.5),
1272 ("initial_horizontal_ratio", "ratio", 0.3),
1273 ("initial_horizontal_secondary_ratio", "ratio", 0.5),
1274 ("initial_vertical_secondary_ratio", "ratio", 0.7),
1275 # ("outline_pane_scrolls_horizontally","bool",False),
1276 ("split_bar_color", "color", "LightSteelBlue2"),
1277 ("split_bar_relief", "relief", "groove"),
1278 ("split_bar_width", "int", 7),
1279 )
1280 #@-<< gcm.defaultsDict >>
1281 #@+<< gcm.encodingIvarsDict >>
1282 #@+node:ekr.20041118062709: *3* << gcm.encodingIvarsDict >>
1283 encodingIvarsDict = g.TypedDict(
1284 name='g.app.config.encodingIvarsDict',
1285 keyType=str,
1286 valType=g.GeneralSetting,
1287 )
1288 encodingIvarsData = (
1289 ("default_at_auto_file_encoding", "string", "utf-8"),
1290 ("default_derived_file_encoding", "string", "utf-8"),
1291 ("new_leo_file_encoding", "string", "UTF-8"),
1292 # Upper case for compatibility with previous versions.
1293 #
1294 # The defaultEncoding ivar is no longer used,
1295 # so it doesn't override better defaults.
1296 )
1297 #@-<< gcm.encodingIvarsDict >>
1298 #@+<< gcm.ivarsDict >>
1299 #@+node:ekr.20041117072055: *3* << gcm.ivarsDict >>
1300 # Each of these settings sets the corresponding ivar.
1301 # Also, the LocalConfigManager class inits the corresponding commander ivar.
1302 ivarsDict = g.TypedDict(
1303 name='g.app.config.ivarsDict',
1304 keyType=str,
1305 valType=g.GeneralSetting,
1306 )
1307 ivarsData = (
1308 ("at_root_bodies_start_in_doc_mode", "bool", True),
1309 # For compatibility with previous versions.
1310 ("create_nonexistent_directories", "bool", False),
1311 ("output_initial_comment", "string", ""),
1312 # "" for compatibility with previous versions.
1313 ("output_newline", "string", "nl"),
1314 ("page_width", "int", "132"),
1315 ("read_only", "bool", True),
1316 ("redirect_execute_script_output_to_log_pane", "bool", False),
1317 ("relative_path_base_directory", "string", "!"),
1318 ("remove_sentinels_extension", "string", ".txt"),
1319 ("save_clears_undo_buffer", "bool", False),
1320 ("stylesheet", "string", None),
1321 ("tab_width", "int", -4),
1322 ("target_language", "language", "python"),
1323 # Bug fix: added: 6/20/2005.
1324 ("trailing_body_newlines", "string", "asis"),
1325 ("use_plugins", "bool", True),
1326 # New in 4.3: use_plugins = True by default.
1327 ("undo_granularity", "string", "word"),
1328 # "char","word","line","node"
1329 ("write_strips_blank_lines", "bool", False),
1330 )
1331 #@-<< gcm.ivarsDict >>
1332 #@+others
1333 #@+node:ekr.20041117083202: *3* gcm.Birth...
1334 #@+node:ekr.20041117062717.2: *4* gcm.ctor
1335 def __init__(self):
1336 #
1337 # Set later. To keep pylint happy.
1338 if 0: # No longer needed, now that setIvarsFromSettings always sets gcm ivars.
1339 self.at_root_bodies_start_in_doc_mode = True
1340 self.default_derived_file_encoding = 'utf-8'
1341 self.output_newline = 'nl'
1342 self.redirect_execute_script_output_to_log_pane = True
1343 self.relative_path_base_directory = '!'
1344 self.use_plugins = False # Required to keep pylint happy.
1345 self.create_nonexistent_directories = False # Required to keep pylint happy.
1346 self.atCommonButtonsList = [] # List of info for common @buttons nodes.
1347 self.atCommonCommandsList = [] # List of info for common @commands nodes.
1348 self.atLocalButtonsList = [] # List of positions of @button nodes.
1349 self.atLocalCommandsList = [] # List of positions of @command nodes.
1350 self.buttonsFileName = ''
1351 self.configsExist = False # True when we successfully open a setting file.
1352 self.defaultFont = None # Set in gui.getDefaultConfigFont.
1353 self.defaultFontFamily = None # Set in gui.getDefaultConfigFont.
1354 self.enabledPluginsFileName = None
1355 self.enabledPluginsString = ''
1356 self.inited = False
1357 self.menusList = []
1358 self.menusFileName = ''
1359 self.modeCommandsDict = g.TypedDict(
1360 name='modeCommandsDict',
1361 keyType=str,
1362 valType=g.TypedDict) # was TypedDictOfLists.
1363 # Inited later...
1364 self.panes = None
1365 self.sc = None
1366 self.tree = None
1367 self.initDicts()
1368 self.initIvarsFromSettings()
1369 self.initRecentFiles()
1370 #@+node:ekr.20041227063801.2: *4* gcm.initDicts
1371 def initDicts(self):
1372 # Only the settings parser needs to search all dicts.
1373 self.dictList = [self.defaultsDict]
1374 for key, kind, val in self.defaultsData:
1375 self.defaultsDict[self.munge(key)] = g.GeneralSetting(
1376 kind, setting=key, val=val, tag='defaults')
1377 for key, kind, val in self.ivarsData:
1378 self.ivarsDict[self.munge(key)] = g.GeneralSetting(
1379 kind, ivar=key, val=val, tag='ivars')
1380 for key, kind, val in self.encodingIvarsData:
1381 self.encodingIvarsDict[self.munge(key)] = g.GeneralSetting(
1382 kind, encoding=val, ivar=key, tag='encoding')
1383 #@+node:ekr.20041117065611.2: *4* gcm.initIvarsFromSettings & helpers
1384 def initIvarsFromSettings(self):
1385 for ivar in sorted(list(self.encodingIvarsDict.keys())):
1386 self.initEncoding(ivar)
1387 for ivar in sorted(list(self.ivarsDict.keys())):
1388 self.initIvar(ivar)
1389 #@+node:ekr.20041117065611.1: *5* initEncoding
1390 def initEncoding(self, key):
1391 """Init g.app.config encoding ivars during initialization."""
1392 # Important: The key is munged.
1393 gs = self.encodingIvarsDict.get(key)
1394 setattr(self, gs.ivar, gs.encoding)
1395 if gs.encoding and not g.isValidEncoding(gs.encoding):
1396 g.es('g.app.config: bad encoding:', f"{gs.ivar}: {gs.encoding}")
1397 #@+node:ekr.20041117065611: *5* initIvar
1398 def initIvar(self, key):
1399 """
1400 Init g.app.config ivars during initialization.
1402 This does NOT init the corresponding commander ivars.
1404 Such initing must be done in setIvarsFromSettings.
1405 """
1406 # Important: the key is munged.
1407 d = self.ivarsDict
1408 gs = d.get(key)
1409 setattr(self, gs.ivar, gs.val)
1410 #@+node:ekr.20041117083202.2: *4* gcm.initRecentFiles
1411 def initRecentFiles(self):
1412 self.recentFiles = []
1413 #@+node:ekr.20041228042224: *4* gcm.setIvarsFromSettings
1414 def setIvarsFromSettings(self, c):
1415 """
1416 Init g.app.config ivars or c's ivars from settings.
1418 - Called from c.initSettings with c = None to init g.app.config ivars.
1419 - Called from c.initSettings to init corresponding commmander ivars.
1420 """
1421 if g.app.loadedThemes:
1422 return
1423 if not self.inited:
1424 return
1425 # Ignore temporary commanders created by readSettingsFiles.
1426 d = self.ivarsDict
1427 keys = list(d.keys())
1428 keys.sort()
1429 for key in keys:
1430 gs = d.get(key)
1431 if gs:
1432 assert isinstance(gs, g.GeneralSetting)
1433 ivar = gs.ivar # The actual name of the ivar.
1434 kind = gs.kind
1435 if c:
1436 val = c.config.get(key, kind)
1437 else:
1438 val = self.get(key, kind) # Don't use bunch.val!
1439 if c:
1440 setattr(c, ivar, val)
1441 if True: # Always set the global ivars.
1442 setattr(self, ivar, val)
1443 #@+node:ekr.20041117081009: *3* gcm.Getters...
1444 #@+node:ekr.20041123070429: *4* gcm.canonicalizeSettingName (munge)
1445 def canonicalizeSettingName(self, name):
1446 if name is None:
1447 return None
1448 name = name.lower()
1449 for ch in ('-', '_', ' ', '\n'):
1450 name = name.replace(ch, '')
1451 return name if name else None
1453 munge = canonicalizeSettingName
1454 #@+node:ekr.20051011105014: *4* gcm.exists
1455 def exists(self, setting, kind):
1456 """Return true if a setting of the given kind exists, even if it is None."""
1457 lm = g.app.loadManager
1458 d = lm.globalSettingsDict
1459 if d:
1460 junk, found = self.getValFromDict(d, setting, kind)
1461 return found
1462 return False
1463 #@+node:ekr.20041117083141: *4* gcm.get & allies
1464 def get(self, setting, kind):
1465 """Get the setting and make sure its type matches the expected type."""
1466 lm = g.app.loadManager
1467 #
1468 # It *is* valid to call this method: it returns the global settings.
1469 d = lm.globalSettingsDict
1470 if d:
1471 assert isinstance(d, g.TypedDict), repr(d)
1472 val, junk = self.getValFromDict(d, setting, kind)
1473 return val
1474 return None
1475 #@+node:ekr.20041121143823: *5* gcm.getValFromDict
1476 def getValFromDict(self, d, setting, requestedType, warn=True):
1477 """
1478 Look up the setting in d. If warn is True, warn if the requested type
1479 does not (loosely) match the actual type.
1480 returns (val,exists)
1481 """
1482 tag = 'gcm.getValFromDict'
1483 gs = d.get(self.munge(setting))
1484 if not gs:
1485 return None, False
1486 assert isinstance(gs, g.GeneralSetting), repr(gs)
1487 val = gs.val
1488 isNone = val in ('None', 'none', '')
1489 if not self.typesMatch(gs.kind, requestedType):
1490 # New in 4.4: make sure the types match.
1491 # A serious warning: one setting may have destroyed another!
1492 # Important: this is not a complete test of conflicting settings:
1493 # The warning is given only if the code tries to access the setting.
1494 if warn:
1495 g.error(
1496 f"{tag}: ignoring '{setting}' setting.\n"
1497 f"{tag}: '@{gs.kind}' is not '@{requestedType}'.\n"
1498 f"{tag}: there may be conflicting settings!")
1499 return None, False
1500 if isNone:
1501 return '', True
1502 # 2011/10/24: Exists, a *user-defined* empty value.
1503 return val, True
1504 #@+node:ekr.20051015093141: *5* gcm.typesMatch
1505 def typesMatch(self, type1, type2):
1506 """
1507 Return True if type1, the actual type, matches type2, the requeseted type.
1509 The following equivalences are allowed:
1511 - None matches anything.
1512 - An actual type of string or strings matches anything *except* shortcuts.
1513 - Shortcut matches shortcuts.
1514 """
1515 # The shortcuts logic no longer uses the get/set code.
1516 shortcuts = ('shortcut', 'shortcuts',)
1517 if type1 in shortcuts or type2 in shortcuts:
1518 g.trace('oops: type in shortcuts')
1519 return (
1520 type1 is None
1521 or type2 is None
1522 or type1.startswith('string') and type2 not in shortcuts
1523 or type1 == 'language' and type2 == 'string'
1524 or type1 == 'int' and type2 == 'size'
1525 or (type1 in shortcuts and type2 in shortcuts)
1526 or type1 == type2
1527 )
1528 #@+node:ekr.20060608224112: *4* gcm.getAbbrevDict
1529 def getAbbrevDict(self):
1530 """Search all dictionaries for the setting & check it's type"""
1531 d = self.get('abbrev', 'abbrev')
1532 return d or {}
1533 #@+node:ekr.20041117081009.3: *4* gcm.getBool
1534 def getBool(self, setting, default=None):
1535 """Return the value of @bool setting, or the default if the setting is not found."""
1536 val = self.get(setting, "bool")
1537 if val in (True, False):
1538 return val
1539 return default
1540 #@+node:ekr.20070926082018: *4* gcm.getButtons
1541 def getButtons(self):
1542 """Return a list of tuples (x,y) for common @button nodes."""
1543 return g.app.config.atCommonButtonsList
1544 #@+node:ekr.20041122070339: *4* gcm.getColor
1545 def getColor(self, setting):
1546 """Return the value of @color setting."""
1547 col = self.get(setting, "color")
1548 while col and col.startswith('@'):
1549 col = self.get(col[1:], "color")
1550 return col
1551 #@+node:ekr.20080312071248.7: *4* gcm.getCommonCommands
1552 def getCommonAtCommands(self):
1553 """Return the list of tuples (headline,script) for common @command nodes."""
1554 return g.app.config.atCommonCommandsList
1555 #@+node:ekr.20071214140900.1: *4* gcm.getData & getOutlineData
1556 def getData(self, setting, strip_comments=True, strip_data=True):
1557 """Return a list of non-comment strings in the body text of @data setting."""
1558 data = self.get(setting, "data") or []
1559 # New in Leo 4.12.1: add two keyword arguments, with legacy defaults.
1560 if data and strip_comments:
1561 data = [z for z in data if not z.strip().startswith('#')]
1562 if data and strip_data:
1563 data = [z.strip() for z in data if z.strip()]
1564 return data
1566 def getOutlineData(self, setting):
1567 """Return the pastable (xml text) of the entire @outline-data tree."""
1568 return self.get(setting, "outlinedata")
1569 #@+node:ekr.20041117093009.1: *4* gcm.getDirectory
1570 def getDirectory(self, setting):
1571 """Return the value of @directory setting, or None if the directory does not exist."""
1572 # Fix https://bugs.launchpad.net/leo-editor/+bug/1173763
1573 theDir = self.get(setting, 'directory')
1574 if g.os_path_exists(theDir) and g.os_path_isdir(theDir):
1575 return theDir
1576 return None
1577 #@+node:ekr.20070224075914.1: *4* gcm.getEnabledPlugins
1578 def getEnabledPlugins(self):
1579 """Return the body text of the @enabled-plugins node."""
1580 return g.app.config.enabledPluginsString
1581 #@+node:ekr.20041117082135: *4* gcm.getFloat
1582 def getFloat(self, setting):
1583 """Return the value of @float setting."""
1584 val = self.get(setting, "float")
1585 try:
1586 val = float(val)
1587 return val
1588 except TypeError:
1589 return None
1590 #@+node:ekr.20041117062717.13: *4* gcm.getFontFromParams
1591 def getFontFromParams(self, family, size, slant, weight, defaultSize=12):
1592 """Compute a font from font parameters.
1594 Arguments are the names of settings to be use.
1595 Default to size=12, slant="roman", weight="normal".
1597 Return None if there is no family setting so we can use system default fonts."""
1598 family = self.get(family, "family")
1599 if family in (None, ""):
1600 family = self.defaultFontFamily
1601 size = self.get(size, "size")
1602 if size in (None, 0):
1603 size = defaultSize
1604 slant = self.get(slant, "slant")
1605 if slant in (None, ""):
1606 slant = "roman"
1607 weight = self.get(weight, "weight")
1608 if weight in (None, ""):
1609 weight = "normal"
1610 return g.app.gui.getFontFromParams(family, size, slant, weight)
1611 #@+node:ekr.20041117081513: *4* gcm.getInt
1612 def getInt(self, setting):
1613 """Return the value of @int setting."""
1614 val = self.get(setting, "int")
1615 try:
1616 val = int(val)
1617 return val
1618 except TypeError:
1619 return None
1620 #@+node:ekr.20041117093009.2: *4* gcm.getLanguage
1621 def getLanguage(self, setting):
1622 """Return the setting whose value should be a language known to Leo."""
1623 language = self.getString(setting)
1624 return language
1625 #@+node:ekr.20070926070412: *4* gcm.getMenusList
1626 def getMenusList(self):
1627 """Return the list of entries for the @menus tree."""
1628 aList = self.get('menus', 'menus')
1629 # aList is typically empty.
1630 return aList or g.app.config.menusList
1631 #@+node:ekr.20070411101643: *4* gcm.getOpenWith
1632 def getOpenWith(self):
1633 """Return a list of dictionaries corresponding to @openwith nodes."""
1634 val = self.get('openwithtable', 'openwithtable')
1635 return val
1636 #@+node:ekr.20041122070752: *4* gcm.getRatio
1637 def getRatio(self, setting):
1638 """Return the value of @float setting.
1640 Warn if the value is less than 0.0 or greater than 1.0."""
1641 val = self.get(setting, "ratio")
1642 try:
1643 val = float(val)
1644 if 0.0 <= val <= 1.0:
1645 return val
1646 except TypeError:
1647 pass
1648 return None
1649 #@+node:ekr.20041117062717.11: *4* gcm.getRecentFiles
1650 def getRecentFiles(self):
1651 """Return the list of recently opened files."""
1652 return self.recentFiles
1653 #@+node:ekr.20041117081009.4: *4* gcm.getString
1654 def getString(self, setting):
1655 """Return the value of @string setting."""
1656 return self.get(setting, "string")
1657 #@+node:ekr.20120222103014.10314: *3* gcm.config_iter
1658 def config_iter(self, c):
1659 """Letters:
1660 leoSettings.leo
1661 D default settings
1662 F loaded .leo File
1663 M myLeoSettings.leo
1664 @ @button, @command, @mode.
1665 """
1666 lm = g.app.loadManager
1667 d = c.config.settingsDict if c else lm.globalSettingsDict
1668 limit = c.config.getInt('print-settings-at-data-limit')
1669 if limit is None:
1670 limit = 20 # A resonable default.
1671 # pylint: disable=len-as-condition
1672 for key in sorted(list(d.keys())):
1673 gs = d.get(key)
1674 assert isinstance(gs, g.GeneralSetting), repr(gs)
1675 if gs and gs.kind:
1676 letter = lm.computeBindingLetter(c, gs.path)
1677 val = gs.val
1678 if gs.kind == 'data':
1679 # #748: Remove comments
1680 aList = [' ' * 8 + z.rstrip() for z in val
1681 if z.strip() and not z.strip().startswith('#')]
1682 if not aList:
1683 val = '[]'
1684 elif limit == 0 or len(aList) < limit:
1685 val = '\n [\n' + '\n'.join(aList) + '\n ]'
1686 # The following doesn't work well.
1687 # val = g.objToString(aList, indent=' '*4)
1688 else:
1689 val = f"<{len(aList)} non-comment lines>"
1690 elif isinstance(val, str) and val.startswith('<?xml'):
1691 val = '<xml>'
1692 key2 = f"@{gs.kind:>6} {key}"
1693 yield key2, val, c, letter
1694 #@+node:ekr.20171115062202.1: *3* gcm.valueInMyLeoSettings
1695 def valueInMyLeoSettings(self, settingName):
1696 """Return the value of the setting, if any, in myLeoSettings.leo."""
1697 lm = g.app.loadManager
1698 d = lm.globalSettingsDict.d
1699 gs = d.get(self.munge(settingName))
1700 # A GeneralSetting object.
1701 if gs:
1702 path = gs.path
1703 if path.find('myLeoSettings.leo') > -1:
1704 return gs.val
1705 return None
1706 #@-others
1707#@+node:ekr.20041118104831.1: ** class LocalConfigManager
1708class LocalConfigManager:
1709 """A class to hold config settings for commanders."""
1710 #@+others
1711 #@+node:ekr.20120215072959.12472: *3* c.config.Birth
1712 #@+node:ekr.20041118104831.2: *4* c.config.ctor
1713 def __init__(self, c, previousSettings=None):
1715 self.c = c
1716 lm = g.app.loadManager
1717 #
1718 # c.__init__ and helpers set the shortcuts and settings dicts for local files.
1719 if previousSettings:
1720 self.settingsDict = previousSettings.settingsDict
1721 self.shortcutsDict = previousSettings.shortcutsDict
1722 assert isinstance(self.settingsDict, g.TypedDict), repr(self.settingsDict)
1723 assert isinstance(self.shortcutsDict, g.TypedDict), repr(self.shortcutsDict)
1724 # was TypedDictOfLists.
1725 else:
1726 self.settingsDict = d1 = lm.globalSettingsDict
1727 self.shortcutsDict = d2 = lm.globalBindingsDict
1728 assert d1 is None or isinstance(d1, g.TypedDict), repr(d1)
1729 assert d2 is None or isinstance(
1730 d2, g.TypedDict), repr(d2) # was TypedDictOfLists.
1731 # Define these explicitly to eliminate a pylint warning.
1732 if 0:
1733 # No longer needed now that c.config.initIvar always sets
1734 # both c and c.config ivars.
1735 self.default_derived_file_encoding = g.app.config.default_derived_file_encoding
1736 self.redirect_execute_script_output_to_log_pane = g.app.config.redirect_execute_script_output_to_log_pane
1737 self.defaultBodyFontSize = g.app.config.defaultBodyFontSize
1738 self.defaultLogFontSize = g.app.config.defaultLogFontSize
1739 self.defaultMenuFontSize = g.app.config.defaultMenuFontSize
1740 self.defaultTreeFontSize = g.app.config.defaultTreeFontSize
1741 for key in sorted(list(g.app.config.encodingIvarsDict.keys())):
1742 self.initEncoding(key)
1743 for key in sorted(list(g.app.config.ivarsDict.keys())):
1744 self.initIvar(key)
1745 #@+node:ekr.20041118104414: *4* c.config.initEncoding
1746 def initEncoding(self, key):
1747 # Important: the key is munged.
1748 gs = g.app.config.encodingIvarsDict.get(key)
1749 encodingName = gs.ivar
1750 encoding = self.get(encodingName, kind='string')
1751 # Use the global setting as a last resort.
1752 if encoding:
1753 setattr(self, encodingName, encoding)
1754 else:
1755 encoding = getattr(g.app.config, encodingName)
1756 setattr(self, encodingName, encoding)
1757 if encoding and not g.isValidEncoding(encoding):
1758 g.es('bad', f"{encodingName}: {encoding}")
1759 #@+node:ekr.20041118104240: *4* c.config.initIvar
1760 def initIvar(self, key):
1762 c = self.c
1763 # Important: the key is munged.
1764 gs = g.app.config.ivarsDict.get(key)
1765 ivarName = gs.ivar
1766 val = self.get(ivarName, kind=None)
1767 if val or not hasattr(self, ivarName):
1768 # Set *both* the commander ivar and the c.config ivar.
1769 setattr(self, ivarName, val)
1770 setattr(c, ivarName, val)
1771 #@+node:ekr.20190831030206.1: *3* c.config.createActivesSettingsOutline (new: #852)
1772 def createActivesSettingsOutline(self):
1773 """
1774 Create and open an outline, summarizing all presently active settings.
1776 The outline retains the organization of all active settings files.
1778 See #852: https://github.com/leo-editor/leo-editor/issues/852
1779 """
1780 ActiveSettingsOutline(self.c)
1781 #@+node:ekr.20190901181116.1: *3* c.config.getSource
1782 def getSource(self, setting):
1783 """
1784 Return a string representing the source file of the given setting,
1785 one of ("local_file", "theme_file", "myLeoSettings", "leoSettings", "ignore", "error")
1786 """
1787 if not isinstance(setting, g.GeneralSetting):
1788 return "error"
1789 try:
1790 path = setting.path
1791 except Exception:
1792 return "error"
1793 if not path:
1794 return "local_file"
1795 path = path.lower()
1796 for tag in ('myLeoSettings.leo', 'leoSettings.leo'):
1797 if path.endswith(tag.lower()):
1798 return tag[:-4] # PR: #2422.
1799 theme_path = g.app.loadManager.theme_path
1800 if theme_path and g.shortFileName(theme_path.lower()) in path:
1801 return "theme_file"
1802 if path == 'register-command' or path.find('mode') > -1:
1803 return 'ignore'
1804 return "local_file"
1805 #@+node:ekr.20120215072959.12471: *3* c.config.Getters
1806 #@+node:ekr.20041123092357: *4* c.config.findSettingsPosition & helper
1807 # This was not used prior to Leo 4.5.
1809 def findSettingsPosition(self, setting):
1810 """Return the position for the setting in the @settings tree for c."""
1811 munge = g.app.config.munge
1812 # c = self.c
1813 root = self.settingsRoot()
1814 if not root:
1815 return None
1816 setting = munge(setting)
1817 for p in root.subtree():
1818 #BJ munge will return None if a headstring is empty
1819 h = munge(p.h) or ''
1820 if h.startswith(setting):
1821 return p.copy()
1822 return None
1823 #@+node:ekr.20041120074536: *5* c.config.settingsRoot
1824 def settingsRoot(self):
1825 """Return the position of the @settings tree."""
1826 c = self.c
1827 for p in c.all_unique_positions():
1828 # #1792: Allow comments after @settings.
1829 if g.match_word(p.h.rstrip(), 0, "@settings"):
1830 return p.copy()
1831 return None
1832 #@+node:ekr.20120215072959.12515: *4* c.config.Getters
1833 #@@nocolor-node
1834 #@+at Only the following need to be defined.
1835 # get (self,setting,theType)
1836 # getAbbrevDict (self)
1837 # getBool (self,setting,default=None)
1838 # getButtons (self)
1839 # getColor (self,setting)
1840 # getData (self,setting)
1841 # getDirectory (self,setting)
1842 # getFloat (self,setting)
1843 # getFontFromParams (self,family,size,slant,weight,defaultSize=12)
1844 # getInt (self,setting)
1845 # getLanguage (self,setting)
1846 # getMenusList (self)
1847 # getOutlineData (self)
1848 # getOpenWith (self)
1849 # getRatio (self,setting)
1850 # getShortcut (self,commandName)
1851 # getString (self,setting)
1852 #@+node:ekr.20120215072959.12519: *5* c.config.get & allies
1853 def get(self, setting, kind):
1854 """Get the setting and make sure its type matches the expected type."""
1855 d = self.settingsDict
1856 if d:
1857 assert isinstance(d, g.TypedDict), repr(d)
1858 val, junk = self.getValFromDict(d, setting, kind)
1859 return val
1860 return None
1861 #@+node:ekr.20120215072959.12520: *6* c.config.getValFromDict
1862 def getValFromDict(self, d, setting, requestedType, warn=True):
1863 """
1864 Look up the setting in d. If warn is True, warn if the requested type
1865 does not (loosely) match the actual type.
1866 returns (val,exists)
1867 """
1868 tag = 'c.config.getValFromDict'
1869 gs = d.get(g.app.config.munge(setting))
1870 if not gs:
1871 return None, False
1872 assert isinstance(gs, g.GeneralSetting), repr(gs)
1873 val = gs.val
1874 isNone = val in ('None', 'none', '')
1875 if not self.typesMatch(gs.kind, requestedType):
1876 # New in 4.4: make sure the types match.
1877 # A serious warning: one setting may have destroyed another!
1878 # Important: this is not a complete test of conflicting settings:
1879 # The warning is given only if the code tries to access the setting.
1880 if warn:
1881 g.error(
1882 f"{tag}: ignoring '{setting}' setting.\n"
1883 f"{tag}: '@{gs.kind}' is not '@{requestedType}'.\n"
1884 f"{tag}: there may be conflicting settings!")
1885 return None, False
1886 if isNone:
1887 return '', True
1888 # 2011/10/24: Exists, a *user-defined* empty value.
1889 return val, True
1890 #@+node:ekr.20120215072959.12521: *6* c.config.typesMatch
1891 def typesMatch(self, type1, type2):
1892 """
1893 Return True if type1, the actual type, matches type2, the requeseted type.
1895 The following equivalences are allowed:
1897 - None matches anything.
1898 - An actual type of string or strings matches anything *except* shortcuts.
1899 - Shortcut matches shortcuts.
1900 """
1901 # The shortcuts logic no longer uses the get/set code.
1902 shortcuts = ('shortcut', 'shortcuts',)
1903 if type1 in shortcuts or type2 in shortcuts:
1904 g.trace('oops: type in shortcuts')
1905 return (
1906 type1 is None
1907 or type2 is None
1908 or type1.startswith('string') and type2 not in shortcuts
1909 or type1 == 'language' and type2 == 'string'
1910 or type1 == 'int' and type2 == 'size'
1911 or (type1 in shortcuts and type2 in shortcuts)
1912 or type1 == type2
1913 )
1914 #@+node:ekr.20120215072959.12522: *5* c.config.getAbbrevDict
1915 def getAbbrevDict(self):
1916 """Search all dictionaries for the setting & check it's type"""
1917 d = self.get('abbrev', 'abbrev')
1918 return d or {}
1919 #@+node:ekr.20120215072959.12523: *5* c.config.getBool
1920 def getBool(self, setting, default=None):
1921 """Return the value of @bool setting, or the default if the setting is not found."""
1922 val = self.get(setting, "bool")
1923 if val in (True, False):
1924 return val
1925 return default
1926 #@+node:ekr.20120215072959.12525: *5* c.config.getColor
1927 def getColor(self, setting):
1928 """Return the value of @color setting."""
1929 col = self.get(setting, "color")
1930 while col and col.startswith('@'):
1931 col = self.get(col[1:], "color")
1932 return col
1933 #@+node:ekr.20120215072959.12527: *5* c.config.getData
1934 def getData(self, setting, strip_comments=True, strip_data=True):
1935 """Return a list of non-comment strings in the body text of @data setting."""
1936 # 904: Add local abbreviations to global settings.
1937 append = setting == 'global-abbreviations'
1938 if append:
1939 data0 = g.app.config.getData(setting,
1940 strip_comments=strip_comments,
1941 strip_data=strip_data,
1942 )
1943 data = self.get(setting, "data")
1944 # New in Leo 4.11: parser.doData strips only comments now.
1945 # New in Leo 4.12: parser.doData strips *nothing*.
1946 if isinstance(data, str):
1947 data = [data]
1948 if data and strip_comments:
1949 data = [z for z in data if not z.strip().startswith('#')]
1950 if data and strip_data:
1951 data = [z.strip() for z in data if z.strip()]
1952 if append and data != data0:
1953 if data:
1954 data.extend(data0)
1955 else:
1956 data = data0
1957 return data
1958 #@+node:ekr.20131114051702.16542: *5* c.config.getOutlineData
1959 def getOutlineData(self, setting):
1960 """Return the pastable (xml) text of the entire @outline-data tree."""
1961 data = self.get(setting, "outlinedata")
1962 if setting == 'tree-abbreviations':
1963 # 904: Append local tree abbreviations to the global abbreviations.
1964 data0 = g.app.config.getOutlineData(setting)
1965 if data and data0 and data != data0:
1966 assert isinstance(data0, str)
1967 assert isinstance(data, str)
1968 # We can't merge the data here: they are .leo files!
1969 # abbrev.init_tree_abbrev_helper does the merge.
1970 data = [data0, data]
1971 return data
1972 #@+node:ekr.20120215072959.12528: *5* c.config.getDirectory
1973 def getDirectory(self, setting):
1974 """Return the value of @directory setting, or None if the directory does not exist."""
1975 # Fix https://bugs.launchpad.net/leo-editor/+bug/1173763
1976 theDir = self.get(setting, 'directory')
1977 if g.os_path_exists(theDir) and g.os_path_isdir(theDir):
1978 return theDir
1979 return None
1980 #@+node:ekr.20120215072959.12530: *5* c.config.getFloat
1981 def getFloat(self, setting):
1982 """Return the value of @float setting."""
1983 val = self.get(setting, "float")
1984 try:
1985 val = float(val)
1986 return val
1987 except TypeError:
1988 return None
1989 #@+node:ekr.20120215072959.12531: *5* c.config.getFontFromParams
1990 def getFontFromParams(self, family, size, slant, weight, defaultSize=12):
1991 """
1992 Compute a font from font parameters. This should be used *only*
1993 by the syntax coloring code. Otherwise, use Leo's style sheets.
1995 Arguments are the names of settings to be use.
1996 Default to size=12, slant="roman", weight="normal".
1998 Return None if there is no family setting so we can use system default fonts.
1999 """
2000 family = self.get(family, "family")
2001 if family in (None, ""):
2002 family = g.app.config.defaultFontFamily
2003 size = self.get(size, "size")
2004 if size in (None, 0):
2005 size = defaultSize
2006 slant = self.get(slant, "slant")
2007 if slant in (None, ""):
2008 slant = "roman"
2009 weight = self.get(weight, "weight")
2010 if weight in (None, ""):
2011 weight = "normal"
2012 return g.app.gui.getFontFromParams(family, size, slant, weight)
2013 #@+node:ekr.20120215072959.12532: *5* c.config.getInt
2014 def getInt(self, setting):
2015 """Return the value of @int setting."""
2016 val = self.get(setting, "int")
2017 try:
2018 val = int(val)
2019 return val
2020 except TypeError:
2021 return None
2022 #@+node:ekr.20120215072959.12533: *5* c.config.getLanguage
2023 def getLanguage(self, setting):
2024 """Return the setting whose value should be a language known to Leo."""
2025 language = self.getString(setting)
2026 return language
2027 #@+node:ekr.20120215072959.12534: *5* c.config.getMenusList
2028 def getMenusList(self):
2029 """Return the list of entries for the @menus tree."""
2030 aList = self.get('menus', 'menus')
2031 # aList is typically empty.
2032 return aList or g.app.config.menusList
2033 #@+node:ekr.20120215072959.12535: *5* c.config.getOpenWith
2034 def getOpenWith(self):
2035 """Return a list of dictionaries corresponding to @openwith nodes."""
2036 val = self.get('openwithtable', 'openwithtable')
2037 return val
2038 #@+node:ekr.20120215072959.12536: *5* c.config.getRatio
2039 def getRatio(self, setting):
2040 """
2041 Return the value of @float setting.
2043 Warn if the value is less than 0.0 or greater than 1.0.
2044 """
2045 val = self.get(setting, "ratio")
2046 try:
2047 val = float(val)
2048 if 0.0 <= val <= 1.0:
2049 return val
2050 except TypeError:
2051 pass
2052 return None
2053 #@+node:ekr.20120215072959.12538: *5* c.config.getSettingSource
2054 def getSettingSource(self, setting):
2055 """return the name of the file responsible for setting."""
2056 d = self.settingsDict
2057 if d:
2058 assert isinstance(d, g.TypedDict), repr(d)
2059 bi = d.get(setting)
2060 if bi is None:
2061 return 'unknown setting', None
2062 return bi.path, bi.val
2063 #
2064 # lm.readGlobalSettingsFiles is opening a settings file.
2065 # lm.readGlobalSettingsFiles has not yet set lm.globalSettingsDict.
2066 assert d is None
2067 return None
2068 #@+node:ekr.20120215072959.12539: *5* c.config.getShortcut
2069 no_menu_dict: Dict[Cmdr, bool] = {}
2071 def getShortcut(self, commandName):
2072 """Return rawKey,accel for shortcutName"""
2073 c = self.c
2074 d = self.shortcutsDict
2075 if not c.frame.menu:
2076 if c not in self.no_menu_dict:
2077 self.no_menu_dict[c] = True
2078 g.trace(f"no menu: {c.shortFileName()}:{commandName}")
2079 return None, []
2080 if d:
2081 assert isinstance(d, g.TypedDict), repr(d) # was TypedDictOfLists.
2082 key = c.frame.menu.canonicalizeMenuName(commandName)
2083 key = key.replace('&', '') # Allow '&' in names.
2084 aList = d.get(commandName, [])
2085 if aList: # A list of g.BindingInfo objects.
2086 # It's important to filter empty strokes here.
2087 aList = [z for z in aList
2088 if z.stroke and z.stroke.lower() != 'none']
2089 return key, aList
2090 #
2091 # lm.readGlobalSettingsFiles is opening a settings file.
2092 # lm.readGlobalSettingsFiles has not yet set lm.globalSettingsDict.
2093 return None, []
2094 #@+node:ekr.20120215072959.12540: *5* c.config.getString
2095 def getString(self, setting):
2096 """Return the value of @string setting."""
2097 return self.get(setting, "string")
2098 #@+node:ekr.20120215072959.12543: *4* c.config.Getters: redirect to g.app.config
2099 def getButtons(self):
2100 """Return a list of tuples (x,y) for common @button nodes."""
2101 return g.app.config.atCommonButtonsList # unusual.
2103 def getCommands(self):
2104 """Return the list of tuples (headline,script) for common @command nodes."""
2105 return g.app.config.atCommonCommandsList # unusual.
2107 def getEnabledPlugins(self):
2108 """Return the body text of the @enabled-plugins node."""
2109 return g.app.config.enabledPluginsString # unusual.
2111 def getRecentFiles(self):
2112 """Return the list of recently opened files."""
2113 return g.app.config.getRecentFiles() # unusual
2114 #@+node:ekr.20140114145953.16691: *4* c.config.isLocalSetting
2115 def isLocalSetting(self, setting, kind):
2116 """Return True if the indicated setting comes from a local .leo file."""
2117 if not kind or kind in ('shortcut', 'shortcuts', 'openwithtable'):
2118 return False
2119 key = g.app.config.munge(setting)
2120 if key is None:
2121 return False
2122 if not self.settingsDict:
2123 return False
2124 gs = self.settingsDict.get(key)
2125 if not gs:
2126 return False
2127 assert isinstance(gs, g.GeneralSetting), repr(gs)
2128 path = gs.path.lower()
2129 for fn in ('myLeoSettings.leo', 'leoSettings.leo'):
2130 if path.endswith(fn.lower()):
2131 return False
2132 return True
2133 #@+node:ekr.20171119222458.1: *4* c.config.isLocalSettingsFile
2134 def isLocalSettingsFile(self):
2135 """Return true if c is not leoSettings.leo or myLeoSettings.leo"""
2136 c = self.c
2137 fn = c.shortFileName().lower()
2138 for fn2 in ('leoSettings.leo', 'myLeoSettings.leo'):
2139 if fn.endswith(fn2.lower()):
2140 return False
2141 return True
2142 #@+node:ekr.20120224140548.10528: *4* c.exists
2143 def exists(self, c, setting, kind):
2144 """Return true if a setting of the given kind exists, even if it is None."""
2145 d = self.settingsDict
2146 if d:
2147 junk, found = self.getValFromDict(d, setting, kind)
2148 if found:
2149 return True
2150 return False
2151 #@+node:ekr.20070418073400: *3* c.config.printSettings
2152 def printSettings(self):
2153 """Prints the value of every setting, except key bindings and commands and open-with tables.
2154 The following shows where the active setting came from:
2156 - leoSettings.leo,
2157 - @ @button, @command, @mode.
2158 - [D] default settings.
2159 - [F] indicates the file being loaded,
2160 - [M] myLeoSettings.leo,
2161 - [T] theme .leo file.
2162 """
2163 legend = '''\
2164 legend:
2165 leoSettings.leo
2166 @ @button, @command, @mode
2167 [D] default settings
2168 [F] loaded .leo File
2169 [M] myLeoSettings.leo
2170 [T] theme .leo file.
2171 '''
2172 c = self.c
2173 legend = textwrap.dedent(legend)
2174 result = []
2175 for name, val, c, letter in g.app.config.config_iter(c):
2176 kind = ' ' if letter == ' ' else f"[{letter}]"
2177 result.append(f"{kind} {name} = {val}\n")
2178 # Use a single g.es statement.
2179 result.append('\n' + legend)
2180 if g.unitTesting:
2181 pass # print(''.join(result))
2182 else:
2183 g.es_print('', ''.join(result), tabName='Settings')
2184 #@+node:ekr.20120215072959.12475: *3* c.config.set
2185 def set(self, p, kind, name, val, warn=True):
2186 """
2187 Init the setting for name to val.
2189 The "p" arg is not used.
2190 """
2191 c = self.c
2192 # Note: when kind is 'shortcut', name is a command name.
2193 key = g.app.config.munge(name)
2194 d = self.settingsDict
2195 assert isinstance(d, g.TypedDict), repr(d)
2196 gs = d.get(key)
2197 if gs:
2198 assert isinstance(gs, g.GeneralSetting), repr(gs)
2199 path = gs.path
2200 if warn and g.os_path_finalize(
2201 c.mFileName) != g.os_path_finalize(path): # #1341.
2202 g.es("over-riding setting:", name, "from", path)
2203 d[key] = g.GeneralSetting(kind, path=c.mFileName, val=val, tag='setting')
2204 #@+node:ekr.20190905082644.1: *3* c.config.settingIsActiveInPath
2205 def settingIsActiveInPath(self, gs, target_path):
2206 """Return True if settings file given by path actually defines the setting, gs."""
2207 assert isinstance(gs, g.GeneralSetting), repr(gs)
2208 return gs.path == target_path
2209 #@+node:ekr.20180121135120.1: *3* c.config.setUserSetting
2210 def setUserSetting(self, setting, value):
2211 """
2212 Find and set the indicated setting, either in the local file or in
2213 myLeoSettings.leo.
2214 """
2215 c = self.c
2216 fn = g.shortFileName(c.fileName())
2217 p = self.findSettingsPosition(setting)
2218 if not p:
2219 c = c.openMyLeoSettings()
2220 if not c:
2221 return
2222 fn = 'myLeoSettings.leo'
2223 p = c.config.findSettingsPosition(setting)
2224 if not p:
2225 root = c.config.settingsRoot()
2226 if not root:
2227 return
2228 fn = 'leoSettings.leo'
2229 p = c.config.findSettingsPosition(setting)
2230 if not p:
2231 p = root.insertAsLastChild()
2232 h = setting
2233 i = h.find('=')
2234 if i > -1:
2235 h = h[:i].strip()
2236 p.h = f"{h} = {value}"
2237 print(f"Updated `{setting}` in {fn}") # #2390.
2238 #
2239 # Delay the second redraw until idle time.
2240 c.setChanged()
2241 p.setDirty()
2242 c.redraw_later()
2243 #@-others
2244#@+node:ekr.20041119203941.3: ** class SettingsTreeParser (ParserBaseClass)
2245class SettingsTreeParser(ParserBaseClass):
2246 """A class that inits settings found in an @settings tree.
2248 Used by read settings logic."""
2250 # def __init__(self, c, localFlag=True):
2251 # super().__init__(c, localFlag)
2252 #@+others
2253 #@+node:ekr.20041119204103: *3* ctor (SettingsTreeParser)
2254 #@+node:ekr.20041119204714: *3* visitNode (SettingsTreeParser)
2255 def visitNode(self, p):
2256 """Init any settings found in node p."""
2257 p = p.copy()
2258 # Bug fix 2011/11/24
2259 # Ensure inner traversals don't change callers's p.
2260 munge = g.app.config.munge
2261 kind, name, val = self.parseHeadline(p.h)
2262 kind = munge(kind)
2263 isNone = val in ('None', 'none', '', None)
2264 if kind is None: # Not an @x node. (New in Leo 4.4.4)
2265 pass
2266 elif kind == "settings":
2267 pass
2268 elif kind in self.basic_types and isNone:
2269 # None is valid for all basic types.
2270 self.set(p, kind, name, None)
2271 elif kind in self.control_types or kind in self.basic_types:
2272 f = self.dispatchDict.get(kind)
2273 if f:
2274 try:
2275 return f(p, kind, name, val) # type:ignore
2276 except Exception:
2277 g.es_exception()
2278 else:
2279 g.pr("*** no handler", kind)
2280 return None
2281 #@-others
2282#@+node:ekr.20171229131953.1: ** parseFont (leoConfig.py)
2283def parseFont(b):
2284 family = None
2285 weight = None
2286 slant = None
2287 size = None
2288 settings_name = None
2289 for line in g.splitLines(b):
2290 line = line.strip()
2291 if line.startswith('#'):
2292 continue
2293 i = line.find('=')
2294 if i < 0:
2295 continue
2296 name = line[:i].strip()
2297 if name.endswith('_family'):
2298 family = line[i + 1 :].strip()
2299 elif name.endswith('_weight'):
2300 weight = line[i + 1 :].strip()
2301 elif name.endswith('_size'):
2302 size = line[i + 1 :].strip()
2303 try:
2304 size = float(size) # type:ignore
2305 except ValueError:
2306 size = 12 # type:ignore
2307 elif name.endswith('_slant'):
2308 slant = line[i + 1 :].strip()
2309 if settings_name is None and name.endswith(
2310 ('_family', '_slant', '_weight', '_size')):
2311 settings_name = name.rsplit('_', 1)[0]
2312 return settings_name, family, weight == 'bold', slant in ('slant', 'italic'), size
2313#@-others
2314#@@language python
2315#@@tabwidth -4
2316#@@pagewidth 70
2317#@-leo