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
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
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()
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.
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
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}")
81 # Always bind the command without a shortcut.
82 # This will create the command bound to any existing settings.
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)
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)
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}")
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}")
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.
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.
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)}>"
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):
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