Coverage for C:\leo.repo\leo-editor\leo\core\leoChapters.py: 58%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

327 statements  

1#@+leo-ver=5-thin 

2#@+node:ekr.20070317085508.1: * @file leoChapters.py 

3"""Classes that manage chapters in Leo's core.""" 

4import re 

5import string 

6from leo.core import leoGlobals as g 

7#@+others 

8#@+node:ekr.20150509030349.1: ** cc.cmd (decorator) 

9def cmd(name): 

10 """Command decorator for the ChapterController class.""" 

11 return g.new_cmd_decorator(name, ['c', 'chapterController',]) 

12#@+node:ekr.20070317085437: ** class ChapterController 

13class ChapterController: 

14 """A per-commander controller that manages chapters and related nodes.""" 

15 #@+others 

16 #@+node:ekr.20070530075604: *3* Birth 

17 #@+node:ekr.20070317085437.2: *4* cc.ctor 

18 def __init__(self, c): 

19 """Ctor for ChapterController class.""" 

20 self.c = c 

21 # Note: chapter names never change, even if their @chapter node changes. 

22 self.chaptersDict = {} # Keys are chapter names, values are chapters. 

23 self.initing = True # #31: True: suppress undo when creating chapters. 

24 self.re_chapter = None # Set where used. 

25 self.selectedChapter = None 

26 self.selectChapterLockout = False # True: cc.selectChapterForPosition does nothing. 

27 self.tt = None # May be set in finishCreate. 

28 self.reloadSettings() 

29 

30 def reloadSettings(self): 

31 c = self.c 

32 self.use_tabs = c.config.getBool('use-chapter-tabs') 

33 #@+node:ekr.20160402024827.1: *4* cc.createIcon 

34 def createIcon(self): 

35 """Create chapter-selection Qt ListBox in the icon area.""" 

36 cc = self 

37 c = cc.c 

38 if cc.use_tabs: 

39 if hasattr(c.frame.iconBar, 'createChaptersIcon'): 

40 if not cc.tt: 

41 cc.tt = c.frame.iconBar.createChaptersIcon() 

42 #@+node:ekr.20070325104904: *4* cc.finishCreate 

43 # This must be called late in the init process, after the first redraw. 

44 

45 def finishCreate(self): 

46 """Create the box in the icon area.""" 

47 c, cc = self.c, self 

48 cc.createIcon() 

49 cc.setAllChapterNames() 

50 # Create all chapters. 

51 # #31. 

52 cc.initing = False 

53 # Always select the main chapter. 

54 # It can be alarming to open a small chapter in a large .leo file. 

55 cc.selectChapterByName('main') 

56 c.redraw() 

57 #@+node:ekr.20160411145155.1: *4* cc.makeCommand 

58 def makeCommand(self, chapterName, binding=None): 

59 """Make chapter-select-<chapterName> command.""" 

60 c, cc = self.c, self 

61 commandName = f"chapter-select-{chapterName}" 

62 # 

63 # For tracing: 

64 # inverseBindingsDict = c.k.computeInverseBindingDict() 

65 if commandName in c.commandsDict: 

66 return 

67 

68 def select_chapter_callback(event, cc=cc, name=chapterName): 

69 chapter = cc.chaptersDict.get(name) 

70 if chapter: 

71 try: 

72 cc.selectChapterLockout = True 

73 cc.selectChapterByNameHelper(chapter, collapse=True) 

74 c.redraw(chapter.p) # 2016/04/20. 

75 finally: 

76 cc.selectChapterLockout = False 

77 elif not g.unitTesting: 

78 # Possible, but not likely. 

79 cc.note(f"no such chapter: {name}") 

80 

81 # Always bind the command without a shortcut. 

82 # This will create the command bound to any existing settings. 

83 

84 bindings = (None, binding) if binding else (None,) 

85 for shortcut in bindings: 

86 c.k.registerCommand(commandName, select_chapter_callback, shortcut=shortcut) 

87 #@+node:ekr.20070604165126: *3* cc.selectChapter 

88 @cmd('chapter-select') 

89 def selectChapter(self, event=None): 

90 """Use the minibuffer to get a chapter name, then create the chapter.""" 

91 cc, k = self, self.c.k 

92 names = cc.setAllChapterNames() 

93 g.es('Chapters:\n' + '\n'.join(names)) 

94 k.setLabelBlue('Select chapter: ') 

95 k.get1Arg(event, handler=self.selectChapter1, tabList=names) 

96 

97 def selectChapter1(self, event): 

98 cc, k = self, self.c.k 

99 k.clearState() 

100 k.resetLabel() 

101 if k.arg: 

102 cc.selectChapterByName(k.arg) 

103 #@+node:ekr.20170202061705.1: *3* cc.selectNext/Back 

104 @cmd('chapter-back') 

105 def backChapter(self, event=None): 

106 cc = self 

107 names = sorted(cc.setAllChapterNames()) 

108 sel_name = cc.selectedChapter.name if cc.selectedChapter else 'main' 

109 i = names.index(sel_name) 

110 new_name = names[i - 1 if i > 0 else len(names) - 1] 

111 cc.selectChapterByName(new_name) 

112 

113 @cmd('chapter-next') 

114 def nextChapter(self, event=None): 

115 cc = self 

116 names = sorted(cc.setAllChapterNames()) 

117 sel_name = cc.selectedChapter.name if cc.selectedChapter else 'main' 

118 i = names.index(sel_name) 

119 new_name = names[i + 1 if i + 1 < len(names) else 0] 

120 cc.selectChapterByName(new_name) 

121 #@+node:ekr.20070317130250: *3* cc.selectChapterByName & helper 

122 def selectChapterByName(self, name): 

123 """Select a chapter without redrawing.""" 

124 cc = self 

125 if self.selectChapterLockout: 

126 return 

127 if isinstance(name, int): 

128 cc.note('PyQt5 chapters not supported') 

129 return 

130 chapter = cc.getChapter(name) 

131 if not chapter: 

132 if not g.unitTesting: 

133 g.es_print(f"no such @chapter node: {name}") 

134 return 

135 try: 

136 cc.selectChapterLockout = True 

137 cc.selectChapterByNameHelper(chapter) 

138 finally: 

139 cc.selectChapterLockout = False 

140 #@+node:ekr.20090306060344.2: *4* cc.selectChapterByNameHelper 

141 def selectChapterByNameHelper(self, chapter, collapse=True): 

142 """Select the chapter.""" 

143 cc, c = self, self.c 

144 if not cc.selectedChapter and chapter.name == 'main': 

145 chapter.p = c.p 

146 return 

147 if chapter == cc.selectedChapter: 

148 chapter.p = c.p 

149 return 

150 if cc.selectedChapter: 

151 cc.selectedChapter.unselect() 

152 else: 

153 main_chapter = cc.getChapter('main') 

154 if main_chapter: 

155 main_chapter.unselect() 

156 if chapter.p and c.positionExists(chapter.p): 

157 pass 

158 elif chapter.name == 'main': 

159 pass # Do not use c.p. 

160 else: 

161 chapter.p = chapter.findRootNode() 

162 chapter.select() 

163 c.contractAllHeadlines() 

164 chapter.p.v.expand() 

165 c.selectPosition(chapter.p) 

166 #@+node:ekr.20070317130648: *3* cc.Utils 

167 #@+node:ekr.20070320085610: *4* cc.error/note/warning 

168 def error(self, s): 

169 g.error(f"Error: {s}") 

170 

171 def note(self, s, killUnitTest=False): 

172 if g.unitTesting: 

173 if 0: # To trace cause of failed unit test. 

174 g.trace('=====', s, g.callers()) 

175 if killUnitTest: 

176 assert False, s 

177 else: 

178 g.note(f"Note: {s}") 

179 

180 def warning(self, s): 

181 g.es_print(f"Warning: {s}") 

182 #@+node:ekr.20160402025448.1: *4* cc.findAnyChapterNode 

183 def findAnyChapterNode(self): 

184 """Return True if the outline contains any @chapter node.""" 

185 cc = self 

186 for p in cc.c.all_unique_positions(): 

187 if p.h.startswith('@chapter '): 

188 return True 

189 return False 

190 #@+node:ekr.20071028091719: *4* cc.findChapterNameForPosition 

191 def findChapterNameForPosition(self, p): 

192 """Return the name of a chapter containing p or None if p does not exist.""" 

193 cc, c = self, self.c 

194 if not p or not c.positionExists(p): 

195 return None 

196 for name in cc.chaptersDict: 

197 if name != 'main': 

198 theChapter = cc.chaptersDict.get(name) 

199 if theChapter.positionIsInChapter(p): 

200 return name 

201 return 'main' 

202 #@+node:ekr.20070325093617: *4* cc.findChapterNode 

203 def findChapterNode(self, name): 

204 """ 

205 Return the position of the first @chapter node with the given name 

206 anywhere in the entire outline. 

207 

208 All @chapter nodes are created as children of the @chapters node, 

209 but users may move them anywhere. 

210 """ 

211 cc = self 

212 name = g.checkUnicode(name) 

213 for p in cc.c.all_positions(): 

214 chapterName, binding = self.parseHeadline(p) 

215 if chapterName == name: 

216 return p 

217 return None # Not an error. 

218 #@+node:ekr.20070318124004: *4* cc.getChapter 

219 def getChapter(self, name): 

220 cc = self 

221 return cc.chaptersDict.get(name) 

222 #@+node:ekr.20070318122708: *4* cc.getSelectedChapter 

223 def getSelectedChapter(self): 

224 cc = self 

225 return cc.selectedChapter 

226 #@+node:ekr.20070605124356: *4* cc.inChapter 

227 def inChapter(self): 

228 cc = self 

229 theChapter = cc.getSelectedChapter() 

230 return theChapter and theChapter.name != 'main' 

231 #@+node:ekr.20160411152842.1: *4* cc.parseHeadline 

232 def parseHeadline(self, p): 

233 """Return the chapter name and key binding for p.h.""" 

234 if not self.re_chapter: 

235 self.re_chapter = re.compile( 

236 r'^@chapter\s+([^@]+)\s*(@key\s*=\s*(.+)\s*)?') 

237 # @chapter (all up to @) (@key=(binding))? 

238 # name=group(1), binding=group(3) 

239 m = self.re_chapter.search(p.h) 

240 if m: 

241 chapterName, binding = m.group(1), m.group(3) 

242 if chapterName: 

243 chapterName = self.sanitize(chapterName) 

244 if binding: 

245 binding = binding.strip() 

246 else: 

247 chapterName = binding = None 

248 return chapterName, binding 

249 #@+node:ekr.20160414183716.1: *4* cc.sanitize 

250 def sanitize(self, s): 

251 """Convert s to a safe chapter name.""" 

252 # Similar to g.sanitize_filename, but simpler. 

253 result = [] 

254 for ch in s.strip(): 

255 # pylint: disable=superfluous-parens 

256 if ch in (string.ascii_letters + string.digits): 

257 result.append(ch) 

258 elif ch in ' \t': 

259 result.append('-') 

260 s = ''.join(result) 

261 s = s.replace('--', '-') 

262 return s[:128] 

263 #@+node:ekr.20070615075643: *4* cc.selectChapterForPosition 

264 def selectChapterForPosition(self, p, chapter=None): 

265 """ 

266 Select a chapter containing position p. 

267 New in Leo 4.11: prefer the given chapter if possible. 

268 Do nothing if p if p does not exist or is in the presently selected chapter. 

269 

270 Note: this code calls c.redraw() if the chapter changes. 

271 """ 

272 c, cc = self.c, self 

273 # New in Leo 4.11 

274 if cc.selectChapterLockout: 

275 return 

276 selChapter = cc.getSelectedChapter() 

277 if not chapter and not selChapter: 

278 return 

279 if not p: 

280 return 

281 if not c.positionExists(p): 

282 return 

283 # New in Leo 4.11: prefer the given chapter if possible. 

284 theChapter = chapter or selChapter 

285 if not theChapter: 

286 return 

287 # First, try the presently selected chapter. 

288 firstName = theChapter.name 

289 if firstName == 'main': 

290 return 

291 if theChapter.positionIsInChapter(p): 

292 cc.selectChapterByName(theChapter.name) 

293 return 

294 for name in cc.chaptersDict: 

295 if name not in (firstName, 'main'): 

296 theChapter = cc.chaptersDict.get(name) 

297 if theChapter.positionIsInChapter(p): 

298 cc.selectChapterByName(name) 

299 break 

300 else: 

301 cc.selectChapterByName('main') 

302 # Fix bug 869385: Chapters make the nav_qt.py plugin useless 

303 assert not self.selectChapterLockout 

304 # New in Leo 5.6: don't call c.redraw immediately. 

305 c.redraw_later() 

306 #@+node:ekr.20130915052002.11289: *4* cc.setAllChapterNames 

307 def setAllChapterNames(self): 

308 """Called early and often to discover all chapter names.""" 

309 c, cc = self.c, self 

310 # sel_name = cc.selectedChapter and cc.selectedChapter.name or 'main' 

311 if 'main' not in cc.chaptersDict: 

312 cc.chaptersDict['main'] = Chapter(c, cc, 'main') 

313 cc.makeCommand('main') 

314 # This binds any existing bindings to chapter-select-main. 

315 result, seen = ['main'], set() 

316 for p in c.all_unique_positions(): 

317 chapterName, binding = self.parseHeadline(p) 

318 if chapterName and p.v not in seen: 

319 seen.add(p.v) 

320 result.append(chapterName) 

321 if chapterName not in cc.chaptersDict: 

322 cc.chaptersDict[chapterName] = Chapter(c, cc, chapterName) 

323 cc.makeCommand(chapterName, binding) 

324 return result 

325 #@-others 

326#@+node:ekr.20070317085708: ** class Chapter 

327class Chapter: 

328 """A class representing the non-gui data of a single chapter.""" 

329 #@+others 

330 #@+node:ekr.20070317085708.1: *3* chapter.__init__ 

331 def __init__(self, c, chapterController, name): 

332 self.c = c 

333 self.cc = cc = chapterController 

334 self.name = g.checkUnicode(name) 

335 self.selectLockout = False # True: in chapter.select logic. 

336 # State variables: saved/restored when the chapter is unselected/selected. 

337 self.p = c.p 

338 self.root = self.findRootNode() 

339 if cc.tt: 

340 cc.tt.createTab(name) 

341 #@+node:ekr.20070317085708.2: *3* chapter.__str__ and __repr__ 

342 def __str__(self): 

343 """Chapter.__str__""" 

344 return f"<chapter: {self.name}, p: {repr(self.p and self.p.h)}>" 

345 

346 __repr__ = __str__ 

347 #@+node:ekr.20110607182447.16464: *3* chapter.findRootNode 

348 def findRootNode(self): 

349 """Return the @chapter node for this chapter.""" 

350 if self.name == 'main': 

351 return None 

352 return self.cc.findChapterNode(self.name) 

353 #@+node:ekr.20070317131205.1: *3* chapter.select & helpers 

354 def select(self, w=None): 

355 """Restore chapter information and redraw the tree when a chapter is selected.""" 

356 if self.selectLockout: 

357 return 

358 try: 

359 tt = self.cc.tt 

360 self.selectLockout = True 

361 self.chapterSelectHelper(w) 

362 if tt: 

363 # A bad kludge: update all the chapter names *after* the selection. 

364 tt.setTabLabel(self.name) 

365 finally: 

366 self.selectLockout = False 

367 #@+node:ekr.20070423102603.1: *4* chapter.chapterSelectHelper 

368 def chapterSelectHelper(self, w=None): 

369 

370 c, cc = self.c, self.cc 

371 cc.selectedChapter = self 

372 if self.name == 'main': 

373 return # 2016/04/20 

374 # Remember the root (it may have changed) for dehoist. 

375 self.root = root = self.findRootNode() 

376 if not root: 

377 # Might happen during unit testing or startup. 

378 return 

379 if self.p and not c.positionExists(self.p): 

380 self.p = p = root.copy() 

381 # Next, recompute p and possibly select a new editor. 

382 if w: 

383 assert w == c.frame.body.wrapper 

384 assert w.leo_p 

385 self.p = p = self.findPositionInChapter(w.leo_p) or root.copy() 

386 else: 

387 # This must be done *after* switching roots. 

388 self.p = p = self.findPositionInChapter(self.p) or root.copy() 

389 # Careful: c.selectPosition would pop the hoist stack. 

390 w = self.findEditorInChapter(p) 

391 c.frame.body.selectEditor(w) # Switches text. 

392 self.p = p # 2016/04/20: Apparently essential. 

393 if g.match_word(p.h, 0, '@chapter'): 

394 if p.hasChildren(): 

395 self.p = p = p.firstChild() 

396 else: 

397 # 2016/04/20: Create a dummy first child. 

398 self.p = p = p.insertAsLastChild() 

399 p.h = 'New Headline' 

400 c.hoistStack.append(g.Bunch(p=root.copy(), expanded=True)) 

401 # Careful: c.selectPosition would pop the hoist stack. 

402 c.setCurrentPosition(p) 

403 g.doHook('hoist-changed', c=c) 

404 #@+node:ekr.20070317131708: *4* chapter.findPositionInChapter 

405 def findPositionInChapter(self, p1, strict=False): 

406 """Return a valid position p such that p.v == v.""" 

407 c, name = self.c, self.name 

408 # Bug fix: 2012/05/24: Search without root arg in the main chapter. 

409 if name == 'main' and c.positionExists(p1): 

410 return p1 

411 if not p1: 

412 return None 

413 root = self.findRootNode() 

414 if not root: 

415 return None 

416 if c.positionExists(p1, root=root.copy()): 

417 return p1 

418 if strict: 

419 return None 

420 if name == 'main': 

421 theIter = c.all_unique_positions 

422 else: 

423 theIter = root.self_and_subtree 

424 for p in theIter(copy=False): 

425 if p.v == p1.v: 

426 return p.copy() 

427 return None 

428 #@+node:ekr.20070425175522: *4* chapter.findEditorInChapter 

429 def findEditorInChapter(self, p): 

430 """return w, an editor displaying position p.""" 

431 chapter, c = self, self.c 

432 w = c.frame.body.findEditorForChapter(chapter, p) 

433 if w: 

434 w.leo_chapter = chapter 

435 w.leo_p = p and p.copy() 

436 return w 

437 #@+node:ekr.20070615065222: *4* chapter.positionIsInChapter 

438 def positionIsInChapter(self, p): 

439 p2 = self.findPositionInChapter(p, strict=True) 

440 return p2 

441 #@+node:ekr.20070320091806.1: *3* chapter.unselect 

442 def unselect(self): 

443 """Remember chapter info when a chapter is about to be unselected.""" 

444 c = self.c 

445 # Always try to return to the same position. 

446 self.p = c.p 

447 if self.name == 'main': 

448 return 

449 root = None 

450 while c.hoistStack: 

451 bunch = c.hoistStack.pop() 

452 root = bunch.p 

453 if root == self.root: 

454 break 

455 # Re-institute the previous hoist. 

456 if c.hoistStack: 

457 p = c.hoistStack[-1].p 

458 # Careful: c.selectPosition would pop the hoist stack. 

459 c.setCurrentPosition(p) 

460 else: 

461 p = root or c.p 

462 c.setCurrentPosition(p) 

463 #@-others 

464#@-others 

465#@@language python 

466#@@tabwidth -4 

467#@@pagewidth 70 

468#@-leo