Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#@+leo-ver=5-thin 

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

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

4# pylint: disable=unsubscriptable-object 

5#@+<< imports >> 

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

7import os 

8import sys 

9import re 

10import textwrap 

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

12from leo.core.leoCommands import Commands as Cmdr 

13from leo.plugins.mod_scripting import build_rclick_tree 

14from leo.core import leoGlobals as g 

15#@-<< imports >> 

16#@+<< class ParserBaseClass >> 

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

18class ParserBaseClass: 

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

20 #@+<< ParserBaseClass data >> 

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

22 # These are the canonicalized names. 

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

24 basic_types = [ 

25 # Headlines have the form @kind name = var 

26 'bool', 

27 'color', 

28 'directory', 

29 'int', 

30 'ints', 

31 'float', 

32 'path', 

33 'ratio', 

34 'string', 

35 'strings', 

36 ] 

37 control_types = [ 

38 'buttons', 

39 'commands', 

40 'data', 

41 'enabledplugins', 

42 'font', 

43 'ifenv', 

44 'ifhostname', 

45 'ifplatform', 

46 'ignore', 

47 'menus', 

48 'mode', 

49 'menuat', 

50 'openwith', 

51 'outlinedata', 

52 'popup', 

53 'settings', 

54 'shortcuts', 

55 ] 

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

57 settingsDict: Dict[str, Tuple[str, Union[g.TypedDict, g.GeneralSetting]]] = {} 

58 #@-<< ParserBaseClass data >> 

59 #@+others 

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

61 #@@nobeautify 

62 

63 def __init__ (self,c,localFlag): 

64 """Ctor for the ParserBaseClass class.""" 

65 self.c = c 

66 self.clipBoard = [] 

67 self.localFlag = localFlag 

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

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

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

71 name='parser.shortcutsDict', 

72 keyType=type('shortcutName'), 

73 valType=g.BindingInfo, 

74 ) 

75 self.openWithList = [] 

76 # A list of dicts containing 'name','shortcut','command' keys. 

77 # Keys are canonicalized names. 

78 self.dispatchDict = { 

79 'bool': self.doBool, 

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

81 'color': self.doColor, 

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

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

84 'directory': self.doDirectory, 

85 'enabledplugins': self.doEnabledPlugins, 

86 'font': self.doFont, 

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

88 'ifhostname': self.doIfHostname, 

89 'ifplatform': self.doIfPlatform, 

90 'ignore': self.doIgnore, 

91 'int': self.doInt, 

92 'ints': self.doInts, 

93 'float': self.doFloat, 

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

95 'menuat': self.doMenuat, 

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

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

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

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

100 'path': self.doPath, 

101 'ratio': self.doRatio, 

102 'shortcuts': self.doShortcuts, 

103 'string': self.doString, 

104 'strings': self.doStrings, 

105 } 

106 self.debug_count = 0 

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

108 def computeModeName(self, name): 

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

110 j = s.find(' ') 

111 if j > -1: 

112 s = s[:j] 

113 if s.endswith('mode'): 

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

115 if s.endswith('-'): 

116 s = s[:-1] 

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

118 if i > -1: 

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

120 # The prompt is everything after the prompt. 

121 s = s[:i] 

122 modeName = s + '-mode' 

123 return modeName 

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

125 def createModeCommand(self, modeName, name, modeDict): 

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

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

128 if i > -1: 

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

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

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

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

133 d = g.app.config.modeCommandsDict 

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

135 d[modeName] = modeDict 

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

137 def error(self, s): 

138 g.pr(s) 

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

140 g.blue(s) 

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

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

143 def doBool(self, p, kind, name, val): 

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

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

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

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

148 else: 

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

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

151 def doButtons(self, p, kind, name, val): 

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

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

154 aList, seen = [], [] 

155 after = p.nodeAfterTree() 

156 while p and p != after: 

157 if p.v in seen: 

158 p.moveToNodeAfterTree() 

159 elif p.isAtIgnoreNode(): 

160 seen.append(p.v) 

161 p.moveToNodeAfterTree() 

162 else: 

163 seen.append(p.v) 

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

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

166 script = g.getScript( 

167 c, 

168 p, 

169 useSelectedText=False, 

170 forcePythonSentinels=True, 

171 useSentinels=True) 

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

173 command_p = p.copy() 

174 rclicks = build_rclick_tree(command_p, top_level=True) 

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

176 p.moveToThreadNext() 

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

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

179 if aList: 

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

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

182 g.app.config.buttonsFileName = ( 

183 c.shortFileName() if c else '<no settings file>' 

184 ) 

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

186 def doColor(self, p, kind, name, val): 

187 # At present no checking is done. 

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

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

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

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

192 def doCommands(self, p, kind, name, val): 

193 """Handle an @commands tree.""" 

194 c = self.c 

195 aList = [] 

196 tag = '@command' 

197 seen = [] 

198 after = p.nodeAfterTree() 

199 while p and p != after: 

200 if p.v in seen: 

201 p.moveToNodeAfterTree() 

202 elif p.isAtIgnoreNode(): 

203 seen.append(p.v) 

204 p.moveToNodeAfterTree() 

205 else: 

206 seen.append(p.v) 

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

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

209 script = g.getScript(c, p, 

210 useSelectedText=False, 

211 forcePythonSentinels=True, 

212 useSentinels=True) 

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

214 p.moveToThreadNext() 

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

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

217 if aList: 

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

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

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

221 def doData(self, p, kind, name, val): 

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

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

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

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

226 # - Ensure all fragments end with a newline. 

227 data = g.splitLines(p.b) 

228 for p2 in p.subtree(): 

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

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

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

232 data.append('\n') 

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

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

235 def doOutlineData(self, p, kind, name, val): 

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

237 data = self.getOutlineDataHelper(p) 

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

239 return 'skip' 

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

241 def getOutlineDataHelper(self, p): 

242 c = self.c 

243 if not p: 

244 return None 

245 try: 

246 # Copy the entire tree to s. 

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

248 s = c.fileCommands.outline_to_clipboard_string(p) 

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

250 except Exception: 

251 g.es_exception() 

252 s = None 

253 return s 

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

255 def doDirectory(self, p, kind, name, val): 

256 # At present no checking is done. 

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

258 

259 doPath = doDirectory 

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

261 def doEnabledPlugins(self, p, kind, name, val): 

262 c = self.c 

263 s = p.b 

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

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

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

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

268 for s in lines: 

269 i = s.find('#') 

270 if i > -1: 

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

272 if s.strip(): 

273 aList.append(s.lstrip()) 

274 s = ''.join(aList) 

275 # Set the global config ivars. 

276 g.app.config.enabledPluginsString = s 

277 g.app.config.enabledPluginsFileName = c.shortFileName( 

278 ) if c else '<no settings file>' 

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

280 def doFloat(self, p, kind, name, val): 

281 try: 

282 val = float(val) 

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

284 except ValueError: 

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

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

287 def doFont(self, p, kind, name, val): 

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

289 d = self.parseFont(p) 

290 # Set individual settings. 

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

292 data = d.get(key) 

293 if data is not None: 

294 name, val = data 

295 setKind = key 

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

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

298 def doIfEnv(self, p, kind, name, val): 

299 """ 

300 Support @ifenv in @settings trees. 

301 

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

303 """ 

304 aList = name.split(',') 

305 if not aList: 

306 return 'skip' 

307 name = aList[0] 

308 env = os.getenv(name) 

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

310 for s in aList[1:]: 

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

312 return None 

313 return 'skip' 

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

315 def doIfHostname(self, p, kind, name, val): 

316 """ 

317 Support @ifhostname in @settings trees. 

318 

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

320 

321 @ifhostname bob 

322 Enable descendant settings if h == 'bob' 

323 @ifhostname !harry 

324 Enable descendant settings if h != 'harry' 

325 """ 

326 lm = g.app.loadManager 

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

328 s = name.strip() 

329 if s.startswith('!'): 

330 if h == s[1:]: 

331 return 'skip' 

332 elif h != s: 

333 return 'skip' 

334 return None 

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

336 def doIfPlatform(self, p, kind, name, val): 

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

338 platform = sys.platform.lower() 

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

340 if platform == s.lower(): 

341 return None 

342 return "skip" 

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

344 def doIgnore(self, p, kind, name, val): 

345 return "skip" 

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

347 def doInt(self, p, kind, name, val): 

348 try: 

349 val = int(val) 

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

351 except ValueError: 

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

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

354 def doInts(self, p, kind, name, val): 

355 """ 

356 We expect either: 

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

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

359 """ 

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

361 i = name.find('[') 

362 j = name.find(']') 

363 if -1 < i < j: 

364 items = name[i + 1 : j] 

365 items = items.split(',') 

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

367 try: 

368 items = [int(item.strip()) for item in items] 

369 except ValueError: 

370 items = [] 

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

372 return 

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

374 try: 

375 val = int(val) 

376 except ValueError: 

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

378 return 

379 if val not in items: 

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

381 return 

382 # At present no checking is done. 

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

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

385 def doMenuat(self, p, kind, name, val): 

386 """Handle @menuat setting.""" 

387 if g.app.config.menusList: 

388 # get the patch fragment 

389 patch: List[Any] = [] 

390 if p.hasChildren(): 

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

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

393 # setup 

394 parts = name.split() 

395 if len(parts) != 3: 

396 parts.append('subtree') 

397 targetPath, mode, source = parts 

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

399 targetPath = '/' + targetPath 

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

401 if ans: 

402 # pylint: disable=unpacking-non-sequence 

403 list_, idx = ans 

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

405 if source != 'clipboard': 

406 use = patch # [0][1] 

407 else: 

408 if isinstance(self.clipBoard, list): 

409 use = self.clipBoard 

410 else: 

411 use = [self.clipBoard] 

412 if mode == 'replace': 

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

414 while use: 

415 idx += 1 

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

417 elif mode == 'before': 

418 while use: 

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

420 elif mode == 'after': 

421 while use: 

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

423 elif mode == 'cut': 

424 self.clipBoard = list_[idx] 

425 del list_[idx] 

426 elif mode == 'copy': 

427 self.clipBoard = list_[idx] 

428 else: # append 

429 list_.extend(use) 

430 else: 

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

432 elif g.app.inBridge: 

433 pass # #48: Not an error. 

434 else: 

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

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

437 def getName(self, val, val2=None): 

438 if val2 and val2.strip(): 

439 val = val2 

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

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

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

443 return val.lower() 

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

445 def dumpMenuTree(self, aList, level=0, path=''): 

446 for z in aList: 

447 kind, val, val2 = z 

448 pad = ' ' * level 

449 if kind == '@item': 

450 name = self.getName(val, val2) 

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

452 else: 

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

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

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

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

457 def patchMenuTree(self, orig, targetPath, path=''): 

458 

459 for n, z in enumerate(orig): 

460 kind, val, val2 = z 

461 if kind == '@item': 

462 name = self.getName(val, val2) 

463 curPath = path + '/' + name 

464 if curPath == targetPath: 

465 return orig, n 

466 else: 

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

468 curPath = path + '/' + name 

469 if curPath == targetPath: 

470 return orig, n 

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

472 if ans: 

473 return ans 

474 return None 

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

476 def doMenus(self, p, kind, name, val): 

477 

478 c = self.c 

479 p = p.copy() 

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

481 after = p.nodeAfterTree() 

482 while p and p != after: 

483 self.debug_count += 1 

484 h = p.h 

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

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

487 if name: 

488 for z in aList: 

489 name2, junk, junk = z 

490 if name2 == name: 

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

492 break 

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

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

495 self.doItems(p, aList2) 

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

497 p.moveToNodeAfterTree() 

498 else: 

499 p.moveToThreadNext() 

500 else: 

501 p.moveToThreadNext() 

502 if self.localFlag: 

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

504 else: 

505 g.app.config.menusList = aList 

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

507 g.app.config.menusFileName = name 

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

509 def doItems(self, p, aList): 

510 

511 p = p.copy() 

512 after = p.nodeAfterTree() 

513 p.moveToThreadNext() 

514 while p and p != after: 

515 self.debug_count += 1 

516 h = p.h 

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

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

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

520 if itemName: 

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

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

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

524 # Only the first body line is significant. 

525 # This allows following comment lines. 

526 if tag == '@menu': 

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

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

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

530 aList.append((kind, aList2, body),) 

531 # #848: Body was None. 

532 p.moveToNodeAfterTree() 

533 break 

534 else: 

535 kind = tag 

536 head = itemName 

537 # Wrong: we must not clean non-unicode characters! 

538 # # Fix #1117: Similar to cleanButtonText in mod_scripting.py. 

539 # s = ''.join([ch if ch in chars else '' for ch in g.toUnicode(head)]) 

540 # head2 = s.replace('--', '-').lower() 

541 # aList.append((kind, head2, body),) 

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

543 p.moveToThreadNext() 

544 break 

545 else: 

546 p.moveToThreadNext() 

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

548 def doMode(self, p, kind, name, val): 

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

550 c = self.c 

551 name1 = name 

552 modeName = self.computeModeName(name) 

553 d = g.TypedDict( 

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

555 keyType=type('commandName'), 

556 valType=g.BindingInfo) 

557 s = p.b 

558 lines = g.splitLines(s) 

559 for line in lines: 

560 line = line.strip() 

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

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

563 if not name: 

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

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

566 elif bi is not None: 

567 # A regular shortcut. 

568 bi.pane = modeName 

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

570 # Important: use previous bindings if possible. 

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

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

573 if aList3: 

574 aList.extend(aList3) 

575 aList.append(bi) 

576 d[name] = aList 

577 # Restore the global shortcutsDict. 

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

579 self.createModeCommand(modeName, name1, d) 

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

581 def doOpenWith(self, p, kind, name, val): 

582 

583 d = self.parseOpenWith(p) 

584 d['name'] = name 

585 d['shortcut'] = val 

586 name = kind = 'openwithtable' 

587 self.openWithList.append(d) 

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

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

590 def doPopup(self, p, kind, name, val): 

591 """ 

592 Handle @popup menu items in @settings trees. 

593 """ 

594 popupName = name 

595 # popupType = val 

596 aList: List[Any] = [] 

597 p = p.copy() 

598 self.doPopupItems(p, aList) 

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

600 g.app.config.context_menus = {} 

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

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

603 def doPopupItems(self, p, aList): 

604 p = p.copy() 

605 after = p.nodeAfterTree() 

606 p.moveToThreadNext() 

607 while p and p != after: 

608 h = p.h 

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

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

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

612 if itemName: 

613 if tag == '@menu': 

614 aList2: List[Any] = [] 

615 kind = f"{itemName}" 

616 body = p.b 

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

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

619 p.moveToNodeAfterTree() 

620 break 

621 else: 

622 kind = tag 

623 head = itemName 

624 body = p.b 

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

626 p.moveToThreadNext() 

627 break 

628 else: 

629 p.moveToThreadNext() 

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

631 def doRatio(self, p, kind, name, val): 

632 try: 

633 val = float(val) 

634 if 0.0 <= val <= 1.0: 

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

636 else: 

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

638 except ValueError: 

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

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

641 def doShortcuts(self, p, kind, junk_name, junk_val, s=None): 

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

643 c, d = self.c, self.shortcutsDict 

644 if s is None: 

645 s = p.b 

646 fn = d.name() 

647 for line in g.splitLines(s): 

648 line = line.strip() 

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

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

651 if bi is None: # Fix #718. 

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

653 else: 

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

655 self.doOneShortcut(bi, commandName, p) 

656 else: 

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

658 if c.config.isLocalSettingsFile(): 

659 c.k.killedBindings.append(commandName) 

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

661 def doOneShortcut(self, bi, commandName, p): 

662 """Handle a regular shortcut.""" 

663 d = self.shortcutsDict 

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

665 aList.append(bi) 

666 d[commandName] = aList 

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

668 def doString(self, p, kind, name, val): 

669 # At present no checking is done. 

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

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

672 def doStrings(self, p, kind, name, val): 

673 """ 

674 We expect one of the following: 

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

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

677 """ 

678 name = name.strip() 

679 i = name.find('[') 

680 j = name.find(']') 

681 if -1 < i < j: 

682 items = name[i + 1 : j] 

683 items = items.split(',') 

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

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

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

687 # At present no checking is done. 

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

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

690 def munge(self, s): 

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

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

693 def oops(self): 

694 g.pr("ParserBaseClass oops:", 

695 g.callers(), 

696 "must be overridden in subclass") 

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

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

699 def parseFont(self, p): 

700 d: Dict[str, Any] = { 

701 'comments': [], 

702 'family': None, 

703 'size': None, 

704 'slant': None, 

705 'weight': None, 

706 } 

707 s = p.b 

708 lines = g.splitLines(s) 

709 for line in lines: 

710 self.parseFontLine(line, d) 

711 comments = d.get('comments') 

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

713 return d 

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

715 def parseFontLine(self, line, d): 

716 s = line.strip() 

717 if not s: 

718 return 

719 try: 

720 s = str(s) 

721 except UnicodeError: 

722 pass 

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

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

725 comments = d.get('comments') 

726 comments.append(s) 

727 d['comments'] = comments 

728 return 

729 # name is everything up to '=' 

730 i = s.find('=') 

731 if i == -1: 

732 name = s 

733 val = None 

734 else: 

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

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

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

738 if name.endswith(tag): 

739 kind = tag[1:] 

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

741 return 

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

743 def parseHeadline(self, s): 

744 """ 

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

746 Return (kind,name,val). 

747 Leo 4.11.1: Ignore everything after @data name. 

748 """ 

749 kind = name = val = None 

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

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

752 i = g.skip_ws(s, i) 

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

754 if kind: 

755 # name is everything up to '=' 

756 if kind == 'data': 

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

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

759 if j == -1: 

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

761 else: 

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

763 else: 

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

765 if j == -1: 

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

767 else: 

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

769 # val is everything after the '=' 

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

771 return kind, name, val 

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

773 def parseOpenWith(self, p): 

774 

775 d = {'command': None} 

776 # d contains args, kind, etc tags. 

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

778 self.parseOpenWithLine(line, d) 

779 return d 

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

781 def parseOpenWithLine(self, line, d): 

782 s = line.strip() 

783 if not s: 

784 return 

785 i = g.skip_ws(s, 0) 

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

787 return 

788 # try: 

789 # s = str(s) 

790 # except UnicodeError: 

791 # pass 

792 if 1: # new code 

793 j = g.skip_c_id(s, i) 

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

795 if not tag: 

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

797 return 

798 i = g.skip_ws(s, j) 

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

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

801 return 

802 i += 1 

803 val = s[i:].strip() or '' 

804 # An empty val is valid. 

805 if tag == 'arg': 

806 aList = d.get('args', []) 

807 aList.append(val) 

808 d['args'] = aList 

809 elif d.get(tag): 

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

811 else: 

812 d[tag] = val 

813 else: 

814 d['command'] = s 

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

816 def parseShortcutLine(self, kind, s): 

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

818 

819 --> entry-command 

820 settingName = shortcut 

821 settingName ! paneName = shortcut 

822 command-name --> mode-name = binding 

823 command-name --> same = binding 

824 """ 

825 # c = self.c 

826 s = s.replace('\x7f', '') 

827 # Can happen on MacOS. Very weird. 

828 name = val = nextMode = None 

829 nextMode = 'none' 

830 i = g.skip_ws(s, 0) 

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

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

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

834 entryCommandName = s[j:i] 

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

836 j = i 

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

838 name = s[j:i] 

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

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

841 if name.startswith(tag): 

842 name = name[len(tag) :] 

843 break 

844 if not name: 

845 return None, None 

846 # New in Leo 4.4b2. 

847 i = g.skip_ws(s, i) 

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

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

850 i = g.skip_id(s, j) 

851 nextMode = s[j:i] 

852 i = g.skip_ws(s, i) 

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

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

855 i = g.skip_id(s, j) 

856 pane = s[j:i] 

857 if not pane.strip(): 

858 pane = 'all' 

859 else: pane = 'all' 

860 i = g.skip_ws(s, i) 

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

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

863 val = s[i:] 

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

865 # Comments must be preceded by whitespace. 

866 if val: 

867 i = val.find('#') 

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

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

870 if not val: 

871 return name, None 

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

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

874 return name, bi 

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

876 def set(self, p, kind, name, val): 

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

878 c = self.c 

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

880 key = self.munge(name) 

881 if key is None: 

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

883 parent = p.parent() 

884 while parent: 

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

886 parent.moveToParent() 

887 return 

888 d = self.settingsDict 

889 gs = d.get(key) 

890 if gs: 

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

892 path = gs.path 

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

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

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

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

897 path=c.mFileName, 

898 tag='setting', 

899 unl=(p and p.get_UNL()), 

900 val=val, 

901 ) 

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

903 def traverse(self): 

904 """Traverse the entire settings tree.""" 

905 c = self.c 

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

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

908 keyType=type('settingName'), 

909 valType=g.GeneralSetting) 

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

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

912 keyType=str, 

913 valType=g.BindingInfo) 

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

915 p = c.config.settingsRoot() 

916 if not p: 

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

918 # This is not an error. 

919 return self.shortcutsDict, self.settingsDict 

920 after = p.nodeAfterTree() 

921 while p and p != after: 

922 result = self.visitNode(p) 

923 if result == "skip": 

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

925 p.moveToNodeAfterTree() 

926 else: 

927 p.moveToThreadNext() 

928 # Return the raw dict, unmerged. 

929 return self.shortcutsDict, self.settingsDict 

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

931 def valueError(self, p, kind, name, val): 

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

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

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

935 def visitNode(self, p): 

936 self.oops() 

937 #@-others 

938#@-<< class ParserBaseClass >> 

939#@+others 

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

941class ActiveSettingsOutline: 

942 

943 def __init__(self, c): 

944 

945 self.c = c 

946 self.start() 

947 self.create_outline() 

948 #@+others 

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

950 def start(self): 

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

952 # Copy settings. 

953 c = self.c 

954 settings = c.config.settingsDict 

955 shortcuts = c.config.shortcutsDict 

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

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

958 settings_copy = settings.copy() 

959 shortcuts_copy = shortcuts.copy() 

960 # Create the new commander. 

961 self.commander = self.new_commander() 

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

963 self.load_hidden_commanders() 

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

965 self.create_commanders_list() 

966 # Jam the old settings into the new commander. 

967 self.commander.config.settingsDict = settings_copy 

968 self.commander.config.shortcutsDict = shortcuts_copy 

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

970 def create_commanders_list(self): 

971 

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

973 lm = g.app.loadManager 

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

975 # "local_file", "theme_file", "myLeoSettings", "leoSettings" 

976 

977 self.commanders = [ 

978 ('leoSettings', lm.leo_settings_c), 

979 ('myLeoSettings', lm.my_settings_c), 

980 ] 

981 if lm.theme_c: 

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

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

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

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

986 def load_hidden_commanders(self): 

987 """ 

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

989 """ 

990 lm = g.app.loadManager 

991 lm.readGlobalSettingsFiles() 

992 # Make sure to reload the local file. 

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

994 fn = c.fileName() 

995 if fn: 

996 self.local_c = lm.openSettingsFile(fn) 

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

998 def new_commander(self): 

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

1000 lm = g.app.loadManager 

1001 old_c = self.c 

1002 # Save any changes so they can be seen. 

1003 if old_c.isChanged(): 

1004 old_c.save() 

1005 old_c.outerUpdate() 

1006 # From file-new... 

1007 g.app.disable_redraw = True 

1008 g.app.setLog(None) 

1009 g.app.lockLog() 

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

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

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

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

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

1015 if not old_c: 

1016 c.frame.setInitialWindowGeometry() 

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

1018 # g.app.restoreWindowState(c) 

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

1020 # From file-new... 

1021 g.app.unlockLog() 

1022 lm.createMenu(c) 

1023 lm.finishOpen(c) 

1024 g.app.writeWaitingLog(c) 

1025 c.setLog() 

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

1027 g.app.disable_redraw = False 

1028 return c 

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

1030 def create_outline(self): 

1031 """Create the summary outline""" 

1032 c = self.commander 

1033 # 

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

1035 root = c.rootPosition() 

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

1037 root.b = self.legend() 

1038 # 

1039 # Create all the inner settings outlines. 

1040 for kind, commander in self.commanders: 

1041 p = root.insertAfter() 

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

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

1044 self.create_inner_outline(commander, kind, p) 

1045 # 

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

1047 for v in c.all_nodes(): 

1048 v.clearDirty() 

1049 c.setChanged() 

1050 c.redraw() 

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

1052 def legend(self): 

1053 """Compute legend for self.c""" 

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

1055 legend = f'''\ 

1056 @language rest 

1057 

1058 legend: 

1059 

1060 leoSettings.leo 

1061 @ @button, @command, @mode 

1062 [D] default settings 

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

1064 [M] myLeoSettings.leo 

1065 ''' 

1066 if lm.theme_path: 

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

1068 return textwrap.dedent(legend) 

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

1070 def create_inner_outline(self, c, kind, root): 

1071 """ 

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

1073 """ 

1074 # Find the settings tree 

1075 settings_root = c.config.settingsRoot() 

1076 if not settings_root: 

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

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

1079 return 

1080 # Unify all settings. 

1081 self.create_unified_settings(kind, root, settings_root) 

1082 self.clean(root) 

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

1084 def create_unified_settings(self, kind, root, settings_root): 

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

1086 c = self.commander 

1087 lm = g.app.loadManager 

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

1089 valid_list = [ 

1090 '@bool', '@color', '@directory', '@encoding', 

1091 '@int', '@float', '@ratio', '@string', 

1092 ] 

1093 d = self.filter_settings(kind) 

1094 ignore, outline_data = None, None 

1095 self.parents = [root] 

1096 self.level = settings_root.level() 

1097 for p in settings_root.subtree(): 

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

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

1100 if ignore: 

1101 if p == ignore: 

1102 ignore = None 

1103 else: 

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

1105 continue 

1106 if outline_data: 

1107 if p == outline_data: 

1108 outline_data = None 

1109 else: 

1110 self.add(p) 

1111 continue 

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

1113 m = settings_pat.match(p.h) 

1114 if not m: 

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

1116 continue 

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

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

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

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

1121 val = d.get(key) 

1122 if isinstance(val, g.GeneralSetting): 

1123 self.add(p) 

1124 else: 

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

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

1127 if isinstance(val, g.GeneralSetting): 

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

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

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

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

1132 self.add(p) 

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

1134 continue 

1135 # Not a setting. Handle special cases. 

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

1137 ignore = p.nodeAfterTree() 

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

1139 outline_data = p.nodeAfterTree() 

1140 self.add(p) 

1141 else: 

1142 self.add(p) 

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

1144 def add(self, p, h=None): 

1145 """ 

1146 Add a node for p. 

1147 

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

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

1150 """ 

1151 if 0: 

1152 pad = ' ' * p.level() 

1153 print(pad, p.h) 

1154 p_level = p.level() 

1155 if p_level > self.level + 1: 

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

1157 return 

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

1159 self.parents.pop() 

1160 self.level -= 1 

1161 parent = self.parents[-1] 

1162 child = parent.insertAsLastChild() 

1163 child.h = h or p.h 

1164 child.b = p.b 

1165 self.parents.append(child) 

1166 self.level += 1 

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

1168 def clean(self, root): 

1169 """ 

1170 Remove all unnecessary nodes. 

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

1172 """ 

1173 self.clean_node(root) 

1174 

1175 def clean_node(self, p): 

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

1177 tag = 'ORG:' 

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

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

1180 self.clean_node(child) 

1181 if p.h.startswith(tag): 

1182 if p.hasChildren(): 

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

1184 else: 

1185 p.doDelete() 

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

1187 def filter_settings(self, target_kind): 

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

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

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

1191 c = self.commander 

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

1193 assert target_kind in valid_kinds, repr(target_kind) 

1194 d = c.config.settingsDict 

1195 result = {} 

1196 for key in d.keys(): 

1197 gs = d.get(key) 

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

1199 if not gs.kind: 

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

1201 continue 

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

1203 if kind == 'ignore': 

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

1205 continue 

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

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

1208 continue 

1209 if kind == target_kind: 

1210 result[key] = gs 

1211 return result 

1212 #@-others 

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

1214class GlobalConfigManager: 

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

1216 # Class data... 

1217 #@+<< gcm.defaultsDict >> 

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

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

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

1221 #@@c 

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

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

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

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

1226 defaultsDict = g.TypedDict( 

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

1228 keyType=str, 

1229 valType=g.GeneralSetting, 

1230 ) 

1231 defaultsData = ( 

1232 # compare options... 

1233 ("ignore_blank_lines", "bool", True), 

1234 ("limit_count", "int", 9), 

1235 ("print_mismatching_lines", "bool", True), 

1236 ("print_trailing_lines", "bool", True), 

1237 # find/change options... 

1238 ("search_body", "bool", True), 

1239 ("whole_word", "bool", True), 

1240 # Prefs panel. 

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

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

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

1244 ("page_width", "int", 132), 

1245 ("output_doc_chunks", "bool", True), 

1246 ("tangle_outputs_header", "bool", True), 

1247 # Syntax coloring options... 

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

1249 ("color_directives_in_plain_text", "bool", True), 

1250 ("underline_undefined_section_names", "bool", True), 

1251 # Window options... 

1252 ("body_pane_wraps", "bool", True), 

1253 ("body_text_font_family", "family", "Courier"), 

1254 ("body_text_font_size", "size", defaultBodyFontSize), 

1255 ("body_text_font_slant", "slant", "roman"), 

1256 ("body_text_font_weight", "weight", "normal"), 

1257 ("enable_drag_messages", "bool", True), 

1258 ("headline_text_font_family", "string", None), 

1259 ("headline_text_font_size", "size", defaultLogFontSize), 

1260 ("headline_text_font_slant", "slant", "roman"), 

1261 ("headline_text_font_weight", "weight", "normal"), 

1262 ("log_text_font_family", "string", None), 

1263 ("log_text_font_size", "size", defaultLogFontSize), 

1264 ("log_text_font_slant", "slant", "roman"), 

1265 ("log_text_font_weight", "weight", "normal"), 

1266 ("initial_window_height", "int", 600), 

1267 ("initial_window_width", "int", 800), 

1268 ("initial_window_left", "int", 10), 

1269 ("initial_window_top", "int", 10), 

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

1271 ("initial_vertical_ratio", "ratio", 0.5), 

1272 ("initial_horizontal_ratio", "ratio", 0.3), 

1273 ("initial_horizontal_secondary_ratio", "ratio", 0.5), 

1274 ("initial_vertical_secondary_ratio", "ratio", 0.7), 

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

1276 ("split_bar_color", "color", "LightSteelBlue2"), 

1277 ("split_bar_relief", "relief", "groove"), 

1278 ("split_bar_width", "int", 7), 

1279 ) 

1280 #@-<< gcm.defaultsDict >> 

1281 #@+<< gcm.encodingIvarsDict >> 

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

1283 encodingIvarsDict = g.TypedDict( 

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

1285 keyType=str, 

1286 valType=g.GeneralSetting, 

1287 ) 

1288 encodingIvarsData = ( 

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

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

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

1292 # Upper case for compatibility with previous versions. 

1293 # 

1294 # The defaultEncoding ivar is no longer used, 

1295 # so it doesn't override better defaults. 

1296 ) 

1297 #@-<< gcm.encodingIvarsDict >> 

1298 #@+<< gcm.ivarsDict >> 

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

1300 # Each of these settings sets the corresponding ivar. 

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

1302 ivarsDict = g.TypedDict( 

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

1304 keyType=str, 

1305 valType=g.GeneralSetting, 

1306 ) 

1307 ivarsData = ( 

1308 ("at_root_bodies_start_in_doc_mode", "bool", True), 

1309 # For compatibility with previous versions. 

1310 ("create_nonexistent_directories", "bool", False), 

1311 ("output_initial_comment", "string", ""), 

1312 # "" for compatibility with previous versions. 

1313 ("output_newline", "string", "nl"), 

1314 ("page_width", "int", "132"), 

1315 ("read_only", "bool", True), 

1316 ("redirect_execute_script_output_to_log_pane", "bool", False), 

1317 ("relative_path_base_directory", "string", "!"), 

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

1319 ("save_clears_undo_buffer", "bool", False), 

1320 ("stylesheet", "string", None), 

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

1322 ("target_language", "language", "python"), 

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

1324 ("trailing_body_newlines", "string", "asis"), 

1325 ("use_plugins", "bool", True), 

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

1327 ("undo_granularity", "string", "word"), 

1328 # "char","word","line","node" 

1329 ("write_strips_blank_lines", "bool", False), 

1330 ) 

1331 #@-<< gcm.ivarsDict >> 

1332 #@+others 

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

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

1335 def __init__(self): 

1336 # 

1337 # Set later. To keep pylint happy. 

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

1339 self.at_root_bodies_start_in_doc_mode = True 

1340 self.default_derived_file_encoding = 'utf-8' 

1341 self.output_newline = 'nl' 

1342 self.redirect_execute_script_output_to_log_pane = True 

1343 self.relative_path_base_directory = '!' 

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

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

1346 self.atCommonButtonsList = [] # List of info for common @buttons nodes. 

1347 self.atCommonCommandsList = [] # List of info for common @commands nodes. 

1348 self.atLocalButtonsList = [] # List of positions of @button nodes. 

1349 self.atLocalCommandsList = [] # List of positions of @command nodes. 

1350 self.buttonsFileName = '' 

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

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

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

1354 self.enabledPluginsFileName = None 

1355 self.enabledPluginsString = '' 

1356 self.inited = False 

1357 self.menusList = [] 

1358 self.menusFileName = '' 

1359 self.modeCommandsDict = g.TypedDict( 

1360 name='modeCommandsDict', 

1361 keyType=str, 

1362 valType=g.TypedDict) # was TypedDictOfLists. 

1363 # Inited later... 

1364 self.panes = None 

1365 self.sc = None 

1366 self.tree = None 

1367 self.initDicts() 

1368 self.initIvarsFromSettings() 

1369 self.initRecentFiles() 

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

1371 def initDicts(self): 

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

1373 self.dictList = [self.defaultsDict] 

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

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

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

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

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

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

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

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

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

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

1384 def initIvarsFromSettings(self): 

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

1386 self.initEncoding(ivar) 

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

1388 self.initIvar(ivar) 

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

1390 def initEncoding(self, key): 

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

1392 # Important: The key is munged. 

1393 gs = self.encodingIvarsDict.get(key) 

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

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

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

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

1398 def initIvar(self, key): 

1399 """ 

1400 Init g.app.config ivars during initialization. 

1401 

1402 This does NOT init the corresponding commander ivars. 

1403 

1404 Such initing must be done in setIvarsFromSettings. 

1405 """ 

1406 # Important: the key is munged. 

1407 d = self.ivarsDict 

1408 gs = d.get(key) 

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

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

1411 def initRecentFiles(self): 

1412 self.recentFiles = [] 

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

1414 def setIvarsFromSettings(self, c): 

1415 """ 

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

1417 

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

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

1420 """ 

1421 if g.app.loadedThemes: 

1422 return 

1423 if not self.inited: 

1424 return 

1425 # Ignore temporary commanders created by readSettingsFiles. 

1426 d = self.ivarsDict 

1427 keys = list(d.keys()) 

1428 keys.sort() 

1429 for key in keys: 

1430 gs = d.get(key) 

1431 if gs: 

1432 assert isinstance(gs, g.GeneralSetting) 

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

1434 kind = gs.kind 

1435 if c: 

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

1437 else: 

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

1439 if c: 

1440 setattr(c, ivar, val) 

1441 if True: # Always set the global ivars. 

1442 setattr(self, ivar, val) 

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

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

1445 def canonicalizeSettingName(self, name): 

1446 if name is None: 

1447 return None 

1448 name = name.lower() 

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

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

1451 return name if name else None 

1452 

1453 munge = canonicalizeSettingName 

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

1455 def exists(self, setting, kind): 

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

1457 lm = g.app.loadManager 

1458 d = lm.globalSettingsDict 

1459 if d: 

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

1461 return found 

1462 return False 

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

1464 def get(self, setting, kind): 

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

1466 lm = g.app.loadManager 

1467 # 

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

1469 d = lm.globalSettingsDict 

1470 if d: 

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

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

1473 return val 

1474 return None 

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

1476 def getValFromDict(self, d, setting, requestedType, warn=True): 

1477 """ 

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

1479 does not (loosely) match the actual type. 

1480 returns (val,exists) 

1481 """ 

1482 tag = 'gcm.getValFromDict' 

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

1484 if not gs: 

1485 return None, False 

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

1487 val = gs.val 

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

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

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

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

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

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

1494 if warn: 

1495 g.error( 

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

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

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

1499 return None, False 

1500 if isNone: 

1501 return '', True 

1502 # 2011/10/24: Exists, a *user-defined* empty value. 

1503 return val, True 

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

1505 def typesMatch(self, type1, type2): 

1506 """ 

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

1508 

1509 The following equivalences are allowed: 

1510 

1511 - None matches anything. 

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

1513 - Shortcut matches shortcuts. 

1514 """ 

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

1516 shortcuts = ('shortcut', 'shortcuts',) 

1517 if type1 in shortcuts or type2 in shortcuts: 

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

1519 return ( 

1520 type1 is None 

1521 or type2 is None 

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

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

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

1525 or (type1 in shortcuts and type2 in shortcuts) 

1526 or type1 == type2 

1527 ) 

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

1529 def getAbbrevDict(self): 

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

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

1532 return d or {} 

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

1534 def getBool(self, setting, default=None): 

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

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

1537 if val in (True, False): 

1538 return val 

1539 return default 

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

1541 def getButtons(self): 

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

1543 return g.app.config.atCommonButtonsList 

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

1545 def getColor(self, setting): 

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

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

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

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

1550 return col 

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

1552 def getCommonAtCommands(self): 

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

1554 return g.app.config.atCommonCommandsList 

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

1556 def getData(self, setting, strip_comments=True, strip_data=True): 

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

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

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

1560 if data and strip_comments: 

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

1562 if data and strip_data: 

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

1564 return data 

1565 

1566 def getOutlineData(self, setting): 

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

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

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

1570 def getDirectory(self, setting): 

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

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

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

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

1575 return theDir 

1576 return None 

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

1578 def getEnabledPlugins(self): 

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

1580 return g.app.config.enabledPluginsString 

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

1582 def getFloat(self, setting): 

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

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

1585 try: 

1586 val = float(val) 

1587 return val 

1588 except TypeError: 

1589 return None 

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

1591 def getFontFromParams(self, family, size, slant, weight, defaultSize=12): 

1592 """Compute a font from font parameters. 

1593 

1594 Arguments are the names of settings to be use. 

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

1596 

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

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

1599 if family in (None, ""): 

1600 family = self.defaultFontFamily 

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

1602 if size in (None, 0): 

1603 size = defaultSize 

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

1605 if slant in (None, ""): 

1606 slant = "roman" 

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

1608 if weight in (None, ""): 

1609 weight = "normal" 

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

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

1612 def getInt(self, setting): 

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

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

1615 try: 

1616 val = int(val) 

1617 return val 

1618 except TypeError: 

1619 return None 

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

1621 def getLanguage(self, setting): 

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

1623 language = self.getString(setting) 

1624 return language 

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

1626 def getMenusList(self): 

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

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

1629 # aList is typically empty. 

1630 return aList or g.app.config.menusList 

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

1632 def getOpenWith(self): 

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

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

1635 return val 

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

1637 def getRatio(self, setting): 

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

1639 

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

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

1642 try: 

1643 val = float(val) 

1644 if 0.0 <= val <= 1.0: 

1645 return val 

1646 except TypeError: 

1647 pass 

1648 return None 

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

1650 def getRecentFiles(self): 

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

1652 return self.recentFiles 

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

1654 def getString(self, setting): 

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

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

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

1658 def config_iter(self, c): 

1659 """Letters: 

1660 leoSettings.leo 

1661 D default settings 

1662 F loaded .leo File 

1663 M myLeoSettings.leo 

1664 @ @button, @command, @mode. 

1665 """ 

1666 lm = g.app.loadManager 

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

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

1669 if limit is None: 

1670 limit = 20 # A resonable default. 

1671 # pylint: disable=len-as-condition 

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

1673 gs = d.get(key) 

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

1675 if gs and gs.kind: 

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

1677 val = gs.val 

1678 if gs.kind == 'data': 

1679 # #748: Remove comments 

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

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

1682 if not aList: 

1683 val = '[]' 

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

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

1686 # The following doesn't work well. 

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

1688 else: 

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

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

1691 val = '<xml>' 

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

1693 yield key2, val, c, letter 

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

1695 def valueInMyLeoSettings(self, settingName): 

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

1697 lm = g.app.loadManager 

1698 d = lm.globalSettingsDict.d 

1699 gs = d.get(self.munge(settingName)) 

1700 # A GeneralSetting object. 

1701 if gs: 

1702 path = gs.path 

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

1704 return gs.val 

1705 return None 

1706 #@-others 

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

1708class LocalConfigManager: 

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

1710 #@+others 

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

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

1713 def __init__(self, c, previousSettings=None): 

1714 

1715 self.c = c 

1716 lm = g.app.loadManager 

1717 # 

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

1719 if previousSettings: 

1720 self.settingsDict = previousSettings.settingsDict 

1721 self.shortcutsDict = previousSettings.shortcutsDict 

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

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

1724 # was TypedDictOfLists. 

1725 else: 

1726 self.settingsDict = d1 = lm.globalSettingsDict 

1727 self.shortcutsDict = d2 = lm.globalBindingsDict 

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

1729 assert d2 is None or isinstance( 

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

1731 # Define these explicitly to eliminate a pylint warning. 

1732 if 0: 

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

1734 # both c and c.config ivars. 

1735 self.default_derived_file_encoding = g.app.config.default_derived_file_encoding 

1736 self.redirect_execute_script_output_to_log_pane = g.app.config.redirect_execute_script_output_to_log_pane 

1737 self.defaultBodyFontSize = g.app.config.defaultBodyFontSize 

1738 self.defaultLogFontSize = g.app.config.defaultLogFontSize 

1739 self.defaultMenuFontSize = g.app.config.defaultMenuFontSize 

1740 self.defaultTreeFontSize = g.app.config.defaultTreeFontSize 

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

1742 self.initEncoding(key) 

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

1744 self.initIvar(key) 

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

1746 def initEncoding(self, key): 

1747 # Important: the key is munged. 

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

1749 encodingName = gs.ivar 

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

1751 # Use the global setting as a last resort. 

1752 if encoding: 

1753 setattr(self, encodingName, encoding) 

1754 else: 

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

1756 setattr(self, encodingName, encoding) 

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

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

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

1760 def initIvar(self, key): 

1761 

1762 c = self.c 

1763 # Important: the key is munged. 

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

1765 ivarName = gs.ivar 

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

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

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

1769 setattr(self, ivarName, val) 

1770 setattr(c, ivarName, val) 

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

1772 def createActivesSettingsOutline(self): 

1773 """ 

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

1775 

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

1777 

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

1779 """ 

1780 ActiveSettingsOutline(self.c) 

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

1782 def getSource(self, setting): 

1783 """ 

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

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

1786 """ 

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

1788 return "error" 

1789 try: 

1790 path = setting.path 

1791 except Exception: 

1792 return "error" 

1793 if not path: 

1794 return "local_file" 

1795 path = path.lower() 

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

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

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

1799 theme_path = g.app.loadManager.theme_path 

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

1801 return "theme_file" 

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

1803 return 'ignore' 

1804 return "local_file" 

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

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

1807 # This was not used prior to Leo 4.5. 

1808 

1809 def findSettingsPosition(self, setting): 

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

1811 munge = g.app.config.munge 

1812 # c = self.c 

1813 root = self.settingsRoot() 

1814 if not root: 

1815 return None 

1816 setting = munge(setting) 

1817 for p in root.subtree(): 

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

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

1820 if h.startswith(setting): 

1821 return p.copy() 

1822 return None 

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

1824 def settingsRoot(self): 

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

1826 c = self.c 

1827 for p in c.all_unique_positions(): 

1828 # #1792: Allow comments after @settings. 

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

1830 return p.copy() 

1831 return None 

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

1833 #@@nocolor-node 

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

1835 # get (self,setting,theType) 

1836 # getAbbrevDict (self) 

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

1838 # getButtons (self) 

1839 # getColor (self,setting) 

1840 # getData (self,setting) 

1841 # getDirectory (self,setting) 

1842 # getFloat (self,setting) 

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

1844 # getInt (self,setting) 

1845 # getLanguage (self,setting) 

1846 # getMenusList (self) 

1847 # getOutlineData (self) 

1848 # getOpenWith (self) 

1849 # getRatio (self,setting) 

1850 # getShortcut (self,commandName) 

1851 # getString (self,setting) 

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

1853 def get(self, setting, kind): 

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

1855 d = self.settingsDict 

1856 if d: 

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

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

1859 return val 

1860 return None 

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

1862 def getValFromDict(self, d, setting, requestedType, warn=True): 

1863 """ 

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

1865 does not (loosely) match the actual type. 

1866 returns (val,exists) 

1867 """ 

1868 tag = 'c.config.getValFromDict' 

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

1870 if not gs: 

1871 return None, False 

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

1873 val = gs.val 

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

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

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

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

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

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

1880 if warn: 

1881 g.error( 

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

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

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

1885 return None, False 

1886 if isNone: 

1887 return '', True 

1888 # 2011/10/24: Exists, a *user-defined* empty value. 

1889 return val, True 

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

1891 def typesMatch(self, type1, type2): 

1892 """ 

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

1894 

1895 The following equivalences are allowed: 

1896 

1897 - None matches anything. 

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

1899 - Shortcut matches shortcuts. 

1900 """ 

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

1902 shortcuts = ('shortcut', 'shortcuts',) 

1903 if type1 in shortcuts or type2 in shortcuts: 

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

1905 return ( 

1906 type1 is None 

1907 or type2 is None 

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

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

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

1911 or (type1 in shortcuts and type2 in shortcuts) 

1912 or type1 == type2 

1913 ) 

1914 #@+node:ekr.20120215072959.12522: *5* c.config.getAbbrevDict 

1915 def getAbbrevDict(self): 

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

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

1918 return d or {} 

1919 #@+node:ekr.20120215072959.12523: *5* c.config.getBool 

1920 def getBool(self, setting, default=None): 

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

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

1923 if val in (True, False): 

1924 return val 

1925 return default 

1926 #@+node:ekr.20120215072959.12525: *5* c.config.getColor 

1927 def getColor(self, setting): 

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

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

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

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

1932 return col 

1933 #@+node:ekr.20120215072959.12527: *5* c.config.getData 

1934 def getData(self, setting, strip_comments=True, strip_data=True): 

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

1936 # 904: Add local abbreviations to global settings. 

1937 append = setting == 'global-abbreviations' 

1938 if append: 

1939 data0 = g.app.config.getData(setting, 

1940 strip_comments=strip_comments, 

1941 strip_data=strip_data, 

1942 ) 

1943 data = self.get(setting, "data") 

1944 # New in Leo 4.11: parser.doData strips only comments now. 

1945 # New in Leo 4.12: parser.doData strips *nothing*. 

1946 if isinstance(data, str): 

1947 data = [data] 

1948 if data and strip_comments: 

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

1950 if data and strip_data: 

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

1952 if append and data != data0: 

1953 if data: 

1954 data.extend(data0) 

1955 else: 

1956 data = data0 

1957 return data 

1958 #@+node:ekr.20131114051702.16542: *5* c.config.getOutlineData 

1959 def getOutlineData(self, setting): 

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

1961 data = self.get(setting, "outlinedata") 

1962 if setting == 'tree-abbreviations': 

1963 # 904: Append local tree abbreviations to the global abbreviations. 

1964 data0 = g.app.config.getOutlineData(setting) 

1965 if data and data0 and data != data0: 

1966 assert isinstance(data0, str) 

1967 assert isinstance(data, str) 

1968 # We can't merge the data here: they are .leo files! 

1969 # abbrev.init_tree_abbrev_helper does the merge. 

1970 data = [data0, data] 

1971 return data 

1972 #@+node:ekr.20120215072959.12528: *5* c.config.getDirectory 

1973 def getDirectory(self, setting): 

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

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

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

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

1978 return theDir 

1979 return None 

1980 #@+node:ekr.20120215072959.12530: *5* c.config.getFloat 

1981 def getFloat(self, setting): 

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

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

1984 try: 

1985 val = float(val) 

1986 return val 

1987 except TypeError: 

1988 return None 

1989 #@+node:ekr.20120215072959.12531: *5* c.config.getFontFromParams 

1990 def getFontFromParams(self, family, size, slant, weight, defaultSize=12): 

1991 """ 

1992 Compute a font from font parameters. This should be used *only* 

1993 by the syntax coloring code. Otherwise, use Leo's style sheets. 

1994 

1995 Arguments are the names of settings to be use. 

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

1997 

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

1999 """ 

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

2001 if family in (None, ""): 

2002 family = g.app.config.defaultFontFamily 

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

2004 if size in (None, 0): 

2005 size = defaultSize 

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

2007 if slant in (None, ""): 

2008 slant = "roman" 

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

2010 if weight in (None, ""): 

2011 weight = "normal" 

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

2013 #@+node:ekr.20120215072959.12532: *5* c.config.getInt 

2014 def getInt(self, setting): 

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

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

2017 try: 

2018 val = int(val) 

2019 return val 

2020 except TypeError: 

2021 return None 

2022 #@+node:ekr.20120215072959.12533: *5* c.config.getLanguage 

2023 def getLanguage(self, setting): 

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

2025 language = self.getString(setting) 

2026 return language 

2027 #@+node:ekr.20120215072959.12534: *5* c.config.getMenusList 

2028 def getMenusList(self): 

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

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

2031 # aList is typically empty. 

2032 return aList or g.app.config.menusList 

2033 #@+node:ekr.20120215072959.12535: *5* c.config.getOpenWith 

2034 def getOpenWith(self): 

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

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

2037 return val 

2038 #@+node:ekr.20120215072959.12536: *5* c.config.getRatio 

2039 def getRatio(self, setting): 

2040 """ 

2041 Return the value of @float setting. 

2042 

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

2044 """ 

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

2046 try: 

2047 val = float(val) 

2048 if 0.0 <= val <= 1.0: 

2049 return val 

2050 except TypeError: 

2051 pass 

2052 return None 

2053 #@+node:ekr.20120215072959.12538: *5* c.config.getSettingSource 

2054 def getSettingSource(self, setting): 

2055 """return the name of the file responsible for setting.""" 

2056 d = self.settingsDict 

2057 if d: 

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

2059 bi = d.get(setting) 

2060 if bi is None: 

2061 return 'unknown setting', None 

2062 return bi.path, bi.val 

2063 # 

2064 # lm.readGlobalSettingsFiles is opening a settings file. 

2065 # lm.readGlobalSettingsFiles has not yet set lm.globalSettingsDict. 

2066 assert d is None 

2067 return None 

2068 #@+node:ekr.20120215072959.12539: *5* c.config.getShortcut 

2069 no_menu_dict: Dict[Cmdr, bool] = {} 

2070 

2071 def getShortcut(self, commandName): 

2072 """Return rawKey,accel for shortcutName""" 

2073 c = self.c 

2074 d = self.shortcutsDict 

2075 if not c.frame.menu: 

2076 if c not in self.no_menu_dict: 

2077 self.no_menu_dict[c] = True 

2078 g.trace(f"no menu: {c.shortFileName()}:{commandName}") 

2079 return None, [] 

2080 if d: 

2081 assert isinstance(d, g.TypedDict), repr(d) # was TypedDictOfLists. 

2082 key = c.frame.menu.canonicalizeMenuName(commandName) 

2083 key = key.replace('&', '') # Allow '&' in names. 

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

2085 if aList: # A list of g.BindingInfo objects. 

2086 # It's important to filter empty strokes here. 

2087 aList = [z for z in aList 

2088 if z.stroke and z.stroke.lower() != 'none'] 

2089 return key, aList 

2090 # 

2091 # lm.readGlobalSettingsFiles is opening a settings file. 

2092 # lm.readGlobalSettingsFiles has not yet set lm.globalSettingsDict. 

2093 return None, [] 

2094 #@+node:ekr.20120215072959.12540: *5* c.config.getString 

2095 def getString(self, setting): 

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

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

2098 #@+node:ekr.20120215072959.12543: *4* c.config.Getters: redirect to g.app.config 

2099 def getButtons(self): 

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

2101 return g.app.config.atCommonButtonsList # unusual. 

2102 

2103 def getCommands(self): 

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

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

2106 

2107 def getEnabledPlugins(self): 

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

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

2110 

2111 def getRecentFiles(self): 

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

2113 return g.app.config.getRecentFiles() # unusual 

2114 #@+node:ekr.20140114145953.16691: *4* c.config.isLocalSetting 

2115 def isLocalSetting(self, setting, kind): 

2116 """Return True if the indicated setting comes from a local .leo file.""" 

2117 if not kind or kind in ('shortcut', 'shortcuts', 'openwithtable'): 

2118 return False 

2119 key = g.app.config.munge(setting) 

2120 if key is None: 

2121 return False 

2122 if not self.settingsDict: 

2123 return False 

2124 gs = self.settingsDict.get(key) 

2125 if not gs: 

2126 return False 

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

2128 path = gs.path.lower() 

2129 for fn in ('myLeoSettings.leo', 'leoSettings.leo'): 

2130 if path.endswith(fn.lower()): 

2131 return False 

2132 return True 

2133 #@+node:ekr.20171119222458.1: *4* c.config.isLocalSettingsFile 

2134 def isLocalSettingsFile(self): 

2135 """Return true if c is not leoSettings.leo or myLeoSettings.leo""" 

2136 c = self.c 

2137 fn = c.shortFileName().lower() 

2138 for fn2 in ('leoSettings.leo', 'myLeoSettings.leo'): 

2139 if fn.endswith(fn2.lower()): 

2140 return False 

2141 return True 

2142 #@+node:ekr.20120224140548.10528: *4* c.exists 

2143 def exists(self, c, setting, kind): 

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

2145 d = self.settingsDict 

2146 if d: 

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

2148 if found: 

2149 return True 

2150 return False 

2151 #@+node:ekr.20070418073400: *3* c.config.printSettings 

2152 def printSettings(self): 

2153 """Prints the value of every setting, except key bindings and commands and open-with tables. 

2154 The following shows where the active setting came from: 

2155 

2156 - leoSettings.leo, 

2157 - @ @button, @command, @mode. 

2158 - [D] default settings. 

2159 - [F] indicates the file being loaded, 

2160 - [M] myLeoSettings.leo, 

2161 - [T] theme .leo file. 

2162 """ 

2163 legend = '''\ 

2164 legend: 

2165 leoSettings.leo 

2166 @ @button, @command, @mode 

2167 [D] default settings 

2168 [F] loaded .leo File 

2169 [M] myLeoSettings.leo 

2170 [T] theme .leo file. 

2171 ''' 

2172 c = self.c 

2173 legend = textwrap.dedent(legend) 

2174 result = [] 

2175 for name, val, c, letter in g.app.config.config_iter(c): 

2176 kind = ' ' if letter == ' ' else f"[{letter}]" 

2177 result.append(f"{kind} {name} = {val}\n") 

2178 # Use a single g.es statement. 

2179 result.append('\n' + legend) 

2180 if g.unitTesting: 

2181 pass # print(''.join(result)) 

2182 else: 

2183 g.es_print('', ''.join(result), tabName='Settings') 

2184 #@+node:ekr.20120215072959.12475: *3* c.config.set 

2185 def set(self, p, kind, name, val, warn=True): 

2186 """ 

2187 Init the setting for name to val. 

2188 

2189 The "p" arg is not used. 

2190 """ 

2191 c = self.c 

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

2193 key = g.app.config.munge(name) 

2194 d = self.settingsDict 

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

2196 gs = d.get(key) 

2197 if gs: 

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

2199 path = gs.path 

2200 if warn and g.os_path_finalize( 

2201 c.mFileName) != g.os_path_finalize(path): # #1341. 

2202 g.es("over-riding setting:", name, "from", path) 

2203 d[key] = g.GeneralSetting(kind, path=c.mFileName, val=val, tag='setting') 

2204 #@+node:ekr.20190905082644.1: *3* c.config.settingIsActiveInPath 

2205 def settingIsActiveInPath(self, gs, target_path): 

2206 """Return True if settings file given by path actually defines the setting, gs.""" 

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

2208 return gs.path == target_path 

2209 #@+node:ekr.20180121135120.1: *3* c.config.setUserSetting 

2210 def setUserSetting(self, setting, value): 

2211 """ 

2212 Find and set the indicated setting, either in the local file or in 

2213 myLeoSettings.leo. 

2214 """ 

2215 c = self.c 

2216 fn = g.shortFileName(c.fileName()) 

2217 p = self.findSettingsPosition(setting) 

2218 if not p: 

2219 c = c.openMyLeoSettings() 

2220 if not c: 

2221 return 

2222 fn = 'myLeoSettings.leo' 

2223 p = c.config.findSettingsPosition(setting) 

2224 if not p: 

2225 root = c.config.settingsRoot() 

2226 if not root: 

2227 return 

2228 fn = 'leoSettings.leo' 

2229 p = c.config.findSettingsPosition(setting) 

2230 if not p: 

2231 p = root.insertAsLastChild() 

2232 h = setting 

2233 i = h.find('=') 

2234 if i > -1: 

2235 h = h[:i].strip() 

2236 p.h = f"{h} = {value}" 

2237 print(f"Updated `{setting}` in {fn}") # #2390. 

2238 # 

2239 # Delay the second redraw until idle time. 

2240 c.setChanged() 

2241 p.setDirty() 

2242 c.redraw_later() 

2243 #@-others 

2244#@+node:ekr.20041119203941.3: ** class SettingsTreeParser (ParserBaseClass) 

2245class SettingsTreeParser(ParserBaseClass): 

2246 """A class that inits settings found in an @settings tree. 

2247 

2248 Used by read settings logic.""" 

2249 

2250 # def __init__(self, c, localFlag=True): 

2251 # super().__init__(c, localFlag) 

2252 #@+others 

2253 #@+node:ekr.20041119204103: *3* ctor (SettingsTreeParser) 

2254 #@+node:ekr.20041119204714: *3* visitNode (SettingsTreeParser) 

2255 def visitNode(self, p): 

2256 """Init any settings found in node p.""" 

2257 p = p.copy() 

2258 # Bug fix 2011/11/24 

2259 # Ensure inner traversals don't change callers's p. 

2260 munge = g.app.config.munge 

2261 kind, name, val = self.parseHeadline(p.h) 

2262 kind = munge(kind) 

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

2264 if kind is None: # Not an @x node. (New in Leo 4.4.4) 

2265 pass 

2266 elif kind == "settings": 

2267 pass 

2268 elif kind in self.basic_types and isNone: 

2269 # None is valid for all basic types. 

2270 self.set(p, kind, name, None) 

2271 elif kind in self.control_types or kind in self.basic_types: 

2272 f = self.dispatchDict.get(kind) 

2273 if f: 

2274 try: 

2275 return f(p, kind, name, val) # type:ignore 

2276 except Exception: 

2277 g.es_exception() 

2278 else: 

2279 g.pr("*** no handler", kind) 

2280 return None 

2281 #@-others 

2282#@+node:ekr.20171229131953.1: ** parseFont (leoConfig.py) 

2283def parseFont(b): 

2284 family = None 

2285 weight = None 

2286 slant = None 

2287 size = None 

2288 settings_name = None 

2289 for line in g.splitLines(b): 

2290 line = line.strip() 

2291 if line.startswith('#'): 

2292 continue 

2293 i = line.find('=') 

2294 if i < 0: 

2295 continue 

2296 name = line[:i].strip() 

2297 if name.endswith('_family'): 

2298 family = line[i + 1 :].strip() 

2299 elif name.endswith('_weight'): 

2300 weight = line[i + 1 :].strip() 

2301 elif name.endswith('_size'): 

2302 size = line[i + 1 :].strip() 

2303 try: 

2304 size = float(size) # type:ignore 

2305 except ValueError: 

2306 size = 12 # type:ignore 

2307 elif name.endswith('_slant'): 

2308 slant = line[i + 1 :].strip() 

2309 if settings_name is None and name.endswith( 

2310 ('_family', '_slant', '_weight', '_size')): 

2311 settings_name = name.rsplit('_', 1)[0] 

2312 return settings_name, family, weight == 'bold', slant in ('slant', 'italic'), size 

2313#@-others 

2314#@@language python 

2315#@@tabwidth -4 

2316#@@pagewidth 70 

2317#@-leo