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