Coverage for C:\Repos\leo-editor\leo\core\leoConfig.py: 37%

1392 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 10:21 -0500

1#@+leo-ver=5-thin 

2#@+node:ekr.20130925160837.11429: * @file leoConfig.py 

3"""Configuration classes for Leo.""" 

4# pylint: disable=unsubscriptable-object 

5#@+<< imports: leoConfig.py >> 

6#@+node:ekr.20041227063801: ** << imports: leoConfig.py >> 

7import os 

8import sys 

9import re 

10import textwrap 

11from typing import Any, Dict, Generator, List, Tuple 

12from typing import TYPE_CHECKING 

13from leo.core.leoCommands import Commands as Cmdr 

14from leo.plugins.mod_scripting import build_rclick_tree 

15from leo.core import leoGlobals as g 

16#@-<< imports: leoConfig.py >> 

17#@+<< type aliases: leoConfig.py >> 

18#@+node:ekr.20220417212402.1: ** << type aliases: leoConfig.py >> 

19if TYPE_CHECKING: # Always False at runtime. 

20 from leo.core.leoNodes import Position as Pos 

21 from PyQt6 import QtWidgets as QtWidgets6 

22 Widget = QtWidgets6.QWidget 

23else: 

24 Pos = Any 

25 Widget = Any 

26#@-<< type aliases: leoConfig.py >> 

27#@+<< class ParserBaseClass >> 

28#@+node:ekr.20041119203941.2: ** << class ParserBaseClass >> 

29class ParserBaseClass: 

30 """The base class for settings parsers.""" 

31 #@+<< ParserBaseClass data >> 

32 #@+node:ekr.20041121130043: *3* << ParserBaseClass data >> 

33 # These are the canonicalized names. 

34 # Case is ignored, as are '_' and '-' characters. 

35 basic_types = [ 

36 # Headlines have the form @kind name = var 

37 'bool', 

38 'color', 

39 'directory', 

40 'int', 

41 'ints', 

42 'float', 

43 'path', 

44 'ratio', 

45 'string', 

46 'strings', 

47 ] 

48 control_types = [ 

49 'buttons', 

50 'commands', 

51 'data', 

52 'enabledplugins', 

53 'font', 

54 'ifenv', 

55 'ifhostname', 

56 'ifplatform', 

57 'ignore', 

58 'menus', 

59 'mode', 

60 'menuat', 

61 'openwith', 

62 'outlinedata', 

63 'popup', 

64 'settings', 

65 'shortcuts', 

66 ] 

67 # Keys are settings names, values are (type,value) tuples. 

68 settingsDict: Dict[str, Any] = {} 

69 #@-<< ParserBaseClass data >> 

70 #@+others 

71 #@+node:ekr.20041119204700: *3* pbc.ctor 

72 #@@nobeautify 

73 

74 def __init__(self, c: Cmdr, localFlag: bool) -> None: 

75 """Ctor for the ParserBaseClass class.""" 

76 self.c = c 

77 self.clipBoard: Any = [] 

78 # True if this is the .leo file being opened, 

79 # as opposed to myLeoSettings.leo or leoSettings.leo. 

80 self.localFlag = localFlag 

81 self.shortcutsDict = g.TypedDict( # was TypedDictOfLists. 

82 name='parser.shortcutsDict', 

83 keyType=type('shortcutName'), 

84 valType=g.BindingInfo, 

85 ) 

86 self.openWithList: List[Dict[str, Any]] = [] # A list of dicts containing 'name','shortcut','command' keys. 

87 # Keys are canonicalized names. 

88 self.dispatchDict = { 

89 'bool': self.doBool, 

90 'buttons': self.doButtons, # New in 4.4.4 

91 'color': self.doColor, 

92 'commands': self.doCommands, # New in 4.4.8. 

93 'data': self.doData, # New in 4.4.6 

94 'directory': self.doDirectory, 

95 'enabledplugins': self.doEnabledPlugins, 

96 'font': self.doFont, 

97 'ifenv': self.doIfEnv, # New in 5.2 b1. 

98 'ifhostname': self.doIfHostname, 

99 'ifplatform': self.doIfPlatform, 

100 'ignore': self.doIgnore, 

101 'int': self.doInt, 

102 'ints': self.doInts, 

103 'float': self.doFloat, 

104 'menus': self.doMenus, # New in 4.4.4 

105 'menuat': self.doMenuat, 

106 'popup': self.doPopup, # New in 4.4.8 

107 'mode': self.doMode, # New in 4.4b1. 

108 'openwith': self.doOpenWith, # New in 4.4.3 b1. 

109 'outlinedata': self.doOutlineData, # New in 4.11.1. 

110 'path': self.doPath, 

111 'ratio': self.doRatio, 

112 'shortcuts': self.doShortcuts, 

113 'string': self.doString, 

114 'strings': self.doStrings, 

115 } 

116 self.debug_count = 0 

117 #@+node:ekr.20080514084054.4: *3* pbc.computeModeName 

118 def computeModeName(self, name: str) -> str: 

119 s = name.strip().lower() 

120 j = s.find(' ') 

121 if j > -1: 

122 s = s[:j] 

123 if s.endswith('mode'): 

124 s = s[:-4].strip() 

125 if s.endswith('-'): 

126 s = s[:-1] 

127 i = s.find('::') 

128 if i > -1: 

129 # The actual mode name is everything up to the "::" 

130 # The prompt is everything after the prompt. 

131 s = s[:i] 

132 modeName = s + '-mode' 

133 return modeName 

134 #@+node:ekr.20060102103625: *3* pbc.createModeCommand 

135 def createModeCommand(self, modeName: str, name: str, modeDict: Any) -> None: 

136 modeName = 'enter-' + modeName.replace(' ', '-') 

137 i = name.find('::') 

138 if i > -1: 

139 # The prompt is everything after the '::' 

140 prompt = name[i + 2 :].strip() 

141 modeDict['*command-prompt*'] = g.BindingInfo(kind=prompt) 

142 # Save the info for k.finishCreate and k.makeAllBindings. 

143 d = g.app.config.modeCommandsDict 

144 # New in 4.4.1 b2: silently allow redefinitions of modes. 

145 d[modeName] = modeDict 

146 #@+node:ekr.20041120103012: *3* pbc.error 

147 def error(self, s: str) -> None: 

148 g.pr(s) 

149 # Does not work at present because we are using a null Gui. 

150 g.blue(s) 

151 #@+node:ekr.20041120094940: *3* pbc.kind handlers 

152 #@+node:ekr.20041120094940.1: *4* pbc.doBool 

153 def doBool(self, p: Pos, kind: str, name: str, val: Any) -> None: 

154 if val in ('True', 'true', '1'): 

155 self.set(p, kind, name, True) 

156 elif val in ('False', 'false', '0'): 

157 self.set(p, kind, name, False) 

158 else: 

159 self.valueError(p, kind, name, val) 

160 #@+node:ekr.20070925144337: *4* pbc.doButtons 

161 def doButtons(self, p: Pos, kind: str, name: str, val: Any) -> None: 

162 """Create buttons for each @button node in an @buttons tree.""" 

163 c, tag = self.c, '@button' 

164 aList, seen = [], [] 

165 after = p.nodeAfterTree() 

166 while p and p != after: 

167 if p.v in seen: 

168 p.moveToNodeAfterTree() 

169 elif p.isAtIgnoreNode(): 

170 seen.append(p.v) 

171 p.moveToNodeAfterTree() 

172 else: 

173 seen.append(p.v) 

174 if g.match_word(p.h, 0, tag): 

175 # We can not assume that p will be valid when it is used. 

176 script = g.getScript( 

177 c, 

178 p, 

179 useSelectedText=False, 

180 forcePythonSentinels=True, 

181 useSentinels=True) 

182 # #2011: put rclicks in aList. Do not inject into command_p. 

183 command_p = p.copy() 

184 rclicks = build_rclick_tree(command_p, top_level=True) 

185 aList.append((command_p, script, rclicks)) 

186 p.moveToThreadNext() 

187 # This setting is handled differently from most other settings, 

188 # because the last setting must be retrieved before any commander exists. 

189 if aList: 

190 # Bug fix: 2011/11/24: Extend the list, don't replace it. 

191 g.app.config.atCommonButtonsList.extend(aList) 

192 g.app.config.buttonsFileName = (c.shortFileName() if c else '<no settings file>') 

193 #@+node:ekr.20041120094940.2: *4* pbc.doColor 

194 def doColor(self, p: Pos, kind: str, name: str, val: Any) -> None: 

195 # At present no checking is done. 

196 val = val.lstrip('"').rstrip('"') 

197 val = val.lstrip("'").rstrip("'") 

198 self.set(p, kind, name, val) 

199 #@+node:ekr.20080312071248.6: *4* pbc.doCommands 

200 def doCommands(self, p: Pos, kind: str, name: str, val: Any) -> None: 

201 """Handle an @commands tree.""" 

202 c = self.c 

203 aList = [] 

204 tag = '@command' 

205 seen = [] 

206 after = p.nodeAfterTree() 

207 while p and p != after: 

208 if p.v in seen: 

209 p.moveToNodeAfterTree() 

210 elif p.isAtIgnoreNode(): 

211 seen.append(p.v) 

212 p.moveToNodeAfterTree() 

213 else: 

214 seen.append(p.v) 

215 if g.match_word(p.h, 0, tag): 

216 # We can not assume that p will be valid when it is used. 

217 script = g.getScript(c, p, 

218 useSelectedText=False, 

219 forcePythonSentinels=True, 

220 useSentinels=True) 

221 aList.append((p.copy(), script),) 

222 p.moveToThreadNext() 

223 # This setting is handled differently from most other settings, 

224 # because the last setting must be retrieved before any commander exists. 

225 if aList: 

226 # Bug fix: 2011/11/24: Extend the list, don't replace it. 

227 g.app.config.atCommonCommandsList.extend(aList) 

228 #@+node:ekr.20071214140900: *4* pbc.doData 

229 def doData(self, p: Pos, kind: str, name: str, val: Any) -> None: 

230 # New in Leo 4.11: do not strip lines. 

231 # New in Leo 4.12.1: strip *nothing* here. 

232 # New in Leo 4.12.1: allow composition of nodes: 

233 # - Append all text in descendants in outline order. 

234 # - Ensure all fragments end with a newline. 

235 data = g.splitLines(p.b) 

236 for p2 in p.subtree(): 

237 if p2.b and not p2.h.startswith('@'): 

238 data.extend(g.splitLines(p2.b)) 

239 if not p2.b.endswith('\n'): 

240 data.append('\n') 

241 self.set(p, kind, name, data) 

242 #@+node:ekr.20131114051702.16545: *4* pbc.doOutlineData & helper 

243 def doOutlineData(self, p: Pos, kind: str, name: str, val: Any) -> str: 

244 # New in Leo 4.11: do not strip lines. 

245 data = self.getOutlineDataHelper(p) 

246 self.set(p, kind, name, data) 

247 return 'skip' 

248 #@+node:ekr.20131114051702.16546: *5* pbc.getOutlineDataHelper 

249 def getOutlineDataHelper(self, p: Pos) -> str: 

250 c = self.c 

251 if not p: 

252 return None 

253 try: 

254 # Copy the entire tree to s. 

255 c.fileCommands.leo_file_encoding = 'utf-8' 

256 s = c.fileCommands.outline_to_clipboard_string(p) 

257 s = g.toUnicode(s, encoding='utf-8') 

258 except Exception: 

259 g.es_exception() 

260 s = None 

261 return s 

262 #@+node:ekr.20041120094940.3: *4* pbc.doDirectory & doPath 

263 def doDirectory(self, p: Pos, kind: str, name: str, val: Any) -> None: 

264 # At present no checking is done. 

265 self.set(p, kind, name, val) 

266 

267 doPath = doDirectory 

268 #@+node:ekr.20070224075914: *4* pbc.doEnabledPlugins 

269 def doEnabledPlugins(self, p: Pos, kind: str, name: str, val: Any) -> None: 

270 c = self.c 

271 s = p.b 

272 # This setting is handled differently from all other settings, 

273 # because the last setting must be retrieved before any commander exists. 

274 # 2011/09/04: Remove comments, comment lines and blank lines. 

275 aList, lines = [], g.splitLines(s) 

276 for s in lines: 

277 i = s.find('#') 

278 if i > -1: 

279 s = s[:i] + '\n' # 2011/09/29: must add newline back in. 

280 if s.strip(): 

281 aList.append(s.lstrip()) 

282 s = ''.join(aList) 

283 # Set the global config ivars. 

284 g.app.config.enabledPluginsString = s 

285 g.app.config.enabledPluginsFileName = c.shortFileName() if c else '<no settings file>' 

286 #@+node:ekr.20041120094940.6: *4* pbc.doFloat 

287 def doFloat(self, p: Pos, kind: str, name: str, val: Any) -> None: 

288 try: 

289 val = float(val) 

290 self.set(p, kind, name, val) 

291 except ValueError: 

292 self.valueError(p, kind, name, val) 

293 #@+node:ekr.20041120094940.4: *4* pbc.doFont 

294 def doFont(self, p: Pos, kind: str, name: str, val: Any) -> None: 

295 """Handle an @font node. Such nodes affect syntax coloring *only*.""" 

296 d = self.parseFont(p) 

297 # Set individual settings. 

298 for key in ('family', 'size', 'slant', 'weight'): 

299 data = d.get(key) 

300 if data is not None: 

301 name, val = data 

302 setKind = key 

303 self.set(p, setKind, name, val) 

304 #@+node:ekr.20150426034813.1: *4* pbc.doIfEnv 

305 def doIfEnv(self, p: Pos, kind: str, name: str, val: Any) -> str: 

306 """ 

307 Support @ifenv in @settings trees. 

308 

309 Enable descendant settings if the value of os.getenv is in any of the names. 

310 """ 

311 aList = name.split(',') 

312 if not aList: 

313 return 'skip' 

314 name = aList[0] 

315 env = os.getenv(name) 

316 env = env.lower().strip() if env else 'none' 

317 for s in aList[1:]: 

318 if s.lower().strip() == env: 

319 return None 

320 return 'skip' 

321 #@+node:dan.20080410121257.2: *4* pbc.doIfHostname 

322 def doIfHostname(self, p: Pos, kind: str, name: str, val: Any) -> str: 

323 """ 

324 Support @ifhostname in @settings trees. 

325 

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

327 

328 @ifhostname bob 

329 Enable descendant settings if h == 'bob' 

330 @ifhostname !harry 

331 Enable descendant settings if h != 'harry' 

332 """ 

333 lm = g.app.loadManager 

334 h = lm.computeMachineName().strip() 

335 s = name.strip() 

336 if s.startswith('!'): 

337 if h == s[1:]: 

338 return 'skip' 

339 elif h != s: 

340 return 'skip' 

341 return None 

342 #@+node:ekr.20041120104215: *4* pbc.doIfPlatform 

343 def doIfPlatform(self, p: Pos, kind: str, name: str, val: Any) -> str: 

344 """Support @ifplatform in @settings trees.""" 

345 platform = sys.platform.lower() 

346 for s in name.split(','): 

347 if platform == s.lower(): 

348 return None 

349 return "skip" 

350 #@+node:ekr.20041120104215.1: *4* pbc.doIgnore 

351 def doIgnore(self, p: Pos, kind: str, name: str, val: Any) -> str: 

352 return "skip" 

353 #@+node:ekr.20041120094940.5: *4* pbc.doInt 

354 def doInt(self, p: Pos, kind: str, name: str, val: Any) -> None: 

355 try: 

356 val = int(val) 

357 self.set(p, kind, name, val) 

358 except ValueError: 

359 self.valueError(p, kind, name, val) 

360 #@+node:ekr.20041217132253: *4* pbc.doInts 

361 def doInts(self, p: Pos, kind: str, name: str, val: Any) -> None: 

362 """ 

363 We expect either: 

364 @ints [val1,val2,...]aName=val 

365 @ints aName[val1,val2,...]=val 

366 """ 

367 name = name.strip() # The name indicates the valid values. 

368 i = name.find('[') 

369 j = name.find(']') 

370 if -1 < i < j: 

371 items_s = name[i + 1 : j] 

372 items = items_s.split(',') 

373 name = name[:i] + name[j + 1 :].strip() 

374 try: 

375 items = [int(item.strip()) for item in items] # type:ignore 

376 except ValueError: 

377 items = [] 

378 self.valueError(p, 'ints[]', name, val) 

379 return 

380 kind = f"ints[{','.join([str(item) for item in items])}]" 

381 try: 

382 val = int(val) 

383 except ValueError: 

384 self.valueError(p, 'int', name, val) 

385 return 

386 if val not in items: 

387 self.error(f"{val} is not in {kind} in {name}") 

388 return 

389 # At present no checking is done. 

390 self.set(p, kind, name, val) 

391 #@+node:tbrown.20080514112857.124: *4* pbc.doMenuat 

392 def doMenuat(self, p: Pos, kind: str, name: str, val: Any) -> None: 

393 """Handle @menuat setting.""" 

394 if g.app.config.menusList: 

395 # get the patch fragment 

396 patch: List[Any] = [] 

397 if p.hasChildren(): 

398 # self.doMenus(p.copy().firstChild(),kind,name,val,storeIn=patch) 

399 self.doItems(p.copy(), patch) 

400 # setup 

401 parts = name.split() 

402 if len(parts) != 3: 

403 parts.append('subtree') 

404 targetPath, mode, source = parts 

405 if not targetPath.startswith('/'): 

406 targetPath = '/' + targetPath 

407 ans = self.patchMenuTree(g.app.config.menusList, targetPath) 

408 if ans: 

409 # pylint: disable=unpacking-non-sequence 

410 list_, idx = ans 

411 if mode not in ('copy', 'cut'): 

412 if source != 'clipboard': 

413 use = patch # [0][1] 

414 else: 

415 if isinstance(self.clipBoard, list): 

416 use = self.clipBoard 

417 else: 

418 use = [self.clipBoard] 

419 if mode == 'replace': 

420 list_[idx] = use.pop(0) 

421 while use: 

422 idx += 1 

423 list_.insert(idx, use.pop(0)) 

424 elif mode == 'before': 

425 while use: 

426 list_.insert(idx, use.pop()) 

427 elif mode == 'after': 

428 while use: 

429 list_.insert(idx + 1, use.pop()) 

430 elif mode == 'cut': 

431 self.clipBoard = list_[idx] 

432 del list_[idx] 

433 elif mode == 'copy': 

434 self.clipBoard = list_[idx] 

435 else: # append 

436 list_.extend(use) 

437 else: 

438 g.es_print("ERROR: didn't find menu path " + targetPath) 

439 elif g.app.inBridge: 

440 pass # #48: Not an error. 

441 else: 

442 g.es_print("ERROR: @menuat found but no menu tree to patch") 

443 #@+node:tbrown.20080514180046.9: *5* pbc.getName 

444 def getName(self, val: str, val2: str=None) -> str: 

445 if val2 and val2.strip(): 

446 val = val2 

447 val = val.split('\n', 1)[0] 

448 for i in "*.-& \t\n": 

449 val = val.replace(i, '') 

450 return val.lower() 

451 #@+node:tbrown.20080514180046.2: *5* pbc.dumpMenuTree 

452 def dumpMenuTree(self, aList: List, level: int=0, path: str='') -> None: 

453 for z in aList: 

454 kind, val, val2 = z 

455 pad = ' ' * level 

456 if kind == '@item': 

457 name = self.getName(val, val2) 

458 g.es_print(f"{pad} {val} ({val2}) [{path + '/' + name}]") 

459 else: 

460 name = self.getName(kind.replace('@menu ', '')) 

461 g.es_print(f"{pad} {kind}... [{path + '/' + name}]") 

462 self.dumpMenuTree(val, level + 1, path=path + '/' + name) 

463 #@+node:tbrown.20080514180046.8: *5* pbc.patchMenuTree 

464 def patchMenuTree(self, orig: List[Any], targetPath: str, path: str='') -> Any: 

465 

466 kind: str 

467 val: Any 

468 val2: Any 

469 for n, z in enumerate(orig): 

470 kind, val, val2 = z 

471 if kind == '@item': 

472 name = self.getName(val, val2) 

473 curPath = path + '/' + name 

474 if curPath == targetPath: 

475 return orig, n 

476 else: 

477 name = self.getName(kind.replace('@menu ', '')) 

478 curPath = path + '/' + name 

479 if curPath == targetPath: 

480 return orig, n 

481 ans = self.patchMenuTree(val, targetPath, path=path + '/' + name) 

482 if ans: 

483 return ans 

484 return None 

485 #@+node:ekr.20070925144337.2: *4* pbc.doMenus & helper 

486 def doMenus(self, p: Pos, kind: str, name: str, val: Any) -> None: 

487 

488 c = self.c 

489 p = p.copy() 

490 aList: List[Any] = [] # This entire logic is mysterious, and likely buggy. 

491 after = p.nodeAfterTree() 

492 while p and p != after: 

493 self.debug_count += 1 

494 h = p.h 

495 if g.match_word(h, 0, '@menu'): 

496 name = h[len('@menu') :].strip() 

497 if name: 

498 for z in aList: 

499 name2, junk, junk = z 

500 if name2 == name: 

501 self.error(f"Replacing previous @menu {name}") 

502 break 

503 aList2: List[Any] = [] # Huh? 

504 kind = f"{'@menu'} {name}" 

505 self.doItems(p, aList2) 

506 aList.append((kind, aList2, None),) 

507 p.moveToNodeAfterTree() 

508 else: 

509 p.moveToThreadNext() 

510 else: 

511 p.moveToThreadNext() 

512 if self.localFlag: 

513 self.set(p, kind='menus', name='menus', val=aList) 

514 else: 

515 g.app.config.menusList = aList 

516 name = c.shortFileName() if c else '<no settings file>' 

517 g.app.config.menusFileName = name 

518 #@+node:ekr.20070926141716: *5* pbc.doItems 

519 def doItems(self, p: Pos, aList: List) -> None: 

520 

521 p = p.copy() 

522 after = p.nodeAfterTree() 

523 p.moveToThreadNext() 

524 while p and p != after: 

525 self.debug_count += 1 

526 h = p.h 

527 for tag in ('@menu', '@item', '@ifplatform'): 

528 if g.match_word(h, 0, tag): 

529 itemName = h[len(tag) :].strip() 

530 if itemName: 

531 lines = [z for z in g.splitLines(p.b) if 

532 z.strip() and not z.strip().startswith('#')] 

533 # Only the first body line is significant. 

534 # This allows following comment lines. 

535 body = lines[0].strip() if lines else '' 

536 if tag == '@menu': 

537 aList2: List[Any] = [] # Huh? 

538 kind = f"{tag} {itemName}" 

539 self.doItems(p, aList2) # Huh? 

540 aList.append((kind, aList2, body),) # #848: Body was None. 

541 p.moveToNodeAfterTree() 

542 break 

543 else: 

544 kind = tag 

545 head = itemName 

546 # We must not clean non-unicode characters! 

547 aList.append((kind, head, body),) 

548 p.moveToThreadNext() 

549 break 

550 else: 

551 p.moveToThreadNext() 

552 #@+node:ekr.20060102103625.1: *4* pbc.doMode 

553 def doMode(self, p: Pos, kind: str, name: str, val: Any) -> None: 

554 """Parse an @mode node and create the enter-<name>-mode command.""" 

555 c = self.c 

556 name1 = name 

557 modeName = self.computeModeName(name) 

558 d = g.TypedDict( 

559 name=f"modeDict for {modeName}", 

560 keyType=type('commandName'), 

561 valType=g.BindingInfo) 

562 s = p.b 

563 lines = g.splitLines(s) 

564 for line in lines: 

565 line = line.strip() 

566 if line and not g.match(line, 0, '#'): 

567 name, bi = self.parseShortcutLine('*mode-setting*', line) 

568 if not name: 

569 # An entry command: put it in the special *entry-commands* key. 

570 d.add_to_list('*entry-commands*', bi) 

571 elif bi is not None: 

572 # A regular shortcut. 

573 bi.pane = modeName 

574 aList = d.get(name, []) 

575 # Important: use previous bindings if possible. 

576 key2, aList2 = c.config.getShortcut(name) 

577 aList3 = [z for z in aList2 if z.pane != modeName] 

578 if aList3: 

579 aList.extend(aList3) 

580 aList.append(bi) 

581 d[name] = aList 

582 # Restore the global shortcutsDict. 

583 # Create the command, but not any bindings to it. 

584 self.createModeCommand(modeName, name1, d) 

585 #@+node:ekr.20070411101643.1: *4* pbc.doOpenWith 

586 def doOpenWith(self, p: Pos, kind: str, name: str, val: Any) -> None: 

587 

588 d = self.parseOpenWith(p) 

589 d['name'] = name 

590 d['shortcut'] = val 

591 name = kind = 'openwithtable' 

592 self.openWithList.append(d) 

593 self.set(p, kind, name, self.openWithList) 

594 #@+node:bobjack.20080324141020.4: *4* pbc.doPopup & helper 

595 def doPopup(self, p: Pos, kind: str, name: str, val: Any) -> None: 

596 """ 

597 Handle @popup menu items in @settings trees. 

598 """ 

599 popupName = name 

600 # popupType = val 

601 aList: List[Any] = [] 

602 p = p.copy() 

603 self.doPopupItems(p, aList) 

604 if not hasattr(g.app.config, 'context_menus'): 

605 g.app.config.context_menus = {} 

606 g.app.config.context_menus[popupName] = aList 

607 #@+node:bobjack.20080324141020.5: *5* pbc.doPopupItems 

608 def doPopupItems(self, p: Pos, aList: List) -> None: 

609 p = p.copy() 

610 after = p.nodeAfterTree() 

611 p.moveToThreadNext() 

612 while p and p != after: 

613 h = p.h 

614 for tag in ('@menu', '@item'): 

615 if g.match_word(h, 0, tag): 

616 itemName = h[len(tag) :].strip() 

617 if itemName: 

618 if tag == '@menu': 

619 aList2: List[Any] = [] 

620 kind = f"{itemName}" 

621 body = p.b 

622 self.doPopupItems(p, aList2) # Huh? 

623 aList.append((kind + '\n' + body, aList2),) 

624 p.moveToNodeAfterTree() 

625 break 

626 else: 

627 kind = tag 

628 head = itemName 

629 body = p.b 

630 aList.append((head, body),) 

631 p.moveToThreadNext() 

632 break 

633 else: 

634 p.moveToThreadNext() 

635 #@+node:ekr.20041121125741: *4* pbc.doRatio 

636 def doRatio(self, p: Pos, kind: str, name: str, val: Any) -> None: 

637 try: 

638 val = float(val) 

639 if 0.0 <= val <= 1.0: 

640 self.set(p, kind, name, val) 

641 else: 

642 self.valueError(p, kind, name, val) 

643 except ValueError: 

644 self.valueError(p, kind, name, val) 

645 #@+node:ekr.20041120105609: *4* pbc.doShortcuts 

646 def doShortcuts(self, p: Pos, kind: str, junk_name: str, junk_val: Any, s: str=None) -> None: 

647 """Handle an @shortcut or @shortcuts node.""" 

648 c, d = self.c, self.shortcutsDict 

649 if s is None: 

650 s = p.b 

651 fn = d.name() 

652 for line in g.splitLines(s): 

653 line = line.strip() 

654 if line and not g.match(line, 0, '#'): 

655 commandName, bi = self.parseShortcutLine(fn, line) 

656 if bi is None: # Fix #718. 

657 print(f"\nWarning: bad shortcut specifier: {line!r}\n") 

658 else: 

659 if bi and bi.stroke not in (None, 'none', 'None'): 

660 self.doOneShortcut(bi, commandName, p) 

661 else: 

662 # New in Leo 5.7: Add local assignments to None to c.k.killedBindings. 

663 if c.config.isLocalSettingsFile(): 

664 c.k.killedBindings.append(commandName) 

665 #@+node:ekr.20111020144401.9585: *5* pbc.doOneShortcut 

666 def doOneShortcut(self, bi: Any, commandName: str, p: Pos) -> None: 

667 """Handle a regular shortcut.""" 

668 d = self.shortcutsDict 

669 aList = d.get(commandName, []) 

670 aList.append(bi) 

671 d[commandName] = aList 

672 #@+node:ekr.20041217132028: *4* pbc.doString 

673 def doString(self, p: Pos, kind: str, name: str, val: Any) -> None: 

674 # At present no checking is done. 

675 self.set(p, kind, name, val) 

676 #@+node:ekr.20041120094940.8: *4* pbc.doStrings 

677 def doStrings(self, p: Pos, kind: str, name: str, val: Any) -> None: 

678 """ 

679 We expect one of the following: 

680 @strings aName[val1,val2...]=val 

681 @strings [val1,val2,...]aName=val 

682 """ 

683 name = name.strip() 

684 i = name.find('[') 

685 j = name.find(']') 

686 if -1 < i < j: 

687 items_s = name[i + 1 : j] 

688 items = items_s.split(',') 

689 items = [item.strip() for item in items] 

690 name = name[:i] + name[j + 1 :].strip() 

691 kind = f"strings[{','.join(items)}]" 

692 # At present no checking is done. 

693 self.set(p, kind, name, val) 

694 #@+node:ekr.20041124063257: *3* pbc.munge 

695 def munge(self, s: str) -> str: 

696 return g.app.config.canonicalizeSettingName(s) 

697 #@+node:ekr.20041119204700.2: *3* pbc.oops 

698 def oops(self) -> None: 

699 g.pr("ParserBaseClass oops:", 

700 g.callers(), 

701 "must be overridden in subclass") 

702 #@+node:ekr.20041213082558: *3* pbc.parsers 

703 #@+node:ekr.20041213082558.1: *4* pbc.parseFont & helper 

704 def parseFont(self, p: Pos) -> Dict[str, Any]: 

705 d: Dict[str, Any] = { 

706 'comments': [], 

707 'family': None, 

708 'size': None, 

709 'slant': None, 

710 'weight': None, 

711 } 

712 s = p.b 

713 lines = g.splitLines(s) 

714 for line in lines: 

715 self.parseFontLine(line, d) 

716 comments = d.get('comments') 

717 d['comments'] = '\n'.join(comments) 

718 return d 

719 #@+node:ekr.20041213082558.2: *5* pbc.parseFontLine 

720 def parseFontLine(self, line: str, d: Dict[str, Any]) -> None: 

721 s = line.strip() 

722 if not s: 

723 return 

724 try: 

725 s = str(s) 

726 except UnicodeError: 

727 pass 

728 if g.match(s, 0, '#'): 

729 s = s[1:].strip() 

730 comments = d.get('comments') 

731 comments.append(s) 

732 d['comments'] = comments 

733 return 

734 # name is everything up to '=' 

735 i = s.find('=') 

736 if i == -1: 

737 name = s 

738 val = None 

739 else: 

740 name = s[:i].strip() 

741 val = s[i + 1 :].strip().strip('"').strip("'") 

742 for tag in ('_family', '_size', '_slant', '_weight'): 

743 if name.endswith(tag): 

744 kind = tag[1:] 

745 d[kind] = name, val # Used only by doFont. 

746 return 

747 #@+node:ekr.20041119205148: *4* pbc.parseHeadline 

748 def parseHeadline(self, s: str) -> Tuple[str, str, Any]: 

749 """ 

750 Parse a headline of the form @kind:name=val 

751 Return (kind,name,val). 

752 Leo 4.11.1: Ignore everything after @data name. 

753 """ 

754 kind = name = val = None 

755 if g.match(s, 0, '@'): 

756 i = g.skip_id(s, 1, chars='-') 

757 i = g.skip_ws(s, i) 

758 kind = s[1:i].strip() 

759 if kind: 

760 # name is everything up to '=' 

761 if kind == 'data': 

762 # i = g.skip_ws(s,i) 

763 j = s.find(' ', i) 

764 if j == -1: 

765 name = s[i:].strip() 

766 else: 

767 name = s[i:j].strip() 

768 else: 

769 j = s.find('=', i) 

770 if j == -1: 

771 name = s[i:].strip() 

772 else: 

773 name = s[i:j].strip() 

774 # val is everything after the '=' 

775 val = s[j + 1 :].strip() 

776 return kind, name, val 

777 #@+node:ekr.20070411101643.2: *4* pbc.parseOpenWith & helper 

778 def parseOpenWith(self, p: Pos) -> Dict[str, Any]: 

779 

780 d = {'command': None} # d contains args, kind, etc tags. 

781 for line in g.splitLines(p.b): 

782 self.parseOpenWithLine(line, d) 

783 return d 

784 #@+node:ekr.20070411101643.4: *5* pbc.parseOpenWithLine 

785 def parseOpenWithLine(self, line: str, d: Dict[str, Any]) -> None: 

786 s = line.strip() 

787 if not s: 

788 return 

789 i = g.skip_ws(s, 0) 

790 if g.match(s, i, '#'): 

791 return 

792 j = g.skip_c_id(s, i) 

793 tag = s[i:j].strip() 

794 if not tag: 

795 g.es_print(f"@openwith lines must start with a tag: {s}") 

796 return 

797 i = g.skip_ws(s, j) 

798 if not g.match(s, i, ':'): 

799 g.es_print(f"colon must follow @openwith tag: {s}") 

800 return 

801 i += 1 

802 val = s[i:].strip() or '' # An empty val is valid. 

803 if tag == 'arg': 

804 aList: List[Any] = d.get('args', []) 

805 aList.append(val) 

806 d['args'] = aList 

807 elif d.get(tag): 

808 g.es_print(f"ignoring duplicate definition of {tag} {s}") 

809 else: 

810 d[tag] = val 

811 #@+node:ekr.20041120112043: *4* pbc.parseShortcutLine 

812 def parseShortcutLine(self, kind: str, s: str) -> Tuple[str, Any]: 

813 """Parse a shortcut line. Valid forms: 

814 

815 --> entry-command 

816 settingName = shortcut 

817 settingName ! paneName = shortcut 

818 command-name --> mode-name = binding 

819 command-name --> same = binding 

820 """ 

821 s = s.replace('\x7f', '') # Can happen on MacOS. Very weird. 

822 name = val = nextMode = None 

823 nextMode = 'none' 

824 i = g.skip_ws(s, 0) 

825 if g.match(s, i, '-->'): # New in 4.4.1 b1: allow mode-entry commands. 

826 j = g.skip_ws(s, i + 3) 

827 i = g.skip_id(s, j, '-') 

828 entryCommandName = s[j:i] 

829 return None, g.BindingInfo('*entry-command*', commandName=entryCommandName) 

830 j = i 

831 i = g.skip_id(s, j, '-@') # #718. 

832 name = s[j:i] 

833 # #718: Allow @button- and @command- prefixes. 

834 for tag in ('@button-', '@command-'): 

835 if name.startswith(tag): 

836 name = name[len(tag) :] 

837 break 

838 if not name: 

839 return None, None 

840 # New in Leo 4.4b2. 

841 i = g.skip_ws(s, i) 

842 if g.match(s, i, '->'): # New in 4.4: allow pane-specific shortcuts. 

843 j = g.skip_ws(s, i + 2) 

844 i = g.skip_id(s, j) 

845 nextMode = s[j:i] 

846 i = g.skip_ws(s, i) 

847 if g.match(s, i, '!'): # New in 4.4: allow pane-specific shortcuts. 

848 j = g.skip_ws(s, i + 1) 

849 i = g.skip_id(s, j) 

850 pane = s[j:i] 

851 if not pane.strip(): 

852 pane = 'all' 

853 else: pane = 'all' 

854 i = g.skip_ws(s, i) 

855 if g.match(s, i, '='): 

856 i = g.skip_ws(s, i + 1) 

857 val = s[i:] 

858 # New in 4.4: Allow comments after the shortcut. 

859 # Comments must be preceded by whitespace. 

860 if val: 

861 i = val.find('#') 

862 if i > 0 and val[i - 1] in (' ', '\t'): 

863 val = val[:i].strip() 

864 if not val: 

865 return name, None 

866 stroke = g.KeyStroke(binding=val) if val else None 

867 bi = g.BindingInfo(kind=kind, nextMode=nextMode, pane=pane, stroke=stroke) 

868 return name, bi 

869 #@+node:ekr.20041120094940.9: *3* pbc.set 

870 def set(self, p: Pos, kind: str, name: str, val: Any) -> None: 

871 """Init the setting for name to val.""" 

872 c = self.c 

873 # Note: when kind is 'shortcut', name is a command name. 

874 key = self.munge(name) 

875 if key is None: 

876 g.es_print('Empty setting name in', p.h in c.fileName()) 

877 parent = p.parent() 

878 while parent: 

879 g.trace('parent', parent.h) 

880 parent.moveToParent() 

881 return 

882 d = self.settingsDict 

883 gs = d.get(key) 

884 if gs: 

885 assert isinstance(gs, g.GeneralSetting), gs 

886 path = gs.path 

887 if g.os_path_finalize(c.mFileName) != g.os_path_finalize(path): 

888 g.es("over-riding setting:", name, "from", path) # 1341 

889 # Important: we can't use c here: it may be destroyed! 

890 d[key] = g.GeneralSetting(kind, # type:ignore 

891 path=c.mFileName, 

892 tag='setting', 

893 unl=p.get_UNL() if p else '', 

894 val=val, 

895 ) 

896 #@+node:ekr.20041119204700.1: *3* pbc.traverse 

897 def traverse(self) -> Tuple[Any, Any]: 

898 """Traverse the entire settings tree.""" 

899 c = self.c 

900 self.settingsDict = g.TypedDict( # type:ignore 

901 name=f"settingsDict for {c.shortFileName()}", 

902 keyType=type('settingName'), 

903 valType=g.GeneralSetting) 

904 self.shortcutsDict = g.TypedDict( # was TypedDictOfLists. 

905 name=f"shortcutsDict for {c.shortFileName()}", 

906 keyType=str, 

907 valType=g.BindingInfo) 

908 # This must be called after the outline has been inited. 

909 p = c.config.settingsRoot() 

910 if not p: 

911 # c.rootPosition() doesn't exist yet. 

912 # This is not an error. 

913 return self.shortcutsDict, self.settingsDict 

914 after = p.nodeAfterTree() 

915 while p and p != after: 

916 result = self.visitNode(p) 

917 if result == "skip": 

918 # g.warning('skipping settings in',p.h) 

919 p.moveToNodeAfterTree() 

920 else: 

921 p.moveToThreadNext() 

922 # Return the raw dict, unmerged. 

923 return self.shortcutsDict, self.settingsDict 

924 #@+node:ekr.20041120094940.10: *3* pbc.valueError 

925 def valueError(self, p: Pos, kind: str, name: str, val: Any) -> None: 

926 """Give an error: val is not valid for kind.""" 

927 self.error(f"{val} is not a valid {kind} for {name}") 

928 #@+node:ekr.20041119204700.3: *3* pbc.visitNode (must be overwritten in subclasses) 

929 def visitNode(self, p: Pos) -> str: 

930 self.oops() 

931 return '' 

932 #@-others 

933#@-<< class ParserBaseClass >> 

934#@+others 

935#@+node:ekr.20190905091614.1: ** class ActiveSettingsOutline 

936class ActiveSettingsOutline: 

937 

938 def __init__(self, c: Cmdr) -> None: 

939 

940 self.c = c 

941 self.start() 

942 self.create_outline() 

943 #@+others 

944 #@+node:ekr.20190905091614.2: *3* aso.start & helpers 

945 def start(self) -> None: 

946 """Do everything except populating the new outline.""" 

947 # Copy settings. 

948 c = self.c 

949 settings = c.config.settingsDict 

950 shortcuts = c.config.shortcutsDict 

951 assert isinstance(settings, g.TypedDict), repr(settings) 

952 assert isinstance(shortcuts, g.TypedDict), repr(shortcuts) 

953 settings_copy = settings.copy() 

954 shortcuts_copy = shortcuts.copy() 

955 # Create the new commander. 

956 self.commander = self.new_commander() 

957 # Open hidden commanders for non-local settings files. 

958 self.load_hidden_commanders() 

959 # Create the ordered list of commander tuples, including the local .leo file. 

960 self.create_commanders_list() 

961 # Jam the old settings into the new commander. 

962 self.commander.config.settingsDict = settings_copy 

963 self.commander.config.shortcutsDict = shortcuts_copy 

964 #@+node:ekr.20190905091614.3: *4* aso.create_commanders_list 

965 def create_commanders_list(self) -> None: 

966 

967 """Create the commanders list. Order matters.""" 

968 lm = g.app.loadManager 

969 # The first element of each tuple must match the return values of c.config.getSource. 

970 # "local_file", "theme_file", "myLeoSettings", "leoSettings" 

971 

972 self.commanders = [ 

973 ('leoSettings', lm.leo_settings_c), 

974 ('myLeoSettings', lm.my_settings_c), 

975 ] 

976 if lm.theme_c: 

977 self.commanders.append(('theme_file', lm.theme_c),) 

978 if self.c.config.settingsRoot(): 

979 self.commanders.append(('local_file', self.c),) 

980 #@+node:ekr.20190905091614.4: *4* aso.load_hidden_commanders 

981 def load_hidden_commanders(self) -> None: 

982 """ 

983 Open hidden commanders for leoSettings.leo, myLeoSettings.leo and theme.leo. 

984 """ 

985 lm = g.app.loadManager 

986 lm.readGlobalSettingsFiles() 

987 # Make sure to reload the local file. 

988 c = g.app.commanders()[0] 

989 fn = c.fileName() 

990 if fn: 

991 self.local_c = lm.openSettingsFile(fn) 

992 #@+node:ekr.20190905091614.5: *4* aso.new_commander 

993 def new_commander(self) -> Cmdr: 

994 """Create the new commander, and load all settings files.""" 

995 lm = g.app.loadManager 

996 old_c = self.c 

997 # Save any changes so they can be seen. 

998 if old_c.isChanged(): 

999 old_c.save() 

1000 old_c.outerUpdate() 

1001 # From file-new... 

1002 g.app.disable_redraw = True 

1003 g.app.setLog(None) 

1004 g.app.lockLog() 

1005 # Switch to the new commander. Do *not* use previous settings. 

1006 fileName = f"{old_c.fileName()}-active-settings" 

1007 g.es(fileName, color='red') 

1008 c = g.app.newCommander(fileName=fileName) 

1009 # Restore the layout, if we have ever saved this file. 

1010 if not old_c: 

1011 c.frame.setInitialWindowGeometry() 

1012 # #1340: Don't do this. It is no longer needed. 

1013 # g.app.restoreWindowState(c) 

1014 c.frame.resizePanesToRatio(c.frame.ratio, c.frame.secondary_ratio) 

1015 # From file-new... 

1016 g.app.unlockLog() 

1017 lm.createMenu(c) 

1018 lm.finishOpen(c) 

1019 g.app.writeWaitingLog(c) 

1020 c.setLog() 

1021 c.clearChanged() # Clears all dirty bits. 

1022 g.app.disable_redraw = False 

1023 return c 

1024 #@+node:ekr.20190905091614.6: *3* aso.create_outline & helper 

1025 def create_outline(self) -> None: 

1026 """Create the summary outline""" 

1027 c = self.commander 

1028 # 

1029 # Create the root node, with the legend in the body text. 

1030 root = c.rootPosition() 

1031 root.h = f"Legend for {self.c.shortFileName()}" 

1032 root.b = self.legend() 

1033 # 

1034 # Create all the inner settings outlines. 

1035 for kind, commander in self.commanders: 

1036 p = root.insertAfter() 

1037 p.h = g.shortFileName(commander.fileName()) 

1038 p.b = '@language rest\n@wrap\n' 

1039 self.create_inner_outline(commander, kind, p) 

1040 # 

1041 # Clean all dirty/changed bits, so closing this outline won't prompt for a save. 

1042 for v in c.all_nodes(): 

1043 v.clearDirty() 

1044 c.setChanged() 

1045 c.redraw() 

1046 #@+node:ekr.20190905091614.7: *4* aso.legend 

1047 def legend(self) -> str: 

1048 """Compute legend for self.c""" 

1049 c, lm = self.c, g.app.loadManager 

1050 legend = f'''\ 

1051 @language rest 

1052 

1053 legend: 

1054 

1055 leoSettings.leo 

1056 @ @button, @command, @mode 

1057 [D] default settings 

1058 [F] local file: {c.shortFileName()} 

1059 [M] myLeoSettings.leo 

1060 ''' 

1061 if lm.theme_path: 

1062 legend = legend + f"[T] theme file: {g.shortFileName(lm.theme_path)}\n" 

1063 return textwrap.dedent(legend) 

1064 #@+node:ekr.20190905091614.8: *3* aso.create_inner_outline 

1065 def create_inner_outline(self, c: Cmdr, kind: str, root: Pos) -> None: 

1066 """ 

1067 Create the outline for the given hidden commander, as descendants of root. 

1068 """ 

1069 # Find the settings tree 

1070 settings_root = c.config.settingsRoot() 

1071 if not settings_root: 

1072 # This should not be called if the local file has no @settings node. 

1073 g.trace('no @settings node!!', c.shortFileName()) 

1074 return 

1075 # Unify all settings. 

1076 self.create_unified_settings(kind, root, settings_root) 

1077 self.clean(root) 

1078 #@+node:ekr.20190905091614.9: *3* aso.create_unified_settings 

1079 def create_unified_settings(self, kind: str, root: Pos, settings_root: Pos) -> None: 

1080 """Create the active settings tree under root.""" 

1081 c = self.commander 

1082 lm = g.app.loadManager 

1083 settings_pat = re.compile(r'^(@[\w-]+)(\s+[\w\-\.]+)?') 

1084 valid_list = [ 

1085 '@bool', '@color', '@directory', '@encoding', 

1086 '@int', '@float', '@ratio', '@string', 

1087 ] 

1088 d = self.filter_settings(kind) 

1089 ignore, outline_data = None, None 

1090 self.parents = [root] 

1091 self.level = settings_root.level() 

1092 for p in settings_root.subtree(): 

1093 #@+<< continue if we should ignore p >> 

1094 #@+node:ekr.20190905091614.10: *4* << continue if we should ignore p >> 

1095 if ignore: 

1096 if p == ignore: 

1097 ignore = None 

1098 else: 

1099 # g.trace('IGNORE', p.h) 

1100 continue 

1101 if outline_data: 

1102 if p == outline_data: 

1103 outline_data = None 

1104 else: 

1105 self.add(p) 

1106 continue 

1107 #@-<< continue if we should ignore p >> 

1108 m = settings_pat.match(p.h) 

1109 if not m: 

1110 self.add(p, h='ORG:' + p.h) 

1111 continue 

1112 if m.group(2) and m.group(1) in valid_list: 

1113 #@+<< handle a real setting >> 

1114 #@+node:ekr.20190905091614.11: *4* << handle a real setting >> 

1115 key = g.app.config.munge(m.group(2).strip()) 

1116 val = d.get(key) 

1117 if isinstance(val, g.GeneralSetting): 

1118 self.add(p) 

1119 else: 

1120 # Look at all the settings to discover where the setting is defined. 

1121 val = c.config.settingsDict.get(key) 

1122 if isinstance(val, g.GeneralSetting): 

1123 # Use self.c, not self.commander. 

1124 letter = lm.computeBindingLetter(self.c, val.path) 

1125 p.h = f"[{letter}] INACTIVE: {p.h}" 

1126 p.h = f"UNUSED: {p.h}" 

1127 self.add(p) 

1128 #@-<< handle a real setting >> 

1129 continue 

1130 # Not a setting. Handle special cases. 

1131 if m.group(1) == '@ignore': 

1132 ignore = p.nodeAfterTree() 

1133 elif m.group(1) in ('@data', '@outline-data'): 

1134 outline_data = p.nodeAfterTree() 

1135 self.add(p) 

1136 else: 

1137 self.add(p) 

1138 #@+node:ekr.20190905091614.12: *3* aso.add 

1139 def add(self, p: Pos, h: str=None) -> None: 

1140 """ 

1141 Add a node for p. 

1142 

1143 We must *never* alter p in any way. 

1144 Instead, the org flag tells whether the "ORG:" prefix. 

1145 """ 

1146 if 0: 

1147 pad = ' ' * p.level() 

1148 print(pad, p.h) 

1149 p_level = p.level() 

1150 if p_level > self.level + 1: 

1151 g.trace('OOPS', p.v.context.shortFileName(), self.level, p_level, p.h) 

1152 return 

1153 while p_level < self.level + 1 and len(self.parents) > 1: 

1154 self.parents.pop() 

1155 self.level -= 1 

1156 parent = self.parents[-1] 

1157 child = parent.insertAsLastChild() 

1158 child.h = h or p.h 

1159 child.b = p.b 

1160 self.parents.append(child) 

1161 self.level += 1 

1162 #@+node:ekr.20190905091614.13: *3* aso.clean 

1163 def clean(self, root: Pos) -> None: 

1164 """ 

1165 Remove all unnecessary nodes. 

1166 Remove the "ORG:" prefix from remaining nodes. 

1167 """ 

1168 self.clean_node(root) 

1169 

1170 def clean_node(self, p: Pos) -> None: 

1171 """Remove p if it contains no children after cleaning its children.""" 

1172 tag = 'ORG:' 

1173 # There are no clones, so deleting children in reverse preserves positions. 

1174 for child in reversed(list(p.children())): 

1175 self.clean_node(child) 

1176 if p.h.startswith(tag): 

1177 if p.hasChildren(): 

1178 p.h = p.h.lstrip(tag).strip() 

1179 else: 

1180 p.doDelete() 

1181 #@+node:ekr.20190905091614.14: *3* aso.filter_settings 

1182 def filter_settings(self, target_kind: str) -> Dict[str, Any]: 

1183 """Return a dict containing only settings defined in the file given by kind.""" 

1184 # Crucial: Always use the newly-created commander. 

1185 # It's settings are guaranteed to be correct. 

1186 c = self.commander 

1187 valid_kinds = ('local_file', 'theme_file', 'myLeoSettings', 'leoSettings') 

1188 assert target_kind in valid_kinds, repr(target_kind) 

1189 d = c.config.settingsDict 

1190 result = {} 

1191 for key in d.keys(): 

1192 gs = d.get(key) 

1193 assert isinstance(gs, g.GeneralSetting), repr(gs) 

1194 if not gs.kind: 

1195 g.trace('OOPS: no kind', repr(gs)) 

1196 continue 

1197 kind = c.config.getSource(setting=gs) 

1198 if kind == 'ignore': 

1199 g.trace('IGNORE:', kind, key) 

1200 continue 

1201 if kind == 'error': # 2021/09/18. 

1202 g.trace('ERROR:', kind, key) 

1203 continue 

1204 if kind == target_kind: 

1205 result[key] = gs 

1206 return result 

1207 #@-others 

1208#@+node:ekr.20041119203941: ** class GlobalConfigManager 

1209class GlobalConfigManager: 

1210 """A class to manage configuration settings.""" 

1211 # Class data... 

1212 #@+<< gcm.defaultsDict >> 

1213 #@+node:ekr.20041117062717.1: *3* << gcm.defaultsDict >> 

1214 #@+at This contains only the "interesting" defaults. 

1215 # Ints and bools default to 0, floats to 0.0 and strings to "". 

1216 #@@c 

1217 defaultBodyFontSize = 12 # 9 if sys.platform == "win32" else 12 

1218 defaultLogFontSize = 12 # 8 if sys.platform == "win32" else 12 

1219 defaultMenuFontSize = 12 # 9 if sys.platform == "win32" else 12 

1220 defaultTreeFontSize = 12 # 9 if sys.platform == "win32" else 12 

1221 defaultsDict = g.TypedDict( 

1222 name='g.app.config.defaultsDict', 

1223 keyType=str, 

1224 valType=g.GeneralSetting, 

1225 ) 

1226 defaultsData = ( 

1227 # compare options... 

1228 ("ignore_blank_lines", "bool", True), 

1229 ("limit_count", "int", 9), 

1230 ("print_mismatching_lines", "bool", True), 

1231 ("print_trailing_lines", "bool", True), 

1232 # find/change options... 

1233 ("search_body", "bool", True), 

1234 ("whole_word", "bool", True), 

1235 # Prefs panel. 

1236 # ("default_target_language","language","python"), 

1237 ("target_language", "language", "python"), # Bug fix: 6/20,2005. 

1238 ("tab_width", "int", -4), 

1239 ("page_width", "int", 132), 

1240 ("output_doc_chunks", "bool", True), 

1241 ("tangle_outputs_header", "bool", True), 

1242 # Syntax coloring options... 

1243 # Defaults for colors are handled by leoColor.py. 

1244 ("color_directives_in_plain_text", "bool", True), 

1245 ("underline_undefined_section_names", "bool", True), 

1246 # Window options... 

1247 ("body_pane_wraps", "bool", True), 

1248 ("body_text_font_family", "family", "Courier"), 

1249 ("body_text_font_size", "size", defaultBodyFontSize), 

1250 ("body_text_font_slant", "slant", "roman"), 

1251 ("body_text_font_weight", "weight", "normal"), 

1252 ("enable_drag_messages", "bool", True), 

1253 ("headline_text_font_family", "string", None), 

1254 ("headline_text_font_size", "size", defaultLogFontSize), 

1255 ("headline_text_font_slant", "slant", "roman"), 

1256 ("headline_text_font_weight", "weight", "normal"), 

1257 ("log_text_font_family", "string", None), 

1258 ("log_text_font_size", "size", defaultLogFontSize), 

1259 ("log_text_font_slant", "slant", "roman"), 

1260 ("log_text_font_weight", "weight", "normal"), 

1261 ("initial_window_height", "int", 600), 

1262 ("initial_window_width", "int", 800), 

1263 ("initial_window_left", "int", 10), 

1264 ("initial_window_top", "int", 10), 

1265 ("initial_split_orientation", "string", "vertical"), # was initial_splitter_orientation. 

1266 ("initial_vertical_ratio", "ratio", 0.5), 

1267 ("initial_horizontal_ratio", "ratio", 0.3), 

1268 ("initial_horizontal_secondary_ratio", "ratio", 0.5), 

1269 ("initial_vertical_secondary_ratio", "ratio", 0.7), 

1270 # ("outline_pane_scrolls_horizontally","bool",False), 

1271 ("split_bar_color", "color", "LightSteelBlue2"), 

1272 ("split_bar_relief", "relief", "groove"), 

1273 ("split_bar_width", "int", 7), 

1274 ) 

1275 #@-<< gcm.defaultsDict >> 

1276 #@+<< gcm.encodingIvarsDict >> 

1277 #@+node:ekr.20041118062709: *3* << gcm.encodingIvarsDict >> 

1278 encodingIvarsDict = g.TypedDict( 

1279 name='g.app.config.encodingIvarsDict', 

1280 keyType=str, 

1281 valType=g.GeneralSetting, 

1282 ) 

1283 encodingIvarsData = ( 

1284 ("default_at_auto_file_encoding", "string", "utf-8"), 

1285 ("default_derived_file_encoding", "string", "utf-8"), 

1286 # Upper case for compatibility with previous versions. 

1287 ("new_leo_file_encoding", "string", "UTF-8"), 

1288 # 

1289 # The defaultEncoding ivar is no longer used, 

1290 # so it doesn't override better defaults. 

1291 ) 

1292 #@-<< gcm.encodingIvarsDict >> 

1293 #@+<< gcm.ivarsDict >> 

1294 #@+node:ekr.20041117072055: *3* << gcm.ivarsDict >> 

1295 # Each of these settings sets the corresponding ivar. 

1296 # Also, the LocalConfigManager class inits the corresponding commander ivar. 

1297 ivarsDict = g.TypedDict( 

1298 name='g.app.config.ivarsDict', 

1299 keyType=str, 

1300 valType=g.GeneralSetting, 

1301 ) 

1302 ivarsData = ( 

1303 # For compatibility with previous versions. 

1304 ("at_root_bodies_start_in_doc_mode", "bool", True), 

1305 ("create_nonexistent_directories", "bool", False), 

1306 # "" for compatibility with previous versions. 

1307 ("output_initial_comment", "string", ""), 

1308 ("output_newline", "string", "nl"), 

1309 ("page_width", "int", "132"), 

1310 ("read_only", "bool", True), 

1311 ("redirect_execute_script_output_to_log_pane", "bool", False), 

1312 ("relative_path_base_directory", "string", "!"), 

1313 ("remove_sentinels_extension", "string", ".txt"), 

1314 ("save_clears_undo_buffer", "bool", False), 

1315 ("stylesheet", "string", None), 

1316 ("tab_width", "int", -4), 

1317 # Bug fix: added: 6/20/2005. 

1318 ("target_language", "language", "python"), 

1319 ("trailing_body_newlines", "string", "asis"), 

1320 # New in 4.3: use_plugins = True by default. 

1321 ("use_plugins", "bool", True), 

1322 ("undo_granularity", "string", "word"), # "char","word","line","node" 

1323 ("write_strips_blank_lines", "bool", False), 

1324 ) 

1325 #@-<< gcm.ivarsDict >> 

1326 #@+others 

1327 #@+node:ekr.20041117083202: *3* gcm.Birth... 

1328 #@+node:ekr.20041117062717.2: *4* gcm.ctor 

1329 def __init__(self) -> None: 

1330 # 

1331 # Set later. To keep pylint happy. 

1332 if 0: # No longer needed, now that setIvarsFromSettings always sets gcm ivars. 

1333 self.at_root_bodies_start_in_doc_mode = True 

1334 self.default_derived_file_encoding = 'utf-8' 

1335 self.output_newline = 'nl' 

1336 self.redirect_execute_script_output_to_log_pane = True 

1337 self.relative_path_base_directory = '!' 

1338 self.use_plugins = False # Required to keep pylint happy. 

1339 self.create_nonexistent_directories = False # Required to keep pylint happy. 

1340 # List of info (command_p, script, rclicks) for common @buttons nodes. 

1341 # where rclicks is a namedtuple('RClick', 'position,children') 

1342 self.atCommonButtonsList: List[Tuple[Cmdr, str, Any]] = [] 

1343 self.atCommonCommandsList: List[Tuple[Cmdr, str]] = [] # List of info for common @commands nodes. 

1344 self.atLocalButtonsList: List[Pos] = [] # List of positions of @button nodes. 

1345 self.atLocalCommandsList: List[Pos] = [] # List of positions of @command nodes. 

1346 self.buttonsFileName = '' 

1347 self.configsExist = False # True when we successfully open a setting file. 

1348 self.defaultFont = None # Set in gui.getDefaultConfigFont. 

1349 self.defaultFontFamily = None # Set in gui.getDefaultConfigFont. 

1350 self.enabledPluginsFileName = None 

1351 self.enabledPluginsString = '' 

1352 self.inited = False 

1353 self.menusList: List[Any] = [] # pbc.doMenu comment: likely buggy. 

1354 self.menusFileName = '' 

1355 self.modeCommandsDict = g.TypedDict( 

1356 name='modeCommandsDict', 

1357 keyType=str, 

1358 valType=g.TypedDict) # was TypedDictOfLists. 

1359 # Inited later... 

1360 self.panes = None 

1361 self.recentFiles: List[str] = [] 

1362 self.sc = None 

1363 self.tree = None 

1364 self.initDicts() 

1365 self.initIvarsFromSettings() 

1366 self.initRecentFiles() 

1367 #@+node:ekr.20041227063801.2: *4* gcm.initDicts 

1368 def initDicts(self) -> None: 

1369 # Only the settings parser needs to search all dicts. 

1370 self.dictList = [self.defaultsDict] 

1371 for key, kind, val in self.defaultsData: 

1372 self.defaultsDict[self.munge(key)] = g.GeneralSetting( 

1373 kind, setting=key, val=val, tag='defaults') 

1374 for key, kind, val in self.ivarsData: 

1375 self.ivarsDict[self.munge(key)] = g.GeneralSetting( 

1376 kind, ivar=key, val=val, tag='ivars') 

1377 for key, kind, val in self.encodingIvarsData: 

1378 self.encodingIvarsDict[self.munge(key)] = g.GeneralSetting( 

1379 kind, encoding=val, ivar=key, tag='encoding') 

1380 #@+node:ekr.20041117065611.2: *4* gcm.initIvarsFromSettings & helpers 

1381 def initIvarsFromSettings(self) -> None: 

1382 for ivar in sorted(list(self.encodingIvarsDict.keys())): 

1383 self.initEncoding(ivar) 

1384 for ivar in sorted(list(self.ivarsDict.keys())): 

1385 self.initIvar(ivar) 

1386 #@+node:ekr.20041117065611.1: *5* initEncoding 

1387 def initEncoding(self, key: str) -> None: 

1388 """Init g.app.config encoding ivars during initialization.""" 

1389 # Important: The key is munged. 

1390 gs = self.encodingIvarsDict.get(key) 

1391 setattr(self, gs.ivar, gs.encoding) 

1392 if gs.encoding and not g.isValidEncoding(gs.encoding): 

1393 g.es('g.app.config: bad encoding:', f"{gs.ivar}: {gs.encoding}") 

1394 #@+node:ekr.20041117065611: *5* initIvar 

1395 def initIvar(self, key: str) -> None: 

1396 """ 

1397 Init g.app.config ivars during initialization. 

1398 

1399 This does NOT init the corresponding commander ivars. 

1400 

1401 Such initing must be done in setIvarsFromSettings. 

1402 """ 

1403 # Important: the key is munged. 

1404 d = self.ivarsDict 

1405 gs = d.get(key) 

1406 setattr(self, gs.ivar, gs.val) 

1407 #@+node:ekr.20041117083202.2: *4* gcm.initRecentFiles 

1408 def initRecentFiles(self) -> None: 

1409 self.recentFiles = [] 

1410 #@+node:ekr.20041228042224: *4* gcm.setIvarsFromSettings 

1411 def setIvarsFromSettings(self, c: Cmdr) -> None: 

1412 """ 

1413 Init g.app.config ivars or c's ivars from settings. 

1414 

1415 - Called from c.initSettings with c = None to init g.app.config ivars. 

1416 - Called from c.initSettings to init corresponding commmander ivars. 

1417 """ 

1418 if g.app.loadedThemes: 

1419 return 

1420 if not self.inited: 

1421 return 

1422 # Ignore temporary commanders created by readSettingsFiles. 

1423 d = self.ivarsDict 

1424 keys = list(d.keys()) 

1425 keys.sort() 

1426 for key in keys: 

1427 gs = d.get(key) 

1428 if gs: 

1429 assert isinstance(gs, g.GeneralSetting) 

1430 ivar = gs.ivar # The actual name of the ivar. 

1431 kind = gs.kind 

1432 if c: 

1433 val = c.config.get(key, kind) 

1434 else: 

1435 val = self.get(key, kind) # Don't use bunch.val! 

1436 if c: 

1437 setattr(c, ivar, val) 

1438 if True: # Always set the global ivars. 

1439 setattr(self, ivar, val) 

1440 #@+node:ekr.20041117081009: *3* gcm.Getters... 

1441 #@+node:ekr.20041123070429: *4* gcm.canonicalizeSettingName (munge) 

1442 def canonicalizeSettingName(self, name: str) -> str: 

1443 if name is None: 

1444 return None 

1445 name = name.lower() 

1446 for ch in ('-', '_', ' ', '\n'): 

1447 name = name.replace(ch, '') 

1448 return name if name else None 

1449 

1450 munge = canonicalizeSettingName 

1451 #@+node:ekr.20051011105014: *4* gcm.exists 

1452 def exists(self, setting: str, kind: str) -> bool: 

1453 """Return true if a setting of the given kind exists, even if it is None.""" 

1454 lm = g.app.loadManager 

1455 d = lm.globalSettingsDict 

1456 if d: 

1457 junk, found = self.getValFromDict(d, setting, kind) 

1458 return found 

1459 return False 

1460 #@+node:ekr.20041117083141: *4* gcm.get & allies 

1461 def get(self, setting: str, kind: str) -> Any: 

1462 """Get the setting and make sure its type matches the expected type.""" 

1463 lm = g.app.loadManager 

1464 # 

1465 # It *is* valid to call this method: it returns the global settings. 

1466 d = lm.globalSettingsDict 

1467 if d: 

1468 assert isinstance(d, g.TypedDict), repr(d) 

1469 val, junk = self.getValFromDict(d, setting, kind) 

1470 return val 

1471 return None 

1472 #@+node:ekr.20041121143823: *5* gcm.getValFromDict 

1473 def getValFromDict(self, 

1474 d: Any, setting: str, requestedType: str, warn: bool=True, 

1475 ) -> Tuple[Any, bool]: 

1476 """ 

1477 Look up the setting in d. If warn is True, warn if the requested type 

1478 does not (loosely) match the actual type. 

1479 returns (val,exists) 

1480 """ 

1481 tag = 'gcm.getValFromDict' 

1482 gs = d.get(self.munge(setting)) 

1483 if not gs: 

1484 return None, False 

1485 assert isinstance(gs, g.GeneralSetting), repr(gs) 

1486 val = gs.val 

1487 isNone = val in ('None', 'none', '') 

1488 if not self.typesMatch(gs.kind, requestedType): 

1489 # New in 4.4: make sure the types match. 

1490 # A serious warning: one setting may have destroyed another! 

1491 # Important: this is not a complete test of conflicting settings: 

1492 # The warning is given only if the code tries to access the setting. 

1493 if warn: 

1494 g.error( 

1495 f"{tag}: ignoring '{setting}' setting.\n" 

1496 f"{tag}: '@{gs.kind}' is not '@{requestedType}'.\n" 

1497 f"{tag}: there may be conflicting settings!") 

1498 return None, False 

1499 if isNone: 

1500 return '', True # 2011/10/24: Exists, a *user-defined* empty value. 

1501 return val, True 

1502 #@+node:ekr.20051015093141: *5* gcm.typesMatch 

1503 def typesMatch(self, type1: str, type2: str) -> bool: 

1504 """ 

1505 Return True if type1, the actual type, matches type2, the requeseted type. 

1506 

1507 The following equivalences are allowed: 

1508 

1509 - None matches anything. 

1510 - An actual type of string or strings matches anything *except* shortcuts. 

1511 - Shortcut matches shortcuts. 

1512 """ 

1513 # The shortcuts logic no longer uses the get/set code. 

1514 shortcuts = ('shortcut', 'shortcuts',) 

1515 if type1 in shortcuts or type2 in shortcuts: 

1516 g.trace('oops: type in shortcuts') 

1517 return ( 

1518 type1 is None 

1519 or type2 is None 

1520 or type1.startswith('string') and type2 not in shortcuts 

1521 or type1 == 'language' and type2 == 'string' 

1522 or type1 == 'int' and type2 == 'size' 

1523 or (type1 in shortcuts and type2 in shortcuts) 

1524 or type1 == type2 

1525 ) 

1526 #@+node:ekr.20060608224112: *4* gcm.getAbbrevDict 

1527 def getAbbrevDict(self) -> Dict[str, Any]: 

1528 """Search all dictionaries for the setting & check it's type""" 

1529 d = self.get('abbrev', 'abbrev') 

1530 return d or {} 

1531 #@+node:ekr.20041117081009.3: *4* gcm.getBool 

1532 def getBool(self, setting: str, default: bool=None) -> bool: 

1533 """Return the value of @bool setting, or the default if the setting is not found.""" 

1534 val = self.get(setting, "bool") 

1535 if val in (True, False): 

1536 return val 

1537 return default 

1538 #@+node:ekr.20070926082018: *4* gcm.getButtons 

1539 def getButtons(self) -> List: 

1540 """Return a list of tuples (x,y) for common @button nodes.""" 

1541 return g.app.config.atCommonButtonsList 

1542 #@+node:ekr.20041122070339: *4* gcm.getColor 

1543 def getColor(self, setting: str) -> str: 

1544 """Return the value of @color setting.""" 

1545 col = self.get(setting, "color") 

1546 while col and col.startswith('@'): 

1547 col = self.get(col[1:], "color") 

1548 return col 

1549 #@+node:ekr.20080312071248.7: *4* gcm.getCommonCommands 

1550 def getCommonAtCommands(self) -> List[Tuple[str, str]]: 

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

1552 return g.app.config.atCommonCommandsList 

1553 #@+node:ekr.20071214140900.1: *4* gcm.getData & getOutlineData 

1554 def getData(self, 

1555 setting: str, strip_comments: bool=True, strip_data: bool=True, 

1556 ) -> List[str]: 

1557 """Return a list of non-comment strings in the body text of @data setting.""" 

1558 data = self.get(setting, "data") or [] 

1559 # New in Leo 4.12.1: add two keyword arguments, with legacy defaults. 

1560 if data and strip_comments: 

1561 data = [z for z in data if not z.strip().startswith('#')] 

1562 if data and strip_data: 

1563 data = [z.strip() for z in data if z.strip()] 

1564 return data 

1565 

1566 def getOutlineData(self, setting: str) -> None: 

1567 """Return the pastable (xml text) of the entire @outline-data tree.""" 

1568 return self.get(setting, "outlinedata") 

1569 #@+node:ekr.20041117093009.1: *4* gcm.getDirectory 

1570 def getDirectory(self, setting: str) -> str: 

1571 """Return the value of @directory setting, or None if the directory does not exist.""" 

1572 # Fix https://bugs.launchpad.net/leo-editor/+bug/1173763 

1573 theDir = self.get(setting, 'directory') 

1574 if g.os_path_exists(theDir) and g.os_path_isdir(theDir): 

1575 return theDir 

1576 return None 

1577 #@+node:ekr.20070224075914.1: *4* gcm.getEnabledPlugins 

1578 def getEnabledPlugins(self) -> str: 

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

1580 return g.app.config.enabledPluginsString 

1581 #@+node:ekr.20041117082135: *4* gcm.getFloat 

1582 def getFloat(self, setting: str) -> float: 

1583 """Return the value of @float setting.""" 

1584 val = self.get(setting, "float") 

1585 try: 

1586 val = float(val) 

1587 return val 

1588 except TypeError: 

1589 return None 

1590 #@+node:ekr.20041117062717.13: *4* gcm.getFontFromParams 

1591 def getFontFromParams(self, 

1592 family: str, size: str, slant: str, weight: str, defaultSize: int=12, 

1593 ) -> Any: 

1594 """Compute a font from font parameters. 

1595 

1596 Arguments are the names of settings to be use. 

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

1598 

1599 Return None if there is no family setting so we can use system default fonts.""" 

1600 family = self.get(family, "family") 

1601 if family in (None, ""): 

1602 family = self.defaultFontFamily 

1603 size = self.get(size, "size") 

1604 if size in (None, 0): 

1605 size = str(defaultSize) # type:ignore 

1606 slant = self.get(slant, "slant") 

1607 if slant in (None, ""): 

1608 slant = "roman" 

1609 weight = self.get(weight, "weight") 

1610 if weight in (None, ""): 

1611 weight = "normal" 

1612 return g.app.gui.getFontFromParams(family, size, slant, weight) 

1613 #@+node:ekr.20041117081513: *4* gcm.getInt 

1614 def getInt(self, setting: str) -> int: 

1615 """Return the value of @int setting.""" 

1616 val = self.get(setting, "int") 

1617 try: 

1618 val = int(val) 

1619 return val 

1620 except TypeError: 

1621 return None 

1622 #@+node:ekr.20041117093009.2: *4* gcm.getLanguage 

1623 def getLanguage(self, setting: str) -> str: 

1624 """Return the setting whose value should be a language known to Leo.""" 

1625 language = self.getString(setting) 

1626 return language 

1627 #@+node:ekr.20070926070412: *4* gcm.getMenusList 

1628 def getMenusList(self) -> List: 

1629 """Return the list of entries for the @menus tree.""" 

1630 aList = self.get('menus', 'menus') 

1631 # aList is typically empty. 

1632 return aList or g.app.config.menusList 

1633 #@+node:ekr.20070411101643: *4* gcm.getOpenWith 

1634 def getOpenWith(self) -> List[Dict[str, Any]]: 

1635 """Return a list of dictionaries corresponding to @openwith nodes.""" 

1636 val = self.get('openwithtable', 'openwithtable') 

1637 return val 

1638 #@+node:ekr.20041122070752: *4* gcm.getRatio 

1639 def getRatio(self, setting: str) -> float: 

1640 """Return the value of @float setting. 

1641 

1642 Warn if the value is less than 0.0 or greater than 1.0.""" 

1643 val = self.get(setting, "ratio") 

1644 try: 

1645 val = float(val) 

1646 if 0.0 <= val <= 1.0: 

1647 return val 

1648 except TypeError: 

1649 pass 

1650 return None 

1651 #@+node:ekr.20041117062717.11: *4* gcm.getRecentFiles 

1652 def getRecentFiles(self) -> List[str]: 

1653 """Return the list of recently opened files.""" 

1654 return self.recentFiles 

1655 #@+node:ekr.20041117081009.4: *4* gcm.getString 

1656 def getString(self, setting: str) -> str: 

1657 """Return the value of @string setting.""" 

1658 return self.get(setting, "string") 

1659 #@+node:ekr.20120222103014.10314: *3* gcm.config_iter 

1660 def config_iter(self, c: Cmdr) -> Generator: 

1661 """Letters: 

1662 leoSettings.leo 

1663 D default settings 

1664 F loaded .leo File 

1665 M myLeoSettings.leo 

1666 @ @button, @command, @mode. 

1667 """ 

1668 lm = g.app.loadManager 

1669 d = c.config.settingsDict if c else lm.globalSettingsDict 

1670 limit = c.config.getInt('print-settings-at-data-limit') 

1671 if limit is None: 

1672 limit = 20 # A resonable default. 

1673 # pylint: disable=len-as-condition 

1674 for key in sorted(list(d.keys())): 

1675 gs = d.get(key) 

1676 assert isinstance(gs, g.GeneralSetting), repr(gs) 

1677 if gs and gs.kind: 

1678 letter = lm.computeBindingLetter(c, gs.path) 

1679 val = gs.val 

1680 if gs.kind == 'data': 

1681 # #748: Remove comments 

1682 aList = [' ' * 8 + z.rstrip() for z in val 

1683 if z.strip() and not z.strip().startswith('#')] 

1684 if not aList: 

1685 val = '[]' 

1686 elif limit == 0 or len(aList) < limit: 

1687 val = '\n [\n' + '\n'.join(aList) + '\n ]' 

1688 # The following doesn't work well. 

1689 # val = g.objToString(aList, indent=' '*4) 

1690 else: 

1691 val = f"<{len(aList)} non-comment lines>" 

1692 elif isinstance(val, str) and val.startswith('<?xml'): 

1693 val = '<xml>' 

1694 key2 = f"@{gs.kind:>6} {key}" 

1695 yield key2, val, c, letter 

1696 #@+node:ekr.20171115062202.1: *3* gcm.valueInMyLeoSettings 

1697 def valueInMyLeoSettings(self, settingName: str) -> Any: 

1698 """Return the value of the setting, if any, in myLeoSettings.leo.""" 

1699 lm = g.app.loadManager 

1700 d = lm.globalSettingsDict.d 

1701 gs = d.get(self.munge(settingName)) # A GeneralSetting. 

1702 if gs: 

1703 path = gs.path 

1704 if path.find('myLeoSettings.leo') > -1: 

1705 return gs.val 

1706 return None 

1707 #@-others 

1708#@+node:ekr.20041118104831.1: ** class LocalConfigManager 

1709class LocalConfigManager: 

1710 """A class to hold config settings for commanders.""" 

1711 #@+others 

1712 #@+node:ekr.20120215072959.12472: *3* c.config.Birth 

1713 #@+node:ekr.20041118104831.2: *4* c.config.ctor 

1714 def __init__(self, c: Cmdr, previousSettings: str=None) -> None: 

1715 

1716 self.c = c 

1717 lm = g.app.loadManager 

1718 # 

1719 # c.__init__ and helpers set the shortcuts and settings dicts for local files. 

1720 if previousSettings: 

1721 self.settingsDict = previousSettings.settingsDict 

1722 self.shortcutsDict = previousSettings.shortcutsDict 

1723 assert isinstance(self.settingsDict, g.TypedDict), repr(self.settingsDict) 

1724 # was TypedDictOfLists. 

1725 assert isinstance(self.shortcutsDict, g.TypedDict), repr(self.shortcutsDict) 

1726 else: 

1727 self.settingsDict = d1 = lm.globalSettingsDict 

1728 self.shortcutsDict = d2 = lm.globalBindingsDict 

1729 assert d1 is None or isinstance(d1, g.TypedDict), repr(d1) 

1730 assert d2 is None or isinstance( 

1731 d2, g.TypedDict), repr(d2) # was TypedDictOfLists. 

1732 # Define these explicitly to eliminate a pylint warning. 

1733 if 0: 

1734 # No longer needed now that c.config.initIvar always sets 

1735 # both c and c.config ivars. 

1736 self.default_derived_file_encoding = g.app.config.default_derived_file_encoding 

1737 self.redirect_execute_script_output_to_log_pane = g.app.config.redirect_execute_script_output_to_log_pane 

1738 self.defaultBodyFontSize = g.app.config.defaultBodyFontSize 

1739 self.defaultLogFontSize = g.app.config.defaultLogFontSize 

1740 self.defaultMenuFontSize = g.app.config.defaultMenuFontSize 

1741 self.defaultTreeFontSize = g.app.config.defaultTreeFontSize 

1742 for key in sorted(list(g.app.config.encodingIvarsDict.keys())): 

1743 self.initEncoding(key) 

1744 for key in sorted(list(g.app.config.ivarsDict.keys())): 

1745 self.initIvar(key) 

1746 #@+node:ekr.20041118104414: *4* c.config.initEncoding 

1747 def initEncoding(self, key: str) -> None: 

1748 # Important: the key is munged. 

1749 gs = g.app.config.encodingIvarsDict.get(key) 

1750 encodingName = gs.ivar 

1751 encoding = self.get(encodingName, kind='string') 

1752 # Use the global setting as a last resort. 

1753 if encoding: 

1754 setattr(self, encodingName, encoding) 

1755 else: 

1756 encoding = getattr(g.app.config, encodingName) 

1757 setattr(self, encodingName, encoding) 

1758 if encoding and not g.isValidEncoding(encoding): 

1759 g.es('bad', f"{encodingName}: {encoding}") 

1760 #@+node:ekr.20041118104240: *4* c.config.initIvar 

1761 def initIvar(self, key: str) -> None: 

1762 

1763 c = self.c 

1764 # Important: the key is munged. 

1765 gs = g.app.config.ivarsDict.get(key) 

1766 ivarName = gs.ivar 

1767 val = self.get(ivarName, kind=None) 

1768 if val or not hasattr(self, ivarName): 

1769 # Set *both* the commander ivar and the c.config ivar. 

1770 setattr(self, ivarName, val) 

1771 setattr(c, ivarName, val) 

1772 #@+node:ekr.20190831030206.1: *3* c.config.createActivesSettingsOutline (new: #852) 

1773 def createActivesSettingsOutline(self) -> None: 

1774 """ 

1775 Create and open an outline, summarizing all presently active settings. 

1776 

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

1778 

1779 See #852: https://github.com/leo-editor/leo-editor/issues/852 

1780 """ 

1781 ActiveSettingsOutline(self.c) 

1782 #@+node:ekr.20190901181116.1: *3* c.config.getSource 

1783 def getSource(self, setting: str) -> str: 

1784 """ 

1785 Return a string representing the source file of the given setting, 

1786 one of ("local_file", "theme_file", "myLeoSettings", "leoSettings", "ignore", "error") 

1787 """ 

1788 if not isinstance(setting, g.GeneralSetting): 

1789 return "error" 

1790 try: 

1791 path = setting.path 

1792 except Exception: 

1793 return "error" 

1794 if not path: 

1795 return "local_file" 

1796 path = path.lower() 

1797 for tag in ('myLeoSettings.leo', 'leoSettings.leo'): 

1798 if path.endswith(tag.lower()): 

1799 return tag[:-4] # PR: #2422. 

1800 theme_path = g.app.loadManager.theme_path 

1801 if theme_path and g.shortFileName(theme_path.lower()) in path: 

1802 return "theme_file" 

1803 if path == 'register-command' or path.find('mode') > -1: 

1804 return 'ignore' 

1805 return "local_file" 

1806 #@+node:ekr.20120215072959.12471: *3* c.config.Getters 

1807 #@+node:ekr.20041123092357: *4* c.config.findSettingsPosition & helper 

1808 # This was not used prior to Leo 4.5. 

1809 

1810 def findSettingsPosition(self, setting: str) -> Pos: 

1811 """Return the position for the setting in the @settings tree for c.""" 

1812 munge = g.app.config.munge 

1813 # c = self.c 

1814 root = self.settingsRoot() 

1815 if not root: 

1816 return None 

1817 setting = munge(setting) 

1818 for p in root.subtree(): 

1819 #BJ munge will return None if a headstring is empty 

1820 h = munge(p.h) or '' 

1821 if h.startswith(setting): 

1822 return p.copy() 

1823 return None 

1824 #@+node:ekr.20041120074536: *5* c.config.settingsRoot 

1825 def settingsRoot(self) -> Pos: 

1826 """Return the position of the @settings tree.""" 

1827 c = self.c 

1828 for p in c.all_unique_positions(): 

1829 # #1792: Allow comments after @settings. 

1830 if g.match_word(p.h.rstrip(), 0, "@settings"): 

1831 return p.copy() 

1832 return None 

1833 #@+node:ekr.20120215072959.12515: *4* c.config.Getters 

1834 #@@nocolor-node 

1835 #@+at Only the following need to be defined. 

1836 # get (self,setting,theType) 

1837 # getAbbrevDict (self) 

1838 # getBool (self,setting,default=None) 

1839 # getButtons (self) 

1840 # getColor (self,setting) 

1841 # getData (self,setting) 

1842 # getDirectory (self,setting) 

1843 # getFloat (self,setting) 

1844 # getFontFromParams (self,family,size,slant,weight,defaultSize=12) 

1845 # getInt (self,setting) 

1846 # getLanguage (self,setting) 

1847 # getMenusList (self) 

1848 # getOutlineData (self) 

1849 # getOpenWith (self) 

1850 # getRatio (self,setting) 

1851 # getShortcut (self,commandName) 

1852 # getString (self,setting) 

1853 #@+node:ekr.20120215072959.12519: *5* c.config.get & allies 

1854 def get(self, setting: str, kind: str) -> Any: 

1855 """Get the setting and make sure its type matches the expected type.""" 

1856 d = self.settingsDict 

1857 if d: 

1858 assert isinstance(d, g.TypedDict), repr(d) 

1859 val, junk = self.getValFromDict(d, setting, kind) 

1860 return val 

1861 return None 

1862 #@+node:ekr.20120215072959.12520: *6* c.config.getValFromDict 

1863 def getValFromDict(self, 

1864 d: Any, setting: str, requestedType: str, warn: bool=True, 

1865 ) -> Tuple[Any, bool]: 

1866 """ 

1867 Look up the setting in d. If warn is True, warn if the requested type 

1868 does not (loosely) match the actual type. 

1869 returns (val,exists) 

1870 """ 

1871 tag = 'c.config.getValFromDict' 

1872 gs = d.get(g.app.config.munge(setting)) 

1873 if not gs: 

1874 return None, False 

1875 assert isinstance(gs, g.GeneralSetting), repr(gs) 

1876 val = gs.val 

1877 isNone = val in ('None', 'none', '') 

1878 if not self.typesMatch(gs.kind, requestedType): 

1879 # New in 4.4: make sure the types match. 

1880 # A serious warning: one setting may have destroyed another! 

1881 # Important: this is not a complete test of conflicting settings: 

1882 # The warning is given only if the code tries to access the setting. 

1883 if warn: 

1884 g.error( 

1885 f"{tag}: ignoring '{setting}' setting.\n" 

1886 f"{tag}: '@{gs.kind}' is not '@{requestedType}'.\n" 

1887 f"{tag}: there may be conflicting settings!") 

1888 return None, False 

1889 if isNone: 

1890 return '', True # 2011/10/24: Exists, a *user-defined* empty value. 

1891 return val, True 

1892 #@+node:ekr.20120215072959.12521: *6* c.config.typesMatch 

1893 def typesMatch(self, type1: str, type2: str) -> bool: 

1894 """ 

1895 Return True if type1, the actual type, matches type2, the requeseted type. 

1896 

1897 The following equivalences are allowed: 

1898 

1899 - None matches anything. 

1900 - An actual type of string or strings matches anything *except* shortcuts. 

1901 - Shortcut matches shortcuts. 

1902 """ 

1903 # The shortcuts logic no longer uses the get/set code. 

1904 shortcuts = ('shortcut', 'shortcuts',) 

1905 if type1 in shortcuts or type2 in shortcuts: 

1906 g.trace('oops: type in shortcuts') 

1907 return ( 

1908 type1 is None 

1909 or type2 is None 

1910 or type1.startswith('string') and type2 not in shortcuts 

1911 or type1 == 'language' and type2 == 'string' 

1912 or type1 == 'int' and type2 == 'size' 

1913 or (type1 in shortcuts and type2 in shortcuts) 

1914 or type1 == type2 

1915 ) 

1916 #@+node:ekr.20120215072959.12522: *5* c.config.getAbbrevDict 

1917 def getAbbrevDict(self) -> Dict[str, Any]: 

1918 """Search all dictionaries for the setting & check it's type""" 

1919 d = self.get('abbrev', 'abbrev') 

1920 return d or {} 

1921 #@+node:ekr.20120215072959.12523: *5* c.config.getBool 

1922 def getBool(self, setting, default=None) -> bool: 

1923 """Return the value of @bool setting, or the default if the setting is not found.""" 

1924 val = self.get(setting, "bool") 

1925 if val in (True, False): 

1926 return val 

1927 return default 

1928 #@+node:ekr.20120215072959.12525: *5* c.config.getColor 

1929 def getColor(self, setting: str) -> str: 

1930 """Return the value of @color setting.""" 

1931 col = self.get(setting, "color") 

1932 while col and col.startswith('@'): 

1933 col = self.get(col[1:], "color") 

1934 return col 

1935 #@+node:ekr.20120215072959.12527: *5* c.config.getData 

1936 def getData(self, setting, strip_comments=True, strip_data=True) -> List[str]: 

1937 """Return a list of non-comment strings in the body text of @data setting.""" 

1938 # 904: Add local abbreviations to global settings. 

1939 append = setting == 'global-abbreviations' 

1940 if append: 

1941 data0 = g.app.config.getData(setting, 

1942 strip_comments=strip_comments, 

1943 strip_data=strip_data, 

1944 ) 

1945 data = self.get(setting, "data") 

1946 # New in Leo 4.11: parser.doData strips only comments now. 

1947 # New in Leo 4.12: parser.doData strips *nothing*. 

1948 if isinstance(data, str): 

1949 data = [data] 

1950 if data and strip_comments: 

1951 data = [z for z in data if not z.strip().startswith('#')] 

1952 if data and strip_data: 

1953 data = [z.strip() for z in data if z.strip()] 

1954 if append and data != data0: 

1955 if data: 

1956 data.extend(data0) 

1957 else: 

1958 data = data0 

1959 return data 

1960 #@+node:ekr.20131114051702.16542: *5* c.config.getOutlineData 

1961 def getOutlineData(self, setting: str) -> Any: 

1962 """Return the pastable (xml) text of the entire @outline-data tree.""" 

1963 data = self.get(setting, "outlinedata") 

1964 if setting == 'tree-abbreviations': 

1965 # 904: Append local tree abbreviations to the global abbreviations. 

1966 data0 = g.app.config.getOutlineData(setting) 

1967 if data and data0 and data != data0: 

1968 assert isinstance(data0, str) 

1969 assert isinstance(data, str) 

1970 # We can't merge the data here: they are .leo files! 

1971 # abbrev.init_tree_abbrev_helper does the merge. 

1972 data = [data0, data] 

1973 return data 

1974 #@+node:ekr.20120215072959.12528: *5* c.config.getDirectory 

1975 def getDirectory(self, setting: str) -> str: 

1976 """Return the value of @directory setting, or None if the directory does not exist.""" 

1977 # Fix https://bugs.launchpad.net/leo-editor/+bug/1173763 

1978 theDir = self.get(setting, 'directory') 

1979 if g.os_path_exists(theDir) and g.os_path_isdir(theDir): 

1980 return theDir 

1981 return None 

1982 #@+node:ekr.20120215072959.12530: *5* c.config.getFloat 

1983 def getFloat(self, setting) -> float: 

1984 """Return the value of @float setting.""" 

1985 val = self.get(setting, "float") 

1986 try: 

1987 val = float(val) 

1988 return val 

1989 except TypeError: 

1990 return None 

1991 #@+node:ekr.20120215072959.12531: *5* c.config.getFontFromParams 

1992 def getFontFromParams(self, 

1993 family: str, size: str, slant: str, weight: str, defaultSize: int=12, 

1994 ) -> Any: 

1995 """ 

1996 Compute a font from font parameters. This should be used *only* 

1997 by the syntax coloring code. Otherwise, use Leo's style sheets. 

1998 

1999 Arguments are the names of settings to be use. 

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

2001 

2002 Return None if there is no family setting so we can use system default fonts. 

2003 """ 

2004 family = self.get(family, "family") 

2005 if family in (None, ""): 

2006 family = g.app.config.defaultFontFamily 

2007 size = self.get(size, "size") 

2008 if size in (None, 0): 

2009 size = str(defaultSize) # type:ignore 

2010 slant = self.get(slant, "slant") 

2011 if slant in (None, ""): 

2012 slant = "roman" 

2013 weight = self.get(weight, "weight") 

2014 if weight in (None, ""): 

2015 weight = "normal" 

2016 return g.app.gui.getFontFromParams(family, size, slant, weight) 

2017 #@+node:ekr.20120215072959.12532: *5* c.config.getInt 

2018 def getInt(self, setting) -> int: 

2019 """Return the value of @int setting.""" 

2020 val = self.get(setting, "int") 

2021 try: 

2022 val = int(val) 

2023 return val 

2024 except TypeError: 

2025 return None 

2026 #@+node:ekr.20120215072959.12533: *5* c.config.getLanguage 

2027 def getLanguage(self, setting: str) -> str: 

2028 """Return the setting whose value should be a language known to Leo.""" 

2029 language = self.getString(setting) 

2030 return language 

2031 #@+node:ekr.20120215072959.12534: *5* c.config.getMenusList 

2032 def getMenusList(self) -> List: 

2033 """Return the list of entries for the @menus tree.""" 

2034 aList = self.get('menus', 'menus') 

2035 # aList is typically empty. 

2036 return aList or g.app.config.menusList 

2037 #@+node:ekr.20120215072959.12535: *5* c.config.getOpenWith 

2038 def getOpenWith(self) -> List[Dict[str, Any]]: 

2039 """Return a list of dictionaries corresponding to @openwith nodes.""" 

2040 val = self.get('openwithtable', 'openwithtable') 

2041 return val 

2042 #@+node:ekr.20120215072959.12536: *5* c.config.getRatio 

2043 def getRatio(self, setting: str) -> float: 

2044 """ 

2045 Return the value of @float setting. 

2046 

2047 Warn if the value is less than 0.0 or greater than 1.0. 

2048 """ 

2049 val = self.get(setting, "ratio") 

2050 try: 

2051 val = float(val) 

2052 if 0.0 <= val <= 1.0: 

2053 return val 

2054 except TypeError: 

2055 pass 

2056 return None 

2057 #@+node:ekr.20120215072959.12538: *5* c.config.getSettingSource 

2058 def getSettingSource(self, setting: str) -> Tuple[str, Any]: 

2059 """return the name of the file responsible for setting.""" 

2060 d = self.settingsDict 

2061 if d: 

2062 assert isinstance(d, g.TypedDict), repr(d) 

2063 bi = d.get(setting) 

2064 if bi is None: 

2065 return 'unknown setting', None 

2066 return bi.path, bi.val 

2067 # 

2068 # lm.readGlobalSettingsFiles is opening a settings file. 

2069 # lm.readGlobalSettingsFiles has not yet set lm.globalSettingsDict. 

2070 assert d is None 

2071 return None 

2072 #@+node:ekr.20120215072959.12539: *5* c.config.getShortcut 

2073 no_menu_dict: Dict[Cmdr, bool] = {} 

2074 

2075 def getShortcut(self, commandName: str) -> Tuple[str, List]: 

2076 """Return rawKey,accel for shortcutName""" 

2077 c = self.c 

2078 d = self.shortcutsDict 

2079 if not c.frame.menu: 

2080 if c not in self.no_menu_dict: 

2081 self.no_menu_dict[c] = True 

2082 g.trace(f"no menu: {c.shortFileName()}:{commandName}") 

2083 return None, [] 

2084 if d: 

2085 assert isinstance(d, g.TypedDict), repr(d) # was TypedDictOfLists. 

2086 key = c.frame.menu.canonicalizeMenuName(commandName) 

2087 key = key.replace('&', '') # Allow '&' in names. 

2088 aList = d.get(commandName, []) 

2089 if aList: # A list of g.BindingInfo objects. 

2090 # It's important to filter empty strokes here. 

2091 aList = [z for z in aList 

2092 if z.stroke and z.stroke.lower() != 'none'] 

2093 return key, aList 

2094 # 

2095 # lm.readGlobalSettingsFiles is opening a settings file. 

2096 # lm.readGlobalSettingsFiles has not yet set lm.globalSettingsDict. 

2097 return None, [] 

2098 #@+node:ekr.20120215072959.12540: *5* c.config.getString 

2099 def getString(self, setting: str) -> str: 

2100 """Return the value of @string setting.""" 

2101 return self.get(setting, "string") 

2102 #@+node:ekr.20120215072959.12543: *4* c.config.Getters: redirect to g.app.config 

2103 def getButtons(self) -> List[Tuple[str, str]]: 

2104 """Return a list of tuples (x,y) for common @button nodes.""" 

2105 return g.app.config.atCommonButtonsList # unusual. 

2106 

2107 def getCommands(self) -> List[Tuple[str, str]]: 

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

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

2110 

2111 def getEnabledPlugins(self) -> str: 

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

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

2114 

2115 def getRecentFiles(self) -> List[str]: 

2116 """Return the list of recently opened files.""" 

2117 return g.app.config.getRecentFiles() # unusual 

2118 #@+node:ekr.20140114145953.16691: *4* c.config.isLocalSetting 

2119 def isLocalSetting(self, setting: str, kind: str) -> bool: 

2120 """Return True if the indicated setting comes from a local .leo file.""" 

2121 if not kind or kind in ('shortcut', 'shortcuts', 'openwithtable'): 

2122 return False 

2123 key = g.app.config.munge(setting) 

2124 if key is None: 

2125 return False 

2126 if not self.settingsDict: 

2127 return False 

2128 gs = self.settingsDict.get(key) 

2129 if not gs: 

2130 return False 

2131 assert isinstance(gs, g.GeneralSetting), repr(gs) 

2132 path = gs.path.lower() 

2133 for fn in ('myLeoSettings.leo', 'leoSettings.leo'): 

2134 if path.endswith(fn.lower()): 

2135 return False 

2136 return True 

2137 #@+node:ekr.20171119222458.1: *4* c.config.isLocalSettingsFile 

2138 def isLocalSettingsFile(self) -> bool: 

2139 """Return true if c is not leoSettings.leo or myLeoSettings.leo""" 

2140 c = self.c 

2141 fn = c.shortFileName().lower() 

2142 for fn2 in ('leoSettings.leo', 'myLeoSettings.leo'): 

2143 if fn.endswith(fn2.lower()): 

2144 return False 

2145 return True 

2146 #@+node:ekr.20120224140548.10528: *4* c.exists 

2147 def exists(self, c: Cmdr, setting: str, kind: str) -> bool: 

2148 """Return true if a setting of the given kind exists, even if it is None.""" 

2149 d = self.settingsDict 

2150 if d: 

2151 junk, found = self.getValFromDict(d, setting, kind) 

2152 if found: 

2153 return True 

2154 return False 

2155 #@+node:ekr.20070418073400: *3* c.config.printSettings 

2156 def printSettings(self) -> None: 

2157 """Prints the value of every setting, except key bindings and commands and open-with tables. 

2158 The following shows where the active setting came from: 

2159 

2160 - leoSettings.leo, 

2161 - @ @button, @command, @mode. 

2162 - [D] default settings. 

2163 - [F] indicates the file being loaded, 

2164 - [M] myLeoSettings.leo, 

2165 - [T] theme .leo file. 

2166 """ 

2167 legend = '''\ 

2168 legend: 

2169 leoSettings.leo 

2170 @ @button, @command, @mode 

2171 [D] default settings 

2172 [F] loaded .leo File 

2173 [M] myLeoSettings.leo 

2174 [T] theme .leo file. 

2175 ''' 

2176 c = self.c 

2177 legend = textwrap.dedent(legend) 

2178 result = [] 

2179 for name, val, c, letter in g.app.config.config_iter(c): 

2180 kind = ' ' if letter == ' ' else f"[{letter}]" 

2181 result.append(f"{kind} {name} = {val}\n") 

2182 # Use a single g.es statement. 

2183 result.append('\n' + legend) 

2184 if g.unitTesting: 

2185 pass # print(''.join(result)) 

2186 else: 

2187 g.es_print('', ''.join(result), tabName='Settings') 

2188 #@+node:ekr.20120215072959.12475: *3* c.config.set 

2189 def set(self, p: Pos, kind: str, name: str, val: Any, warn: bool=True) -> None: 

2190 """ 

2191 Init the setting for name to val. 

2192 

2193 The "p" arg is not used. 

2194 """ 

2195 c = self.c 

2196 # Note: when kind is 'shortcut', name is a command name. 

2197 key = g.app.config.munge(name) 

2198 d = self.settingsDict 

2199 assert isinstance(d, g.TypedDict), repr(d) 

2200 gs = d.get(key) 

2201 if gs: 

2202 assert isinstance(gs, g.GeneralSetting), repr(gs) 

2203 path = gs.path 

2204 if warn and g.os_path_finalize( 

2205 c.mFileName) != g.os_path_finalize(path): # #1341. 

2206 g.es("over-riding setting:", name, "from", path) 

2207 d[key] = g.GeneralSetting(kind, path=c.mFileName, val=val, tag='setting') 

2208 #@+node:ekr.20190905082644.1: *3* c.config.settingIsActiveInPath 

2209 def settingIsActiveInPath(self, gs: str, target_path: str) -> bool: 

2210 """Return True if settings file given by path actually defines the setting, gs.""" 

2211 assert isinstance(gs, g.GeneralSetting), repr(gs) 

2212 return gs.path == target_path 

2213 #@+node:ekr.20180121135120.1: *3* c.config.setUserSetting 

2214 def setUserSetting(self, setting: str, value: str) -> None: 

2215 """ 

2216 Find and set the indicated setting, either in the local file or in 

2217 myLeoSettings.leo. 

2218 """ 

2219 c = self.c 

2220 fn = g.shortFileName(c.fileName()) 

2221 p = self.findSettingsPosition(setting) 

2222 if not p: 

2223 c = c.openMyLeoSettings() 

2224 if not c: 

2225 return 

2226 fn = 'myLeoSettings.leo' 

2227 p = c.config.findSettingsPosition(setting) 

2228 if not p: 

2229 root = c.config.settingsRoot() 

2230 if not root: 

2231 return 

2232 fn = 'leoSettings.leo' 

2233 p = c.config.findSettingsPosition(setting) 

2234 if not p: 

2235 p = root.insertAsLastChild() 

2236 h = setting 

2237 i = h.find('=') 

2238 if i > -1: 

2239 h = h[:i].strip() 

2240 p.h = f"{h} = {value}" 

2241 print(f"Updated `{setting}` in {fn}") # #2390. 

2242 # 

2243 # Delay the second redraw until idle time. 

2244 c.setChanged() 

2245 p.setDirty() 

2246 c.redraw_later() 

2247 #@-others 

2248#@+node:ekr.20041119203941.3: ** class SettingsTreeParser (ParserBaseClass) 

2249class SettingsTreeParser(ParserBaseClass): 

2250 """A class that inits settings found in an @settings tree. 

2251 

2252 Used by read settings logic.""" 

2253 

2254 # def __init__(self, c, localFlag=True): 

2255 # super().__init__(c, localFlag) 

2256 #@+others 

2257 #@+node:ekr.20041119204103: *3* ctor (SettingsTreeParser) 

2258 #@+node:ekr.20041119204714: *3* visitNode (SettingsTreeParser) 

2259 def visitNode(self, p: Pos) -> str: 

2260 """Init any settings found in node p.""" 

2261 p = p.copy() 

2262 munge = g.app.config.munge 

2263 kind, name, val = self.parseHeadline(p.h) 

2264 kind = munge(kind) 

2265 isNone = val in ('None', 'none', '', None) 

2266 if kind is None: # Not an @x node. (New in Leo 4.4.4) 

2267 pass 

2268 elif kind == "settings": 

2269 pass 

2270 elif kind in self.basic_types and isNone: 

2271 # None is valid for all basic types. 

2272 self.set(p, kind, name, None) 

2273 elif kind in self.control_types or kind in self.basic_types: 

2274 f = self.dispatchDict.get(kind) 

2275 if f: 

2276 try: 

2277 return f(p, kind, name, val) # type:ignore 

2278 except Exception: 

2279 g.es_exception() 

2280 else: 

2281 g.pr("*** no handler", kind) 

2282 return None 

2283 #@-others 

2284#@+node:ekr.20171229131953.1: ** parseFont (leoConfig.py) 

2285def parseFont(b: str) -> Tuple[str, str, bool, bool, float]: 

2286 family = None 

2287 weight = None 

2288 slant = None 

2289 size = None 

2290 settings_name = None 

2291 for line in g.splitLines(b): 

2292 line = line.strip() 

2293 if line.startswith('#'): 

2294 continue 

2295 i = line.find('=') 

2296 if i < 0: 

2297 continue 

2298 name = line[:i].strip() 

2299 if name.endswith('_family'): 

2300 family = line[i + 1 :].strip() 

2301 elif name.endswith('_weight'): 

2302 weight = line[i + 1 :].strip() 

2303 elif name.endswith('_size'): 

2304 size_s = line[i + 1 :].strip() 

2305 try: 

2306 size = float(size_s) 

2307 except ValueError: 

2308 size = 12.0 

2309 elif name.endswith('_slant'): 

2310 slant = line[i + 1 :].strip() 

2311 if settings_name is None and name.endswith( 

2312 ('_family', '_slant', '_weight', '_size')): 

2313 settings_name = name.rsplit('_', 1)[0] 

2314 return settings_name, family, weight == 'bold', slant in ('slant', 'italic'), size 

2315#@-others 

2316#@@language python 

2317#@@tabwidth -4 

2318#@@pagewidth 70 

2319#@-leo