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.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 self.chaptersDict = {} 

22 # Keys are chapter names, values are chapters. 

23 # Important: chapter names never change, 

24 # even if their @chapter node changes. 

25 self.initing = True 

26 # #31 

27 # True: suppress undo when creating chapters. 

28 self.re_chapter = None 

29 # Set where used. 

30 self.selectedChapter = None 

31 self.selectChapterLockout = False 

32 # True: cc.selectChapterForPosition does nothing. 

33 # Note: Used in qt_frame.py. 

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

35 self.reloadSettings() 

36 

37 def reloadSettings(self): 

38 c = self.c 

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

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

41 def createIcon(self): 

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

43 cc = self 

44 c = cc.c 

45 if cc.use_tabs: 

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

47 if not cc.tt: 

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

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

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

51 

52 def finishCreate(self): 

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

54 c, cc = self.c, self 

55 cc.createIcon() 

56 cc.setAllChapterNames() 

57 # Create all chapters. 

58 # #31. 

59 cc.initing = False 

60 # Always select the main chapter. 

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

62 cc.selectChapterByName('main') 

63 c.redraw() 

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

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

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

67 c, cc = self.c, self 

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

69 # 

70 # For tracing: 

71 # inverseBindingsDict = c.k.computeInverseBindingDict() 

72 if commandName in c.commandsDict: 

73 return 

74 

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

76 chapter = cc.chaptersDict.get(name) 

77 if chapter: 

78 try: 

79 cc.selectChapterLockout = True 

80 cc.selectChapterByNameHelper(chapter, collapse=True) 

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

82 finally: 

83 cc.selectChapterLockout = False 

84 elif not g.unitTesting: 

85 # Possible, but not likely. 

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

87 

88 # Always bind the command without a shortcut. 

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

90 

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

92 for shortcut in bindings: 

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

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

95 @cmd('chapter-select') 

96 def selectChapter(self, event=None): 

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

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

99 names = cc.setAllChapterNames() 

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

101 k.setLabelBlue('Select chapter: ') 

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

103 

104 def selectChapter1(self, event): 

105 cc, k = self, self.c.k 

106 k.clearState() 

107 k.resetLabel() 

108 if k.arg: 

109 cc.selectChapterByName(k.arg) 

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

111 @cmd('chapter-back') 

112 def backChapter(self, event=None): 

113 cc = self 

114 names = sorted(cc.setAllChapterNames()) 

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

116 i = names.index(sel_name) 

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

118 cc.selectChapterByName(new_name) 

119 

120 @cmd('chapter-next') 

121 def nextChapter(self, event=None): 

122 cc = self 

123 names = sorted(cc.setAllChapterNames()) 

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

125 i = names.index(sel_name) 

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

127 cc.selectChapterByName(new_name) 

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

129 def selectChapterByName(self, name): 

130 """Select a chapter without redrawing.""" 

131 cc = self 

132 if self.selectChapterLockout: 

133 return 

134 if isinstance(name, int): 

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

136 return 

137 chapter = cc.getChapter(name) 

138 if not chapter: 

139 if not g.unitTesting: 

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

141 return 

142 try: 

143 cc.selectChapterLockout = True 

144 cc.selectChapterByNameHelper(chapter) 

145 finally: 

146 cc.selectChapterLockout = False 

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

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

149 """Select the chapter.""" 

150 cc, c = self, self.c 

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

152 chapter.p = c.p 

153 return 

154 if chapter == cc.selectedChapter: 

155 chapter.p = c.p 

156 return 

157 if cc.selectedChapter: 

158 cc.selectedChapter.unselect() 

159 else: 

160 main_chapter = cc.getChapter('main') 

161 if main_chapter: 

162 main_chapter.unselect() 

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

164 pass 

165 elif chapter.name == 'main': 

166 pass # Do not use c.p. 

167 else: 

168 chapter.p = chapter.findRootNode() 

169 chapter.select() 

170 c.contractAllHeadlines() 

171 chapter.p.v.expand() 

172 c.selectPosition(chapter.p) 

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

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

175 def error(self, s): 

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

177 

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

179 if g.unitTesting: 

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

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

182 if killUnitTest: 

183 assert False, s 

184 else: 

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

186 

187 def warning(self, s): 

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

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

190 def findAnyChapterNode(self): 

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

192 cc = self 

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

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

195 return True 

196 return False 

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

198 def findChapterNameForPosition(self, p): 

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

200 cc, c = self, self.c 

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

202 return None 

203 for name in cc.chaptersDict: 

204 if name != 'main': 

205 theChapter = cc.chaptersDict.get(name) 

206 if theChapter.positionIsInChapter(p): 

207 return name 

208 return 'main' 

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

210 def findChapterNode(self, name): 

211 """ 

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

213 anywhere in the entire outline. 

214 

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

216 but users may move them anywhere. 

217 """ 

218 cc = self 

219 name = g.checkUnicode(name) 

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

221 chapterName, binding = self.parseHeadline(p) 

222 if chapterName == name: 

223 return p 

224 return None # Not an error. 

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

226 def getChapter(self, name): 

227 cc = self 

228 return cc.chaptersDict.get(name) 

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

230 def getSelectedChapter(self): 

231 cc = self 

232 return cc.selectedChapter 

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

234 def inChapter(self): 

235 cc = self 

236 theChapter = cc.getSelectedChapter() 

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

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

239 def parseHeadline(self, p): 

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

241 if not self.re_chapter: 

242 self.re_chapter = re.compile( 

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

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

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

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

247 if m: 

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

249 if chapterName: 

250 chapterName = self.sanitize(chapterName) 

251 if binding: 

252 binding = binding.strip() 

253 else: 

254 chapterName = binding = None 

255 return chapterName, binding 

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

257 def sanitize(self, s): 

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

259 # Similar to g.sanitize_filename, but simpler. 

260 result = [] 

261 for ch in s.strip(): 

262 # pylint: disable=superfluous-parens 

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

264 result.append(ch) 

265 elif ch in ' \t': 

266 result.append('-') 

267 s = ''.join(result) 

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

269 return s[:128] 

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

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

272 """ 

273 Select a chapter containing position p. 

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

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

276 

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

278 """ 

279 c, cc = self.c, self 

280 # New in Leo 4.11 

281 if cc.selectChapterLockout: 

282 return 

283 selChapter = cc.getSelectedChapter() 

284 if not chapter and not selChapter: 

285 return 

286 if not p: 

287 return 

288 if not c.positionExists(p): 

289 return 

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

291 theChapter = chapter or selChapter 

292 if not theChapter: 

293 return 

294 # First, try the presently selected chapter. 

295 firstName = theChapter.name 

296 if firstName == 'main': 

297 return 

298 if theChapter.positionIsInChapter(p): 

299 cc.selectChapterByName(theChapter.name) 

300 return 

301 for name in cc.chaptersDict: 

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

303 theChapter = cc.chaptersDict.get(name) 

304 if theChapter.positionIsInChapter(p): 

305 cc.selectChapterByName(name) 

306 break 

307 else: 

308 cc.selectChapterByName('main') 

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

310 assert not self.selectChapterLockout 

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

312 c.redraw_later() 

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

314 def setAllChapterNames(self): 

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

316 c, cc = self.c, self 

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

318 if 'main' not in cc.chaptersDict: 

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

320 cc.makeCommand('main') 

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

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

323 for p in c.all_unique_positions(): 

324 chapterName, binding = self.parseHeadline(p) 

325 if chapterName and p.v not in seen: 

326 seen.add(p.v) 

327 result.append(chapterName) 

328 if chapterName not in cc.chaptersDict: 

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

330 cc.makeCommand(chapterName, binding) 

331 return result 

332 #@-others 

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

334class Chapter: 

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

336 #@+others 

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

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

339 self.c = c 

340 self.cc = cc = chapterController 

341 self.name = g.checkUnicode(name) 

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

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

344 self.p = c.p 

345 self.root = self.findRootNode() 

346 if cc.tt: 

347 cc.tt.createTab(name) 

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

349 def __str__(self): 

350 """Chapter.__str__""" 

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

352 

353 __repr__ = __str__ 

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

355 def findRootNode(self): 

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

357 if self.name == 'main': 

358 return None 

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

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

361 def select(self, w=None): 

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

363 if self.selectLockout: 

364 return 

365 try: 

366 tt = self.cc.tt 

367 self.selectLockout = True 

368 self.chapterSelectHelper(w) 

369 if tt: 

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

371 tt.setTabLabel(self.name) 

372 finally: 

373 self.selectLockout = False 

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

375 def chapterSelectHelper(self, w=None): 

376 

377 c, cc = self.c, self.cc 

378 cc.selectedChapter = self 

379 if self.name == 'main': 

380 return # 2016/04/20 

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

382 self.root = root = self.findRootNode() 

383 if not root: 

384 # Might happen during unit testing or startup. 

385 return 

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

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

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

389 if w: 

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

391 assert w.leo_p 

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

393 else: 

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

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

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

397 w = self.findEditorInChapter(p) 

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

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

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

401 if p.hasChildren(): 

402 self.p = p = p.firstChild() 

403 else: 

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

405 self.p = p = p.insertAsLastChild() 

406 p.h = 'New Headline' 

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

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

409 c.setCurrentPosition(p) 

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

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

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

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

414 c, name = self.c, self.name 

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

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

417 return p1 

418 if not p1: 

419 return None 

420 root = self.findRootNode() 

421 if not root: 

422 return None 

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

424 return p1 

425 if strict: 

426 return None 

427 if name == 'main': 

428 theIter = c.all_unique_positions 

429 else: 

430 theIter = root.self_and_subtree 

431 for p in theIter(copy=False): 

432 if p.v == p1.v: 

433 return p.copy() 

434 return None 

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

436 def findEditorInChapter(self, p): 

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

438 chapter, c = self, self.c 

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

440 if w: 

441 w.leo_chapter = chapter 

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

443 return w 

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

445 def positionIsInChapter(self, p): 

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

447 return p2 

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

449 def unselect(self): 

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

451 c = self.c 

452 # Always try to return to the same position. 

453 self.p = c.p 

454 if self.name == 'main': 

455 return 

456 root = None 

457 while c.hoistStack: 

458 bunch = c.hoistStack.pop() 

459 root = bunch.p 

460 if root == self.root: 

461 break 

462 # Re-institute the previous hoist. 

463 if c.hoistStack: 

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

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

466 c.setCurrentPosition(p) 

467 else: 

468 p = root or c.p 

469 c.setCurrentPosition(p) 

470 #@-others 

471#@-others 

472#@@language python 

473#@@tabwidth -4 

474#@@pagewidth 70 

475#@-leo