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