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

1383 statements  

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 

62 

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) 

257 

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. 

300 

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. 

317 

318 Examples: Let h = os.environ('HOSTNAME') 

319 

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=''): 

457 

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): 

476 

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): 

509 

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): 

576 

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): 

768 

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: 

804 

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: 

926 

927 def __init__(self, c): 

928 

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): 

955 

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" 

960 

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 

1041 

1042 legend: 

1043 

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. 

1131 

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) 

1158 

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. 

1385 

1386 This does NOT init the corresponding commander ivars. 

1387 

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. 

1401 

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 

1436 

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. 

1492 

1493 The following equivalences are allowed: 

1494 

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 

1549 

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. 

1577 

1578 Arguments are the names of settings to be use. 

1579 Default to size=12, slant="roman", weight="normal". 

1580 

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. 

1623 

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): 

1697 

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): 

1744 

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. 

1758 

1759 The outline retains the organization of all active settings files. 

1760 

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. 

1791 

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. 

1877 

1878 The following equivalences are allowed: 

1879 

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. 

1977 

1978 Arguments are the names of settings to be use. 

1979 Default to size=12, slant="roman", weight="normal". 

1980 

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. 

2025 

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] = {} 

2053 

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. 

2085 

2086 def getCommands(self): 

2087 """Return the list of tuples (headline,script) for common @command nodes.""" 

2088 return g.app.config.atCommonCommandsList # unusual. 

2089 

2090 def getEnabledPlugins(self): 

2091 """Return the body text of the @enabled-plugins node.""" 

2092 return g.app.config.enabledPluginsString # unusual. 

2093 

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: 

2138 

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. 

2171 

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. 

2230 

2231 Used by read settings logic.""" 

2232 

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